auth: publish passkey foundations preview
verify / verify (push) Successful in 3m40s

This commit is contained in:
2026-08-21 17:33:00 -04:00
parent fb6bbd0dad
commit bfe6cfd29e
230 changed files with 44547 additions and 17 deletions
@@ -0,0 +1,67 @@
package webauthn
import (
"gamertan.com/web/internal/webauthnvendored/protocol"
)
//go:generate msgp
//msgp:replace protocol.AuthenticatorAttachment with:string
//msgp:clearomitted
// Authenticator represents a specific authenticator in the context of a [Credential].
type Authenticator struct {
// The AAGUID of the authenticator. An AAGUID is defined as an array containing the globally unique
// identifier of the authenticator model being sought.
AAGUID []byte `json:"AAGUID,omitempty" msg:"aaguid,omitempty"`
// SignCount is a representation of the number of times the Authenticator or Credential have been used to login.
// Upon a new login operation, the Relying Party compares the stored signature counter value with the new SignCount
// value returned in the assertions authenticator data. If this new SignCount value is less than or equal to the
// stored value, a cloned authenticator may exist, or the authenticator may be malfunctioning.
SignCount uint32 `json:"signCount,omitempty" msg:"sc,omitempty"`
// CloneWarning is a signal that the authenticator may be cloned, i.e. at least two copies of the
// credential private key may exist and are being used in parallel. Relying Parties should incorporate
// this information into their risk scoring. Whether the Relying Party updates the stored signature
// counter value in this case, or not, or fails the authentication ceremony or not, is Relying Party-specific.
CloneWarning bool `json:"cloneWarning,omitempty" msg:"cw,omitempty"`
// Attachment is the authenticatorAttachment value returned by the request.
Attachment protocol.AuthenticatorAttachment `json:"attachment,omitempty" msg:"aa,omitempty"`
}
// SelectAuthenticator is a convenience function that constructs a [protocol.AuthenticatorSelection] from individual
// string and boolean parameters. Use [protocol.ResidentKeyRequired] or [protocol.ResidentKeyNotRequired] for the rrk
// parameter.
func SelectAuthenticator(att string, rrk *bool, uv string) protocol.AuthenticatorSelection {
return protocol.AuthenticatorSelection{
AuthenticatorAttachment: protocol.AuthenticatorAttachment(att),
RequireResidentKey: rrk,
UserVerification: protocol.UserVerificationRequirement(uv),
}
}
// UpdateCounter updates the authenticator and either sets the clone warning value or the sign count.
//
// Step 17 of §7.2. about verifying attestation. If the signature counter value authData.signCount
// is nonzero or the value stored in conjunction with credentials id attribute is nonzero, then
// run the following sub-step:
//
// If the signature counter value authData.signCount is
//
// → Greater than the signature counter value stored in conjunction with credentials id attribute.
// Update the stored signature counter value, associated with credentials id attribute, to be the value of
// authData.signCount.
//
// → Less than or equal to the signature counter value stored in conjunction with credentials id attribute.
// This is a signal that the authenticator may be cloned, see CloneWarning above for more information.
func (a *Authenticator) UpdateCounter(authDataCount uint32) {
if authDataCount <= a.SignCount && (authDataCount != 0 || a.SignCount != 0) {
a.CloneWarning = true
return
}
a.SignCount = authDataCount
}
@@ -0,0 +1,305 @@
// Code generated by github.com/tinylib/msgp DO NOT EDIT.
package webauthn
import (
"gamertan.com/web/internal/webauthnvendored/protocol"
"github.com/tinylib/msgp/msgp"
)
// DecodeMsg implements msgp.Decodable
func (z *Authenticator) DecodeMsg(dc *msgp.Reader) (err error) {
var field []byte
_ = field
var zb0001 uint32
zb0001, err = dc.ReadMapHeader()
if err != nil {
err = msgp.WrapError(err)
return
}
var zb0001Mask uint8 /* 4 bits */
_ = zb0001Mask
for zb0001 > 0 {
zb0001--
field, err = dc.ReadMapKeyPtr()
if err != nil {
err = msgp.WrapError(err)
return
}
switch msgp.UnsafeString(field) {
case "aaguid":
z.AAGUID, err = dc.ReadBytes(z.AAGUID)
if err != nil {
err = msgp.WrapError(err, "AAGUID")
return
}
zb0001Mask |= 0x1
case "sc":
z.SignCount, err = dc.ReadUint32()
if err != nil {
err = msgp.WrapError(err, "SignCount")
return
}
zb0001Mask |= 0x2
case "cw":
z.CloneWarning, err = dc.ReadBool()
if err != nil {
err = msgp.WrapError(err, "CloneWarning")
return
}
zb0001Mask |= 0x4
case "aa":
{
var zb0002 string
zb0002, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "Attachment")
return
}
z.Attachment = protocol.AuthenticatorAttachment(zb0002)
}
zb0001Mask |= 0x8
default:
err = dc.Skip()
if err != nil {
err = msgp.WrapError(err)
return
}
}
}
// Clear omitted fields.
if zb0001Mask != 0xf {
if (zb0001Mask & 0x1) == 0 {
z.AAGUID = nil
}
if (zb0001Mask & 0x2) == 0 {
z.SignCount = 0
}
if (zb0001Mask & 0x4) == 0 {
z.CloneWarning = false
}
if (zb0001Mask & 0x8) == 0 {
z.Attachment = ""
}
}
return
}
// EncodeMsg implements msgp.Encodable
func (z *Authenticator) EncodeMsg(en *msgp.Writer) (err error) {
// check for omitted fields
zb0001Len := uint32(4)
var zb0001Mask uint8 /* 4 bits */
_ = zb0001Mask
if z.AAGUID == nil {
zb0001Len--
zb0001Mask |= 0x1
}
if z.SignCount == 0 {
zb0001Len--
zb0001Mask |= 0x2
}
if z.CloneWarning == false {
zb0001Len--
zb0001Mask |= 0x4
}
if z.Attachment == "" {
zb0001Len--
zb0001Mask |= 0x8
}
// variable map header, size zb0001Len
err = en.Append(0x80 | uint8(zb0001Len))
if err != nil {
return
}
// skip if no fields are to be emitted
if zb0001Len != 0 {
if (zb0001Mask & 0x1) == 0 { // if not omitted
// write "aaguid"
err = en.Append(0xa6, 0x61, 0x61, 0x67, 0x75, 0x69, 0x64)
if err != nil {
return
}
err = en.WriteBytes(z.AAGUID)
if err != nil {
err = msgp.WrapError(err, "AAGUID")
return
}
}
if (zb0001Mask & 0x2) == 0 { // if not omitted
// write "sc"
err = en.Append(0xa2, 0x73, 0x63)
if err != nil {
return
}
err = en.WriteUint32(z.SignCount)
if err != nil {
err = msgp.WrapError(err, "SignCount")
return
}
}
if (zb0001Mask & 0x4) == 0 { // if not omitted
// write "cw"
err = en.Append(0xa2, 0x63, 0x77)
if err != nil {
return
}
err = en.WriteBool(z.CloneWarning)
if err != nil {
err = msgp.WrapError(err, "CloneWarning")
return
}
}
if (zb0001Mask & 0x8) == 0 { // if not omitted
// write "aa"
err = en.Append(0xa2, 0x61, 0x61)
if err != nil {
return
}
err = en.WriteString(string(z.Attachment))
if err != nil {
err = msgp.WrapError(err, "Attachment")
return
}
}
}
return
}
// MarshalMsg implements msgp.Marshaler
func (z *Authenticator) MarshalMsg(b []byte) (o []byte, err error) {
o = msgp.Require(b, z.Msgsize())
// check for omitted fields
zb0001Len := uint32(4)
var zb0001Mask uint8 /* 4 bits */
_ = zb0001Mask
if z.AAGUID == nil {
zb0001Len--
zb0001Mask |= 0x1
}
if z.SignCount == 0 {
zb0001Len--
zb0001Mask |= 0x2
}
if z.CloneWarning == false {
zb0001Len--
zb0001Mask |= 0x4
}
if z.Attachment == "" {
zb0001Len--
zb0001Mask |= 0x8
}
// variable map header, size zb0001Len
o = append(o, 0x80|uint8(zb0001Len))
// skip if no fields are to be emitted
if zb0001Len != 0 {
if (zb0001Mask & 0x1) == 0 { // if not omitted
// string "aaguid"
o = append(o, 0xa6, 0x61, 0x61, 0x67, 0x75, 0x69, 0x64)
o = msgp.AppendBytes(o, z.AAGUID)
}
if (zb0001Mask & 0x2) == 0 { // if not omitted
// string "sc"
o = append(o, 0xa2, 0x73, 0x63)
o = msgp.AppendUint32(o, z.SignCount)
}
if (zb0001Mask & 0x4) == 0 { // if not omitted
// string "cw"
o = append(o, 0xa2, 0x63, 0x77)
o = msgp.AppendBool(o, z.CloneWarning)
}
if (zb0001Mask & 0x8) == 0 { // if not omitted
// string "aa"
o = append(o, 0xa2, 0x61, 0x61)
o = msgp.AppendString(o, string(z.Attachment))
}
}
return
}
// UnmarshalMsg implements msgp.Unmarshaler
func (z *Authenticator) UnmarshalMsg(bts []byte) (o []byte, err error) {
var field []byte
_ = field
var zb0001 uint32
zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
var zb0001Mask uint8 /* 4 bits */
_ = zb0001Mask
for zb0001 > 0 {
zb0001--
field, bts, err = msgp.ReadMapKeyZC(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
switch msgp.UnsafeString(field) {
case "aaguid":
z.AAGUID, bts, err = msgp.ReadBytesBytes(bts, z.AAGUID)
if err != nil {
err = msgp.WrapError(err, "AAGUID")
return
}
zb0001Mask |= 0x1
case "sc":
z.SignCount, bts, err = msgp.ReadUint32Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "SignCount")
return
}
zb0001Mask |= 0x2
case "cw":
z.CloneWarning, bts, err = msgp.ReadBoolBytes(bts)
if err != nil {
err = msgp.WrapError(err, "CloneWarning")
return
}
zb0001Mask |= 0x4
case "aa":
{
var zb0002 string
zb0002, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Attachment")
return
}
z.Attachment = protocol.AuthenticatorAttachment(zb0002)
}
zb0001Mask |= 0x8
default:
bts, err = msgp.Skip(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
}
}
// Clear omitted fields.
if zb0001Mask != 0xf {
if (zb0001Mask & 0x1) == 0 {
z.AAGUID = nil
}
if (zb0001Mask & 0x2) == 0 {
z.SignCount = 0
}
if (zb0001Mask & 0x4) == 0 {
z.CloneWarning = false
}
if (zb0001Mask & 0x8) == 0 {
z.Attachment = ""
}
}
o = bts
return
}
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
func (z *Authenticator) Msgsize() (s int) {
s = 1 + 7 + msgp.BytesPrefixSize + len(z.AAGUID) + 3 + msgp.Uint32Size + 3 + msgp.BoolSize + 3 + msgp.StringPrefixSize + len(string(z.Attachment))
return
}
@@ -0,0 +1,15 @@
package webauthn
import (
"time"
)
const (
errFmtFieldNotValidDomainString = "field '%s' is not a valid domain string: %w"
errFmtConfigValidate = "error occurred validating the configuration: %w"
)
const (
defaultTimeoutUVD = time.Millisecond * 120000
defaultTimeout = time.Millisecond * 300000
)
@@ -0,0 +1,330 @@
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"`
}
@@ -0,0 +1,914 @@
// Code generated by github.com/tinylib/msgp DO NOT EDIT.
package webauthn
import (
"gamertan.com/web/internal/webauthnvendored/protocol"
"github.com/tinylib/msgp/msgp"
)
// DecodeMsg implements msgp.Decodable
func (z *Credential) DecodeMsg(dc *msgp.Reader) (err error) {
var field []byte
_ = field
var zb0001 uint32
zb0001, err = dc.ReadMapHeader()
if err != nil {
err = msgp.WrapError(err)
return
}
var zb0001Mask uint8 /* 3 bits */
_ = zb0001Mask
for zb0001 > 0 {
zb0001--
field, err = dc.ReadMapKeyPtr()
if err != nil {
err = msgp.WrapError(err)
return
}
switch msgp.UnsafeString(field) {
case "id":
z.ID, err = dc.ReadBytes(z.ID)
if err != nil {
err = msgp.WrapError(err, "ID")
return
}
case "pk":
z.PublicKey, err = dc.ReadBytes(z.PublicKey)
if err != nil {
err = msgp.WrapError(err, "PublicKey")
return
}
case "atttype":
z.AttestationType, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "AttestationType")
return
}
zb0001Mask |= 0x1
case "attfmt":
z.AttestationFormat, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "AttestationFormat")
return
}
zb0001Mask |= 0x2
case "t":
var zb0002 uint32
zb0002, err = dc.ReadArrayHeader()
if err != nil {
err = msgp.WrapError(err, "Transport")
return
}
if cap(z.Transport) >= int(zb0002) {
z.Transport = (z.Transport)[:zb0002]
} else {
z.Transport = make([]protocol.AuthenticatorTransport, zb0002)
}
for za0001 := range z.Transport {
{
var zb0003 string
zb0003, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "Transport", za0001)
return
}
z.Transport[za0001] = protocol.AuthenticatorTransport(zb0003)
}
}
zb0001Mask |= 0x4
case "flg":
{
var zb0004 byte
zb0004, err = dc.ReadByte()
if err != nil {
err = msgp.WrapError(err, "Flags")
return
}
z.Flags = CredentialFlagsFromMsgpByte(zb0004)
}
case "a":
err = z.Authenticator.DecodeMsg(dc)
if err != nil {
err = msgp.WrapError(err, "Authenticator")
return
}
case "att":
err = z.Attestation.DecodeMsg(dc)
if err != nil {
err = msgp.WrapError(err, "Attestation")
return
}
default:
err = dc.Skip()
if err != nil {
err = msgp.WrapError(err)
return
}
}
}
// Clear omitted fields.
if zb0001Mask != 0x7 {
if (zb0001Mask & 0x1) == 0 {
z.AttestationType = ""
}
if (zb0001Mask & 0x2) == 0 {
z.AttestationFormat = ""
}
if (zb0001Mask & 0x4) == 0 {
z.Transport = nil
}
}
return
}
// EncodeMsg implements msgp.Encodable
func (z *Credential) EncodeMsg(en *msgp.Writer) (err error) {
// check for omitted fields
zb0001Len := uint32(8)
var zb0001Mask uint8 /* 8 bits */
_ = zb0001Mask
if z.AttestationType == "" {
zb0001Len--
zb0001Mask |= 0x4
}
if z.AttestationFormat == "" {
zb0001Len--
zb0001Mask |= 0x8
}
if z.Transport == nil {
zb0001Len--
zb0001Mask |= 0x10
}
// variable map header, size zb0001Len
err = en.Append(0x80 | uint8(zb0001Len))
if err != nil {
return
}
// skip if no fields are to be emitted
if zb0001Len != 0 {
// write "id"
err = en.Append(0xa2, 0x69, 0x64)
if err != nil {
return
}
err = en.WriteBytes(z.ID)
if err != nil {
err = msgp.WrapError(err, "ID")
return
}
// write "pk"
err = en.Append(0xa2, 0x70, 0x6b)
if err != nil {
return
}
err = en.WriteBytes(z.PublicKey)
if err != nil {
err = msgp.WrapError(err, "PublicKey")
return
}
if (zb0001Mask & 0x4) == 0 { // if not omitted
// write "atttype"
err = en.Append(0xa7, 0x61, 0x74, 0x74, 0x74, 0x79, 0x70, 0x65)
if err != nil {
return
}
err = en.WriteString(z.AttestationType)
if err != nil {
err = msgp.WrapError(err, "AttestationType")
return
}
}
if (zb0001Mask & 0x8) == 0 { // if not omitted
// write "attfmt"
err = en.Append(0xa6, 0x61, 0x74, 0x74, 0x66, 0x6d, 0x74)
if err != nil {
return
}
err = en.WriteString(z.AttestationFormat)
if err != nil {
err = msgp.WrapError(err, "AttestationFormat")
return
}
}
if (zb0001Mask & 0x10) == 0 { // if not omitted
// write "t"
err = en.Append(0xa1, 0x74)
if err != nil {
return
}
err = en.WriteArrayHeader(uint32(len(z.Transport)))
if err != nil {
err = msgp.WrapError(err, "Transport")
return
}
for za0001 := range z.Transport {
err = en.WriteString(string(z.Transport[za0001]))
if err != nil {
err = msgp.WrapError(err, "Transport", za0001)
return
}
}
}
// write "flg"
err = en.Append(0xa3, 0x66, 0x6c, 0x67)
if err != nil {
return
}
err = en.WriteByte((CredentialFlags).MsgpByte(z.Flags))
if err != nil {
err = msgp.WrapError(err, "Flags")
return
}
// write "a"
err = en.Append(0xa1, 0x61)
if err != nil {
return
}
err = z.Authenticator.EncodeMsg(en)
if err != nil {
err = msgp.WrapError(err, "Authenticator")
return
}
// write "att"
err = en.Append(0xa3, 0x61, 0x74, 0x74)
if err != nil {
return
}
err = z.Attestation.EncodeMsg(en)
if err != nil {
err = msgp.WrapError(err, "Attestation")
return
}
}
return
}
// MarshalMsg implements msgp.Marshaler
func (z *Credential) MarshalMsg(b []byte) (o []byte, err error) {
o = msgp.Require(b, z.Msgsize())
// check for omitted fields
zb0001Len := uint32(8)
var zb0001Mask uint8 /* 8 bits */
_ = zb0001Mask
if z.AttestationType == "" {
zb0001Len--
zb0001Mask |= 0x4
}
if z.AttestationFormat == "" {
zb0001Len--
zb0001Mask |= 0x8
}
if z.Transport == nil {
zb0001Len--
zb0001Mask |= 0x10
}
// variable map header, size zb0001Len
o = append(o, 0x80|uint8(zb0001Len))
// skip if no fields are to be emitted
if zb0001Len != 0 {
// string "id"
o = append(o, 0xa2, 0x69, 0x64)
o = msgp.AppendBytes(o, z.ID)
// string "pk"
o = append(o, 0xa2, 0x70, 0x6b)
o = msgp.AppendBytes(o, z.PublicKey)
if (zb0001Mask & 0x4) == 0 { // if not omitted
// string "atttype"
o = append(o, 0xa7, 0x61, 0x74, 0x74, 0x74, 0x79, 0x70, 0x65)
o = msgp.AppendString(o, z.AttestationType)
}
if (zb0001Mask & 0x8) == 0 { // if not omitted
// string "attfmt"
o = append(o, 0xa6, 0x61, 0x74, 0x74, 0x66, 0x6d, 0x74)
o = msgp.AppendString(o, z.AttestationFormat)
}
if (zb0001Mask & 0x10) == 0 { // if not omitted
// string "t"
o = append(o, 0xa1, 0x74)
o = msgp.AppendArrayHeader(o, uint32(len(z.Transport)))
for za0001 := range z.Transport {
o = msgp.AppendString(o, string(z.Transport[za0001]))
}
}
// string "flg"
o = append(o, 0xa3, 0x66, 0x6c, 0x67)
o = msgp.AppendByte(o, (CredentialFlags).MsgpByte(z.Flags))
// string "a"
o = append(o, 0xa1, 0x61)
o, err = z.Authenticator.MarshalMsg(o)
if err != nil {
err = msgp.WrapError(err, "Authenticator")
return
}
// string "att"
o = append(o, 0xa3, 0x61, 0x74, 0x74)
o, err = z.Attestation.MarshalMsg(o)
if err != nil {
err = msgp.WrapError(err, "Attestation")
return
}
}
return
}
// UnmarshalMsg implements msgp.Unmarshaler
func (z *Credential) UnmarshalMsg(bts []byte) (o []byte, err error) {
var field []byte
_ = field
var zb0001 uint32
zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
var zb0001Mask uint8 /* 3 bits */
_ = zb0001Mask
for zb0001 > 0 {
zb0001--
field, bts, err = msgp.ReadMapKeyZC(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
switch msgp.UnsafeString(field) {
case "id":
z.ID, bts, err = msgp.ReadBytesBytes(bts, z.ID)
if err != nil {
err = msgp.WrapError(err, "ID")
return
}
case "pk":
z.PublicKey, bts, err = msgp.ReadBytesBytes(bts, z.PublicKey)
if err != nil {
err = msgp.WrapError(err, "PublicKey")
return
}
case "atttype":
z.AttestationType, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "AttestationType")
return
}
zb0001Mask |= 0x1
case "attfmt":
z.AttestationFormat, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "AttestationFormat")
return
}
zb0001Mask |= 0x2
case "t":
var zb0002 uint32
zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Transport")
return
}
if cap(z.Transport) >= int(zb0002) {
z.Transport = (z.Transport)[:zb0002]
} else {
z.Transport = make([]protocol.AuthenticatorTransport, zb0002)
}
for za0001 := range z.Transport {
{
var zb0003 string
zb0003, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Transport", za0001)
return
}
z.Transport[za0001] = protocol.AuthenticatorTransport(zb0003)
}
}
zb0001Mask |= 0x4
case "flg":
{
var zb0004 byte
zb0004, bts, err = msgp.ReadByteBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Flags")
return
}
z.Flags = CredentialFlagsFromMsgpByte(zb0004)
}
case "a":
bts, err = z.Authenticator.UnmarshalMsg(bts)
if err != nil {
err = msgp.WrapError(err, "Authenticator")
return
}
case "att":
bts, err = z.Attestation.UnmarshalMsg(bts)
if err != nil {
err = msgp.WrapError(err, "Attestation")
return
}
default:
bts, err = msgp.Skip(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
}
}
// Clear omitted fields.
if zb0001Mask != 0x7 {
if (zb0001Mask & 0x1) == 0 {
z.AttestationType = ""
}
if (zb0001Mask & 0x2) == 0 {
z.AttestationFormat = ""
}
if (zb0001Mask & 0x4) == 0 {
z.Transport = nil
}
}
o = bts
return
}
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
func (z *Credential) Msgsize() (s int) {
s = 1 + 3 + msgp.BytesPrefixSize + len(z.ID) + 3 + msgp.BytesPrefixSize + len(z.PublicKey) + 8 + msgp.StringPrefixSize + len(z.AttestationType) + 7 + msgp.StringPrefixSize + len(z.AttestationFormat) + 2 + msgp.ArrayHeaderSize
for za0001 := range z.Transport {
s += msgp.StringPrefixSize + len(string(z.Transport[za0001]))
}
s += 4 + msgp.ByteSize + 2 + z.Authenticator.Msgsize() + 4 + z.Attestation.Msgsize()
return
}
// DecodeMsg implements msgp.Decodable
func (z *CredentialAttestation) DecodeMsg(dc *msgp.Reader) (err error) {
var field []byte
_ = field
var zb0001 uint32
zb0001, err = dc.ReadMapHeader()
if err != nil {
err = msgp.WrapError(err)
return
}
var zb0001Mask uint8 /* 5 bits */
_ = zb0001Mask
for zb0001 > 0 {
zb0001--
field, err = dc.ReadMapKeyPtr()
if err != nil {
err = msgp.WrapError(err)
return
}
switch msgp.UnsafeString(field) {
case "cdj":
z.ClientDataJSON, err = dc.ReadBytes(z.ClientDataJSON)
if err != nil {
err = msgp.WrapError(err, "ClientDataJSON")
return
}
zb0001Mask |= 0x1
case "cdh":
z.ClientDataHash, err = dc.ReadBytes(z.ClientDataHash)
if err != nil {
err = msgp.WrapError(err, "ClientDataHash")
return
}
zb0001Mask |= 0x2
case "data":
z.AuthenticatorData, err = dc.ReadBytes(z.AuthenticatorData)
if err != nil {
err = msgp.WrapError(err, "AuthenticatorData")
return
}
zb0001Mask |= 0x4
case "alg":
z.PublicKeyAlgorithm, err = dc.ReadInt64()
if err != nil {
err = msgp.WrapError(err, "PublicKeyAlgorithm")
return
}
zb0001Mask |= 0x8
case "obj":
z.Object, err = dc.ReadBytes(z.Object)
if err != nil {
err = msgp.WrapError(err, "Object")
return
}
zb0001Mask |= 0x10
default:
err = dc.Skip()
if err != nil {
err = msgp.WrapError(err)
return
}
}
}
// Clear omitted fields.
if zb0001Mask != 0x1f {
if (zb0001Mask & 0x1) == 0 {
z.ClientDataJSON = nil
}
if (zb0001Mask & 0x2) == 0 {
z.ClientDataHash = nil
}
if (zb0001Mask & 0x4) == 0 {
z.AuthenticatorData = nil
}
if (zb0001Mask & 0x8) == 0 {
z.PublicKeyAlgorithm = 0
}
if (zb0001Mask & 0x10) == 0 {
z.Object = nil
}
}
return
}
// EncodeMsg implements msgp.Encodable
func (z *CredentialAttestation) EncodeMsg(en *msgp.Writer) (err error) {
// check for omitted fields
zb0001Len := uint32(5)
var zb0001Mask uint8 /* 5 bits */
_ = zb0001Mask
if z.ClientDataJSON == nil {
zb0001Len--
zb0001Mask |= 0x1
}
if z.ClientDataHash == nil {
zb0001Len--
zb0001Mask |= 0x2
}
if z.AuthenticatorData == nil {
zb0001Len--
zb0001Mask |= 0x4
}
if z.PublicKeyAlgorithm == 0 {
zb0001Len--
zb0001Mask |= 0x8
}
if z.Object == nil {
zb0001Len--
zb0001Mask |= 0x10
}
// variable map header, size zb0001Len
err = en.Append(0x80 | uint8(zb0001Len))
if err != nil {
return
}
// skip if no fields are to be emitted
if zb0001Len != 0 {
if (zb0001Mask & 0x1) == 0 { // if not omitted
// write "cdj"
err = en.Append(0xa3, 0x63, 0x64, 0x6a)
if err != nil {
return
}
err = en.WriteBytes(z.ClientDataJSON)
if err != nil {
err = msgp.WrapError(err, "ClientDataJSON")
return
}
}
if (zb0001Mask & 0x2) == 0 { // if not omitted
// write "cdh"
err = en.Append(0xa3, 0x63, 0x64, 0x68)
if err != nil {
return
}
err = en.WriteBytes(z.ClientDataHash)
if err != nil {
err = msgp.WrapError(err, "ClientDataHash")
return
}
}
if (zb0001Mask & 0x4) == 0 { // if not omitted
// write "data"
err = en.Append(0xa4, 0x64, 0x61, 0x74, 0x61)
if err != nil {
return
}
err = en.WriteBytes(z.AuthenticatorData)
if err != nil {
err = msgp.WrapError(err, "AuthenticatorData")
return
}
}
if (zb0001Mask & 0x8) == 0 { // if not omitted
// write "alg"
err = en.Append(0xa3, 0x61, 0x6c, 0x67)
if err != nil {
return
}
err = en.WriteInt64(z.PublicKeyAlgorithm)
if err != nil {
err = msgp.WrapError(err, "PublicKeyAlgorithm")
return
}
}
if (zb0001Mask & 0x10) == 0 { // if not omitted
// write "obj"
err = en.Append(0xa3, 0x6f, 0x62, 0x6a)
if err != nil {
return
}
err = en.WriteBytes(z.Object)
if err != nil {
err = msgp.WrapError(err, "Object")
return
}
}
}
return
}
// MarshalMsg implements msgp.Marshaler
func (z *CredentialAttestation) MarshalMsg(b []byte) (o []byte, err error) {
o = msgp.Require(b, z.Msgsize())
// check for omitted fields
zb0001Len := uint32(5)
var zb0001Mask uint8 /* 5 bits */
_ = zb0001Mask
if z.ClientDataJSON == nil {
zb0001Len--
zb0001Mask |= 0x1
}
if z.ClientDataHash == nil {
zb0001Len--
zb0001Mask |= 0x2
}
if z.AuthenticatorData == nil {
zb0001Len--
zb0001Mask |= 0x4
}
if z.PublicKeyAlgorithm == 0 {
zb0001Len--
zb0001Mask |= 0x8
}
if z.Object == nil {
zb0001Len--
zb0001Mask |= 0x10
}
// variable map header, size zb0001Len
o = append(o, 0x80|uint8(zb0001Len))
// skip if no fields are to be emitted
if zb0001Len != 0 {
if (zb0001Mask & 0x1) == 0 { // if not omitted
// string "cdj"
o = append(o, 0xa3, 0x63, 0x64, 0x6a)
o = msgp.AppendBytes(o, z.ClientDataJSON)
}
if (zb0001Mask & 0x2) == 0 { // if not omitted
// string "cdh"
o = append(o, 0xa3, 0x63, 0x64, 0x68)
o = msgp.AppendBytes(o, z.ClientDataHash)
}
if (zb0001Mask & 0x4) == 0 { // if not omitted
// string "data"
o = append(o, 0xa4, 0x64, 0x61, 0x74, 0x61)
o = msgp.AppendBytes(o, z.AuthenticatorData)
}
if (zb0001Mask & 0x8) == 0 { // if not omitted
// string "alg"
o = append(o, 0xa3, 0x61, 0x6c, 0x67)
o = msgp.AppendInt64(o, z.PublicKeyAlgorithm)
}
if (zb0001Mask & 0x10) == 0 { // if not omitted
// string "obj"
o = append(o, 0xa3, 0x6f, 0x62, 0x6a)
o = msgp.AppendBytes(o, z.Object)
}
}
return
}
// UnmarshalMsg implements msgp.Unmarshaler
func (z *CredentialAttestation) UnmarshalMsg(bts []byte) (o []byte, err error) {
var field []byte
_ = field
var zb0001 uint32
zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
var zb0001Mask uint8 /* 5 bits */
_ = zb0001Mask
for zb0001 > 0 {
zb0001--
field, bts, err = msgp.ReadMapKeyZC(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
switch msgp.UnsafeString(field) {
case "cdj":
z.ClientDataJSON, bts, err = msgp.ReadBytesBytes(bts, z.ClientDataJSON)
if err != nil {
err = msgp.WrapError(err, "ClientDataJSON")
return
}
zb0001Mask |= 0x1
case "cdh":
z.ClientDataHash, bts, err = msgp.ReadBytesBytes(bts, z.ClientDataHash)
if err != nil {
err = msgp.WrapError(err, "ClientDataHash")
return
}
zb0001Mask |= 0x2
case "data":
z.AuthenticatorData, bts, err = msgp.ReadBytesBytes(bts, z.AuthenticatorData)
if err != nil {
err = msgp.WrapError(err, "AuthenticatorData")
return
}
zb0001Mask |= 0x4
case "alg":
z.PublicKeyAlgorithm, bts, err = msgp.ReadInt64Bytes(bts)
if err != nil {
err = msgp.WrapError(err, "PublicKeyAlgorithm")
return
}
zb0001Mask |= 0x8
case "obj":
z.Object, bts, err = msgp.ReadBytesBytes(bts, z.Object)
if err != nil {
err = msgp.WrapError(err, "Object")
return
}
zb0001Mask |= 0x10
default:
bts, err = msgp.Skip(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
}
}
// Clear omitted fields.
if zb0001Mask != 0x1f {
if (zb0001Mask & 0x1) == 0 {
z.ClientDataJSON = nil
}
if (zb0001Mask & 0x2) == 0 {
z.ClientDataHash = nil
}
if (zb0001Mask & 0x4) == 0 {
z.AuthenticatorData = nil
}
if (zb0001Mask & 0x8) == 0 {
z.PublicKeyAlgorithm = 0
}
if (zb0001Mask & 0x10) == 0 {
z.Object = nil
}
}
o = bts
return
}
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
func (z *CredentialAttestation) Msgsize() (s int) {
s = 1 + 4 + msgp.BytesPrefixSize + len(z.ClientDataJSON) + 4 + msgp.BytesPrefixSize + len(z.ClientDataHash) + 5 + msgp.BytesPrefixSize + len(z.AuthenticatorData) + 4 + msgp.Int64Size + 4 + msgp.BytesPrefixSize + len(z.Object)
return
}
// DecodeMsg implements msgp.Decodable
func (z *CredentialFlags) DecodeMsg(dc *msgp.Reader) (err error) {
{
var zb0001 byte
zb0001, err = dc.ReadByte()
if err != nil {
err = msgp.WrapError(err)
return
}
(*z) = CredentialFlagsFromMsgpByte(zb0001)
}
return
}
// EncodeMsg implements msgp.Encodable
func (z CredentialFlags) EncodeMsg(en *msgp.Writer) (err error) {
err = en.WriteByte((CredentialFlags).MsgpByte(z))
if err != nil {
err = msgp.WrapError(err)
return
}
return
}
// MarshalMsg implements msgp.Marshaler
func (z CredentialFlags) MarshalMsg(b []byte) (o []byte, err error) {
o = msgp.Require(b, z.Msgsize())
o = msgp.AppendByte(o, (CredentialFlags).MsgpByte(z))
return
}
// UnmarshalMsg implements msgp.Unmarshaler
func (z *CredentialFlags) UnmarshalMsg(bts []byte) (o []byte, err error) {
{
var zb0001 byte
zb0001, bts, err = msgp.ReadByteBytes(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
(*z) = CredentialFlagsFromMsgpByte(zb0001)
}
o = bts
return
}
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
func (z CredentialFlags) Msgsize() (s int) {
s = msgp.ByteSize
return
}
// DecodeMsg implements msgp.Decodable
func (z *Credentials) DecodeMsg(dc *msgp.Reader) (err error) {
var zb0002 uint32
zb0002, err = dc.ReadArrayHeader()
if err != nil {
err = msgp.WrapError(err)
return
}
if cap((*z)) >= int(zb0002) {
(*z) = (*z)[:zb0002]
} else {
(*z) = make(Credentials, zb0002)
}
for zb0001 := range *z {
err = (*z)[zb0001].DecodeMsg(dc)
if err != nil {
err = msgp.WrapError(err, zb0001)
return
}
}
return
}
// EncodeMsg implements msgp.Encodable
func (z Credentials) EncodeMsg(en *msgp.Writer) (err error) {
err = en.WriteArrayHeader(uint32(len(z)))
if err != nil {
err = msgp.WrapError(err)
return
}
for zb0003 := range z {
err = z[zb0003].EncodeMsg(en)
if err != nil {
err = msgp.WrapError(err, zb0003)
return
}
}
return
}
// MarshalMsg implements msgp.Marshaler
func (z Credentials) MarshalMsg(b []byte) (o []byte, err error) {
o = msgp.Require(b, z.Msgsize())
o = msgp.AppendArrayHeader(o, uint32(len(z)))
for zb0003 := range z {
o, err = z[zb0003].MarshalMsg(o)
if err != nil {
err = msgp.WrapError(err, zb0003)
return
}
}
return
}
// UnmarshalMsg implements msgp.Unmarshaler
func (z *Credentials) UnmarshalMsg(bts []byte) (o []byte, err error) {
var zb0002 uint32
zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
if cap((*z)) >= int(zb0002) {
(*z) = (*z)[:zb0002]
} else {
(*z) = make(Credentials, zb0002)
}
for zb0001 := range *z {
bts, err = (*z)[zb0001].UnmarshalMsg(bts)
if err != nil {
err = msgp.WrapError(err, zb0001)
return
}
}
o = bts
return
}
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
func (z Credentials) Msgsize() (s int) {
s = msgp.ArrayHeaderSize
for zb0003 := range z {
s += z[zb0003].Msgsize()
}
return
}
+185
View File
@@ -0,0 +1,185 @@
// Package webauthn contains the API functionality of the library. After creating and configuring a webauthn object,
// users can call the object to create and validate web authentication credentials.
//
// This documentation section highlights key functions within the library which are recommended and often have
// examples attached. Functions which are discouraged due to their lack of functionality are expressly not documented
// here, and you're on your own with these functions. Generally speaking, if the function is not documented here, it is
// either used by another function documented here, and it hides one of the arguments or return values, or it is lower
// level logic only intended for advanced use cases.
//
// The [New] function is a key function in creating a new instance of a WebAuthn Relying Party which is required to
// perform most actions.
//
// To start the credential creation ceremony, the [WebAuthn.BeginMediatedRegistration] or [WebAuthn.BeginRegistration]
// functions are used which returns [*SessionData] and a [*protocol.CredentialCreation] struct which can be easily
// serialized as JSON for the frontend library/logic. The [*SessionData] must be saved in a way which allows the
// implementer to restore it later. This [*SessionData] should be safely anchored to a user agent without allowing the
// user agent to modify the contents (i.e. opaque session cookie).
//
// To finish the credential creation ceremony, the [WebAuthn.FinishRegistration] function can be used. This function
// requires a [*http.Request] and performs all the necessary and requested validations. If you have other requirements,
// you can use [protocol.ParseCredentialCreationResponseBody] or [protocol.ParseCredentialCreationResponseBytes] which
// require an [io.Reader] or byte array respectively, then use [WebAuthn.CreateCredential] to
// perform validations against the [*protocol.ParsedCredentialCreationData] and saved [*SessionData] and finalize the
// process. For complete customizability, just produce the [*protocol.ParsedCredentialCreationData] with a custom parser
// and provide it to [WebAuthn.CreateCredential].
//
// To start a Passkey login ceremony, the [WebAuthn.BeginDiscoverableMediatedLogin] or [WebAuthn.BeginDiscoverableLogin]
// functions are used which returns [*SessionData] and a [*protocol.CredentialAssertion] struct which can easily be
// serialized as JSON for the frontend library/logic. The [*SessionData] should be safely handled as previously described.
//
// To finish a Passkey login ceremony, the [WebAuthn.FinishPasskeyLogin] function can be used. This function requires a
// [*http.Request] and performs all the necessary validations. If you have other requirements, you can use the
// [protocol.ParseCredentialRequestResponseBody] or [protocol.ParseCredentialRequestResponseBytes] which require an
// [io.Reader] or byte array respectively, then use [WebAuthn.ValidatePasskeyLogin] to perform validations against the
// [*protocol.ParsedCredentialAssertionData] and saved [*SessionData] and finalize the process. For complete customizabilty,
// just produce the [protocol.ParsedCredentialAssertionData] with a custom parser and provide it to
// [WebAuthn.ValidatePasskeyLogin].
//
// To start a Multi-Factor login ceremony, the [WebAuthn.BeginMediatedLogin] or [WebAuthn.BeginLogin]
// functions are used which returns [SessionData] and a [*protocol.CredentialAssertion] struct which can easily be
// serialized as JSON for the frontend library/logic. The [*SessionData] should be safely handled as previously described.
//
// To finish a Multi-Factor login ceremony, the [WebAuthn.FinishLogin] function can be used. This function requires a
// [*http.Request] and performs all the necessary validations. If you have other requirements, you can use the
// [protocol.ParseCredentialRequestResponseBody] or [protocol.ParseCredentialRequestResponseBytes] which require an
// [io.Reader] or byte array respectively, then use [WebAuthn.ValidateLogin] to perform validations against the
// [*protocol.ParsedCredentialAssertionData] and saved [*SessionData] and finalize the process. For complete
// customizabilty, just produce the [protocol.ParsedCredentialAssertionData] with a custom parser and provide it to
// [WebAuthn.ValidateLogin].
//
// # Relying Party Usage
//
// This library hadnles the relying party server-side concerns. The browser or other user agent is responsible for
// handling the JSON responses from this library and translating them for the WebAUthn API appropriately. There are two
// primary ways to handle this other than doing so manually:
//
// 1. Using a client side library like [@simplewebauthn/browser].
// 2. Some browsers support the [parseCreationOptionsFromJSON] static method on the WebAuthn object.
//
// [parseCreationOptionsFromJSON]: https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential/parseCreationOptionsFromJSON_static
// [@simplewebauthn/browser]: https://simplewebauthn.dev/docs/packages/browser
//
// # Storage
//
// This section describes how a Relying Party should persist the state produced by the library: the [Credential]
// records returned from registration (which must survive for the lifetime of the credential) and the
// [SessionData] records exchanged between the Begin and Finish/Validate calls of each ceremony (which need only
// live long enough to span the ceremony).
//
// Guidance here assumes PostgreSQL as the backing store; the same shape translates to other SQL engines but the
// column types given below are written against PostgreSQL.
//
// Two persistence shapes are supported for the [Credential] struct and the first is strongly recommended:
//
// 1. Explicit fields (recommended). Map each field of the struct (and for [Credential] the nested
// [Authenticator] and [CredentialAttestation] fields) to its own column, using native types (BYTEA for raw
// bytes, BOOLEAN for each flag, TIMESTAMPTZ for time values, etc.). This gives the database a typed,
// queryable view of each record, allows per-field constraints and indexes, and lets an operator audit or
// migrate individual values without having to decode an opaque blob.
//
// 2. Opaque serialized value. Serialize the whole struct into a single BYTEA (or JSONB) column. Both
// encoding/json and MessagePack are supported via the struct tags on every field (the `msg:` tags drive the
// msgp-generated code in *_gen.go, and the `json:` tags drive encoding/json); either encoding will
// round-trip a [Credential]. Prefer this only when the explicit-field approach genuinely
// does not fit; you lose the ability to query, index, or update individual fields in the database.
//
// One persistence shape is supported for the [SessionData] struct which is to store it as bytes via encoding/json or
// using MessagePack as bytes in whatever storage system you're using for user sessions. This data MUST be definitively
// anchored to a user's active session, and it must be restored between the ceremony steps.
//
// Regardless of which shape is chosen, the following values MUST be persisted as their own columns so records
// can be located and scoped correctly without first decoding attestation or key material. The User Handle in
// particular is per-user state (one value shared by every credential that user owns) and MUST NOT be stored on
// the credential row; store it once per user on a separate table (`webauthn_users` in the example below) and
// link credentials to that row via the application user identifier.
//
// On each [Credential] row:
//
// - Credential ID; [Credential.ID], the identifier returned by the authenticator and echoed in every
// assertion. This is the primary lookup key at login.
// - Relying Party ID; the RP ID the credential was registered against. Credentials must be partitioned by
// RP ID and the stored value must match the RP ID in effect at authentication time.
// - Application user identifier; your application's own unique user id (the primary key used elsewhere in
// your schema to reference the user). This is what ties a credential back to the user record and,
// transitively via `webauthn_users`, to the User Handle.
//
// On a separate per-user row (`webauthn_users` or equivalent), keyed uniquely by (RP ID, application user id)
// and also uniquely by (RP ID, User Handle):
//
// - Relying Party ID; same scoping rules as above; a user may have distinct User Handles under different
// RP IDs, so the RP ID must be part of both unique keys on this table.
// - Application user identifier; the same value stored on each of that user's credential rows; this is
// the join column between `webauthn_users` and `webauthn_credentials`.
// - User Handle; the opaque per-user byte sequence returned by [User.WebAuthnID], equivalently
// [SessionData.UserID]. This is the value exchanged with the authenticator and is what
// discoverable-credential flows return at login. It MUST be stable for the lifetime of the account and
// MUST be the same across every credential that user owns; storing it once, per user, is what enforces
// that. It is NOT the same as the application user identifier: the User Handle is an opaque WebAuthn
// value emitted to authenticators, whereas the application user identifier is your schema's primary
// key for the user. Keeping the two as separate columns lets you resolve from either direction.
//
// A minimal PostgreSQL schema covering the above plus the remaining [Credential], [Authenticator], and
// [CredentialAttestation] fields is shown below.
//
// Example users table:
//
// CREATE TABLE webauthn_users (
// id UUID PRIMARY KEY DEFAULT uuidv7(),
// created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
// rpid VARCHAR(512) NOT NULL, -- Relying Party ID
// user_id UUID NOT NULL, -- Application-side unique user id (FK to your users table)
// handle BYTEA NOT NULL -- User.WebAuthnID (WebAuthn User Handle); stable per (rpid, user_id)
// );
//
// CREATE UNIQUE INDEX webauthn_users_user_id_key ON webauthn_users (rpid, user_id);
// CREATE UNIQUE INDEX webauthn_users_handle_key ON webauthn_users (rpid, handle);
//
// Example credentials table:
//
// CREATE TABLE webauthn_credentials (
// id UUID PRIMARY KEY DEFAULT uuidv7(),
// created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
// last_used_at TIMESTAMPTZ NULL,
// rpid VARCHAR(512) NOT NULL, -- Relying Party ID
// user_id UUID NOT NULL, -- Application-side unique user id
// kid BYTEA NOT NULL, -- Credential.ID
// aaguid BYTEA NULL, -- Authenticator.AAGUID
// public_key BYTEA NOT NULL, -- Credential.PublicKey (encrypt at rest)
// attestation_type VARCHAR(32) NOT NULL, -- CredentialAttestation.AttestationType
// attestation_format VARCHAR(32) NOT NULL, -- CredentialAttestation.AttestationFormat
// attestation BYTEA NULL DEFAULT NULL, -- CredentialAttestation serialized as Message Pack or JSON (encrypt at rest)
// transport VARCHAR(64) NOT NULL DEFAULT '', -- Credential.Transport serialized as a comma-separated value
// sign_count BIGINT NOT NULL DEFAULT 0, -- Authenticator.SignCount
// clone_warning BOOLEAN NOT NULL DEFAULT FALSE, -- Authenticator.CloneWarning
// attachment VARCHAR(64) NOT NULL DEFAULT '', -- Authenticator.Attachment
// flags BYTEA NOT NULL, -- Value of Flags.ProtocolValue (a single octet), restored with NewCredentialFlags, could also be SMALLINT
// present BOOLEAN NOT NULL DEFAULT FALSE, -- Flags.UserPresent, optionally stored so you can either display it to the user or for filtering credentials
// verified BOOLEAN NOT NULL DEFAULT FALSE, -- Flags.UserVerified, optionally stored so you can either display it to the user or for filtering credentials
// backup_eligible BOOLEAN NOT NULL DEFAULT FALSE, -- Flags.BackupEligible, optionally stored so you can either display it to the user or for filtering credentials
// backup_state BOOLEAN NOT NULL DEFAULT FALSE -- Flags.BackupState, optionally stored so you can either display it to the user or for filtering credentials
// );
//
// CREATE UNIQUE INDEX webauthn_credentials_kid_key ON webauthn_credentials (rpid, kid);
// CREATE INDEX webauthn_credentials_user_id ON webauthn_credentials (rpid, user_id);
//
// With that shape, the two login lookup paths resolve as:
//
// - Credential-ID-first (allowCredentials flows): match `webauthn_credentials.kid` to the credential ID
// returned by the authenticator, then optionally join `webauthn_users` on (rpid, user_id) to compare the
// authenticator-supplied User Handle against the stored one.
// - User-Handle-first (discoverable / passkey flows): match `webauthn_users.handle` under the current
// RP ID to resolve the application user id, then load that user's credentials from
// `webauthn_credentials`.
//
// Fields that change across assertions; [Authenticator.SignCount], [Authenticator.CloneWarning], and
// [CredentialFlags.BackupState] when [CredentialFlags.BackupEligible] is true MUST be written back to storage
// on every successful FinishLogin / ValidateLogin so the next ceremony observes the current values.
//
// For [SessionData] stored in a database (rather than a server-side session store), use the same persistence
// shapes described above. The User Handle on a [SessionData] row is per-session ceremony state rather than
// per-user state, so it is fine to keep [SessionData.UserID] on the session row itself; the per-user-table
// rule applies to [Credential] storage, not to [SessionData]. Additionally index the challenge (unique) and
// the expiry timestamp so sessions can be looked up by challenge at Finish time and expired rows reaped
// cheaply. Stored sessions must only be consumed by a Finish call operating under the same RP ID.
package webauthn
+387
View File
@@ -0,0 +1,387 @@
package webauthn
import (
"bytes"
"context"
"fmt"
"net/http"
"time"
"github.com/google/uuid"
"gamertan.com/web/internal/webauthnvendored/protocol"
)
// LoginOption is a functional option that modifies the [protocol.PublicKeyCredentialRequestOptions] sent to the
// client during a login ceremony. Use the With* functions in this package (i.e. [WithUserVerification],
// [WithAllowedCredentials]) to create login options.
type LoginOption func(*protocol.PublicKeyCredentialRequestOptions)
// DiscoverableUserHandler is a callback function that the Relying Party must provide when performing a discoverable
// (passkey) login. It is called with the rawID of the credential and the userHandle from the authenticator response,
// and must return the [User] who owns the credential. This is necessary because in discoverable login flows, the
// Relying Party does not know which user is authenticating until the authenticator response is received.
type DiscoverableUserHandler func(rawID, userHandle []byte) (user User, err error)
// BeginLogin creates the [*protocol.CredentialAssertion] data payload that should be sent to the user agent for beginning
// the login/assertion process. This function is used to perform a login when the identity of the user is known such as
// multifactor authentications, to specify a conditional mediation requirement use [WebAuthn.BeginMediatedLogin], to
// perform a login when the identity of the user is not known see [WebAuthn.BeginDiscoverableLogin] and
// [WebAuthn.BeginDiscoverableMediatedLogin] instead. The format of this data can be seen in §5.5 of the WebAuthn
// specification. These default values can be amended by providing additional [LoginOption] parameters. This function
// also returns [SessionData], that must be stored by the RP in a secure manner and then provided to the
// [WebAuthn.FinishLogin] function. This data helps us verify the ownership of the credential being retrieved.
//
// Specification: §5.5. Options for Assertion Generation (https://www.w3.org/TR/webauthn/#dictionary-assertion-options)
func (webauthn *WebAuthn) BeginLogin(user User, opts ...LoginOption) (*protocol.CredentialAssertion, *SessionData, error) {
return webauthn.BeginMediatedLogin(user, protocol.MediationDefault, opts...)
}
// BeginDiscoverableLogin creates the [*protocol.CredentialAssertion] data payload that should be sent to the user agent
// for beginning the login/assertion process. This function is used to perform a client-side discoverable login when the
// identity of the user is not known such as passwordless or usernameless authentication, to specify a conditional
// mediation requirement use [WebAuthn.BeginDiscoverableMediatedLogin], to perform logins where the identity of the user
// is known such as multifactor authentication see [WebAuthn.BeginLogin] and [WebAuthn.BeginMediatedLogin] instead.
// The format of this data can be seen in §5.5 of the WebAuthn specification. These default values can be amended by
// providing additional [LoginOption] parameters. This function also returns [SessionData], that
// must be stored by the RP in a secure manner and then provided to the [WebAuthn.FinishLogin] function. This data helps
// us verify the ownership of the credential being retrieved.
//
// Specification: §5.5. Options for Assertion Generation (https://www.w3.org/TR/webauthn/#dictionary-assertion-options)
func (webauthn *WebAuthn) BeginDiscoverableLogin(opts ...LoginOption) (*protocol.CredentialAssertion, *SessionData, error) {
return webauthn.beginLogin(nil, nil, protocol.MediationDefault, opts...)
}
// BeginMediatedLogin is similar to [WebAuthn.BeginLogin] however it also allows specifying a credential mediation
// requirement.
func (webauthn *WebAuthn) BeginMediatedLogin(user User, mediation protocol.CredentialMediationRequirement, opts ...LoginOption) (*protocol.CredentialAssertion, *SessionData, error) {
credentials := user.WebAuthnCredentials()
if len(credentials) == 0 { // If the user does not have any credentials, we cannot perform an assertion.
return nil, nil, protocol.ErrBadRequest.WithDetails("Found no credentials for user")
}
var allowedCredentials = make([]protocol.CredentialDescriptor, len(credentials))
for i, credential := range credentials {
allowedCredentials[i] = credential.Descriptor()
}
return webauthn.beginLogin(user.WebAuthnID(), allowedCredentials, mediation, opts...)
}
// BeginDiscoverableMediatedLogin is similar to [WebAuthn.BeginDiscoverableLogin] however it also allows specifying a
// credential mediation requirement.
func (webauthn *WebAuthn) BeginDiscoverableMediatedLogin(mediation protocol.CredentialMediationRequirement, opts ...LoginOption) (*protocol.CredentialAssertion, *SessionData, error) {
return webauthn.beginLogin(nil, nil, mediation, opts...)
}
func (webauthn *WebAuthn) beginLogin(userID []byte, allowedCredentials []protocol.CredentialDescriptor, mediation protocol.CredentialMediationRequirement, opts ...LoginOption) (assertion *protocol.CredentialAssertion, session *SessionData, err error) {
if err = webauthn.Config.validate(); err != nil {
return nil, nil, fmt.Errorf(errFmtConfigValidate, err)
}
assertion = &protocol.CredentialAssertion{
Response: protocol.PublicKeyCredentialRequestOptions{
RelyingPartyID: webauthn.Config.RPID,
UserVerification: webauthn.Config.AuthenticatorSelection.UserVerification,
AllowedCredentials: allowedCredentials,
},
Mediation: mediation,
}
for _, opt := range opts {
opt(&assertion.Response)
}
if len(assertion.Response.Challenge) == 0 {
var challenge protocol.URLEncodedBase64
if challenge, err = protocol.CreateChallenge(); err != nil {
return nil, nil, err
}
assertion.Response.Challenge = challenge
}
if len(assertion.Response.Challenge) < protocol.MinimumChallengeLength {
return nil, nil, fmt.Errorf("error generating assertion: the challenge must be at least 16 bytes")
}
if len(assertion.Response.RelyingPartyID) == 0 {
return nil, nil, fmt.Errorf("error generating assertion: the relying party id must be provided via the configuration or a functional option for a login")
} else if err = protocol.ValidateRPID(assertion.Response.RelyingPartyID); err != nil {
return nil, nil, fmt.Errorf("error generating assertion: the relying party id failed to validate as it's not a valid domain string with error: %w", err)
}
if assertion.Response.Timeout == 0 {
switch assertion.Response.UserVerification {
case protocol.VerificationDiscouraged:
assertion.Response.Timeout = int(webauthn.Config.Timeouts.Login.TimeoutUVD.Milliseconds())
default:
assertion.Response.Timeout = int(webauthn.Config.Timeouts.Login.Timeout.Milliseconds())
}
}
session = &SessionData{
Challenge: assertion.Response.Challenge.String(),
RelyingPartyID: assertion.Response.RelyingPartyID,
UserID: userID,
AllowedCredentialIDs: assertion.Response.GetAllowedCredentialIDs(),
UserVerification: assertion.Response.UserVerification,
Extensions: assertion.Response.Extensions,
}
if webauthn.Config.Timeouts.Login.Enforce {
session.Expires = time.Now().Add(time.Millisecond * time.Duration(assertion.Response.Timeout))
}
return assertion, session, nil
}
// FinishLogin takes the response from the client and validates it against the user credentials and stored session data.
//
// As with all Finish functions, this function requires a [*http.Request] but you can perform the same steps with the
// [protocol.ParseCredentialRequestResponseBody] or [protocol.ParseCredentialRequestResponseBytes] which require an
// [io.Reader] or byte array respectively, you can also use an arbitrary [*protocol.ParsedCredentialAssertionData] which is
// returned from all of these functions i.e. by implementing a custom parser. The [*SessionData],
// and [*protocol.ParsedCredentialAssertionData] can then be used with the [WebAuthn.ValidateLogin] function.
//
// This function will return the [protocol.ErrorUnknownCredential] error type when the [User] provided does not contain
// a [Credential] with the same ID byte array provided all [Credential]'s in the [SessionData] exist in the [User]'s
// [Credential] list.
func (webauthn *WebAuthn) FinishLogin(user User, session SessionData, response *http.Request) (credential *Credential, err error) {
var parsedResponse *protocol.ParsedCredentialAssertionData
if parsedResponse, err = protocol.ParseCredentialRequestResponse(response); err != nil {
return nil, err
}
return webauthn.ValidateLogin(user, session, parsedResponse)
}
// FinishDiscoverableLogin takes the response from the client and validates it against the handler and stored session data.
// The handler helps to find out which user must be used to validate the response. This is a function defined in your
// business code that will retrieve the user from your persistent data.
//
// As with all Finish functions, this function requires a [*http.Request] but you can perform the same steps with the
// [protocol.ParseCredentialRequestResponseBody] or [protocol.ParseCredentialRequestResponseBytes] which require an
// [io.Reader] or byte array respectively, you can also use an arbitrary [*protocol.ParsedCredentialAssertionData] which is
// returned from all of these functions i.e. by implementing a custom parser. The [DiscoverableUserHandler], [*SessionData],
// and [*protocol.ParsedCredentialAssertionData] can then be used with the [WebAuthn.ValidatePasskeyLogin] function.
//
// This function will return the [protocol.ErrorUnknownCredential] error type when the [User] returned by the
// handler does not contain a [Credential] with the same ID byte array provided all [Credential]'s
// in the [SessionData] exist in the [User]'s [Credential] list.
func (webauthn *WebAuthn) FinishDiscoverableLogin(handler DiscoverableUserHandler, session SessionData, response *http.Request) (credential *Credential, err error) {
var parsedResponse *protocol.ParsedCredentialAssertionData
if parsedResponse, err = protocol.ParseCredentialRequestResponse(response); err != nil {
return nil, err
}
return webauthn.ValidateDiscoverableLogin(handler, session, parsedResponse)
}
// FinishPasskeyLogin takes the response from the client and validate it against the handler and stored session data.
// The handler helps to find out which user must be used to validate the response. This is a function defined in your
// business code that will retrieve the user from your persistent data.
//
// As with all Finish functions this function requires a [*http.Request] but you can perform the same steps with the
// [protocol.ParseCredentialRequestResponseBody] or [protocol.ParseCredentialRequestResponseBytes] which require an
// io.Reader or byte array respectively, you can also use an arbitrary [*protocol.ParsedCredentialAssertionData] which is
// returned from all of these functions i.e. by implementing a custom parser. The [DiscoverableUserHandler], [*SessionData],
// and [*protocol.ParsedCredentialAssertionData] can then be used with the [WebAuthn.ValidatePasskeyLogin] function.
//
// This function will return the [protocol.ErrorUnknownCredential] error type when the [User] returned by the
// handler does not contain a [Credential] with the same ID byte array provided all [Credential]'s
// in the [SessionData] exist in the [User]'s [Credential] list.
func (webauthn *WebAuthn) FinishPasskeyLogin(handler DiscoverableUserHandler, session SessionData, response *http.Request) (user User, credential *Credential, err error) {
var parsedResponse *protocol.ParsedCredentialAssertionData
if parsedResponse, err = protocol.ParseCredentialRequestResponse(response); err != nil {
return nil, nil, err
}
return webauthn.ValidatePasskeyLogin(handler, session, parsedResponse)
}
// ValidateLogin takes a parsed response and validates it against the user credentials and session data.
//
// If you wish to skip performing the step required to parse the *protocol.ParsedCredentialAssertionData and
// you're using net/http then you can use [WebAuthn.FinishLogin] instead.
//
// This function will return the [protocol.ErrorUnknownCredential] error type when the [User] provided does not contain
// a [Credential] with the same ID byte array provided all [Credential]'s in the [SessionData] exist in
// the [User]'s [Credential] list.
func (webauthn *WebAuthn) ValidateLogin(user User, session SessionData, parsedResponse *protocol.ParsedCredentialAssertionData) (credential *Credential, err error) {
if !bytes.Equal(user.WebAuthnID(), session.UserID) {
return nil, protocol.ErrBadRequest.WithDetails("ID mismatch for User and Session")
}
if !session.Expires.IsZero() && session.Expires.Before(time.Now()) {
return nil, protocol.ErrBadRequest.WithDetails("Session has Expired")
}
return webauthn.validateLogin(user, session, parsedResponse)
}
// ValidateDiscoverableLogin is similar to [WebAuthn.ValidateLogin] that allows for discoverable credentials. It's
// recommended that [WebAuthn.ValidatePasskeyLogin] is used instead.
//
// If you wish to skip performing the step required to parse the [*protocol.ParsedCredentialAssertionData] and
// you're using net/http then you can use [WebAuthn.FinishDiscoverableLogin] instead.
//
// This function will return the [protocol.ErrorUnknownCredential] error type when the [User] returned by the
// handler does not contain a [Credential] with the same ID byte array provided all [Credential]'s
// in the [SessionData] exist in the [User]'s [Credential] list.
//
// Note: this is just a backwards compatibility layer over [WebAuthn.ValidatePasskeyLogin] which returns more information.
func (webauthn *WebAuthn) ValidateDiscoverableLogin(handler DiscoverableUserHandler, session SessionData, parsedResponse *protocol.ParsedCredentialAssertionData) (credential *Credential, err error) {
_, credential, err = webauthn.ValidatePasskeyLogin(handler, session, parsedResponse)
return credential, err
}
// ValidatePasskeyLogin is similar to [WebAuthn.ValidateLogin] that allows for discoverable credentials.
//
// If you wish to skip performing the step required to parse the [*protocol.ParsedCredentialAssertionData] and
// you're using net/http then you can use [WebAuthn.FinishPasskeyLogin] instead.
//
// This function will return the [protocol.ErrorUnknownCredential] error type when the [User] returned by the
// handler does not contain a [Credential] with the same ID byte array provided all [Credential]'s
// in the [SessionData] exist in the [User]'s [Credential] list.
func (webauthn *WebAuthn) ValidatePasskeyLogin(handler DiscoverableUserHandler, session SessionData, parsedResponse *protocol.ParsedCredentialAssertionData) (user User, credential *Credential, err error) {
if len(session.UserID) != 0 {
return nil, nil, protocol.ErrBadRequest.WithDetails("Session was not initiated as a client-side discoverable login")
}
if !session.Expires.IsZero() && session.Expires.Before(time.Now()) {
return nil, nil, protocol.ErrBadRequest.WithDetails("Session has Expired")
}
if len(parsedResponse.Response.UserHandle) == 0 {
return nil, nil, protocol.ErrBadRequest.WithDetails("Client-side Discoverable Assertion was attempted with a blank User Handle")
}
if user, err = handler(parsedResponse.RawID, parsedResponse.Response.UserHandle); err != nil {
return nil, nil, protocol.ErrBadRequest.WithDetails(fmt.Sprintf("Failed to lookup Client-side Discoverable Credential: %s", err)).WithError(err)
}
if user == nil {
return nil, nil, protocol.ErrBadRequest.WithDetails("Failed to lookup Client-side Discoverable Credential: handler returned a nil user")
}
if credential, err = webauthn.validateLogin(user, session, parsedResponse); err != nil {
return nil, nil, err
}
return user, credential, nil
}
// validateLogin takes a parsed response and validates it against the user credentials and session data.
//
//nolint:gocyclo
func (webauthn *WebAuthn) validateLogin(user User, session SessionData, parsedResponse *protocol.ParsedCredentialAssertionData) (*Credential, error) {
// Step 1. If the allowCredentials option was given when this authentication ceremony was initiated,
// verify that credential.id identifies one of the public key credentials that were listed in
// allowCredentials.
// NON-NORMATIVE Prior Step: Verify that the allowCredentials for the session are owned by the user provided.
credentials := user.WebAuthnCredentials()
if len(session.AllowedCredentialIDs) > 0 {
if !isCredentialsAllowedMatchingOwned(session.AllowedCredentialIDs, credentials) {
return nil, protocol.ErrBadRequest.WithDetails("User does not own all credentials from the allowed credential list")
}
if !isCredentialIDInCredentials(parsedResponse.RawID, credentials) {
return nil, &protocol.ErrorUnknownCredential{Err: protocol.ErrBadRequest.WithDetails("The credential ID provided is not owned by the user")}
}
if !isByteArrayInSlice(parsedResponse.RawID, session.AllowedCredentialIDs...) {
return nil, protocol.ErrBadRequest.WithDetails("The credential ID provided is not in the sessions allowed credential list")
}
}
// Step 2. If credential.response.userHandle is present, verify that the user identified by this value is
// the owner of the public key credential identified by credential.id. This is in part handled by our Step 1.
userHandle := parsedResponse.Response.UserHandle
if len(userHandle) > 0 {
if !bytes.Equal(userHandle, user.WebAuthnID()) {
return nil, protocol.ErrBadRequest.WithDetails("User handle and User ID do not match")
}
}
var (
found bool
credential Credential
)
// Step 3. Using credentials id attribute (or the corresponding rawId, if base64url encoding is inappropriate
// for your use case), look up the corresponding credential public key.
for _, credential = range credentials {
if bytes.Equal(credential.ID, parsedResponse.RawID) {
found = true
break
}
}
if !found {
return nil, protocol.ErrBadRequest.WithDetails("Unable to find the credential for the returned credential ID")
}
var (
appID string
err error
)
// Ensure authenticators with a bad status are not used.
if webauthn.Config.MDS != nil {
var aaguid uuid.UUID
if len(credential.Authenticator.AAGUID) == 0 {
aaguid = uuid.Nil
} else if aaguid, err = uuid.FromBytes(credential.Authenticator.AAGUID); err != nil {
return nil, protocol.ErrBadRequest.WithDetails("Failed to decode AAGUID").WithInfo(fmt.Sprintf("Error occurred decoding AAGUID from the credential record: %s", err)).WithError(err)
}
if e := protocol.ValidateMetadata(context.Background(), webauthn.Config.MDS, aaguid, credential.AttestationType, credential.AttestationFormat, nil); e != nil {
return nil, protocol.ErrBadRequest.WithDetails("Failed to validate credential record metadata").WithInfo(e.DevInfo).WithError(e)
}
}
shouldVerifyUser := session.UserVerification == protocol.VerificationRequired
shouldVerifyUserPresence := true
rpID := webauthn.Config.RPID
rpOrigins := webauthn.Config.RPOrigins
rpTopOrigins := webauthn.Config.RPTopOrigins
if appID, err = parsedResponse.GetAppID(session.Extensions, credential.AttestationFormat); err != nil {
return nil, err
}
// Handle steps 4 through 16.
if err = parsedResponse.Verify(session.Challenge, rpID, appID, rpOrigins, rpTopOrigins, webauthn.Config.RPTopOriginVerificationMode, webauthn.Config.RPAllowCrossOrigin, shouldVerifyUser, shouldVerifyUserPresence, credential.PublicKey); err != nil {
return nil, err
}
// Check if the BackupEligible flag has changed.
if credential.Flags.BackupEligible != parsedResponse.Response.AuthenticatorData.Flags.HasBackupEligible() {
return nil, protocol.ErrBadRequest.WithDetails("Backup Eligible flag inconsistency detected during login validation")
}
// Check for the invalid combination BE=0 and BS=1.
if !parsedResponse.Response.AuthenticatorData.Flags.HasBackupEligible() && parsedResponse.Response.AuthenticatorData.Flags.HasBackupState() {
return nil, protocol.ErrBadRequest.WithDetails("Backup State Flag is true but Backup Eligible flag is false which is invalid")
}
// Handle step 17.
credential.Authenticator.UpdateCounter(parsedResponse.Response.AuthenticatorData.Counter)
// Update flags from response data.
credential.Flags = NewCredentialFlags(parsedResponse.Response.AuthenticatorData.Flags)
return &credential, nil
}
@@ -0,0 +1,101 @@
package webauthn
import "gamertan.com/web/internal/webauthnvendored/protocol"
// WithChallenge overrides the random challenge that [WebAuthn.BeginLogin] would otherwise generate for this
// ceremony. The supplied value is used verbatim.
//
// The only safe reason to call this is when the relying party needs to record the challenge in a server-side store
// before the ceremony is initiated; for example to maintain a set of previously-issued challenges so it can
// reject a replay that reuses one. Generating the challenge inside a separate step lets the RP persist it
// atomically before it is ever handed to the client.
//
// If you have that need, the supplied challenge MUST be produced by [protocol.CreateChallenge] (32 bytes from
// crypto/rand). Do not use timestamps, counters, UUIDs, hashed user inputs, or any other deterministic or
// partially-predictable source; these defeat the cryptographic guarantees the challenge provides and open the
// ceremony to replay and guessing attacks. [WebAuthn.BeginLogin] enforces a minimum length of
// [protocol.MinimumChallengeLength] bytes, but that check is a backstop only and is not a substitute for using a
// CSPRNG.
//
// If you do not have a specific persistence requirement, do not use this function; let the library generate the
// challenge automatically.
//
// Specification: §5.5. Options for Assertion Generation (https://www.w3.org/TR/webauthn/#dom-publickeycredentialrequestoptions-challenge)
//
// Specification: §13.4.3. Cryptographic Challenges (https://www.w3.org/TR/webauthn/#sctn-cryptographic-challenges)
func WithChallenge(challenge []byte) LoginOption {
return func(cco *protocol.PublicKeyCredentialRequestOptions) {
cco.Challenge = challenge
}
}
// WithLoginRelyingPartyID sets the Relying Party ID for this particular login.
//
// Specification: §5.5. Options for Assertion Generation (https://www.w3.org/TR/webauthn/#dom-publickeycredentialrequestoptions-rpid)
func WithLoginRelyingPartyID(id string) LoginOption {
return func(cco *protocol.PublicKeyCredentialRequestOptions) {
cco.RelyingPartyID = id
}
}
// WithAllowedCredentials adjusts the allowed credentials via a slice of [protocol.CredentialDescriptor] values,
// discussed in the included specification sections with user-supplied values.
//
// Specification: §5.5. Options for Assertion Generation (https://www.w3.org/TR/webauthn/#dom-publickeycredentialrequestoptions-allowcredentials)
//
// Specification: §5.10.3. Credential Descriptor (https://www.w3.org/TR/webauthn/#dictdef-publickeycredentialdescriptor)
func WithAllowedCredentials(allowList []protocol.CredentialDescriptor) LoginOption {
return func(cco *protocol.PublicKeyCredentialRequestOptions) {
cco.AllowedCredentials = allowList
}
}
// WithUserVerification adjusts the user verification preference by providing a [protocol.UserVerificationRequirement].
//
// Specification: §5.5. Options for Assertion Generation (https://www.w3.org/TR/webauthn/#dom-publickeycredentialrequestoptions-userverification)
func WithUserVerification(userVerification protocol.UserVerificationRequirement) LoginOption {
return func(cco *protocol.PublicKeyCredentialRequestOptions) {
cco.UserVerification = userVerification
}
}
// WithAssertionPublicKeyCredentialHints adjusts the non-default hints for credential types to select during login by
// providing a slice of [protocol.PublicKeyCredentialHints].
//
// WebAuthn Level 3.
//
// Specification: §5.5. Options for Assertion Generation (https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptions-hints)
func WithAssertionPublicKeyCredentialHints(hints []protocol.PublicKeyCredentialHints) LoginOption {
return func(cco *protocol.PublicKeyCredentialRequestOptions) {
cco.Hints = hints
}
}
// WithAssertionExtensions adjusts the requested extensions by providing a [protocol.AuthenticationExtensions].
//
// Specification: §5.5. Options for Assertion Generation (https://www.w3.org/TR/webauthn/#dom-publickeycredentialrequestoptions-extensions)
func WithAssertionExtensions(extensions protocol.AuthenticationExtensions) LoginOption {
return func(cco *protocol.PublicKeyCredentialRequestOptions) {
cco.Extensions = extensions
}
}
// WithAppIdExtension automatically includes the specified appid if the AllowedCredentials contains a credential
// with the type `fido-u2f`.
//
// Specification: §5.5. Options for Assertion Generation (https://www.w3.org/TR/webauthn/#dom-publickeycredentialrequestoptions-extensions)
func WithAppIdExtension(appid string) LoginOption {
return func(cco *protocol.PublicKeyCredentialRequestOptions) {
for _, credential := range cco.AllowedCredentials {
if credential.AttestationFormat == string(protocol.AttestationFormatFIDOUniversalSecondFactor) {
if cco.Extensions == nil {
cco.Extensions = map[string]any{}
}
cco.Extensions[protocol.ExtensionAppID] = appid
break
}
}
}
}
@@ -0,0 +1,230 @@
package webauthn
import (
"bytes"
"fmt"
"net/http"
"time"
"github.com/google/uuid"
"gamertan.com/web/internal/webauthnvendored/protocol"
)
// RegistrationOption is a functional option that modifies the [protocol.PublicKeyCredentialCreationOptions] sent
// to the client during a registration ceremony. Use the With* functions in this package (i.e.
// [WithConveyancePreference], [WithExclusions], [WithAuthenticatorSelection]) to create registration options.
type RegistrationOption func(*protocol.PublicKeyCredentialCreationOptions)
// BeginRegistration generates a new set of registration data to be sent to the client and authenticator. To set a
// conditional mediation requirement for the registration see [WebAuthn.BeginMediatedRegistration].
func (webauthn *WebAuthn) BeginRegistration(user User, opts ...RegistrationOption) (creation *protocol.CredentialCreation, session *SessionData, err error) {
return webauthn.BeginMediatedRegistration(user, protocol.MediationDefault, opts...)
}
// BeginMediatedRegistration is similar to [WebAuthn.BeginRegistration] however it also allows specifying a credential
// mediation requirement.
func (webauthn *WebAuthn) BeginMediatedRegistration(user User, mediation protocol.CredentialMediationRequirement, opts ...RegistrationOption) (creation *protocol.CredentialCreation, session *SessionData, err error) {
if err = webauthn.Config.validate(); err != nil {
return nil, nil, fmt.Errorf(errFmtConfigValidate, err)
}
var (
challenge protocol.URLEncodedBase64
entityUserID any
)
if challenge, err = protocol.CreateChallenge(); err != nil {
return nil, nil, err
}
if webauthn.Config.EncodeUserIDAsString {
entityUserID = string(user.WebAuthnID())
} else {
entityUserID = protocol.URLEncodedBase64(user.WebAuthnID())
}
entityUser := protocol.UserEntity{
ID: entityUserID,
DisplayName: user.WebAuthnDisplayName(),
CredentialEntity: protocol.CredentialEntity{
Name: user.WebAuthnName(),
},
}
entityRelyingParty := protocol.RelyingPartyEntity{
ID: webauthn.Config.RPID,
CredentialEntity: protocol.CredentialEntity{
Name: webauthn.Config.RPDisplayName,
},
}
credentialParams := CredentialParametersDefault()
creation = &protocol.CredentialCreation{
Response: protocol.PublicKeyCredentialCreationOptions{
RelyingParty: entityRelyingParty,
User: entityUser,
Challenge: challenge,
Parameters: credentialParams,
AuthenticatorSelection: webauthn.Config.AuthenticatorSelection,
Attestation: webauthn.Config.AttestationPreference,
},
Mediation: mediation,
}
for _, opt := range opts {
opt(&creation.Response)
}
if len(creation.Response.RelyingParty.ID) == 0 {
return nil, nil, fmt.Errorf("error generating credential creation: the relying party id must be provided via the configuration or a functional option for a creation")
} else if err = protocol.ValidateRPID(creation.Response.RelyingParty.ID); err != nil {
return nil, nil, fmt.Errorf("error generating credential creation: the relying party id failed to validate as it's not a valid domain string with error: %w", err)
}
if len(creation.Response.RelyingParty.Name) == 0 {
return nil, nil, fmt.Errorf("error generating credential creation: the relying party display name must be provided via the configuration or a functional option for a creation")
}
if len(creation.Response.Challenge) < protocol.MinimumChallengeLength {
return nil, nil, fmt.Errorf("error generating credential creation: the challenge must be at least 16 bytes")
}
if creation.Response.Timeout == 0 {
switch creation.Response.AuthenticatorSelection.UserVerification {
case protocol.VerificationDiscouraged:
creation.Response.Timeout = int(webauthn.Config.Timeouts.Registration.TimeoutUVD.Milliseconds())
default:
creation.Response.Timeout = int(webauthn.Config.Timeouts.Registration.Timeout.Milliseconds())
}
}
session = &SessionData{
Challenge: creation.Response.Challenge.String(),
RelyingPartyID: creation.Response.RelyingParty.ID,
UserID: user.WebAuthnID(),
UserVerification: creation.Response.AuthenticatorSelection.UserVerification,
CredParams: creation.Response.Parameters,
Mediation: creation.Mediation,
}
if webauthn.Config.Timeouts.Registration.Enforce {
session.Expires = time.Now().Add(time.Millisecond * time.Duration(creation.Response.Timeout))
}
return creation, session, nil
}
// FinishRegistration takes the response from the authenticator and client and verify the credential against the user's
// credentials and session data.
//
// As with all Finish functions this function requires a [*http.Request] but you can perform the same steps with the
// [protocol.ParseCredentialCreationResponseBody] or [protocol.ParseCredentialCreationResponseBytes] which require an
// [io.Reader] or byte array respectively, you can also use an arbitrary [*protocol.ParsedCredentialCreationData] which is
// returned from all of these functions i.e. by implementing a custom parser. The [User], [*SessionData], and
// [*protocol.ParsedCredentialCreationData] can then be used with the [WebAuthn.CreateCredential] function.
func (webauthn *WebAuthn) FinishRegistration(user User, session SessionData, request *http.Request) (credential *Credential, err error) {
parsedResponse, err := protocol.ParseCredentialCreationResponse(request)
if err != nil {
return nil, err
}
return webauthn.CreateCredential(user, session, parsedResponse)
}
// CreateCredential verifies a parsed response against the user's credentials and session data.
//
// If you wish to skip performing the step required to parse the [*protocol.ParsedCredentialCreationData] and
// you're using net/http then you can use [WebAuthn.FinishRegistration] instead.
func (webauthn *WebAuthn) CreateCredential(user User, session SessionData, parsedResponse *protocol.ParsedCredentialCreationData) (credential *Credential, err error) {
if !bytes.Equal(user.WebAuthnID(), session.UserID) {
return nil, protocol.ErrBadRequest.WithDetails("ID mismatch for User and Session")
}
if !session.Expires.IsZero() && session.Expires.Before(time.Now()) {
return nil, protocol.ErrBadRequest.WithDetails("Session has Expired")
}
shouldVerifyUser := session.UserVerification == protocol.VerificationRequired
shouldVerifyUserPresence := session.Mediation != protocol.MediationConditional
var clientDataHash []byte
if clientDataHash, err = parsedResponse.Verify(session.Challenge, webauthn.Config.RPID, webauthn.Config.RPOrigins, webauthn.Config.RPTopOrigins, webauthn.Config.RPTopOriginVerificationMode, webauthn.Config.RPAllowCrossOrigin, shouldVerifyUser, shouldVerifyUserPresence, webauthn.Config.MDS, session.CredParams); err != nil {
return nil, err
}
if credential, err = NewCredential(clientDataHash, parsedResponse); err != nil {
return nil, err
}
if err = ValidateFilteredCredential(credential, webauthn.Config.Filtering); err != nil {
return nil, err
}
return credential, nil
}
// ValidateFilteredCredential applies the supplied [FilteringConfig] to a freshly-created [Credential]
// and returns a non-nil error when the credential violates any configured filtering rule (backup-eligibility
// prohibition, permitted-AAGUID allow-list, prohibited-AAGUID deny-list). A nil filtering argument is treated
// as "no filtering" and the function returns nil.
//
// The zero AAGUID ([uuid.Nil]) is never excluded by the permitted list, preserving the documented
// [FilteringConfig] contract for authenticators that report no AAGUID.
//
// This function is invoked automatically by [WebAuthn.CreateCredential] using the [Config.Filtering] value;
// relying parties may also call it directly (e.g. to pre-validate a credential before persistence) with any
// FilteringConfig value of their choosing.
//
// The credential argument must be non-nil.
func ValidateFilteredCredential(credential *Credential, filtering *FilteringConfig) (err error) {
if filtering == nil {
return nil
}
if credential == nil {
return protocol.ErrBadRequest.WithInfo("Credential is nil")
}
if filtering.ProhibitBackupEligibility && credential.Flags.BackupEligible {
return protocol.ErrPolicyRestriction.WithInfo("Credential is Backup Eligible")
}
var aaguid uuid.UUID
if err = aaguid.UnmarshalBinary(credential.Authenticator.AAGUID); err != nil {
return protocol.ErrBadRequest.WithInfo("The AAGUID of the credential is not a valid UUID")
}
if len(filtering.PermittedAAGUIDs) != 0 {
var success = false
if aaguid == uuid.Nil {
success = true
} else {
for _, permitted := range filtering.PermittedAAGUIDs {
if permitted == aaguid {
success = true
break
}
}
}
if !success {
return protocol.ErrPolicyRestriction.WithInfo("Credential has an AAGUID which is not permitted")
}
}
if len(filtering.ProhibitedAAGUIDs) != 0 {
for _, prohibited := range filtering.ProhibitedAAGUIDs {
if prohibited == aaguid {
return protocol.ErrPolicyRestriction.WithInfo("Credential has an AAGUID which is prohibited")
}
}
}
return nil
}
@@ -0,0 +1,121 @@
package webauthn
import (
"gamertan.com/web/internal/webauthnvendored/protocol"
"gamertan.com/web/internal/webauthnvendored/protocol/webauthncose"
)
// CredentialParametersDefault returns the default list of acceptable credential algorithms. This includes ES256,
// ES384, ES512, RS256, RS384, RS512, PS256, PS384, PS512, and EdDSA. The order indicates preference.
func CredentialParametersDefault() []protocol.CredentialParameter {
return []protocol.CredentialParameter{
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgES256,
},
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgES384,
},
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgES512,
},
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgRS256,
},
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgRS384,
},
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgRS512,
},
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgPS256,
},
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgPS384,
},
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgPS512,
},
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgEdDSA,
},
}
}
// CredentialParametersRecommendedL3 returns the WebAuthn Level 3 recommended credential algorithm list: EdDSA,
// ES256, and RS256 (in that order). This is the minimal set recommended by the specification for broad
// authenticator compatibility.
func CredentialParametersRecommendedL3() []protocol.CredentialParameter {
return []protocol.CredentialParameter{
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgEdDSA,
},
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgES256,
},
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgRS256,
},
}
}
// CredentialParametersExtendedL3 returns the WebAuthn Level 3 recommended credential algorithm list (EdDSA, ES256,
// RS256) extended with all other algorithms supported by this library (ES384, ES512, RS384, RS512, PS256, PS384,
// PS512). The Level 3 recommended algorithms appear first to indicate preference.
func CredentialParametersExtendedL3() []protocol.CredentialParameter {
return []protocol.CredentialParameter{
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgEdDSA,
},
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgES256,
},
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgES384,
},
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgES512,
},
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgRS256,
},
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgRS384,
},
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgRS512,
},
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgPS256,
},
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgPS384,
},
{
Type: protocol.PublicKeyCredentialType,
Algorithm: webauthncose.AlgPS512,
},
}
}
@@ -0,0 +1,132 @@
package webauthn
import "gamertan.com/web/internal/webauthnvendored/protocol"
// WithCredentialParameters adjusts the credential parameters in the registration options.
//
// Specification: §5.4. Parameters for Credential Generation (https://www.w3.org/TR/webauthn/#dom-publickeycredentialcreationoptions-pubkeycredparams)
func WithCredentialParameters(credentialParams []protocol.CredentialParameter) RegistrationOption {
return func(cco *protocol.PublicKeyCredentialCreationOptions) {
cco.Parameters = credentialParams
}
}
// WithExclusions adjusts the non-default parameters regarding credentials to exclude from registration.
//
// Specification: §5.4. Parameters for Credential Generation (https://www.w3.org/TR/webauthn/#dom-publickeycredentialcreationoptions-excludecredentials)
func WithExclusions(excludeList []protocol.CredentialDescriptor) RegistrationOption {
return func(cco *protocol.PublicKeyCredentialCreationOptions) {
cco.CredentialExcludeList = excludeList
}
}
// WithAuthenticatorSelection adjusts the non-default parameters regarding the authenticator to select during
// registration.
//
// Specification: §5.4. Parameters for Credential Generation (https://www.w3.org/TR/webauthn/#dom-publickeycredentialcreationoptions-authenticatorselection)
//
// Specification: §5.4.4. Authenticator Selection Criteria (https://www.w3.org/TR/webauthn/#dictdef-authenticatorselectioncriteria)
func WithAuthenticatorSelection(authenticatorSelection protocol.AuthenticatorSelection) RegistrationOption {
return func(cco *protocol.PublicKeyCredentialCreationOptions) {
cco.AuthenticatorSelection = authenticatorSelection
}
}
// WithResidentKeyRequirement sets both the resident key and require resident key protocol options.
//
// Specification: §5.4. Parameters for Credential Generation (https://www.w3.org/TR/webauthn/#dom-publickeycredentialcreationoptions-authenticatorselection)
//
// Specification: §5.4.4. Authenticator Selection Criteria (https://www.w3.org/TR/webauthn/#dictdef-authenticatorselectioncriteria)
func WithResidentKeyRequirement(requirement protocol.ResidentKeyRequirement) RegistrationOption {
return func(cco *protocol.PublicKeyCredentialCreationOptions) {
cco.AuthenticatorSelection.ResidentKey = requirement
switch requirement {
case protocol.ResidentKeyRequirementRequired:
cco.AuthenticatorSelection.RequireResidentKey = protocol.ResidentKeyRequired()
default:
cco.AuthenticatorSelection.RequireResidentKey = protocol.ResidentKeyNotRequired()
}
}
}
// WithPublicKeyCredentialHints adjusts the non-default hints for credential types to select during registration.
//
// WebAuthn Level 3.
//
// Specification: §5.4. Parameters for Credential Generation (https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-hints)
func WithPublicKeyCredentialHints(hints []protocol.PublicKeyCredentialHints) RegistrationOption {
return func(cco *protocol.PublicKeyCredentialCreationOptions) {
cco.Hints = hints
}
}
// WithConveyancePreference adjusts the non-default parameters regarding whether the authenticator should attest to the
// credential.
//
// Specification: §5.4. Parameters for Credential Generation (https://www.w3.org/TR/webauthn/#dom-publickeycredentialcreationoptions-attestation)
func WithConveyancePreference(preference protocol.ConveyancePreference) RegistrationOption {
return func(cco *protocol.PublicKeyCredentialCreationOptions) {
cco.Attestation = preference
}
}
// WithAttestationFormats adjusts the non-default formats for credential types to select during registration.
//
// WebAuthn Level 3.
//
// Specification: §5.4. Parameters for Credential Generation (https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-attestationformats)
func WithAttestationFormats(formats []protocol.AttestationFormat) RegistrationOption {
return func(cco *protocol.PublicKeyCredentialCreationOptions) {
cco.AttestationFormats = formats
}
}
// WithExtensions adjusts the extension parameter in the registration options.
//
// Specification: §5.4. Parameters for Credential Generation (https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-extensions)
//
// Specification: §9. Extensions (https://www.w3.org/TR/webauthn/#webauthn-extensions)
func WithExtensions(extension protocol.AuthenticationExtensions) RegistrationOption {
return func(cco *protocol.PublicKeyCredentialCreationOptions) {
cco.Extensions = extension
}
}
// WithAppIdExcludeExtension automatically includes the specified appid if the CredentialExcludeList contains a credential
// with the type `fido-u2f`.
//
// Specification: §5.4. Parameters for Credential Generation (https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-extensions)
//
// Specification: §9. Extensions (https://www.w3.org/TR/webauthn/#webauthn-extensions)
//
// Specification: §10.1.2. FIDO AppID Exclusion Extension (https://www.w3.org/TR/webauthn/#sctn-appid-exclude-extension)
func WithAppIdExcludeExtension(appid string) RegistrationOption {
return func(cco *protocol.PublicKeyCredentialCreationOptions) {
for _, credential := range cco.CredentialExcludeList {
if credential.AttestationFormat == string(protocol.AttestationFormatFIDOUniversalSecondFactor) {
if cco.Extensions == nil {
cco.Extensions = map[string]any{}
}
cco.Extensions[protocol.ExtensionAppIDExclude] = appid
break
}
}
}
}
// WithRegistrationRelyingPartyID sets the relying party id for the registration.
func WithRegistrationRelyingPartyID(id string) RegistrationOption {
return func(cco *protocol.PublicKeyCredentialCreationOptions) {
cco.RelyingParty.ID = id
}
}
// WithRegistrationRelyingPartyName sets the relying party name for the registration.
func WithRegistrationRelyingPartyName(name string) RegistrationOption {
return func(cco *protocol.PublicKeyCredentialCreationOptions) {
cco.RelyingParty.Name = name
}
}
+251
View File
@@ -0,0 +1,251 @@
package webauthn
import (
"fmt"
"time"
"github.com/google/uuid"
"gamertan.com/web/internal/webauthnvendored/metadata"
"gamertan.com/web/internal/webauthnvendored/protocol"
)
// New creates a new [WebAuthn] instance from the provided [Config]. The configuration is validated before the
// instance is returned.
func New(config *Config) (*WebAuthn, error) {
if err := config.validate(); err != nil {
return nil, fmt.Errorf(errFmtConfigValidate, err)
}
return &WebAuthn{
config,
}, nil
}
// WebAuthn is the primary interface of this package. It provides methods to begin and finish both registration and
// login ceremonies. Create an instance using [New] and then call the appropriate Begin/Finish methods for your
// use case. See the package documentation for detailed ceremony flows.
type WebAuthn struct {
Config *Config
}
// Config represents the Relying Party configuration for WebAuthn operations. At minimum, RPID and RPOrigins must
// be configured. The RPID should be the effective domain of the Relying Party (i.e. "example.com") and RPOrigins
// should contain the fully qualified origins that are permitted (i.e. "https://example.com").
type Config struct {
// RPID configures the Relying Party Server ID. This should generally be the origin without a scheme and port.
RPID string
// RPDisplayName configures the display name for the Relying Party Server. This can be any string.
RPDisplayName string
// RPOrigins configures the list of Relying Party Server Origins that are permitted. The provided origins can either
// be fully qualified origins or strings for simple string comparison. The strings are matched using canonical
// origin matching semantics specifically if they start with 'http://' or 'https://' if the provided origin has a
// case-insensitive equal scheme and host component they are equal, otherwise simple string comparison is utilized
// to determine equality.
RPOrigins []string
// RPTopOrigins configures the list of Relying Party Server Top Origins that are permitted. The provided origins can
// either be fully qualified origins or strings for simple string comparison. The strings are matched using
// canonical origin matching semantics specifically if they start with 'http://' or 'https://' if the provided
// origin has a case-insensitive equal scheme and host component they are equal, otherwise simple string comparison
// is utilized to determine equality.
RPTopOrigins []string
// RPTopOriginVerificationMode determines the verification mode for the Top Origin value used in cross-origin
// ceremonies. When the zero value ([protocol.TopOriginDefaultVerificationMode]) is provided, the config
// validator coerces this field to [protocol.TopOriginExplicitVerificationMode]; i.e. any Top Origin supplied
// by the client must appear in [Config.RPTopOrigins]. Set this field explicitly to
// [protocol.TopOriginAutoVerificationMode] or [protocol.TopOriginImplicitVerificationMode] if you need
// different matching semantics; there is no longer a mode that disables verification entirely.
RPTopOriginVerificationMode protocol.TopOriginVerificationMode
// RPAllowCrossOrigin determines whether the RP is allowed to be used in cross-origin contexts. This is disabled
// by default.
RPAllowCrossOrigin bool
// AttestationPreference sets the default attestation conveyance preferences.
AttestationPreference protocol.ConveyancePreference
// AuthenticatorSelection sets the default authenticator selection options.
AuthenticatorSelection protocol.AuthenticatorSelection
// Debug enables various debug options.
Debug bool
// EncodeUserIDAsString ensures the user.id value during registrations is encoded as a raw UTF8 string. This is
// useful when you only use printable ASCII characters for the random user.id but the browser library does not
// decode the URL Safe Base64 data.
EncodeUserIDAsString bool
// Timeouts configures various timeouts.
Timeouts TimeoutsConfig
// MDS configures a FIDO Metadata Service provider for authenticator trust validation. When set, the library
// validates attestation statements against known authenticator metadata including trust anchors, attestation
// types, and authenticator status. Use the providers in [gamertan.com/web/internal/webauthnvendored/metadata/providers/memory]
// or [gamertan.com/web/internal/webauthnvendored/metadata/providers/cached] to create a provider instance.
MDS metadata.Provider
// Filtering configures the filtering of authenticators based on their AAGUIDs. This is useful for enforcing
// policy on the authenticators that are available to be registered with the Relying Party.
Filtering *FilteringConfig
validated bool
}
// FilteringConfig configures the filtering of authenticators based on their AAGUIDs. This is useful for enforcing
// policy on the authenticators that are available to be registered with the Relying Party.
type FilteringConfig struct {
// ProhibitBackupEligibility if set will prohibit the use of authenticators with the backup eligible flag set.
ProhibitBackupEligibility bool
// PermittedAAGUIDs if set is used to filter authenticators by their AAGUID only allowing specific values. This
// option is mutually exclusive with ProhibitedAAGUIDs and will never exclude a zero AAGUID. To prohibit the use
// of Zero AAGUIDs, use [Config.MDS] or [FilteringConfig.ProhibitedAAGUIDs].
PermittedAAGUIDs []uuid.UUID
// ProhibitedAAGUIDs if set is used to filter authenticators by their AAGUID only prohibiting specific values. This
// option is mutually exclusive with PermittedAAGUIDs.
ProhibitedAAGUIDs []uuid.UUID
}
// TimeoutsConfig configures the timeout durations for both login and registration ceremonies. These values are sent
// to the client as the timeout field in the credential request/creation options and optionally enforced server-side.
type TimeoutsConfig struct {
Login TimeoutConfig
Registration TimeoutConfig
}
// TimeoutConfig configures timeout behavior for a specific WebAuthn ceremony (registration or login).
type TimeoutConfig struct {
// Enforce the timeouts at the Relying Party / Server. This means if enabled and the user takes too long that even
// if the browser does not enforce the timeout the Relying Party / Server will.
Enforce bool
// Timeout is the timeout for logins/registrations when the UserVerificationRequirement is set to anything other
// than discouraged.
Timeout time.Duration
// TimeoutUVD is the timeout for logins/registrations when the UserVerificationRequirement is set to discouraged.
TimeoutUVD time.Duration
}
// Validate that the config flags in Config are properly set.
func (config *Config) validate() (err error) {
if config.validated {
return nil
}
if len(config.RPID) != 0 {
if err = protocol.ValidateRPID(config.RPID); err != nil {
return fmt.Errorf(errFmtFieldNotValidDomainString, "RPID", err)
}
}
defaultTimeoutConfig := defaultTimeout
defaultTimeoutUVDConfig := defaultTimeoutUVD
if config.Timeouts.Login.Timeout.Milliseconds() == 0 {
config.Timeouts.Login.Timeout = defaultTimeoutConfig
}
if config.Timeouts.Login.TimeoutUVD.Milliseconds() == 0 {
config.Timeouts.Login.TimeoutUVD = defaultTimeoutUVDConfig
}
if config.Timeouts.Registration.Timeout.Milliseconds() == 0 {
config.Timeouts.Registration.Timeout = defaultTimeoutConfig
}
if config.Timeouts.Registration.TimeoutUVD.Milliseconds() == 0 {
config.Timeouts.Registration.TimeoutUVD = defaultTimeoutUVDConfig
}
if len(config.RPOrigins) == 0 {
return fmt.Errorf("must provide at least one value to the 'RPOrigins' field")
}
if config.RPTopOriginVerificationMode == protocol.TopOriginDefaultVerificationMode {
config.RPTopOriginVerificationMode = protocol.TopOriginExplicitVerificationMode
}
if config.Filtering != nil {
if len(config.Filtering.PermittedAAGUIDs) > 0 && len(config.Filtering.ProhibitedAAGUIDs) > 0 {
return fmt.Errorf("cannot set both 'PermittedAAGUIDs' and 'ProhibitedAAGUIDs' in the filtering config")
}
}
config.validated = true
return nil
}
// GetRPID returns the configured Relying Party ID.
func (c *Config) GetRPID() string {
return c.RPID
}
// GetOrigins returns the configured Relying Party Origins.
func (c *Config) GetOrigins() []string {
return c.RPOrigins
}
// GetTopOrigins returns the configured Relying Party Top Origins.
func (c *Config) GetTopOrigins() []string {
return c.RPTopOrigins
}
// GetTopOriginVerificationMode returns the configured Top Origin verification mode.
func (c *Config) GetTopOriginVerificationMode() protocol.TopOriginVerificationMode {
return c.RPTopOriginVerificationMode
}
// GetMetaDataProvider returns the configured FIDO Metadata Service provider.
func (c *Config) GetMetaDataProvider() metadata.Provider {
return c.MDS
}
// ConfigProvider is an interface that provides access to the WebAuthn [Config] values. This is useful for
// implementations that wish to provide configuration from alternative sources.
type ConfigProvider interface {
GetRPID() string
GetOrigins() []string
GetTopOrigins() []string
GetTopOriginVerificationMode() protocol.TopOriginVerificationMode
GetMetaDataProvider() metadata.Provider
}
// User is an interface with the Relying Party's User entry and provides the fields and methods needed for WebAuthn
// registration operations.
type User interface {
// WebAuthnID provides the user handle of the user account. A user handle is an opaque byte sequence with a maximum
// size of 64 bytes, and is not meant to be displayed to the user.
//
// To ensure secure operation, authentication and authorization decisions MUST be made on the basis of this id
// member, not the displayName nor name members. See Section 6.1 of [RFC8266].
//
// It's recommended this value is completely random and uses the entire 64 bytes.
//
// Specification: §5.4.3. User Account Parameters for Credential Generation (https://w3c.github.io/webauthn/#dom-publickeycredentialuserentity-id)
WebAuthnID() []byte
// WebAuthnName provides the name attribute of the user account during registration and is a human-palatable name
// for the user account, intended only for display. For example, "Alex Müller" or "田中倫". The Relying Party SHOULD
// let the user choose this, and SHOULD NOT restrict the choice more than necessary.
//
// Specification: §5.4.3. User Account Parameters for Credential Generation (https://w3c.github.io/webauthn/#dictdef-publickeycredentialuserentity)
WebAuthnName() string
// WebAuthnDisplayName provides the name attribute of the user account during registration and is a human-palatable
// name for the user account, intended only for display. For example, "Alex Müller" or "田中倫". The Relying Party
// SHOULD let the user choose this, and SHOULD NOT restrict the choice more than necessary.
//
// Specification: §5.4.3. User Account Parameters for Credential Generation (https://www.w3.org/TR/webauthn/#dom-publickeycredentialuserentity-displayname)
WebAuthnDisplayName() string
// WebAuthnCredentials provides the slice of [Credential] objects owned by the user. This generally should be all
// the [Credential] objects owned by the user regardless of which flow is being used.
WebAuthnCredentials() []Credential
}
@@ -0,0 +1,43 @@
package webauthn
import (
"time"
"gamertan.com/web/internal/webauthnvendored/protocol"
)
//go:generate msgp
//msgp:replace protocol.UserVerificationRequirement with:string
//msgp:replace protocol.AuthenticationExtensions with:map[string]any
//msgp:replace protocol.CredentialMediationRequirement with:string
//msgp:clearomitted
// SessionData is the data that must be stored by the Relying Party between the Begin and Finish steps of a WebAuthn
// ceremony. It contains the challenge and other parameters needed to verify the authenticator's response.
//
// The Relying Party must store this data securely and associate it with the user's session. It should not be
// modifiable by the client (i.e. store it server-side or in a signed, opaque cookie). After the ceremony completes,
// the session data should be discarded.
//
// Every field returned by the Begin* functions must be delivered to the matching Finish* / Validate* call with
// the same values; if anything is dropped or reshaped in transit, verification will fail. Treat [SessionData] as
// an atomic record between those two calls.
//
// For consolidated persistence guidance; recommended schema shape, required lookup columns, and the rules
// that also apply to [Credential] records; see the [Storage] section of the
// [gamertan.com/web/internal/webauthnvendored/webauthn] package documentation.
//
// [Storage]: https://pkg.go.dev/gamertan.com/web/internal/webauthnvendored/webauthn#hdr-Storage
type SessionData struct {
Challenge string `json:"challenge" msg:"c"`
RelyingPartyID string `json:"rpId,omitempty" msg:"r,omitempty"`
UserID []byte `json:"user_id,omitempty" msg:"u,omitempty"`
AllowedCredentialIDs [][]byte `json:"allowed_credentials,omitempty" msg:"allow,omitempty"`
Expires time.Time `json:"expires" msg:"exp"`
UserVerification protocol.UserVerificationRequirement `json:"userVerification,omitempty" msg:"uv,omitempty"`
Extensions protocol.AuthenticationExtensions `json:"extensions,omitempty" msg:"exts,omitempty"`
CredParams []protocol.CredentialParameter `json:"credParams,omitempty" msg:"params,omitempty"`
Mediation protocol.CredentialMediationRequirement `json:"mediation,omitempty" msg:"cmr,omitempty"`
}
@@ -0,0 +1,653 @@
// Code generated by github.com/tinylib/msgp DO NOT EDIT.
package webauthn
import (
"gamertan.com/web/internal/webauthnvendored/protocol"
"github.com/tinylib/msgp/msgp"
)
// DecodeMsg implements msgp.Decodable
func (z *SessionData) DecodeMsg(dc *msgp.Reader) (err error) {
var field []byte
_ = field
var zb0001 uint32
zb0001, err = dc.ReadMapHeader()
if err != nil {
err = msgp.WrapError(err)
return
}
var zb0001Mask uint8 /* 7 bits */
_ = zb0001Mask
for zb0001 > 0 {
zb0001--
field, err = dc.ReadMapKeyPtr()
if err != nil {
err = msgp.WrapError(err)
return
}
switch msgp.UnsafeString(field) {
case "c":
z.Challenge, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "Challenge")
return
}
case "r":
z.RelyingPartyID, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "RelyingPartyID")
return
}
zb0001Mask |= 0x1
case "u":
z.UserID, err = dc.ReadBytes(z.UserID)
if err != nil {
err = msgp.WrapError(err, "UserID")
return
}
zb0001Mask |= 0x2
case "allow":
var zb0002 uint32
zb0002, err = dc.ReadArrayHeader()
if err != nil {
err = msgp.WrapError(err, "AllowedCredentialIDs")
return
}
if cap(z.AllowedCredentialIDs) >= int(zb0002) {
z.AllowedCredentialIDs = (z.AllowedCredentialIDs)[:zb0002]
} else {
z.AllowedCredentialIDs = make([][]byte, zb0002)
}
for za0003 := range z.AllowedCredentialIDs {
z.AllowedCredentialIDs[za0003], err = dc.ReadBytes(z.AllowedCredentialIDs[za0003])
if err != nil {
err = msgp.WrapError(err, "AllowedCredentialIDs", za0003)
return
}
}
zb0001Mask |= 0x4
case "exp":
z.Expires, err = dc.ReadTime()
if err != nil {
err = msgp.WrapError(err, "Expires")
return
}
case "uv":
{
var zb0003 string
zb0003, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "UserVerification")
return
}
z.UserVerification = protocol.UserVerificationRequirement(zb0003)
}
zb0001Mask |= 0x8
case "exts":
var zb0004 uint32
zb0004, err = dc.ReadMapHeader()
if err != nil {
err = msgp.WrapError(err, "Extensions")
return
}
if z.Extensions == nil {
z.Extensions = make(map[string]interface{}, zb0004)
} else if len(z.Extensions) > 0 {
clear(z.Extensions)
}
for zb0004 > 0 {
zb0004--
var za0004 string
za0004, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "Extensions")
return
}
var za0005 interface{}
za0005, err = dc.ReadIntf()
if err != nil {
err = msgp.WrapError(err, "Extensions", za0004)
return
}
z.Extensions[za0004] = za0005
}
zb0001Mask |= 0x10
case "params":
var zb0005 uint32
zb0005, err = dc.ReadArrayHeader()
if err != nil {
err = msgp.WrapError(err, "CredParams")
return
}
if cap(z.CredParams) >= int(zb0005) {
z.CredParams = (z.CredParams)[:zb0005]
} else {
z.CredParams = make([]protocol.CredentialParameter, zb0005)
}
for za0006 := range z.CredParams {
err = z.CredParams[za0006].DecodeMsg(dc)
if err != nil {
err = msgp.WrapError(err, "CredParams", za0006)
return
}
}
zb0001Mask |= 0x20
case "cmr":
{
var zb0006 string
zb0006, err = dc.ReadString()
if err != nil {
err = msgp.WrapError(err, "Mediation")
return
}
z.Mediation = protocol.CredentialMediationRequirement(zb0006)
}
zb0001Mask |= 0x40
default:
err = dc.Skip()
if err != nil {
err = msgp.WrapError(err)
return
}
}
}
// Clear omitted fields.
if zb0001Mask != 0x7f {
if (zb0001Mask & 0x1) == 0 {
z.RelyingPartyID = ""
}
if (zb0001Mask & 0x2) == 0 {
z.UserID = nil
}
if (zb0001Mask & 0x4) == 0 {
z.AllowedCredentialIDs = nil
}
if (zb0001Mask & 0x8) == 0 {
z.UserVerification = ""
}
if (zb0001Mask & 0x10) == 0 {
z.Extensions = nil
}
if (zb0001Mask & 0x20) == 0 {
z.CredParams = nil
}
if (zb0001Mask & 0x40) == 0 {
z.Mediation = ""
}
}
return
}
// EncodeMsg implements msgp.Encodable
func (z *SessionData) EncodeMsg(en *msgp.Writer) (err error) {
// check for omitted fields
zb0001Len := uint32(9)
var zb0001Mask uint16 /* 9 bits */
_ = zb0001Mask
if z.RelyingPartyID == "" {
zb0001Len--
zb0001Mask |= 0x2
}
if z.UserID == nil {
zb0001Len--
zb0001Mask |= 0x4
}
if z.AllowedCredentialIDs == nil {
zb0001Len--
zb0001Mask |= 0x8
}
if z.UserVerification == "" {
zb0001Len--
zb0001Mask |= 0x20
}
if z.Extensions == nil {
zb0001Len--
zb0001Mask |= 0x40
}
if z.CredParams == nil {
zb0001Len--
zb0001Mask |= 0x80
}
if z.Mediation == "" {
zb0001Len--
zb0001Mask |= 0x100
}
// variable map header, size zb0001Len
err = en.Append(0x80 | uint8(zb0001Len))
if err != nil {
return
}
// skip if no fields are to be emitted
if zb0001Len != 0 {
// write "c"
err = en.Append(0xa1, 0x63)
if err != nil {
return
}
err = en.WriteString(z.Challenge)
if err != nil {
err = msgp.WrapError(err, "Challenge")
return
}
if (zb0001Mask & 0x2) == 0 { // if not omitted
// write "r"
err = en.Append(0xa1, 0x72)
if err != nil {
return
}
err = en.WriteString(z.RelyingPartyID)
if err != nil {
err = msgp.WrapError(err, "RelyingPartyID")
return
}
}
if (zb0001Mask & 0x4) == 0 { // if not omitted
// write "u"
err = en.Append(0xa1, 0x75)
if err != nil {
return
}
err = en.WriteBytes(z.UserID)
if err != nil {
err = msgp.WrapError(err, "UserID")
return
}
}
if (zb0001Mask & 0x8) == 0 { // if not omitted
// write "allow"
err = en.Append(0xa5, 0x61, 0x6c, 0x6c, 0x6f, 0x77)
if err != nil {
return
}
err = en.WriteArrayHeader(uint32(len(z.AllowedCredentialIDs)))
if err != nil {
err = msgp.WrapError(err, "AllowedCredentialIDs")
return
}
for za0003 := range z.AllowedCredentialIDs {
err = en.WriteBytes(z.AllowedCredentialIDs[za0003])
if err != nil {
err = msgp.WrapError(err, "AllowedCredentialIDs", za0003)
return
}
}
}
// write "exp"
err = en.Append(0xa3, 0x65, 0x78, 0x70)
if err != nil {
return
}
err = en.WriteTime(z.Expires)
if err != nil {
err = msgp.WrapError(err, "Expires")
return
}
if (zb0001Mask & 0x20) == 0 { // if not omitted
// write "uv"
err = en.Append(0xa2, 0x75, 0x76)
if err != nil {
return
}
err = en.WriteString(string(z.UserVerification))
if err != nil {
err = msgp.WrapError(err, "UserVerification")
return
}
}
if (zb0001Mask & 0x40) == 0 { // if not omitted
// write "exts"
err = en.Append(0xa4, 0x65, 0x78, 0x74, 0x73)
if err != nil {
return
}
err = en.WriteMapHeader(uint32(len(z.Extensions)))
if err != nil {
err = msgp.WrapError(err, "Extensions")
return
}
for za0004, za0005 := range z.Extensions {
err = en.WriteString(za0004)
if err != nil {
err = msgp.WrapError(err, "Extensions")
return
}
err = en.WriteIntf(za0005)
if err != nil {
err = msgp.WrapError(err, "Extensions", za0004)
return
}
}
}
if (zb0001Mask & 0x80) == 0 { // if not omitted
// write "params"
err = en.Append(0xa6, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73)
if err != nil {
return
}
err = en.WriteArrayHeader(uint32(len(z.CredParams)))
if err != nil {
err = msgp.WrapError(err, "CredParams")
return
}
for za0006 := range z.CredParams {
err = z.CredParams[za0006].EncodeMsg(en)
if err != nil {
err = msgp.WrapError(err, "CredParams", za0006)
return
}
}
}
if (zb0001Mask & 0x100) == 0 { // if not omitted
// write "cmr"
err = en.Append(0xa3, 0x63, 0x6d, 0x72)
if err != nil {
return
}
err = en.WriteString(string(z.Mediation))
if err != nil {
err = msgp.WrapError(err, "Mediation")
return
}
}
}
return
}
// MarshalMsg implements msgp.Marshaler
func (z *SessionData) MarshalMsg(b []byte) (o []byte, err error) {
o = msgp.Require(b, z.Msgsize())
// check for omitted fields
zb0001Len := uint32(9)
var zb0001Mask uint16 /* 9 bits */
_ = zb0001Mask
if z.RelyingPartyID == "" {
zb0001Len--
zb0001Mask |= 0x2
}
if z.UserID == nil {
zb0001Len--
zb0001Mask |= 0x4
}
if z.AllowedCredentialIDs == nil {
zb0001Len--
zb0001Mask |= 0x8
}
if z.UserVerification == "" {
zb0001Len--
zb0001Mask |= 0x20
}
if z.Extensions == nil {
zb0001Len--
zb0001Mask |= 0x40
}
if z.CredParams == nil {
zb0001Len--
zb0001Mask |= 0x80
}
if z.Mediation == "" {
zb0001Len--
zb0001Mask |= 0x100
}
// variable map header, size zb0001Len
o = append(o, 0x80|uint8(zb0001Len))
// skip if no fields are to be emitted
if zb0001Len != 0 {
// string "c"
o = append(o, 0xa1, 0x63)
o = msgp.AppendString(o, z.Challenge)
if (zb0001Mask & 0x2) == 0 { // if not omitted
// string "r"
o = append(o, 0xa1, 0x72)
o = msgp.AppendString(o, z.RelyingPartyID)
}
if (zb0001Mask & 0x4) == 0 { // if not omitted
// string "u"
o = append(o, 0xa1, 0x75)
o = msgp.AppendBytes(o, z.UserID)
}
if (zb0001Mask & 0x8) == 0 { // if not omitted
// string "allow"
o = append(o, 0xa5, 0x61, 0x6c, 0x6c, 0x6f, 0x77)
o = msgp.AppendArrayHeader(o, uint32(len(z.AllowedCredentialIDs)))
for za0003 := range z.AllowedCredentialIDs {
o = msgp.AppendBytes(o, z.AllowedCredentialIDs[za0003])
}
}
// string "exp"
o = append(o, 0xa3, 0x65, 0x78, 0x70)
o = msgp.AppendTime(o, z.Expires)
if (zb0001Mask & 0x20) == 0 { // if not omitted
// string "uv"
o = append(o, 0xa2, 0x75, 0x76)
o = msgp.AppendString(o, string(z.UserVerification))
}
if (zb0001Mask & 0x40) == 0 { // if not omitted
// string "exts"
o = append(o, 0xa4, 0x65, 0x78, 0x74, 0x73)
o = msgp.AppendMapHeader(o, uint32(len(z.Extensions)))
for za0004, za0005 := range z.Extensions {
o = msgp.AppendString(o, za0004)
o, err = msgp.AppendIntf(o, za0005)
if err != nil {
err = msgp.WrapError(err, "Extensions", za0004)
return
}
}
}
if (zb0001Mask & 0x80) == 0 { // if not omitted
// string "params"
o = append(o, 0xa6, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x73)
o = msgp.AppendArrayHeader(o, uint32(len(z.CredParams)))
for za0006 := range z.CredParams {
o, err = z.CredParams[za0006].MarshalMsg(o)
if err != nil {
err = msgp.WrapError(err, "CredParams", za0006)
return
}
}
}
if (zb0001Mask & 0x100) == 0 { // if not omitted
// string "cmr"
o = append(o, 0xa3, 0x63, 0x6d, 0x72)
o = msgp.AppendString(o, string(z.Mediation))
}
}
return
}
// UnmarshalMsg implements msgp.Unmarshaler
func (z *SessionData) UnmarshalMsg(bts []byte) (o []byte, err error) {
var field []byte
_ = field
var zb0001 uint32
zb0001, bts, err = msgp.ReadMapHeaderBytes(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
var zb0001Mask uint8 /* 7 bits */
_ = zb0001Mask
for zb0001 > 0 {
zb0001--
field, bts, err = msgp.ReadMapKeyZC(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
switch msgp.UnsafeString(field) {
case "c":
z.Challenge, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Challenge")
return
}
case "r":
z.RelyingPartyID, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "RelyingPartyID")
return
}
zb0001Mask |= 0x1
case "u":
z.UserID, bts, err = msgp.ReadBytesBytes(bts, z.UserID)
if err != nil {
err = msgp.WrapError(err, "UserID")
return
}
zb0001Mask |= 0x2
case "allow":
var zb0002 uint32
zb0002, bts, err = msgp.ReadArrayHeaderBytes(bts)
if err != nil {
err = msgp.WrapError(err, "AllowedCredentialIDs")
return
}
if cap(z.AllowedCredentialIDs) >= int(zb0002) {
z.AllowedCredentialIDs = (z.AllowedCredentialIDs)[:zb0002]
} else {
z.AllowedCredentialIDs = make([][]byte, zb0002)
}
for za0003 := range z.AllowedCredentialIDs {
z.AllowedCredentialIDs[za0003], bts, err = msgp.ReadBytesBytes(bts, z.AllowedCredentialIDs[za0003])
if err != nil {
err = msgp.WrapError(err, "AllowedCredentialIDs", za0003)
return
}
}
zb0001Mask |= 0x4
case "exp":
z.Expires, bts, err = msgp.ReadTimeBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Expires")
return
}
case "uv":
{
var zb0003 string
zb0003, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "UserVerification")
return
}
z.UserVerification = protocol.UserVerificationRequirement(zb0003)
}
zb0001Mask |= 0x8
case "exts":
var zb0004 uint32
zb0004, bts, err = msgp.ReadMapHeaderBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Extensions")
return
}
if z.Extensions == nil {
z.Extensions = make(map[string]interface{}, zb0004)
} else if len(z.Extensions) > 0 {
clear(z.Extensions)
}
for zb0004 > 0 {
var za0005 interface{}
zb0004--
var za0004 string
za0004, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Extensions")
return
}
za0005, bts, err = msgp.ReadIntfBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Extensions", za0004)
return
}
z.Extensions[za0004] = za0005
}
zb0001Mask |= 0x10
case "params":
var zb0005 uint32
zb0005, bts, err = msgp.ReadArrayHeaderBytes(bts)
if err != nil {
err = msgp.WrapError(err, "CredParams")
return
}
if cap(z.CredParams) >= int(zb0005) {
z.CredParams = (z.CredParams)[:zb0005]
} else {
z.CredParams = make([]protocol.CredentialParameter, zb0005)
}
for za0006 := range z.CredParams {
bts, err = z.CredParams[za0006].UnmarshalMsg(bts)
if err != nil {
err = msgp.WrapError(err, "CredParams", za0006)
return
}
}
zb0001Mask |= 0x20
case "cmr":
{
var zb0006 string
zb0006, bts, err = msgp.ReadStringBytes(bts)
if err != nil {
err = msgp.WrapError(err, "Mediation")
return
}
z.Mediation = protocol.CredentialMediationRequirement(zb0006)
}
zb0001Mask |= 0x40
default:
bts, err = msgp.Skip(bts)
if err != nil {
err = msgp.WrapError(err)
return
}
}
}
// Clear omitted fields.
if zb0001Mask != 0x7f {
if (zb0001Mask & 0x1) == 0 {
z.RelyingPartyID = ""
}
if (zb0001Mask & 0x2) == 0 {
z.UserID = nil
}
if (zb0001Mask & 0x4) == 0 {
z.AllowedCredentialIDs = nil
}
if (zb0001Mask & 0x8) == 0 {
z.UserVerification = ""
}
if (zb0001Mask & 0x10) == 0 {
z.Extensions = nil
}
if (zb0001Mask & 0x20) == 0 {
z.CredParams = nil
}
if (zb0001Mask & 0x40) == 0 {
z.Mediation = ""
}
}
o = bts
return
}
// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message
func (z *SessionData) Msgsize() (s int) {
s = 1 + 2 + msgp.StringPrefixSize + len(z.Challenge) + 2 + msgp.StringPrefixSize + len(z.RelyingPartyID) + 2 + msgp.BytesPrefixSize + len(z.UserID) + 6 + msgp.ArrayHeaderSize
for za0003 := range z.AllowedCredentialIDs {
s += msgp.BytesPrefixSize + len(z.AllowedCredentialIDs[za0003])
}
s += 4 + msgp.TimeSize + 3 + msgp.StringPrefixSize + len(string(z.UserVerification)) + 5 + msgp.MapHeaderSize
if z.Extensions != nil {
for za0004, za0005 := range z.Extensions {
_ = za0005
s += msgp.StringPrefixSize + len(za0004) + msgp.GuessSize(za0005)
}
}
s += 7 + msgp.ArrayHeaderSize
for za0006 := range z.CredParams {
s += z.CredParams[za0006].Msgsize()
}
s += 4 + msgp.StringPrefixSize + len(string(z.Mediation))
return
}
@@ -0,0 +1,40 @@
package webauthn
import "bytes"
func isByteArrayInSlice(needle []byte, haystack ...[]byte) (valid bool) {
for _, hay := range haystack {
if bytes.Equal(needle, hay) {
return true
}
}
return false
}
func isCredentialsAllowedMatchingOwned(allowedCredentialIDs [][]byte, credentials []Credential) (valid bool) {
var credential Credential
allowed:
for _, allowedCredentialID := range allowedCredentialIDs {
for _, credential = range credentials {
if bytes.Equal(credential.ID, allowedCredentialID) {
continue allowed
}
}
return false
}
return true
}
func isCredentialIDInCredentials(credentialID []byte, credentials []Credential) (valid bool) {
for _, credential := range credentials {
if bytes.Equal(credential.ID, credentialID) {
return true
}
}
return false
}