mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-04 18:31:41 +00:00
@@ -0,0 +1,254 @@
|
||||
package webauthn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/sonr-io/sonr/types/webauthn/metadata"
|
||||
"github.com/sonr-io/sonr/types/webauthn/webauthncbor"
|
||||
"github.com/sonr-io/sonr/types/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
|
||||
}
|
||||
Reference in New Issue
Block a user