331 lines
16 KiB
Go
Vendored
331 lines
16 KiB
Go
Vendored
package webauthn
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/go-webauthn/webauthn/metadata"
|
|
"github.com/go-webauthn/webauthn/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
|
|
// [github.com/go-webauthn/webauthn/webauthn] package documentation.
|
|
//
|
|
// See: §4. Terminology: Credential Record (https://www.w3.org/TR/webauthn-3/#credential-record)
|
|
//
|
|
// [Storage]: https://pkg.go.dev/github.com/go-webauthn/webauthn/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"`
|
|
}
|