This page aims to document the security related aspects of the FIDO2 implementation on Solo. This is to make it easier for public review and comments.

Key generation

Solo aims to achieve 256 bit (32 byte) security with its FIDO2 implementation, even in light of physical side channels.

When Solo is first programmed, it will be "uninitialized," meaning it won't have any secret material, until the first time it boots, then it will leverage the TRNG to generate all necessary material. This only happens once.

A master secret, M, is generated at initialization. This is only used for all key generation and derivation in FIDO2. Solo uses a key wrapping method for FIDO2 operation.

NOTE: The masked implementation of AES is planned, but not yet implemented. Currently it is normal AES.

Key wrapping

When you register a service with a FIDO2 or U2F authenticator, the authenticator must generate a new keypair unique to that service. This keypair could be stored on the authenticator to be used in subsequent authentications, but a certain amount of memory would need to be allocated for this. On embedded devices, there isn't much memory to spare and users would frustratingly hit the limit of this memory.

The answer to this problem is to do key wrapping. The authenticator just stores M and uses M and the TRNG to generate new keys and derive previous keys on the fly. A random number, R, is generated, and is placed in the FIDO2/U2F KEYID parameter. The service stores KEYID after registering a key and will issue it back to the authenticator for subsequent authentications.

In essence, the following happens at registration.

  1. Generate R, calculate private key, K, using HMAC(M,R)
  2. Derive public key, P, from K
  3. Return P and R to service. (R is in KEYID parameter)
  4. Service stores P and R.

Now on authentication.

  1. Service issues authentication request with R in KEYID parameter.
  2. * Authenticator generates K by calculating HMAC(M,R).
  3. Proceed normally as if K was loaded from storage memory.

Key derivation

Planned, but not yet implemented.

Master secret M consists of 64 bytes, split into equal parts M1 and M2. In theory, we should only need 32 bytes to achieve 256 security, but we also plan to have side channel security hence the added bytes.

Our HMAC currently is a two step process. First, just generate a normal SHA256-HMAC.

  1. tmp = SHA256_HMAC(M1, R)

We could proceed using tmp as our secret key, K. But our SHA256-HMAC implementation isn't side channel resistant and we won't bother trying to add side channel resistance. So we add an additional stage that is side channel resistant.

  1. K = aes256_masked(M2, tmp)

We add a masked AES encryption to provide side channel resistance. Masked AES is well studied and relatively easy to implement. An adversary may be able to recover M1 via SCA but not M2.

* There are other details I leave out. There's also an authentication tag in the KEYID parameter to ensure this is a key generated by the Solo key.