Files
web/internal/webauthnvendored/webauthn/credential.go
T
gamertan bfe6cfd29e
verify / verify (push) Successful in 3m40s
auth: publish passkey foundations preview
2026-08-21 17:33:00 -04:00

331 lines
16 KiB
Go

package webauthn
import (
"bytes"
"crypto/sha256"
"encoding/json"
"fmt"
"gamertan.com/web/internal/webauthnvendored/metadata"
"gamertan.com/web/internal/webauthnvendored/protocol"
)
//go:generate msgp
//msgp:replace protocol.AuthenticatorTransport with:string
//msgp:shim CredentialFlags as:byte using:(CredentialFlags).MsgpByte/CredentialFlagsFromMsgpByte
//msgp:clearomitted
// NewCredential returns a [*Credential] from a successfully validated registration response. The returned Credential
// includes a populated [CredentialAttestation] containing the raw attestation data needed for future verification;
// see the [CredentialAttestation] documentation for why these values must be persisted.
func NewCredential(clientDataHash []byte, c *protocol.ParsedCredentialCreationData) (credential *Credential, err error) {
credential = &Credential{
ID: c.Response.AttestationObject.AuthData.AttData.CredentialID,
PublicKey: c.Response.AttestationObject.AuthData.AttData.CredentialPublicKey,
AttestationType: c.Response.AttestationObject.Type,
AttestationFormat: c.Response.AttestationObject.Format,
Transport: c.Response.Transports,
Flags: NewCredentialFlags(c.Response.AttestationObject.AuthData.Flags),
Authenticator: Authenticator{
AAGUID: c.Response.AttestationObject.AuthData.AttData.AAGUID,
SignCount: c.Response.AttestationObject.AuthData.Counter,
Attachment: c.AuthenticatorAttachment,
},
Attestation: CredentialAttestation{
ClientDataJSON: c.Raw.AttestationResponse.ClientDataJSON,
ClientDataHash: clientDataHash,
AuthenticatorData: c.Raw.AttestationResponse.AuthenticatorData,
PublicKeyAlgorithm: c.Raw.AttestationResponse.PublicKeyAlgorithm,
Object: c.Raw.AttestationResponse.AttestationObject,
},
}
return credential, nil
}
// Credential contains all needed information about a WebAuthn credential for storage. This struct is effectively the
// Credential Record as described in the specification.
//
// Provided this data structure is preserved properly, a Credential can be verified against the FIDO Metadata Service
// at a later date using the [Credential.Verify] method with a [metadata.Provider].
//
// It is strongly recommended for the best security that a [Credential] is encrypted at rest with the exception of the
// ID and the value you use to lookup the user. This prevents a person with access to the database being able to
// compromise privacy by being able to view this data, as well as prevents them being able to compromise security by
// adding or modifying a Credential without them also having access to the encryption key.
//
// For consolidated persistence guidance; recommended schema shape, required lookup columns, and which fields
// must be written back on every successful FinishLogin / ValidateLogin; see the [Storage] section of the
// [gamertan.com/web/internal/webauthnvendored/webauthn] package documentation.
//
// See: §4. Terminology: Credential Record (https://www.w3.org/TR/webauthn-3/#credential-record)
//
// [Storage]: https://pkg.go.dev/gamertan.com/web/internal/webauthnvendored/webauthn#hdr-Storage
type Credential struct {
// The ID is the ID of the public key credential source. Described by the Credential Record 'id' field.
ID []byte `json:"id" msg:"id"`
// The credential public key of the public key credential source. Described by the Credential Record 'publicKey'
// field.
PublicKey []byte `json:"publicKey" msg:"pk"`
// AttestationType is the attestation type as conveyed by the authenticator during the registration ceremonyl
// one of the values defined by [metadata.AuthenticatorAttestationType] ("basic_full", "basic_surrogate",
// "attca", "anonca", "ecdaa", "none"). Prior releases incorrectly stored the attestation FORMAT here; see the
// custom [Credential.UnmarshalJSON] for the backward-compatibility migration applied when decoding such
// records.
AttestationType string `json:"attestationType,omitempty" msg:"atttype,omitempty"`
// AttestationFormat is the attestation statement format identifier ("packed", "tpm", "android-key",
// "android-safetynet", "fido-u2f", "apple", "compound", "none"); see §8 of the WebAuthn specification and
// the AttestationFormat constants in the protocol package.
AttestationFormat string `json:"attestationFormat,omitempty" msg:"attfmt,omitempty"`
// Transport types the authenticator supports. Described by the Credential Record 'transports' field.
Transport []protocol.AuthenticatorTransport `json:"transport,omitempty" msg:"t,omitempty"`
// Flags represent the commonly stored flags.
Flags CredentialFlags `json:"flags" msg:"flg"`
// The Authenticator information for a given Credential.
Authenticator Authenticator `json:"authenticator" msg:"a"`
// The attestation values that can be used to validate this Credential via the MDS3 at a later date.
Attestation CredentialAttestation `json:"attestation" msg:"att"`
}
// UnmarshalJSON decodes a [Credential] from JSON, applying a backward-compatibility migration for records produced
// by earlier versions of this library: if the decoded record has no AttestationFormat and the AttestationType value
// is a recognised attestation FORMAT identifier (i.e. "packed", "tpm", "none"), the value is moved to
// AttestationFormat and AttestationType is cleared so callers can re-derive the true attestation type by calling
// [Credential.Verify]. Records that already carry an AttestationFormat are untouched.
func (c *Credential) UnmarshalJSON(data []byte) error {
type credentialAlias Credential
var tmp credentialAlias
if err := json.Unmarshal(data, &tmp); err != nil {
return err
}
*c = Credential(tmp)
if c.AttestationFormat == "" && protocol.IsAttestationFormatString(c.AttestationType) {
c.AttestationFormat = c.AttestationType
c.AttestationType = ""
}
return nil
}
// SignalUnknownCredential creates a struct that can easily be marshaled to JSON which indicates this is an unknown
// Credential.
func (c *Credential) SignalUnknownCredential(rpid string) *protocol.SignalUnknownCredential {
return c.Descriptor().SignalUnknownCredential(rpid)
}
// Descriptor converts a [Credential] into a [protocol.CredentialDescriptor].
func (c *Credential) Descriptor() (descriptor protocol.CredentialDescriptor) {
return protocol.CredentialDescriptor{
Type: protocol.PublicKeyCredentialType,
CredentialID: c.ID,
Transport: c.Transport,
AttestationType: c.AttestationType,
AttestationFormat: c.AttestationFormat,
}
}
// Verify re-runs the full attestation verification for this credential against the given [metadata.Provider]. The
// stored raw attestation bytes are re-parsed, the attestation signature is re-verified, and the authenticator is
// validated against the MDS via [protocol.AttestationObject.VerifyAttestation] (which internally dispatches
// [protocol.ValidateMetadata]). This is the canonical audit path and is at least as strong as the original
// registration-time verification; call it on a schedule (i.e. on login or periodically) to catch MDS status changes
// such as a newly-revoked authenticator model or a compromise advisory published after registration.
//
// Requirements:
//
// - The mds argument must be a non-nil [metadata.Provider]; a nil provider returns an error.
//
// - [CredentialAttestation.ClientDataJSON] must be preserved byte-for-byte; it is re-parsed for its collected
// client data fields and re-hashed when [CredentialAttestation.ClientDataHash] is absent.
//
// - [CredentialAttestation.Object] must be preserved byte-for-byte; it is the raw CBOR attestation object and
// is decoded to recover the authenticator data, statement format, and statement for full re-verification.
//
// - [Credential.PublicKey] must be populated with the CBOR-encoded COSE key as emitted by the authenticator at
// registration. As an integrity check, Verify compares this value byte-for-byte against the credential public
// key carried inside the attestation object and returns an error on mismatch.
//
// - [CredentialAttestation.ClientDataHash] is optional; if empty it is recomputed as the SHA-256 of
// ClientDataJSON.
//
// - [Credential.Transport], [CredentialAttestation.AuthenticatorData], and [CredentialAttestation.PublicKeyAlgorithm]
// are not read by the current Verify implementation (the authenticator data is re-derived from the attestation
// object, and the top-level AuthenticatorData / PublicKeyAlgorithm convenience fields are informational). They
// are still stored so future versions of this library, or alternative verification paths, can consume them;
// see [CredentialAttestation] for why every field should be persisted.
//
// As a side-effect, a successful Verify call will populate [Credential.AttestationType] from the re-derived value
// when the field is empty (i.e. on a record migrated from a pre-split JSON layout by [Credential.UnmarshalJSON]);
// the next marshal of the Credential will then carry the correct attestation type. For this reason Verify uses a
// pointer receiver.
//
// See [CredentialAttestation] for guidance on persisting these raw values securely.
func (c *Credential) Verify(mds metadata.Provider) (err error) {
if mds == nil {
return fmt.Errorf("error verifying credential: the metadata provider must be provided but it's nil")
}
raw := c.toAuthenticatorAttestationResponse()
var attestation *protocol.ParsedAttestationResponse
if attestation, err = raw.Parse(); err != nil {
return fmt.Errorf("error verifying credential: error parsing attestation: %w", err)
}
if !bytes.Equal(c.PublicKey, attestation.AttestationObject.AuthData.AttData.CredentialPublicKey) {
return fmt.Errorf("error verifying credential: stored public key does not match the credential public key embedded in the attestation object")
}
clientDataHash := c.Attestation.ClientDataHash
if len(clientDataHash) == 0 {
sum := sha256.Sum256(c.Attestation.ClientDataJSON)
clientDataHash = sum[:]
}
if err = attestation.AttestationObject.VerifyAttestation(clientDataHash, mds); err != nil {
return fmt.Errorf("error verifying credential: error verifying attestation: %w", err)
}
if c.AttestationType == "" {
c.AttestationType = attestation.AttestationObject.Type
}
return nil
}
func (c *Credential) toAuthenticatorAttestationResponse() *protocol.AuthenticatorAttestationResponse {
raw := &protocol.AuthenticatorAttestationResponse{
AuthenticatorResponse: protocol.AuthenticatorResponse{
ClientDataJSON: c.Attestation.ClientDataJSON,
},
Transports: make([]string, len(c.Transport)),
AuthenticatorData: c.Attestation.AuthenticatorData,
PublicKey: c.PublicKey,
PublicKeyAlgorithm: c.Attestation.PublicKeyAlgorithm,
AttestationObject: c.Attestation.Object,
}
for i, transport := range c.Transport {
raw.Transports[i] = string(transport)
}
return raw
}
// Credentials is a decorator type which allows easily converting a [Credential] slice into a
// [protocol.CredentialDescriptor] slice by utilizing the [Credentials.CredentialDescriptors] method. This will be the
// type used globally for the library in a future release.
type Credentials []Credential
// CredentialDescriptors returns the [protocol.CredentialDescriptor] slice for this [Credentials] type.
func (c Credentials) CredentialDescriptors() (descriptors []protocol.CredentialDescriptor) {
descriptors = make([]protocol.CredentialDescriptor, len(c))
for i, credential := range c {
descriptors[i] = credential.Descriptor()
}
return descriptors
}
// NewCredentialFlags is a utility function that is used to derive the [Credential]'s Flags field given a
// [protocol.AuthenticatorFlags]. This allows implementers to solely save the Raw field of the [CredentialFlags] to
// restore them appropriately for appropriate processing without concern that changes forced upon implementers by the
// W3C will introduce breaking changes.
func NewCredentialFlags(flags protocol.AuthenticatorFlags) CredentialFlags {
return CredentialFlags{
UserPresent: flags.HasUserPresent(),
UserVerified: flags.HasUserVerified(),
BackupEligible: flags.HasBackupEligible(),
BackupState: flags.HasBackupState(),
raw: flags,
}
}
// CredentialFlagsFromMsgpByte reconstructs a [CredentialFlags] from the single-byte representation produced by
// [CredentialFlags.MsgpByte]. It is intended for use by the msgp-generated serialization layer; normal callers
// should prefer [NewCredentialFlags].
func CredentialFlagsFromMsgpByte(b byte) CredentialFlags {
return NewCredentialFlags(protocol.AuthenticatorFlags(b))
}
// CredentialFlags contains the boolean flags derived from the authenticator data during registration or login.
// These flags indicate the state of user presence, user verification, and backup eligibility/state at the time
// the credential was used.
type CredentialFlags struct {
// Flag UP indicates the users presence.
UserPresent bool `json:"userPresent"`
// Flag UV indicates the user performed verification.
UserVerified bool `json:"userVerified"`
// Flag BE indicates the credential is able to be backed up and/or sync'd between devices. This should NEVER change.
BackupEligible bool `json:"backupEligible"`
// Flag BS indicates the credential has been backed up and/or sync'd. This value can change but it's recommended
// that RP's keep track of this value.
BackupState bool `json:"backupState"`
raw protocol.AuthenticatorFlags
}
// ProtocolValue returns the underlying [protocol.AuthenticatorFlags] provided this [CredentialFlags] was created using
// NewCredentialFlags.
func (f CredentialFlags) ProtocolValue() protocol.AuthenticatorFlags {
return f.raw
}
// MsgpByte returns the [CredentialFlags] encoded as a single byte, equivalent to the raw
// [protocol.AuthenticatorFlags] value. It is intended for use by the msgp-generated serialization layer (see the
// //msgp:shim directive in this file); normal callers should prefer [CredentialFlags.ProtocolValue].
func (f CredentialFlags) MsgpByte() byte {
return byte(f.raw)
}
// CredentialAttestation holds the raw attestation data from a registration ceremony. These values are intentionally
// stored in their original unparsed form rather than as parsed structures. This is critical because:
//
// - It enables the [Credential] to be verified against the FIDO Metadata Service at a later date using
// [Credential.Verify], even long after the registration ceremony has completed.
// - The WebAuthn specification evolves over time, introducing new validation procedures. Preserving the raw data
// ensures that credentials created today can be re-validated against future rules without requiring re-registration.
// - Raw data serves as an auditable record of exactly what the authenticator and client provided during registration,
// independent of how the library parsed it at that point in time.
//
// Implementers MUST persist all fields of this struct.
type CredentialAttestation struct {
// ClientDataJSON is the raw JSON-encoded client data from the registration response. This is the verbatim value
// provided by the client and is used to recompute the client data hash during later verification.
ClientDataJSON []byte `json:"clientDataJSON,omitempty" msg:"cdj,omitempty"`
// ClientDataHash is the SHA-256 hash of ClientDataJSON computed during registration verification. If empty,
// [Credential.Verify] will recompute it from ClientDataJSON.
ClientDataHash []byte `json:"clientDataHash,omitempty" msg:"cdh,omitempty"`
// AuthenticatorData is the raw authenticator data from the registration response as provided in the
// RegistrationResponseJSON. This is the unparsed byte representation that can be re-parsed for future validation.
AuthenticatorData []byte `json:"authenticatorData,omitempty" msg:"data,omitempty"`
// PublicKeyAlgorithm is the COSE algorithm identifier for the credential's public key.
PublicKeyAlgorithm int64 `json:"publicKeyAlgorithm,omitempty" msg:"alg,omitempty"`
// Object is the raw CBOR-encoded attestation object from the registration response. This contains the attestation
// statement, format, and authenticator data needed by [Credential.Verify] to re-perform attestation verification.
Object []byte `json:"object,omitempty" msg:"obj,omitempty"`
}