authenticator

package
v0.0.0-...-e8f36b6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 11, 2026 License: Apache-2.0 Imports: 22 Imported by: 0

Documentation

Overview

Package authenticator defines credentials and factors that prove a principal's identity. Verifier material is one-way: nothing in this package can recover a password from what it stores.

Passkeys.

A passkey is the only factor SESAME supports that is phishing-resistant by construction. The browser signs over the origin it is actually talking to, so a convincing replica of the login page cannot obtain a usable assertion — the signature it collects names the attacker's origin and fails here.

Scope is deliberately narrow and stated rather than implied:

  • attestation format "none" only. Attestation statements assert what kind of hardware holds the key; verifying them means shipping and rotating vendor root certificates, and for passkeys the platform guidance is not to require it. SESAME refuses any other format rather than accepting it unverified, which would be worse than not asking.
  • COSE ES256 only, matching the token signing boundary. One algorithm means nothing to negotiate and nothing to confuse.

Everything security-relevant in an assertion is checked here: the challenge is single-use and supplied by the engine, the origin and RP ID must match the deployment exactly, the user-presence flag must be set, and a sign counter that fails to advance is treated as a cloned authenticator.

Index

Constants

View Source
const (
	// KindPassword is the only authenticator kind in this slice.
	KindPassword = "password"

	// EventPasswordSet records a password verifier being set or replaced.
	EventPasswordSet = "authenticator.password_set"

	// MinPasswordLength follows NIST SP 800-63B: length is the primary
	// strength factor and composition rules are counterproductive.
	MinPasswordLength = 12
	// MaxPasswordLength bounds the hashing work an unauthenticated caller
	// can request.
	MaxPasswordLength = 1024
)
View Source
const (
	// KindRecoveryCode is a single-use backup factor.
	KindRecoveryCode = "recovery_code"

	// EventRecoveryCodesIssued records a freshly generated set, replacing
	// any previous one.
	EventRecoveryCodesIssued = "authenticator.recovery_codes_issued"
	// EventRecoveryCodeUsed records one code being spent.
	EventRecoveryCodeUsed = "authenticator.recovery_code_used"

	// RecoveryCodeCount is how many codes an issue produces.
	RecoveryCodeCount = 10
)

Recovery codes are the way back in when the second-factor device is gone. Without them, losing a phone means an operator has to disable MFA by hand, which is both a support burden and the weakest link an attacker will aim at.

View Source
const (
	// KindTOTP is a time-based one-time password authenticator.
	KindTOTP = "totp"

	// EventTOTPEnrolled records an enrolled but not yet usable authenticator.
	EventTOTPEnrolled = "authenticator.totp_enrolled"
	// EventTOTPActivated records the enrollment being proven and made usable.
	EventTOTPActivated = "authenticator.totp_activated"
	// EventTOTPUsed records the counter consumed by a successful code, which
	// is what makes replay detectable.
	EventTOTPUsed = "authenticator.totp_used"

	// TOTPDigits is the code length every authenticator app expects.
	TOTPDigits = 6
	// TOTPPeriodSeconds is the time step.
	TOTPPeriodSeconds = 30
	// TOTPDriftSteps accepts one step either side of now, tolerating about
	// 30 seconds of clock skew in each direction. Widening this multiplies
	// the codes valid at any instant, so it stays at the RFC's suggestion.
	TOTPDriftSteps = 1
)

TOTP as specified by RFC 6238.

HMAC-SHA1 is the algorithm every authenticator app implements, and RFC 6238 still specifies it. SHA-1's collision weaknesses do not apply to HMAC, and choosing an algorithm nothing can enrol would be worse security than one whose weakness is irrelevant here.

View Source
const (
	// EventPasskeyRegistered records a new passkey bound to a principal.
	EventPasskeyRegistered = "authenticator.passkey_registered"
	// EventPasskeyUsed records a successful assertion. It carries the new
	// sign counter, which is what makes clone detection survive a restart.
	EventPasskeyUsed = "authenticator.passkey_used"
	// EventPasskeyRemoved records a durable, replay-safe unregistration —
	// the response to a lost or stolen authenticator.
	EventPasskeyRemoved = "authenticator.passkey_removed"

	// AttestationNone is the only attestation format SESAME accepts.
	AttestationNone = "none"
)
View Source
const (
	// SealedSecretKeyBytes is the required AES-256 key length.
	SealedSecretKeyBytes = 32
)

Variables

View Source
var (
	ErrPasskeyUnsupportedAttestation = errors.New("only the none attestation format is supported")
	ErrPasskeyUnsupportedAlgorithm   = errors.New("only COSE ES256 passkeys are supported")
	ErrPasskeyInvalidClientData      = errors.New("passkey client data is not valid for this request")
	ErrPasskeyInvalidAuthData        = errors.New("passkey authenticator data is not valid")
	ErrPasskeyInvalidSignature       = errors.New("passkey assertion signature is not valid")
	ErrPasskeyCloned                 = errors.New("passkey sign counter did not advance; the authenticator may be cloned")
)

Stable passkey errors.

View Source
var CurrentParameters = Parameters{Memory: 64 * 1024, Iterations: 1, Parallelism: 4}

CurrentParameters is the cost this binary hashes with. Raising any value makes existing verifiers eligible for transparent upgrade on next use.

64 MiB with one pass and four lanes is the OWASP-documented Argon2id configuration; the memory cost is what makes GPU attack expensive.

View Source
var ErrNoSealingKey = errors.New("no secret sealing key is configured; run sesame init to create a deployment")

ErrNoSealingKey reports an operation that needs the deployment's secret key when none is configured. Enrolling a recoverable credential without a key would mean storing it in the clear, so it fails closed instead.

Functions

func MatchRecoveryCode

func MatchRecoveryCode(digests []string, code string) (string, bool)

MatchRecoveryCode finds the digest a presented code satisfies, comparing every candidate in constant time so a match does not reveal its position.

func NewPasskeyChallenge

func NewPasskeyChallenge() (string, error)

NewPasskeyChallenge returns a fresh challenge for a registration or assertion. The engine supplies it; a challenge chosen by the browser would let a replayed assertion pass.

func NewPasswordVerifier

func NewPasswordVerifier(password string) (string, error)

NewPasswordVerifier hashes a password into an encoded Argon2id verifier using the current parameters and a fresh random salt.

func NewRecoveryCodes

func NewRecoveryCodes() (codes []string, digests []string, err error)

NewRecoveryCodes generates a fresh set and their digests. The two slices are index-aligned.

func NewTOTPSecret

func NewTOTPSecret() (string, error)

NewTOTPSecret returns a fresh base32 shared secret.

func NormalizeRecoveryCode

func NormalizeRecoveryCode(code string) string

NormalizeRecoveryCode makes retyping forgiving without weakening the code: case and separators carry no entropy.

func Open

func Open(key []byte, sealed string) (string, error)

Open decrypts a sealed secret. A wrong key, a truncated value, or any tampering fails rather than returning a plausible-looking secret.

func RecoveryCodeDigest

func RecoveryCodeDigest(code string) string

RecoveryCodeDigest hashes a code for storage and comparison.

SHA-256 without a password-hashing construction is deliberate: a code is 80 bits of uniform randomness, so there is no guessable input space to slow an attacker through.

func RelyingPartyID

func RelyingPartyID(issuer string) (string, error)

RelyingPartyID derives the RP ID from an issuer URL. WebAuthn scopes a credential to a domain, and the issuer's host is the domain SESAME already identifies itself by.

func Seal

func Seal(key []byte, plaintext string) (string, error)

Seal encrypts a recoverable secret for storage. The result is a versioned, self-describing string safe to place in a security event.

func TOTPCode

func TOTPCode(secret string, counter int64) (string, error)

TOTPCode computes the code for one counter.

func TOTPCounter

func TOTPCounter(now time.Time) int64

TOTPCounter returns the time-step counter for an instant.

func TOTPProvisioningURI

func TOTPProvisioningURI(issuer, account, secret string) string

TOTPProvisioningURI builds the otpauth URI an authenticator app scans.

The issuer and account label are shown to the person enrolling, so the account uses their identifier. The URI carries the secret and is therefore as sensitive as the secret itself.

func ValidateCredentialID

func ValidateCredentialID(id string) error

ValidateCredentialID rejects values that cannot be credential identifiers.

func ValidatePassword

func ValidatePassword(password string) error

ValidatePassword rejects passwords SESAME will not accept. It reports nothing about the password itself beyond why it was rejected.

func ValidateRecoveryDigests

func ValidateRecoveryDigests(digests []string) error

ValidateRecoveryDigests rejects a malformed stored set.

func ValidateTOTPSecret

func ValidateTOTPSecret(secret string) error

ValidateTOTPSecret rejects secrets SESAME will not accept.

func VerifyPassword

func VerifyPassword(verifier, password string) (matched bool, needsUpgrade bool, err error)

VerifyPassword checks a password against an encoded verifier in constant time and reports whether the verifier should be rehashed because it was produced with weaker parameters than the current ones.

A malformed verifier is an error, never a silent false: a deployment whose stored credentials cannot be parsed must fail closed rather than deny every login as if the passwords were wrong.

func VerifyTOTPCode

func VerifyTOTPCode(
	secret string,
	code string,
	now time.Time,
	lastCounter int64,
) (matched bool, counter int64, err error)

VerifyTOTPCode checks a code against the secret within the drift window and returns the counter it consumed.

lastCounter is the highest counter already spent by this authenticator. Codes at or below it are rejected even when otherwise valid, so a code observed in transit cannot be replayed during the rest of its own window. The comparison is constant-time.

Types

type AssertedPasskey

type AssertedPasskey struct {
	SignCount    uint32
	UserVerified bool
}

AssertedPasskey is the result of verifying an assertion.

func VerifyPasskeyAssertion

func VerifyPasskeyAssertion(
	stored Passkey,
	authenticatorDataRaw []byte,
	clientDataJSON []byte,
	signature []byte,
	expectedChallenge string,
	expectedOrigin string,
	relyingPartyID string,
) (AssertedPasskey, error)

VerifyPasskeyAssertion checks a signed assertion against a stored credential.

The signature covers the authenticator data concatenated with the SHA-256 of the client data, so one signature commits to the RP ID, the flags, the counter, the challenge, and the origin together. None of them can be substituted independently.

type Parameters

type Parameters struct {
	Memory      uint32 `json:"memory_kib"`
	Iterations  uint32 `json:"iterations"`
	Parallelism uint8  `json:"parallelism"`
}

Parameters are Argon2id cost parameters. They are stored with every verifier so a deployment can raise them without invalidating existing credentials.

func (Parameters) AtLeast

func (p Parameters) AtLeast(other Parameters) bool

AtLeast reports whether these parameters are no weaker than other in every dimension.

type Passkey

type Passkey struct {
	CredentialID string `json:"credential_id"`
	PrincipalID  string `json:"principal_id"`
	TenantID     string `json:"tenant_id"`
	PublicKey    string `json:"public_key"`
	SignCount    uint32 `json:"sign_count"`
	// UserVerified records whether the authenticator verified the user at
	// registration. It is advisory; each assertion carries its own flag.
	UserVerified bool   `json:"user_verified"`
	RegisteredAt string `json:"registered_at"`
}

Passkey is one registered credential.

PublicKey is the stored ES256 key in uncompressed SEC1 form. SignCount is the last counter the authenticator reported; it is the only mutable part.

type PasskeyRegisteredPayload

type PasskeyRegisteredPayload struct {
	CredentialID string `json:"credential_id"`
	PrincipalID  string `json:"principal_id"`
	TenantID     string `json:"tenant_id"`
	PublicKey    string `json:"public_key"`
	SignCount    uint32 `json:"sign_count"`
	UserVerified bool   `json:"user_verified"`
	RegisteredAt string `json:"registered_at"`
}

PasskeyRegisteredPayload is the versioned payload of EventPasskeyRegistered. Every field is a scalar, per FYLO's document model.

type PasskeyRemovedPayload

type PasskeyRemovedPayload struct {
	CredentialID string `json:"credential_id"`
	PrincipalID  string `json:"principal_id"`
	TenantID     string `json:"tenant_id"`
}

PasskeyRemovedPayload is the versioned payload of EventPasskeyRemoved.

type PasskeyUsedPayload

type PasskeyUsedPayload struct {
	CredentialID string `json:"credential_id"`
	PrincipalID  string `json:"principal_id"`
	TenantID     string `json:"tenant_id"`
	SignCount    uint32 `json:"sign_count"`
	UserVerified bool   `json:"user_verified"`
}

PasskeyUsedPayload is the versioned payload of EventPasskeyUsed.

type PasswordSetPayload

type PasswordSetPayload struct {
	PrincipalID string `json:"principal_id"`
	TenantID    string `json:"tenant_id"`
	Verifier    string `json:"verifier"`
}

PasswordSetPayload is the versioned payload of an EventPasswordSet event. It carries the verifier, never the password.

type RecoveryCodeSet

type RecoveryCodeSet struct {
	Codes []string `json:"codes"`
}

RecoveryCodeSet is returned once, at issue. The plaintext codes exist only here; afterwards only their digests are durable.

type RecoveryCodeUsedPayload

type RecoveryCodeUsedPayload struct {
	PrincipalID string `json:"principal_id"`
	TenantID    string `json:"tenant_id"`
	Digest      string `json:"digest"`
}

RecoveryCodeUsedPayload records one spent code by its digest, which is what makes reuse detectable after a restart.

type RecoveryCodesIssuedPayload

type RecoveryCodesIssuedPayload struct {
	PrincipalID string   `json:"principal_id"`
	TenantID    string   `json:"tenant_id"`
	Digests     []string `json:"digests"`
}

RecoveryCodesIssuedPayload is the versioned payload of an issue event. It carries digests, never the codes.

type RegisteredPasskey

type RegisteredPasskey struct {
	CredentialID string
	PublicKey    string
	SignCount    uint32
	UserVerified bool
}

RegisteredPasskey is the result of verifying a registration.

func VerifyPasskeyRegistration

func VerifyPasskeyRegistration(
	attestationObject []byte,
	clientDataJSON []byte,
	expectedChallenge string,
	expectedOrigin string,
	relyingPartyID string,
) (RegisteredPasskey, error)

VerifyPasskeyRegistration checks an attestation object and returns the credential to store.

The attestation format must be "none". Any other format is refused rather than accepted without verifying its statement, because an unverified attestation is a claim about hardware that nothing checked.

type TOTPActivatedPayload

type TOTPActivatedPayload struct {
	PrincipalID string `json:"principal_id"`
	TenantID    string `json:"tenant_id"`
	Counter     int64  `json:"counter"`
}

TOTPActivatedPayload is the versioned payload of an EventTOTPActivated event.

type TOTPEnrolledPayload

type TOTPEnrolledPayload struct {
	PrincipalID  string `json:"principal_id"`
	TenantID     string `json:"tenant_id"`
	SealedSecret string `json:"sealed_secret"`
}

TOTPEnrolledPayload is the versioned payload of an EventTOTPEnrolled event. It carries the sealed secret, never the plaintext.

type TOTPEnrollment

type TOTPEnrollment struct {
	Secret          string `json:"secret"`
	ProvisioningURI string `json:"provisioning_uri"`
}

TOTPEnrollment is returned once, at enrollment. The secret is shown to its owner exactly here; afterwards only the sealed form is durable.

type TOTPUsedPayload

type TOTPUsedPayload struct {
	PrincipalID string `json:"principal_id"`
	TenantID    string `json:"tenant_id"`
	Counter     int64  `json:"counter"`
}

TOTPUsedPayload records the time-step counter a successful code consumed.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL