tweag / webauthn

A library for parsing and validating webauthn/fido2 credentials
Apache License 2.0
34 stars 11 forks source link

Add README to server containing example #137

Open ErinvanderVeen opened 2 years ago

ErinvanderVeen commented 2 years ago

In the WIP blogpost, we created an example of the entire flow of the library+webauthn. This should be written as documentation somewhere.

Example

Apple’s attestation format is by far the simplest, and so we will use it to provide a technical example of WebAuthn, and how to use the Haskell library. The information in this chapter is based on the example implementation provided with the library, with some things being abstracted or generalised.

Registration Once a user uses their Apple device to visits our example website “example.org” that supports WebAuthn, enters their Display Name and username, and clicks register, the RP is informed of an intent to register:

{
  "accountDisplayName": "Jane Doe",
  "accountName": "janedoe"
}

The RP keeps track of this information and responds with the PublicKeyCredentialCreationOptions. In our library, these are represented by the CredentialOptions where we have to fill in corUser with the information we received earlier and corChallenge with a randomly generated bytestring of at least 16 bytes. The other fields can be set at the discretion of the RP, but will be constants in the simplest implementations. See the documentation of our library or the WebAuthn specification for more information.

CredentialOptionsRegistration ::
  {
    corRp :: CredentialRpEntity,
    corUser :: CredentialUserEntity,
    corChallenge :: Challenge,
    corPubKeyCredParams :: [CredentialParameters],
    corTimeout :: Maybe Timeout,
    corExcludeCredentials :: [CredentialDescriptor],
    corAuthenticatorSelection :: Maybe AuthenticatorSelectionCriteria,
    corAttestation :: AttestationConveyancePreference,
    corExtensions :: Maybe AuthenticationExtensionsClientInputs
  } -> CredentialOptions 'Registration

The library provides methods to encode these types to valid JSON (in this instance encodeCredentialOptionsRegistration), from now on this step will be considered implicit. In our example RP, the client receives the following data:

{
  "attestation": "direct",
  "authenticatorSelection": {
    "requireResidentKey": false,
    "residentKey": "discouraged",
    "userVerification": "preferred"
  },
  "challenge": "YCT9YQAAAADXjUiOzjA6B40g5naNZoxi",
  "excludeCredentials": [],
  "pubKeyCredParams": [
    {
      "alg": -7,
      "type": "public-key"
    }
  ],
  "rp": {
    "name": "ACME"
  },
  "user": {
    "displayName": "Jane Doe",
    "id": "49zghG0wU5WbOAtfbHM1wg",
    "name": "janedoe"
  }
}

The client will take these options and select a suitable authenticator to create a credential. In our example, Safari will communicate with the Apple Secure Enclave to generate a key pair and the Apple attestation servers to attest this key pair. It will then respond to the server with the following JSON data:

{
  "clientExtensionResults": {},
  "rawId": "hxc7kmahqrKLT9XIpiX4lU_VxqY",
  "response": {
    "attestationObject": "o2NmbXRlYXB...",
    "clientDataJSON": "eyJ0eX...",
    "transports": []
  }
}

Which is decoded to:

Credential {
  cIdentifier = "87173b...",
  cResponse = AuthenticatorResponseRegistration {
    arrClientData = CollectedClientData {
      ccdChallenge = Challenge "490afd...",
      ccdOrigin = Origin "https://example.org",
      ccdCrossOrigin = False,
      ccdRawData = "eyJ0eX...",
    },
    arrAttestationObject = AttestationObject {
      aoAuthData = AuthenticatorData {
        adRpIdHash = RpIdHash "bfabc3...",
        adFlags = AuthenticatorDataFlags {
          adfUserPresent = True,
          adfUserVerified = True
        },
        adSignCount = SignatureCounter 0,
        adAttestedCredentialData = AttestedCredentialData {
          acdAaguid = AAGUID "f24a8e...",
          acdCredentialId = CredentialId "hxc7km...",
          acdCredentialPublicKey = CosePublicKeyECDSA {..},
          acdCredentialPublicKeyBytes = WithRaw "jCCAkI..."
        },
        adExtensions = Nothing,
        adRawData = WithRaw "o2NmbX..."
      },
      aoAttStmt = Statement {
        x5c = [ ... ];
      },
      aoFmt = Apple.Format -- Qualified for clarity
    },
  },
  cClientExtensionResults = AuthenticationExtensionsClientOutputs {}
}

Where the RawData and acdCredentialPublicKeyBytes fields contain the undecoded representations of parts of the data for use during verification. After decoding, this type is passed to the library’s verifyRegistrationResponse where the attestation is verified. Let’s quickly go over the purpose of the fields.

cIdentifier is used as a key in the RP database for future identification of the credential. arrClientData is the field that contains data the client received from the RP, the hash of which is part of the data that is verified by the verifyRegistrationResponse. Notable is the ccdChallenge that the RP must check to be equal to the one send to the client earlier.

arrAttestationObject is the collection of all data that is attested. In our case, we can trust this data after we verify that it was signed by Apple. Of the attestation object, the aoAuthData contains information about the (internal) state of the authenticator. This allows the RP to make sure the authenticator can be trusted. The adSignCount tracks the number of times the authenticator was used (either specific to the credential, or as a global counter), the RP can that this value exclusively increases to prevent cloning of the authenticator.

aoAttStmt is the attestation format specific information. The contents of this field is used by the RP to verify if the attestation is correct. In our example, the verifyRegistrationResponse function checks if the certificate chain is verifiable up to the Apple root certificate. The public key belonging to the private key that was used to sign the aoAuthData and arrClientData is in the leaf certificate of the x5c chain.

The verifyRegistrationResponse performs the verification and will result in RegistrationResult if the response was successfully verified. This type contains two fields, both with their own purpose.

data RegistrationResult = RegistrationResult
  { rrEntry :: CredentialEntry,
    rrAttestationStatement :: SomeAttestationStatement
  }

rrEntry contains the information the RP must store in a database. This information will be used when a user attempts log in in the future. Most importantly, this type contains the user handle of the user, their public key, and the signature counter.

rrAttestationStatement contains information on the validity of the attestation, including information on the type of authenticator used. The RP can use the information in this field to determine if they do or do not want to accept the registration. In our case this field will tell us the device has a Fido2 identifier (not present in the metadata) and provide us with certificate chain (we can store this chain and check it against revocation lists in the future).

After the RP has accepted the new credential based on the rrAttestationStatement and has written the rrEntry the server should respond with an implementation specific response indicating success, setting any cookies as necessary.

Logging in The login ceremony of WebAuthn is much simpler than the registration procedure. Most of this is related to the fact that the RP already knows they can trust public key created by the authenticator.

To begin the login ceremony begins by the client notifying the server of the intent to login, the only data the RP requires at this stage is the username of the user. In our case this would be "``janedoe``". The RP uses this information to lookup if it has any credentials stored for this user. If so, it replies with the [CredentialOptionsAuthentication](https://www.w3.org/TR/webauthn-2/#dictionary-assertion-options). In our example, we only have a single credential stored for the current user, and so the options look very simple:

CredentialOptionsAuthentication {
  coaChallenge = Challenge "6d0afd...",
  coaTimeout = Nothing,
  coaRpId = Nothing,
  coaAllowCredentials = [
    CredentialDescriptor {
      cdTyp = CredentialTypePublicKey,
      cdId = CredentialId "87173b...",
      cdTransports = Nothing
    }
  ],
  coaUserVerification = UserVerificationRequirementPreferred,
  coaExtensions = Nothing
}

In this example, all optional fields are set to Nothing. For most of these fields the client will instantiate a default. What these defaults are exactly can be found in the WebAuthn specification. Again, the RP should remember the challenge for verification later.

The client will use these options to select an authenticator and task the authenticator with signing the challenge. Once signed, the client will respond with the AuthenticatorAssertionResponse which is represented as the identically named record in our library. However, in RP’s using our library, this intermediate representation should be decoded to the [Credential](https://hackage.haskell.org/package/webauthn-0.3.0.0/docs/Crypto-WebAuthn-Model-Types.html#t:Credential), which would look like this:

Credential {
  cIdentifier = CredentialId "87173b...",
  cResponse = AuthenticatorResponseAuthentication {
    araClientData = CollectedClientData {
      ccdChallenge = Challenge "6d0afd...",
      ccdOrigin = Origin "https://example.org",
      ccdCrossOrigin = False,
      ccdRawData = WithRaw "eyJ0eX..."
    },
    araAuthenticatorData = AuthenticatorData {
      adRpIdHash = RpIdHash "bfabc3...",
      adFlags = AuthenticatorDataFlags {
        adfUserPresent = True,
        adfUserVerified = True
      },
      adSignCount = SignatureCounter 0,
      adAttestedCredentialData = NoAttestedCredentialData,
      adExtensions = Nothing,
      adRawData = WithRaw "fNuAP5..."
    },
    araSignature = AssertionSignature "304602...",
    araUserHandle = Just (UserHandle "5c863b...")
  },
  cClientExtensionResults = AuthenticationExtensionsClientOutputs {}
}

We have seen most of these fields before during registration. Significant is the addition of the araSignature field that signs the challenge. Noteworthy is the fact that Apple’s TouchID doesn’t relay the signature counter. If the RP receives a counter of 0, it may assume that the device doesn’t have a signature counter, and can thus skip that check.

Using the [verifyAuthenticationResponse](https://hackage.haskell.org/package/webauthn-0.3.0.0/docs/Crypto-WebAuthn-Operation-Authentication.html#v:verifyAuthenticationResponse), this signature and accompanying data is verified, the result is a either a list of errors or an AuthenticationResult. Because authentication doesn’t require the RP to write anything to a database, this result is very simple, containing only the signature counter, which we know to be the constant 0.

AuthenticationResult {
  arSignatureCounterResult = SignatureCounterZero
}

From here on, the RP performs some implementation specific operations to set the connected client to “logged in”, and the user can be presented with the content they logged in for.