crypto

package
v1.0.4 Latest Latest
Warning

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

Go to latest
Published: Aug 21, 2026 License: MIT Imports: 33 Imported by: 0

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

View Source
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
)
View Source
const AllowedAlgorithm = "RS256"

AllowedAlgorithm is the only signing algorithm name we accept.

View Source
const (
	// MaxJWTSize is the maximum allowed JWT size in bytes (8KB).
	MaxJWTSize = 8 * 1024
)

Variables

View Source
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.

View Source
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

func BuildOTPAuthURL(secret, issuer, accountName string) string

BuildOTPAuthURL builds an otpauth:// URL for QR code generation.

func CompareFingerprints

func CompareFingerprints(a, b string) bool

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

func ComputeJWKThumbprint(key crypto.PublicKey) (string, error)

ComputeJWKThumbprint computes the RFC 7638 JWK Thumbprint of a public key.

func Decrypt

func Decrypt(ciphertext, key []byte, aad ...[]byte) ([]byte, error)

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

func Encrypt(plaintext, key []byte, aad ...[]byte) ([]byte, error)

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

func EncryptRecovery(pub *rsa.PublicKey, plaintext, binding []byte) ([]byte, error)

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

func GenerateTOTPCode(secret string, t time.Time) (string, error)

GenerateTOTPCode generates the current TOTP code for the given secret.

func GenerateTOTPSecret

func GenerateTOTPSecret() (string, error)

GenerateTOTPSecret generates a 20-byte base32-encoded TOTP secret.

func HMACSign

func HMACSign(message, key []byte) string

HMACSign computes HMAC-SHA256 of message with key, returns hex-encoded.

func HMACVerify

func HMACVerify(message, key []byte, signature string) bool

HMACVerify checks that the hex-encoded signature matches the HMAC-SHA256 of message with key. Uses constant-time comparison.

func HashPassword

func HashPassword(password string, pepper ...string) (string, error)

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

func KIDFromPublicKey(pub *rsa.PublicKey) string

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

func LoadRSAPublicKeyPEM(pemData []byte) (*rsa.PublicKey, error)

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

func RandomBytes(n int) ([]byte, error)

RandomBytes returns n cryptographically secure random bytes.

func RandomHex

func RandomHex(n int) (string, error)

RandomHex returns a hex-encoded string of n random bytes (2n hex chars).

func RandomToken

func RandomToken(n int) (string, error)

RandomToken generates a URL-safe token of n random bytes, hex-encoded.

func RandomUUID

func RandomUUID() (string, error)

RandomUUID generates a v4 UUID from crypto/rand.

func RecoveryBinding added in v1.0.3

func RecoveryBinding(recordID, pseudonym string) []byte

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

func SHA256Base64URL(s string) string

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 SHA256Hex

func SHA256Hex(s string) string

SHA256Hex returns the hex-encoded SHA-256 hash of the input string.

func SecureCompare

func SecureCompare(a, b string) bool

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

func SerializeJWKSJSON(keys map[string]*rsa.PublicKey) ([]byte, error)

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

func ValidateTOTPCode(secret, code string, t time.Time) (int64, error)

ValidateTOTPCode validates a TOTP code with ±1 period skew. Returns the time step that matched, or -1 if no match.

func VerifyPassword

func VerifyPassword(password, encoded string, pepper ...string) (bool, error)

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 JWKS

type JWKS struct {
	Keys []JWK `json:"keys"`
}

JWKS represents a JSON Web Key Set.

func SerializeJWKS

func SerializeJWKS(keys map[string]*rsa.PublicKey) JWKS

SerializeJWKS converts RSA public keys into a JWKS structure. Keys are sorted by kid for deterministic output.

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

Jump to

Keyboard shortcuts

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