Documentation
¶
Overview ¶
Package crypto provides cryptographic primitives for the Vault auth service, including AES-256-GCM encryption, Argon2id password hashing, HMAC-SHA256, RS256 JWT signing and validation, TOTP, DPoP proof verification, device fingerprinting, and secure random generation.
Index ¶
- Constants
- Variables
- func Argon2ActiveCount() int64
- func Argon2MaxConcurrent() int
- func Argon2MaxVerifyMemory() uint32
- func Argon2RejectedCount() int64
- func Argon2WaitNanos() int64
- func Argon2WaitingCount() int64
- func BuildOTPAuthURL(secret, issuer, accountName string) string
- func CompareFingerprints(a, b string) bool
- func ComputeFingerprint(input FingerprintInput) string
- func ComputeJWKThumbprint(key crypto.PublicKey) (string, error)
- func Decrypt(ciphertext, key []byte, aad ...[]byte) ([]byte, error)
- func DecryptRecovery(priv *rsa.PrivateKey, blob, binding []byte) ([]byte, error)
- func DecryptRecoveryLegacy(priv *rsa.PrivateKey, blob []byte) ([]byte, error)
- func Encrypt(plaintext, key []byte, aad ...[]byte) ([]byte, error)
- func EncryptRecovery(pub *rsa.PublicKey, plaintext, binding []byte) ([]byte, error)
- func GenerateRSAKeyPair() (*rsa.PrivateKey, error)
- func GenerateTOTPCode(secret string, t time.Time) (string, error)
- func GenerateTOTPSecret() (string, error)
- func HMACSign(message, key []byte) string
- func HMACVerify(message, key []byte, signature string) bool
- func HashPassword(password string, pepper ...string) (string, error)
- func KIDFromPublicKey(pub *rsa.PublicKey) string
- func LoadRSAPrivateKeyPEM(pemData []byte) (*rsa.PrivateKey, error)
- func LoadRSAPublicKeyPEM(pemData []byte) (*rsa.PublicKey, error)
- func LoadSigningKeyPEM(pemData []byte) (*rsa.PrivateKey, string, error)
- func MarshalSigningKeyPEM(key *rsa.PrivateKey) ([]byte, error)
- func RandomBytes(n int) ([]byte, error)
- func RandomHex(n int) (string, error)
- func RandomToken(n int) (string, error)
- func RandomUUID() (string, error)
- func RecoveryBinding(recordID, pseudonym string) []byte
- func SHA256Base64URL(s string) string
- func SHA256Hex(s string) string
- func SecureCompare(a, b string) bool
- func SerializeJWKSJSON(keys map[string]*rsa.PublicKey) ([]byte, error)
- func SignToken(claims VaultClaims, privateKey *rsa.PrivateKey, kid string) (string, error)
- func ValidateDPoPProof(proofString, httpMethod, httpURI string, accessTokenHash string) (string, string, error)
- func ValidateTOTPCode(secret, code string, t time.Time) (int64, error)
- func VerifyPassword(password, encoded string, pepper ...string) (bool, error)
- type Confirmation
- type DPoPClaims
- type FingerprintInput
- type JWK
- type JWKS
- type RecoveryFormat
- type VaultClaims
Constants ¶
const ( // DPoPMaxAge is the maximum age of a DPoP proof (5 minutes). DPoPMaxAge = 5 * time.Minute // DPoPMaxSize is the maximum DPoP proof size in bytes. DPoPMaxSize = 4 * 1024 )
const AllowedAlgorithm = "RS256"
AllowedAlgorithm is the only signing algorithm name we accept.
const (
// MaxJWTSize is the maximum allowed JWT size in bytes (8KB).
MaxJWTSize = 8 * 1024
)
Variables ¶
var DummyHash string
DummyHash is an Argon2id hash used for constant-time user enumeration prevention. When a user is not found, VerifyPassword is called with this hash to burn the same CPU time as a real verification. Generated at startup with a random salt to avoid recognizable memory patterns.
The variable is assigned once in init and never reassigned, so the unsynchronized reads in other packages are race-free. It acts as a sentinel: VerifyPassword substitutes the current rotating dummy hash for it, and a background loop re-derives that hash on a slow timer so the dummy salt does not stay fixed for the process lifetime.
var ErrArgon2Overloaded = errors.New("argon2: too many concurrent hashing operations")
ErrArgon2Overloaded is returned when the argon2id semaphore cannot be acquired within the timeout, indicating the server is under heavy load.
Functions ¶
func Argon2ActiveCount ¶
func Argon2ActiveCount() int64
Argon2ActiveCount returns the current number of in-flight argon2id operations.
func Argon2MaxConcurrent ¶
func Argon2MaxConcurrent() int
Argon2MaxConcurrent returns the semaphore capacity.
func Argon2MaxVerifyMemory ¶ added in v1.0.3
func Argon2MaxVerifyMemory() uint32
Argon2MaxVerifyMemory returns the memory ceiling, in KiB, that a stored hash may declare. Exported so a test can build a worst-case hash from the real bound instead of copying the number, which is how the client-backpressure fixture silently stopped being verifiable when the ceiling moved.
func Argon2RejectedCount ¶
func Argon2RejectedCount() int64
Argon2RejectedCount returns the total number of rejected argon2id requests.
func Argon2WaitNanos ¶ added in v1.0.3
func Argon2WaitNanos() int64
Argon2WaitNanos returns the cumulative time callers have spent waiting for a semaphore slot. Divided by the number of operations it gives mean queueing delay, which is the number an alert should be written against.
func Argon2WaitingCount ¶ added in v1.0.3
func Argon2WaitingCount() int64
Argon2WaitingCount returns how many callers are currently queued for a semaphore slot. It rises as soon as the service starts queueing, which is the point at which logins start getting slower, rather than at the point work is finally refused.
func BuildOTPAuthURL ¶
BuildOTPAuthURL builds an otpauth:// URL for QR code generation.
func CompareFingerprints ¶
CompareFingerprints compares two fingerprints using constant-time comparison. Fingerprints are already lowercase hex from ComputeFingerprint, so no normalization is needed — SecureCompare handles the constant-time check.
func ComputeFingerprint ¶
func ComputeFingerprint(input FingerprintInput) string
ComputeFingerprint computes SHA256 over length-prefixed fields to prevent separator collision attacks (where a field containing the separator character could produce the same hash as a different combination of fields).
func ComputeJWKThumbprint ¶
ComputeJWKThumbprint computes the RFC 7638 JWK Thumbprint of a public key.
func Decrypt ¶
Decrypt decrypts AES-256-GCM ciphertext (nonce || ciphertext). Key must be exactly 32 bytes. Optional aad must match the value used during encryption.
func DecryptRecovery ¶ added in v0.8.0
func DecryptRecovery(priv *rsa.PrivateKey, blob, binding []byte) ([]byte, error)
DecryptRecovery is the inverse of EncryptRecovery. It is used by the offline recovery tool (cmd/recover), never by the running server, which holds no private key.
It reads the bound format only. A legacy blob is refused here and must go through DecryptRecoveryLegacy, so the caller can never open one without knowing that is what it did.
func DecryptRecoveryLegacy ¶ added in v1.0.3
func DecryptRecoveryLegacy(priv *rsa.PrivateKey, blob []byte) ([]byte, error)
DecryptRecoveryLegacy reads an escrow record written before the payload was bound to its row: nil OAEP label, no AAD, and a profile that does not name its own subject.
It exists for one reason. Escrow records already in auth.account_recovery are the only recoverable copy of the accounts they describe, and refusing to read them would destroy the recoverability of every erasure performed before the binding shipped - the exact opposite of what this subsystem is for.
It is bounded on three sides, deliberately:
- The name. Every legacy read in the tree is one grep away, and there is one caller (cmd/recover). There is no legacy WRITER anywhere in the product: nothing can create a new unbound record.
- The clock. auth.account_recovery is swept by VAULT_RECOVERY_RETENTION_DAYS. Once the last record written before the binding shipped has aged past that horizon, this function and its call site can be deleted outright, and the tests that pin the legacy framing go with them.
- The operator. cmd/recover --allow-legacy=false refuses these records, so a deployment that believes it has no legacy rows left can prove it before the code is removed.
What it cannot do is verify anything. A legacy blob is not bound to its row, so a record read through this path carries no evidence that its deleted_at, deleted_by and reason belong to the profile inside it. Callers must say so.
func Encrypt ¶
Encrypt encrypts plaintext using AES-256-GCM with a random nonce. Key must be exactly 32 bytes. Returns nonce || ciphertext. Optional aad (Additional Authenticated Data) binds the ciphertext to a context (e.g., user ID or record ID) so it cannot be swapped between owners.
func EncryptRecovery ¶ added in v0.8.0
EncryptRecovery encrypts plaintext for recovery escrow under the given RSA public key, sealed to binding. The returned blob can only be decrypted with the matching private key AND the same binding, via DecryptRecovery.
binding is a required argument rather than a variadic option on purpose. The vulnerability this function was rewritten to fix was precisely an optional AAD that a call site did not pass, and an optional binding would leave that door open for the next caller. There is deliberately no way to write an unbound escrow record from this package any more.
func GenerateRSAKeyPair ¶
func GenerateRSAKeyPair() (*rsa.PrivateKey, error)
GenerateRSAKeyPair generates a 2048-bit RSA key pair.
func GenerateTOTPCode ¶
GenerateTOTPCode generates the current TOTP code for the given secret.
func GenerateTOTPSecret ¶
GenerateTOTPSecret generates a 20-byte base32-encoded TOTP secret.
func HMACVerify ¶
HMACVerify checks that the hex-encoded signature matches the HMAC-SHA256 of message with key. Uses constant-time comparison.
func HashPassword ¶
HashPassword hashes a password using Argon2id with spec-mandated parameters. If pepper is provided and non-empty, the password is pre-hashed with HMAC-SHA256(pepper, password) before Argon2id to bind hashes to a server secret. Returns a PHC-format string: $argon2id$v=19$m=47104,t=1,p=1$<salt>$<hash>
Concurrent calls are limited by an internal semaphore (4 max) to prevent OOM under load. Returns ErrArgon2Overloaded if the semaphore cannot be acquired.
func KIDFromPublicKey ¶
KIDFromPublicKey derives a deterministic key ID from the RSA public key. Format: first 16 hex chars of SHA-256 over the PKIX DER encoding, split as xxxxxxxx-xxxxxxxx.
The DER covers both the modulus and the exponent. Hashing N alone meant two keys sharing a modulus but differing in exponent produced the same kid, and keystore.Import upserts ON CONFLICT (kid) DO UPDATE, so importing the second overwrote the first key's private material in place. Reaching it needs admin import of a crafted key, which is why this is hardening rather than a live break, but the fix costs nothing.
Changing the derivation does not disturb existing keys. Both call sites derive the kid once, when a key is generated or imported, and store it; nothing recomputes a kid and compares it against a stored one, so keys already in the keystore keep the id they were filed under and the JWKS keeps publishing it.
func LoadRSAPrivateKeyPEM ¶ added in v0.8.0
func LoadRSAPrivateKeyPEM(pemData []byte) (*rsa.PrivateKey, error)
LoadRSAPrivateKeyPEM parses an RSA private key from PEM data. It accepts both PKCS#8 ("BEGIN PRIVATE KEY") and PKCS#1 ("BEGIN RSA PRIVATE KEY") encodings. Used by the offline recovery tool.
func LoadRSAPublicKeyPEM ¶ added in v0.8.0
LoadRSAPublicKeyPEM parses an RSA public key from PEM data. It accepts both PKIX ("BEGIN PUBLIC KEY") and PKCS#1 ("BEGIN RSA PUBLIC KEY") encodings.
func LoadSigningKeyPEM ¶
func LoadSigningKeyPEM(pemData []byte) (*rsa.PrivateKey, string, error)
LoadSigningKeyPEM parses an RSA private key from PKCS#8 PEM data and derives a deterministic kid from the public key modulus. Used to share the same signing key across all pods for horizontal scaling.
func MarshalSigningKeyPEM ¶
func MarshalSigningKeyPEM(key *rsa.PrivateKey) ([]byte, error)
MarshalSigningKeyPEM encodes an RSA private key as PKCS#8 PEM.
func RandomBytes ¶
RandomBytes returns n cryptographically secure random bytes.
func RandomToken ¶
RandomToken generates a URL-safe token of n random bytes, hex-encoded.
func RandomUUID ¶
RandomUUID generates a v4 UUID from crypto/rand.
func RecoveryBinding ¶ added in v1.0.3
RecoveryBinding builds the context bytes an escrow blob is sealed to, from the two columns of auth.account_recovery that identify the row: its primary key and the subject pseudonym.
It must produce identical bytes on the write side (internal/service/erasure.go, which holds the values before they reach the database) and on the read side (cmd/recover, which reads them back out of the row). A divergence between the two does not misbehave subtly: every record stops decrypting, which is the whole recoverability of every erasure. That is why this lives here, called from both sides, rather than being spelled out twice.
Two normalisations earn their keep:
- recordID is lowercased. It is written as a Go string and read back through PostgreSQL's UUID type, which has its own canonical text output. A producer that ever emitted uppercase hex would seal blobs no reader could open, because PostgreSQL would hand the reader the lowercase form of the same UUID. Case is not meaningful in a UUID, so folding it costs nothing and removes the trap.
- The fields are NUL-separated and domain-prefixed. Plain concatenation would let a record id ending in part of a pseudonym produce the same bytes as a different (id, pseudonym) pair; neither value can contain a NUL, so the encoding is unambiguous.
func SHA256Base64URL ¶
SHA256Base64URL returns the base64url-encoded (no padding) SHA-256 hash of the input string. Used for DPoP access token hash (ath) per RFC 9449 §4.2.
func SecureCompare ¶
SecureCompare performs constant-time comparison of two strings. Returns true if they are equal. When lengths differ, burns constant time to avoid leaking length information, then returns false.
This is the only comparison helper this package exports. A byte-slice sibling, SecureCompareBytes, sat beside it with no caller outside tests while the three production sites that do compare byte slices constant-time -- the Argon2id verify in argon2.go, the HIBP suffix match, and the bridge admin token -- each call subtle.ConstantTimeCompare directly.
Timing note: the early-return on length mismatch is not a practical timing concern for current callers, which always compare same-length values (hex- encoded hashes, HMAC signatures, UUIDs). The burn loop is a defense-in-depth measure in case a future caller compares variable-length inputs.
func SerializeJWKSJSON ¶
SerializeJWKSJSON returns the JWKS as JSON bytes.
func SignToken ¶
func SignToken(claims VaultClaims, privateKey *rsa.PrivateKey, kid string) (string, error)
SignToken creates a signed RS256 JWT with the given claims and key ID.
func ValidateDPoPProof ¶
func ValidateDPoPProof(proofString, httpMethod, httpURI string, accessTokenHash string) (string, string, error)
ValidateDPoPProof validates a DPoP proof JWT per RFC 9449. Returns the JWK thumbprint of the proof's public key and the JTI claim for replay prevention.
func ValidateTOTPCode ¶
ValidateTOTPCode validates a TOTP code with ±1 period skew. Returns the time step that matched, or -1 if no match.
func VerifyPassword ¶
VerifyPassword checks a password against an Argon2id PHC-format hash. If pepper is provided, the password is pre-hashed with HMAC-SHA256(pepper, password) before verification (must match the pepper used during hashing). Always runs the full Argon2id computation regardless of validity (constant-time).
Concurrent calls are limited by an internal semaphore (4 max) to prevent OOM under load. Returns ErrArgon2Overloaded if the semaphore cannot be acquired.
Types ¶
type Confirmation ¶
type Confirmation struct {
JKT string `json:"jkt,omitempty"` // JWK SHA-256 Thumbprint
}
Confirmation holds DPoP proof-of-possession binding (RFC 9449).
type DPoPClaims ¶
type DPoPClaims struct {
vjwt.RegisteredClaims
HTM string `json:"htm"` // HTTP method
HTU string `json:"htu"` // HTTP URI
ATH string `json:"ath,omitempty"` // access token hash (for resource requests)
}
DPoPClaims represents claims in a DPoP proof JWT (RFC 9449).
type FingerprintInput ¶
type FingerprintInput struct {
IP string
UserAgent string
AcceptLanguage string
TLSFingerprint string
}
FingerprintInput contains the components used to compute a device fingerprint.
type JWK ¶
type JWK struct {
KTY string `json:"kty"`
Use string `json:"use"`
KID string `json:"kid"`
ALG string `json:"alg"`
N string `json:"n"`
E string `json:"e"`
}
JWK represents a JSON Web Key for JWKS serialization.
type RecoveryFormat ¶ added in v1.0.3
type RecoveryFormat int
RecoveryFormat names which escrow serialization a stored blob uses. It exists so cmd/recover can report per record which format it read, and so that the legacy path is a decision the tool makes explicitly and logs, rather than a fallback it stumbles into after a failed decrypt.
const ( // RecoveryFormatUnknown is a blob that is neither framing: too short to // classify, or carrying the magic with an unrecognized version. RecoveryFormatUnknown RecoveryFormat = iota // RecoveryFormatBound is the current format, sealed to a row binding. RecoveryFormatBound // RecoveryFormatLegacy is the pre-binding format: nil OAEP label, no AAD, // and a payload that does not name its own subject. RecoveryFormatLegacy )
func RecoveryBlobFormat ¶ added in v1.0.3
func RecoveryBlobFormat(blob []byte) RecoveryFormat
RecoveryBlobFormat classifies a stored escrow blob by its framing alone. It never touches a key, so it is safe to call on hostile input before deciding what to do with it.
func (RecoveryFormat) String ¶ added in v1.0.3
func (f RecoveryFormat) String() string
type VaultClaims ¶
type VaultClaims struct {
vjwt.RegisteredClaims
Roles []string `json:"roles,omitempty"`
Scopes []string `json:"scopes,omitempty"`
ClientID string `json:"client_id,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"`
Confirmation *Confirmation `json:"cnf,omitempty"`
TokenType string `json:"token_type,omitempty"`
// MintedBy names the client that requested a minted subject assertion. It
// is set only by the mint path and carries no authority: it is attribution
// for a relying party, not a credential. It is deliberately not ClientID,
// which is the claim that marks a client-credentials caller and is read as
// such by the service document store.
MintedBy string `json:"minted_by,omitempty"`
// ACR is the OIDC Core §2 authentication context class reference: the
// assurance level this session reached, as "urn:vault42:aal:N". OIDC leaves
// the value space to the issuer, so the URN is vault42's own and its
// meaning is the NIST SP 800-63B AAL of the same number.
ACR string `json:"acr,omitempty"`
// AMR is the OIDC Core §2 authentication methods reference: the RFC 8176
// values for the authenticators this session actually presented.
AMR []string `json:"amr,omitempty"`
// AuthTime is the OIDC Core §2 auth_time: seconds since the Unix epoch at
// which the end user authenticated, which for a rotated token is when the
// refresh family began rather than when the token was minted. Zero means no
// authentication event is recorded, and the claim is omitted.
AuthTime int64 `json:"auth_time,omitempty"`
// Factors lists the vault42 authenticator methods already completed. It
// appears only on a 2fa_challenge token, where it carries the first factor
// across to the second-factor verify so the completed login knows whether it
// began with a password or with an upstream identity provider. It is not an
// authorization claim and no access token carries it.
Factors []string `json:"factors,omitempty"`
}
VaultClaims extends RegisteredClaims with Vault-specific fields.
func ParseAndValidate ¶
func ParseAndValidate(tokenString string, keyFunc vjwt.Keyfunc, issuer string, audience string) (*VaultClaims, error)
ParseAndValidate parses a JWT string, enforcing: - Max size (8KB) - Algorithm whitelist (RS256 only) - kid is present and non-empty - jku/x5u/x5c/jwk headers are rejected - any crit header is rejected (RFC 7515 4.1.11: no JOSE extensions implemented) - Standard claims (exp, nbf, iss, aud) are validated