package webauthn import ( "context" "crypto/sha256" "encoding/json" "fmt" "github.com/google/uuid" "github.com/sonr-io/common/webauthn/metadata" "github.com/sonr-io/common/webauthn/webauthncbor" "github.com/sonr-io/common/webauthn/webauthncose" ) // AuthenticatorAttestationResponse is the initial unpacked 'response' object received by the relying party. This // contains the clientDataJSON object, which will be marshalled into [CollectedClientData], and the 'attestationObject', // which contains information about the authenticator, and the newly minted public key credential. The information in // both objects are used to verify the authenticity of the ceremony and new credential. // // See: https://www.w3.org/TR/webauthn/#typedefdef-publickeycredentialjson type AuthenticatorAttestationResponse struct { // The byte slice of clientDataJSON, which becomes CollectedClientData AuthenticatorResponse Transports []string `json:"transports,omitempty"` AuthenticatorData URLEncodedBase64 `json:"authenticatorData"` PublicKey URLEncodedBase64 `json:"publicKey"` PublicKeyAlgorithm int64 `json:"publicKeyAlgorithm"` // AttestationObject is the byte slice version of attestationObject. // This attribute contains an attestation object, which is opaque to, and // cryptographically protected against tampering by, the client. The // attestation object contains both authenticator data and an attestation // statement. The former contains the AAGUID, a unique credential ID, and // the credential public key. The contents of the attestation statement are // determined by the attestation statement format used by the authenticator. // It also contains any additional information that the Relying Party's server // requires to validate the attestation statement, as well as to decode and // validate the authenticator data along with the JSON-serialized client data. AttestationObject URLEncodedBase64 `json:"attestationObject"` } // ParsedAttestationResponse is the parsed version of [AuthenticatorAttestationResponse]. type ParsedAttestationResponse struct { CollectedClientData CollectedClientData AttestationObject AttestationObject Transports []AuthenticatorTransport } // AttestationObject is the raw attestationObject. // // Authenticators SHOULD also provide some form of attestation, if possible. If an authenticator does, the basic // requirement is that the authenticator can produce, for each credential public key, an attestation statement // verifiable by the WebAuthn Relying Party. Typically, this attestation statement contains a signature by an // attestation private key over the attested credential public key and a challenge, as well as a certificate or similar // data providing provenance information for the attestation public key, enabling the Relying Party to make a trust // decision. However, if an attestation key pair is not available, then the authenticator MAY either perform self // attestation of the credential public key with the corresponding credential private key, or otherwise perform no // attestation. All this information is returned by authenticators any time a new public key credential is generated, in // the overall form of an attestation object. // // Specification: §6.5. Attestation (https://www.w3.org/TR/webauthn/#sctn-attestation) type AttestationObject struct { // The authenticator data, including the newly created public key. See [AuthenticatorData] for more info AuthData AuthenticatorData // The byteform version of the authenticator data, used in part for signature validation RawAuthData []byte `json:"authData"` // The format of the Attestation data. Format string `json:"fmt"` // The attestation statement data sent back if attestation is requested. AttStatement map[string]any `json:"attStmt,omitempty"` } // attestationFormatValidationHandler defines the signature for attestation format validation functions. // Returns attestation type, trust path (x5c), and error. type attestationFormatValidationHandler func(AttestationObject, []byte, metadata.Provider) (string, []any, error) // attestationRegistry holds all registered attestation format handlers. // Formats are automatically registered via init() functions in their respective files. var attestationRegistry = make(map[AttestationFormat]attestationFormatValidationHandler) // RegisterAttestationFormat registers an attestation format handler with the library. // This is called automatically by init() functions for built-in formats: // - packed: WebAuthn-optimized format (attestation_packed.go) // - tpm: Trusted Platform Module format (attestation_tpm.go) // - apple: Apple Anonymous Attestation (attestation_apple.go) // - android-key: Android hardware attestation (attestation_androidkey.go) // - android-safetynet: Android SafetyNet (attestation_safetynet.go) // - fido-u2f: Legacy FIDO U2F format (attestation_u2f.go) // // Custom attestation formats can be registered using this function. func RegisterAttestationFormat( format AttestationFormat, handler attestationFormatValidationHandler, ) { attestationRegistry[format] = handler } // Parse the values returned in the authenticator response and perform attestation verification // Step 8. This returns a fully decoded struct with the data put into a format that can be // used to verify the user and credential that was created. func (ccr *AuthenticatorAttestationResponse) Parse() (p *ParsedAttestationResponse, err error) { p = &ParsedAttestationResponse{} if err = json.Unmarshal(ccr.ClientDataJSON, &p.CollectedClientData); err != nil { return nil, ErrParsingData.WithInfo(err.Error()).WithError(err) } if err = webauthncbor.Unmarshal(ccr.AttestationObject, &p.AttestationObject); err != nil { return nil, ErrParsingData.WithInfo(err.Error()).WithError(err) } // Step 8. Perform CBOR decoding on the attestationObject field of the AuthenticatorAttestationResponse // structure to obtain the attestation statement format fmt, the authenticator data authData, and // the attestation statement attStmt. if err = p.AttestationObject.AuthData.Unmarshal(p.AttestationObject.RawAuthData); err != nil { return nil, err } if !p.AttestationObject.AuthData.Flags.HasAttestedCredentialData() { return nil, ErrAttestationFormat.WithInfo( "Attestation missing attested credential data flag", ) } for _, t := range ccr.Transports { if transport, ok := internalRemappedAuthenticatorTransport[t]; ok { p.Transports = append(p.Transports, transport) } else { p.Transports = append(p.Transports, AuthenticatorTransport(t)) } } return p, nil } // Verify performs Steps 13 through 19 of registration verification. // // Steps 13 through 15 are verified against the auth data. These steps are identical to 15 through 18 for assertion so we // handle them with AuthData. func (a *AttestationObject) Verify( relyingPartyID string, clientDataHash []byte, userVerificationRequired bool, userPresenceRequired bool, mds metadata.Provider, credParams []CredentialParameter, ) (err error) { rpIDHash := sha256.Sum256([]byte(relyingPartyID)) // Begin Step 13 through 15. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the RP. if err = a.AuthData.Verify(rpIDHash[:], nil, userVerificationRequired, userPresenceRequired); err != nil { return err } // Step 16. Verify that the "alg" parameter in the credential public key in // authData matches the alg attribute of one of the items in options.pubKeyCredParams. pk := webauthncose.PublicKeyData{} if err = webauthncbor.Unmarshal(a.AuthData.AttData.CredentialPublicKey, &pk); err != nil { return err } found := false for _, credParam := range credParams { if int(pk.Algorithm) == int(credParam.Algorithm) { found = true break } } if !found { return ErrAttestationFormat.WithInfo("Credential public key algorithm not supported") } return a.VerifyAttestation(clientDataHash, mds) } // VerifyAttestation only verifies the attestation object excluding the AuthData values. If you wish to also verify the // AuthData values you should use [Verify]. func (a *AttestationObject) VerifyAttestation( clientDataHash []byte, mds metadata.Provider, ) (err error) { // Step 18. Determine the attestation statement format by performing a // USASCII case-sensitive match on fmt against the set of supported // WebAuthn Attestation Statement Format Identifier values. The up-to-date // list of registered WebAuthn Attestation Statement Format Identifier // values is maintained in the IANA registry of the same name // [WebAuthn-Registries] (https://www.w3.org/TR/webauthn/#biblio-webauthn-registries). // // Since there is not an active registry yet, we'll check it against our internal // Supported types. // // But first let's make sure attestation is present. If it isn't, we don't need to handle // any of the following steps. if AttestationFormat(a.Format) == AttestationFormatNone { if len(a.AttStatement) != 0 { return ErrAttestationFormat.WithInfo("Attestation format none with attestation present") } return nil } var ( handler attestationFormatValidationHandler valid bool ) if handler, valid = attestationRegistry[AttestationFormat(a.Format)]; !valid { return ErrAttestationFormat.WithInfo( fmt.Sprintf("Attestation format %s is unsupported", a.Format), ) } var ( aaguid uuid.UUID attestationType string x5cs []any ) // Step 19. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature, by using // the attestation statement format fmt’s verification procedure given attStmt, authData and the hash of the serialized // client data computed in step 7. if attestationType, x5cs, err = handler(*a, clientDataHash, mds); err != nil { return err.(*Error).WithInfo(attestationType) } if len(a.AuthData.AttData.AAGUID) != 0 { if aaguid, err = uuid.FromBytes(a.AuthData.AttData.AAGUID); err != nil { return ErrInvalidAttestation.WithInfo("Error occurred parsing AAGUID during attestation validation"). WithDetails(err.Error()). WithError(err) } } if mds == nil { return nil } var protoErr *Error if protoErr = ValidateMetadata(context.Background(), mds, aaguid, attestationType, x5cs); protoErr != nil { return ErrInvalidAttestation.WithInfo(fmt.Sprintf("Error occurred validating metadata during attestation validation: %+v", protoErr)). WithDetails(protoErr.DevInfo). WithError(protoErr) } return nil }