Files
common/webauthn/authenticator.go

445 lines
19 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package webauthn
import (
"bytes"
"encoding/binary"
"fmt"
"github.com/sonr-io/common/webauthn/webauthncbor"
)
const (
minAuthDataLength = 37
minAttestedAuthLength = 55
maxCredentialIDLength = 1023
)
// AuthenticatorResponse represents the IDL with the same name.
//
// Authenticators respond to Relying Party requests by returning an object derived from the AuthenticatorResponse
// interface
//
// Specification: §5.2. Authenticator Responses (https://www.w3.org/TR/webauthn/#iface-authenticatorresponse)
type AuthenticatorResponse struct {
// From the spec https://www.w3.org/TR/webauthn/#dom-authenticatorresponse-clientdatajson
// This attribute contains a JSON serialization of the client data passed to the authenticator
// by the client in its call to either create() or get().
ClientDataJSON URLEncodedBase64 `json:"clientDataJSON"`
}
// AuthenticatorData represents the IDL with the same name.
//
// The authenticator data structure encodes contextual bindings made by the authenticator. These bindings are controlled
// by the authenticator itself, and derive their trust from the WebAuthn Relying Party's assessment of the security
// properties of the authenticator. In one extreme case, the authenticator may be embedded in the client, and its
// bindings may be no more trustworthy than the client data. At the other extreme, the authenticator may be a discrete
// entity with high-security hardware and software, connected to the client over a secure channel. In both cases, the
// Relying Party receives the authenticator data in the same format, and uses its knowledge of the authenticator to make
// trust decisions.
//
// The authenticator data has a compact but extensible encoding. This is desired since authenticators can be devices
// with limited capabilities and low power requirements, with much simpler software stacks than the client platform.
//
// Specification: §6.1. Authenticator Data (https://www.w3.org/TR/webauthn/#sctn-authenticator-data)
type AuthenticatorData struct {
RPIDHash []byte `json:"rpid"`
Flags AuthenticatorFlags `json:"flags"`
Counter uint32 `json:"sign_count"`
AttData AttestedCredentialData `json:"att_data"`
ExtData []byte `json:"ext_data"`
}
type AttestedCredentialData struct {
AAGUID []byte `json:"aaguid"`
CredentialID []byte `json:"credential_id"`
// The raw credential public key bytes received from the attestation data. This is the CBOR representation of the
// credentials public key.
CredentialPublicKey []byte `json:"public_key"`
}
// CredentialMediationRequirement represents mediation requirements for clients. When making a request via get(options)
// or create(options), developers can set a case-by-case requirement for user mediation by choosing the appropriate
// CredentialMediationRequirement enum value.
//
// See https://www.w3.org/TR/credential-management-1/#mediation-requirements
type CredentialMediationRequirement string
const (
// MediationDefault lets the browser choose the mediation flow completely as if it wasn't specified at all.
MediationDefault CredentialMediationRequirement = ""
// MediationSilent indicates user mediation is suppressed for the given operation. If the operation can be performed
// without user involvement, wonderful. If user involvement is necessary, then the operation will return null rather
// than involving the user.
MediationSilent CredentialMediationRequirement = "silent"
// MediationOptional indicates if credentials can be handed over for a given operation without user mediation, they
// will be. If user mediation is required, then the user agent will involve the user in the decision.
MediationOptional CredentialMediationRequirement = "optional"
// MediationConditional indicates for get(), discovered credentials are presented to the user in a non-modal dialog
// along with an indication of the origin which is requesting credentials. If the user makes a gesture outside of
// the dialog, the dialog closes without resolving or rejecting the Promise returned by the get() method and without
// causing a user-visible error condition. If the user makes a gesture that selects a credential, that credential is
// returned to the caller. The prevent silent access flag is treated as being true regardless of its actual value:
// the conditional behavior always involves user mediation of some sort if applicable credentials are discovered.
MediationConditional CredentialMediationRequirement = "conditional"
// MediationRequired indicates the user agent will not hand over credentials without user mediation, even if the
// prevent silent access flag is unset for an origin.
MediationRequired CredentialMediationRequirement = "required"
)
// AuthenticatorAttachment represents the IDL enum of the same name, and is used as part of the Authenticator Selection
// Criteria.
//
// This enumerations values describe authenticators' attachment modalities. Relying Parties use this to express a
// preferred authenticator attachment modality when calling navigator.credentials.create() to create a credential.
//
// If this member is present, eligible authenticators are filtered to only authenticators attached with the specified
// §5.4.5 Authenticator Attachment Enumeration (enum AuthenticatorAttachment). The value SHOULD be a member of
// AuthenticatorAttachment but client platforms MUST ignore unknown values, treating an unknown value as if the member
// does not exist.
//
// Specification: §5.4.4. Authenticator Selection Criteria (https://www.w3.org/TR/webauthn/#dom-authenticatorselectioncriteria-authenticatorattachment)
//
// Specification: §5.4.5. Authenticator Attachment Enumeration (https://www.w3.org/TR/webauthn/#enum-attachment)
type AuthenticatorAttachment string
const (
// Platform represents a platform authenticator is attached using a client device-specific transport, called
// platform attachment, and is usually not removable from the client device. A public key credential bound to a
// platform authenticator is called a platform credential.
Platform AuthenticatorAttachment = "platform"
// CrossPlatform represents a roaming authenticator is attached using cross-platform transports, called
// cross-platform attachment. Authenticators of this class are removable from, and can "roam" among, client devices.
// A public key credential bound to a roaming authenticator is called a roaming credential.
CrossPlatform AuthenticatorAttachment = "cross-platform"
)
// ResidentKeyRequirement represents the IDL of the same name.
//
// This enumerations values describe the Relying Party's requirements for client-side discoverable credentials
// (formerly known as resident credentials or resident keys).
//
// Specifies the extent to which the Relying Party desires to create a client-side discoverable credential. For
// historical reasons the naming retains the deprecated “resident” terminology. The value SHOULD be a member of
// ResidentKeyRequirement but client platforms MUST ignore unknown values, treating an unknown value as if the member
// does not exist. If no value is given then the effective value is required if requireResidentKey is true or
// discouraged if it is false or absent.
//
// Specification: §5.4.4. Authenticator Selection Criteria (https://www.w3.org/TR/webauthn/#dom-authenticatorselectioncriteria-residentkey)
//
// Specification: §5.4.6. Resident Key Requirement Enumeration (https://www.w3.org/TR/webauthn/#enumdef-residentkeyrequirement)
type ResidentKeyRequirement string
const (
// ResidentKeyRequirementDiscouraged indicates the Relying Party prefers creating a server-side credential, but will
// accept a client-side discoverable credential. This is the default.
ResidentKeyRequirementDiscouraged ResidentKeyRequirement = "discouraged"
// ResidentKeyRequirementPreferred indicates to the client we would prefer a discoverable credential.
ResidentKeyRequirementPreferred ResidentKeyRequirement = "preferred"
// ResidentKeyRequirementRequired indicates the Relying Party requires a client-side discoverable credential, and is
// prepared to receive an error if a client-side discoverable credential cannot be created.
ResidentKeyRequirementRequired ResidentKeyRequirement = "required"
)
// AuthenticatorTransport represents the IDL enum with the same name.
//
// Authenticators may implement various transports for communicating with clients. This enumeration defines hints as to
// how clients might communicate with a particular authenticator in order to obtain an assertion for a specific
// credential. Note that these hints represent the WebAuthn Relying Party's best belief as to how an authenticator may
// be reached. A Relying Party will typically learn of the supported transports for a public key credential via
// getTransports().
//
// Specification: §5.8.4. Authenticator Transport Enumeration (https://www.w3.org/TR/webauthn/#enumdef-authenticatortransport)
type AuthenticatorTransport string
const (
// USB indicates the respective authenticator can be contacted over removable USB.
USB AuthenticatorTransport = "usb"
// NFC indicates the respective authenticator can be contacted over Near Field Communication (NFC).
NFC AuthenticatorTransport = "nfc"
// BLE indicates the respective authenticator can be contacted over Bluetooth Smart (Bluetooth Low Energy / BLE).
BLE AuthenticatorTransport = "ble"
// SmartCard indicates the respective authenticator can be contacted over ISO/IEC 7816 smart card with contacts.
//
// WebAuthn Level 3.
SmartCard AuthenticatorTransport = "smart-card"
// Hybrid indicates the respective authenticator can be contacted using a combination of (often separate)
// data-transport and proximity mechanisms. This supports, for example, authentication on a desktop computer using
// a smartphone.
//
// WebAuthn Level 3.
Hybrid AuthenticatorTransport = "hybrid"
// Internal indicates the respective authenticator is contacted using a client device-specific transport, i.e., it
// is a platform authenticator. These authenticators are not removable from the client device.
Internal AuthenticatorTransport = "internal"
)
// UserVerificationRequirement is a representation of the UserVerificationRequirement IDL enum.
//
// A WebAuthn Relying Party may require user verification for some of its operations but not for others,
// and may use this type to express its needs.
//
// Specification: §5.8.6. User Verification Requirement Enumeration (https://www.w3.org/TR/webauthn/#enum-userVerificationRequirement)
type UserVerificationRequirement string
const (
// VerificationRequired User verification is required to create/release a credential
VerificationRequired UserVerificationRequirement = "required"
// VerificationPreferred User verification is preferred to create/release a credential
VerificationPreferred UserVerificationRequirement = "preferred" // This is the default
// VerificationDiscouraged The authenticator should not verify the user for the credential
VerificationDiscouraged UserVerificationRequirement = "discouraged"
)
// AuthenticatorFlags A byte of information returned during during ceremonies in the
// authenticatorData that contains bits that give us information about the
// whether the user was present and/or verified during authentication, and whether
// there is attestation or extension data present. Bit 0 is the least significant bit.
//
// Specification: §6.1. Authenticator Data - Flags (https://www.w3.org/TR/webauthn/#flags)
type AuthenticatorFlags byte
// The bits that do not have flags are reserved for future use.
const (
// FlagUserPresent Bit 00000001 in the byte sequence. Tells us if user is present. Also referred to as the UP flag.
FlagUserPresent AuthenticatorFlags = 1 << iota // Referred to as UP
// FlagRFU1 is a reserved for future use flag.
FlagRFU1
// FlagUserVerified Bit 00000100 in the byte sequence. Tells us if user is verified
// by the authenticator using a biometric or PIN. Also referred to as the UV flag.
FlagUserVerified
// FlagBackupEligible Bit 00001000 in the byte sequence. Tells us if a backup is eligible for device. Also referred
// to as the BE flag.
FlagBackupEligible // Referred to as BE
// FlagBackupState Bit 00010000 in the byte sequence. Tells us if a backup state for device. Also referred to as the
// BS flag.
FlagBackupState
// FlagRFU2 is a reserved for future use flag.
FlagRFU2
// FlagAttestedCredentialData Bit 01000000 in the byte sequence. Indicates whether
// the authenticator added attested credential data. Also referred to as the AT flag.
FlagAttestedCredentialData
// FlagHasExtensions Bit 10000000 in the byte sequence. Indicates if the authenticator data has extensions. Also
// referred to as the ED flag.
FlagHasExtensions
)
// UserPresent returns if the UP flag was set.
func (flag AuthenticatorFlags) UserPresent() bool {
return flag.HasUserPresent()
}
// UserVerified returns if the UV flag was set.
func (flag AuthenticatorFlags) UserVerified() bool {
return flag.HasUserVerified()
}
// HasUserPresent returns if the UP flag was set.
func (flag AuthenticatorFlags) HasUserPresent() bool {
return (flag & FlagUserPresent) == FlagUserPresent
}
// HasUserVerified returns if the UV flag was set.
func (flag AuthenticatorFlags) HasUserVerified() bool {
return (flag & FlagUserVerified) == FlagUserVerified
}
// HasAttestedCredentialData returns if the AT flag was set.
func (flag AuthenticatorFlags) HasAttestedCredentialData() bool {
return (flag & FlagAttestedCredentialData) == FlagAttestedCredentialData
}
// HasExtensions returns if the ED flag was set.
func (flag AuthenticatorFlags) HasExtensions() bool {
return (flag & FlagHasExtensions) == FlagHasExtensions
}
// HasBackupEligible returns if the BE flag was set.
func (flag AuthenticatorFlags) HasBackupEligible() bool {
return (flag & FlagBackupEligible) == FlagBackupEligible
}
// HasBackupState returns if the BS flag was set.
func (flag AuthenticatorFlags) HasBackupState() bool {
return (flag & FlagBackupState) == FlagBackupState
}
// Unmarshal will take the raw Authenticator Data and marshals it into AuthenticatorData for further validation.
// The authenticator data has a compact but extensible encoding. This is desired since authenticators can be
// devices with limited capabilities and low power requirements, with much simpler software stacks than the client platform.
// The authenticator data structure is a byte array of 37 bytes or more, and is laid out in this table:
// https://www.w3.org/TR/webauthn/#table-authData
func (a *AuthenticatorData) Unmarshal(rawAuthData []byte) (err error) {
if minAuthDataLength > len(rawAuthData) {
return ErrBadRequest.
WithDetails("Authenticator data length too short").
WithInfo(fmt.Sprintf("Expected data greater than %d bytes. Got %d bytes", minAuthDataLength, len(rawAuthData)))
}
a.RPIDHash = rawAuthData[:32]
a.Flags = AuthenticatorFlags(rawAuthData[32])
a.Counter = binary.BigEndian.Uint32(rawAuthData[33:37])
remaining := len(rawAuthData) - minAuthDataLength
if a.Flags.HasAttestedCredentialData() {
if len(rawAuthData) > minAttestedAuthLength {
if err = a.unmarshalAttestedData(rawAuthData); err != nil {
return err
}
attDataLen := len(
a.AttData.AAGUID,
) + 2 + len(
a.AttData.CredentialID,
) + len(
a.AttData.CredentialPublicKey,
)
remaining = remaining - attDataLen
} else {
return ErrBadRequest.WithDetails("Attested credential flag set but data is missing")
}
} else {
if !a.Flags.HasExtensions() && len(rawAuthData) != 37 {
return ErrBadRequest.WithDetails("Attested credential flag not set")
}
}
if a.Flags.HasExtensions() {
if remaining != 0 {
a.ExtData = rawAuthData[len(rawAuthData)-remaining:]
remaining -= len(a.ExtData)
} else {
return ErrBadRequest.WithDetails("Extensions flag set but extensions data is missing")
}
}
if remaining != 0 {
return ErrBadRequest.WithDetails("Leftover bytes decoding AuthenticatorData")
}
return nil
}
// If Attestation Data is present, unmarshall that into the appropriate public key structure.
func (a *AuthenticatorData) unmarshalAttestedData(rawAuthData []byte) (err error) {
a.AttData.AAGUID = rawAuthData[37:53]
idLength := binary.BigEndian.Uint16(rawAuthData[53:55])
if len(rawAuthData) < int(55+idLength) {
return ErrBadRequest.WithDetails("Authenticator attestation data length too short")
}
if idLength > maxCredentialIDLength {
return ErrBadRequest.WithDetails(
"Authenticator attestation data credential id length too long",
)
}
a.AttData.CredentialID = rawAuthData[55 : 55+idLength]
a.AttData.CredentialPublicKey, err = unmarshalCredentialPublicKey(rawAuthData[55+idLength:])
if err != nil {
return ErrBadRequest.WithDetails(fmt.Sprintf("Could not unmarshal Credential Public Key: %v", err)).
WithError(err)
}
return nil
}
// Unmarshall the credential's Public Key into CBOR encoding.
func unmarshalCredentialPublicKey(keyBytes []byte) (rawBytes []byte, err error) {
var m any
if err = webauthncbor.Unmarshal(keyBytes, &m); err != nil {
return nil, err
}
if rawBytes, err = webauthncbor.Marshal(m); err != nil {
return nil, err
}
return rawBytes, nil
}
// ResidentKeyRequired - Require that the key be private key resident to the client device.
func ResidentKeyRequired() *bool {
required := true
return &required
}
// ResidentKeyNotRequired - Do not require that the private key be resident to the client device.
func ResidentKeyNotRequired() *bool {
required := false
return &required
}
// Verify on AuthenticatorData handles Steps 13 through 15 & 17 for Registration
// and Steps 15 through 18 for Assertion.
func (a *AuthenticatorData) Verify(
rpIDHash []byte,
appIDHash []byte,
userVerificationRequired bool,
userPresenceRequired bool,
) (err error) {
// Registration Step 13 & Assertion Step 15
// Verify that the RP ID hash in authData is indeed the SHA-256
// hash of the RP ID expected by the RP.
if !bytes.Equal(a.RPIDHash, rpIDHash) && !bytes.Equal(a.RPIDHash, appIDHash) {
return ErrVerification.WithInfo(
fmt.Sprintf("RP Hash mismatch. Expected %x and Received %x", a.RPIDHash, rpIDHash),
)
}
// Registration Step 15 & Assertion Step 16
// Verify that the User Present bit of the flags in authData is set.
if userPresenceRequired && !a.Flags.UserPresent() {
return ErrVerification.WithInfo("User presence required but flag not set by authenticator")
}
// Registration Step 15 & Assertion Step 17
// If user verification is required for this assertion, verify that
// the User Verified bit of the flags in authData is set.
if userVerificationRequired && !a.Flags.UserVerified() {
return ErrVerification.WithInfo(
"User verification required but flag not set by authenticator",
)
}
// Registration Step 17 & Assertion Step 18
// Verify that the values of the client extension outputs in clientExtensionResults
// and the authenticator extension outputs in the extensions in authData are as
// expected, considering the client extension input values that were given as the
// extensions option in the create() call. In particular, any extension identifier
// values in the clientExtensionResults and the extensions in authData MUST be also be
// present as extension identifier values in the extensions member of options, i.e., no
// extensions are present that were not requested. In the general case, the meaning
// of "are as expected" is specific to the Relying Party and which extensions are in use.
// This is not yet fully implemented by the spec or by browsers.
return nil
}