This commit is contained in:
@@ -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 assertion’s 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 credential’s 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 credential’s id attribute.
|
||||
// Update the stored signature counter value, associated with credential’s id attribute, to be the value of
|
||||
// authData.signCount.
|
||||
//
|
||||
// → Less than or equal to the signature counter value stored in conjunction with credential’s 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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 credential’s 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user