mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
255 lines
10 KiB
Go
255 lines
10 KiB
Go
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
|
|||
|
|
}
|