Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement basic extensions infrastructure #77

Merged
merged 1 commit into from
Feb 21, 2025

Conversation

msirringhaus
Copy link
Collaborator

@msirringhaus msirringhaus commented Feb 19, 2025

A few things going on here:

  • Previously, AuthenticatorData was just a ByteBuf. I added it as a struct with de/serialize-functions.
  • Input and Output of the extensions vary for given commands, so I made AuthenticatorData generic over that, and specify which extension-struct it will contain inside MakeCredential or GetAssertion. This should be totally invisible to the end-user, who will just get 'the right thing'. It does, however, make the de/serialize-code a bit messy.
  • hmac-secret extension needs more work, but this PR was already getting quite big, so I figured, this would be better in a follow-up PR.

Tested credBlob, minPinLength and credProtect extension with my Yubikey Bio successfully.

@AlfioEmanueleFresta
Copy link
Member

AlfioEmanueleFresta commented Feb 21, 2025

Thank you @msirringhaus! This is good work and a great start for extension processing.

Dumping all my notes and ideas on extension processing here - would love to hear your thoughts:

  • I think it may make sense to define a traits for client extension processing, where each extension implements the trait including at least two methods - equivalent to create() and get() from the CTAP 2.2 spec, for processing on MakeCredential and GetAssertion respectively. In turn, this could be generic over the respective extension's client extension input, eg.

    12.2.1. Client extension input

    create() : ArrayBuffer containing opaque data in an RP-specific format.

    partial dictionary AuthenticationExtensionsClientInputs {
      ArrayBuffer credBlob;
    };

    get() : A boolean value to indicate that this extension is requested by the Relying Party.

    partial dictionary AuthenticationExtensionsClientInputs {
      boolean getCredBlob;
    };
    
  • From my reading of the spec, the result of client extension processing on either get() or create() could be:

    1. Accept - Use the client extension input as client extension output. This is used for authenticator extensions that require no processing on the client, eg. 12.3. Large Blob Key (largeBlobKey): Client extension input / output / processing.
    2. Accept & update - The client re-writes the authenticator extension input, with different fields, e.g. 12.5 HMAC Secret Extension (hmac-secret): Client extension processing.
    3. Drop - Remove this extension's payload altogether from the authenticator request, e.g. 12.1. Credential Protection (credProtect): Client extension processing drops the create() payload if credBlob size is unexpected.
    enum ClientExtensionProcessingResult {
      Accept(cbor::Map),
      Drop,
    }  
  • In addition to taking any action above, extension processing also alter platform behaviour. As an example 12.1. Credential Protection (credProtect) may require performing additional authenticator checks, in certain circumstances. The need for these checks may be represented in a new ClientBehaviour struct, to be potentially altered during extension processing:

    trait ClientExtensionProcessing<G, C> {
      fun get(&self, input: &G, &mut clientBehaviour: ClientBehaviour): ExtensionResult<CborMap>
      fun create(&self, input: &C, &mut clientBehaviour: ClientBehaviour): ExtensionResult<CborMap>
    }
  • Extensions unknown to the client should be passed through as CBOR. Maybe the trait itself could expose the expected input key, and if no extension is found, the extension data can be passed as-is?

    trait ClientExtensionParsing<G, C> where G: TryFrom<cbor::Map>, C: TryFrom<cbor::Map> {
      const MAKE_INPUT_KEY: &str;
      const GET_INPUT_KEY: &str;
    }
    trait ClientExtension<G, C>: ClientExtensionProcessing<G, C> + ClientExtensionParsing<G, C>
  • As we need to represent both pre-client processing and post-client processing extension payloads, IMO it may make sense to begin differentiating WebAuthn requests from CTAP2 request models - by removing the alias. Conversion will no longer be a TryFrom affair.

    • WebAuthn request would then need to represent pre-client processing requests.
    • CTAP2 models could directly use a CBOR Map. This could allow modelling both (a) known extensions' client processing outputs, and (b) passthrough extensions unknown to the client.
  • Ideally, if we fail to parse input for an extension, we should ignore the individual extension - but continue with the rest of the request. Maybe the trait for each extension could be generic over its input type, and perform parsing, with a Result so the caller can handle these errors appropriately?

  • We should also process WebAuthn extensions. These are defined as client extensions, so they should fit right in - we can possibly prefix these with WebAuthn (eg. WebAuthnPRFExtension for my favourite extension, Pseudo-random function extension (prf).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants