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
- Variables
- func MatchRecoveryCode(digests []string, code string) (string, bool)
- func NewPasskeyChallenge() (string, error)
- func NewPasswordVerifier(password string) (string, error)
- func NewRecoveryCodes() (codes []string, digests []string, err error)
- func NewTOTPSecret() (string, error)
- func NormalizeRecoveryCode(code string) string
- func Open(key []byte, sealed string) (string, error)
- func RecoveryCodeDigest(code string) string
- func RelyingPartyID(issuer string) (string, error)
- func Seal(key []byte, plaintext string) (string, error)
- func TOTPCode(secret string, counter int64) (string, error)
- func TOTPCounter(now time.Time) int64
- func TOTPProvisioningURI(issuer, account, secret string) string
- func ValidateCredentialID(id string) error
- func ValidatePassword(password string) error
- func ValidateRecoveryDigests(digests []string) error
- func ValidateTOTPSecret(secret string) error
- func VerifyPassword(verifier, password string) (matched bool, needsUpgrade bool, err error)
- func VerifyTOTPCode(secret string, code string, now time.Time, lastCounter int64) (matched bool, counter int64, err error)
- type AssertedPasskey
- type Parameters
- type Passkey
- type PasskeyRegisteredPayload
- type PasskeyRemovedPayload
- type PasskeyUsedPayload
- type PasswordSetPayload
- type RecoveryCodeSet
- type RecoveryCodeUsedPayload
- type RecoveryCodesIssuedPayload
- type RegisteredPasskey
- type TOTPActivatedPayload
- type TOTPEnrolledPayload
- type TOTPEnrollment
- type TOTPUsedPayload
Constants ¶
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 )
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.
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.
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" )
const (
// SealedSecretKeyBytes is the required AES-256 key length.
SealedSecretKeyBytes = 32
)
Variables ¶
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.
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.
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 ¶
MatchRecoveryCode finds the digest a presented code satisfies, comparing every candidate in constant time so a match does not reveal its position.
func NewPasskeyChallenge ¶
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 ¶
NewPasswordVerifier hashes a password into an encoded Argon2id verifier using the current parameters and a fresh random salt.
func NewRecoveryCodes ¶
NewRecoveryCodes generates a fresh set and their digests. The two slices are index-aligned.
func NewTOTPSecret ¶
NewTOTPSecret returns a fresh base32 shared secret.
func NormalizeRecoveryCode ¶
NormalizeRecoveryCode makes retyping forgiving without weakening the code: case and separators carry no entropy.
func Open ¶
Open decrypts a sealed secret. A wrong key, a truncated value, or any tampering fails rather than returning a plausible-looking secret.
func RecoveryCodeDigest ¶
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 ¶
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 ¶
Seal encrypts a recoverable secret for storage. The result is a versioned, self-describing string safe to place in a security event.
func TOTPCounter ¶
TOTPCounter returns the time-step counter for an instant.
func TOTPProvisioningURI ¶
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 ¶
ValidateCredentialID rejects values that cannot be credential identifiers.
func ValidatePassword ¶
ValidatePassword rejects passwords SESAME will not accept. It reports nothing about the password itself beyond why it was rejected.
func ValidateRecoveryDigests ¶
ValidateRecoveryDigests rejects a malformed stored set.
func ValidateTOTPSecret ¶
ValidateTOTPSecret rejects secrets SESAME will not accept.
func VerifyPassword ¶
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 ¶
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.