114 lines
3.7 KiB
Go
114 lines
3.7 KiB
Go
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
// Package authwebauthn provides storage-neutral, passkey-only WebAuthn
|
|
// ceremonies. It owns relying-party policy, bounded single-use ceremony state,
|
|
// credential lifecycle, and recovery tokens while delegating protocol parsing
|
|
// and signature verification to a pinned WebAuthn implementation.
|
|
package authwebauthn
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"errors"
|
|
"time"
|
|
|
|
"gamertan.com/web/auth"
|
|
)
|
|
|
|
var (
|
|
ErrCeremonyNotFound = errors.New("authwebauthn: ceremony not found")
|
|
ErrCredentialNotFound = errors.New("authwebauthn: credential not found")
|
|
ErrEnrollmentNotFound = errors.New("authwebauthn: enrollment token not found")
|
|
ErrCredentialFloor = errors.New("authwebauthn: the required credential floor cannot be crossed")
|
|
ErrLastCredential = errors.New("authwebauthn: the last credential cannot be removed remotely")
|
|
ErrOperationBinding = errors.New("authwebauthn: operation binding does not match")
|
|
ErrPasskeyReadiness = errors.New("authwebauthn: at least two passkeys are required")
|
|
ErrUnsupportedCredential = errors.New("authwebauthn: credential algorithm is unsupported")
|
|
)
|
|
|
|
const (
|
|
CeremonyRegistration = "registration"
|
|
CeremonyLogin = "login"
|
|
CeremonyApproval = "approval"
|
|
)
|
|
|
|
type Credential struct {
|
|
ID []byte
|
|
UserID string
|
|
Label string
|
|
Data json.RawMessage
|
|
CreatedAt time.Time
|
|
LastUsedAt time.Time
|
|
}
|
|
|
|
// CredentialSummary is the non-secret credential metadata applications may
|
|
// show to an authenticated account owner. It intentionally excludes the
|
|
// stored public-key document and user identifier.
|
|
type CredentialSummary struct {
|
|
ID []byte
|
|
Label string
|
|
CreatedAt time.Time
|
|
LastUsedAt time.Time
|
|
}
|
|
|
|
type EnrollmentToken struct {
|
|
Digest [32]byte
|
|
UserID string
|
|
CreatedAt time.Time
|
|
ExpiresAt time.Time
|
|
}
|
|
|
|
type Ceremony struct {
|
|
Digest [32]byte
|
|
Kind string
|
|
UserID string
|
|
Label string
|
|
SessionData json.RawMessage
|
|
BindingDigest [32]byte
|
|
CreatedAt time.Time
|
|
ExpiresAt time.Time
|
|
}
|
|
|
|
type BeginResult struct {
|
|
CeremonyToken string `json:"ceremony_token"`
|
|
PublicKey json.RawMessage `json:"public_key"`
|
|
ExpiresAt time.Time `json:"expires_at"`
|
|
}
|
|
|
|
type Authentication struct {
|
|
SessionToken string
|
|
Principal auth.Principal
|
|
CredentialID []byte
|
|
CloneWarning bool
|
|
}
|
|
|
|
type Approval struct {
|
|
User auth.User
|
|
CredentialID []byte
|
|
BindingDigest [32]byte
|
|
CloneWarning bool
|
|
ApprovedAt time.Time
|
|
}
|
|
|
|
// Repository persists passkey-specific state. Implementations must consume
|
|
// enrollment tokens and ceremonies atomically and must perform recovery and
|
|
// credential removal invariants in transactions.
|
|
type Repository interface {
|
|
CreatePasskeyUser(context.Context, auth.User, EnrollmentToken, auth.AuditEvent) error
|
|
UserByID(context.Context, string) (auth.User, error)
|
|
UserByIdentifier(context.Context, string) (auth.User, error)
|
|
UserByCredentialID(context.Context, []byte) (auth.User, error)
|
|
CredentialsByUserID(context.Context, string) ([]Credential, error)
|
|
SaveCredential(context.Context, Credential, auth.AuditEvent) error
|
|
UpdateCredential(context.Context, Credential) error
|
|
DeleteCredential(context.Context, string, []byte, int, auth.AuditEvent) error
|
|
CredentialCount(context.Context, string) (int, error)
|
|
CreateCeremony(context.Context, Ceremony) error
|
|
TakeCeremony(context.Context, [32]byte, time.Time) (Ceremony, error)
|
|
ConsumeEnrollmentToken(context.Context, [32]byte, time.Time) (auth.User, error)
|
|
RecoverUser(context.Context, string, EnrollmentToken, auth.AuditEvent) (auth.User, error)
|
|
}
|
|
|
|
func BindingDigest(value []byte) [32]byte { return sha256.Sum256(value) }
|