crypto

import "github.com/infrapi/kayra/internal/crypto"

The cryptographic concerns Kayra owns itself: the RSA keypair that encrypts personal access token records at rest, loading the RSA signing keys a local: authentication provider trusts, and minting the self-signed bootstrap JWT.

PAT keypair#

priv, err := crypto.LoadOrCreatePatKeypair(s, keys, cfg.Tokens.Pat)

Resolves the RSA keypair tokenapi encrypts every PAT record under. When cfg.PrivateKeyPath names a file, that key is read directly; otherwise a 2048-bit keypair is generated once and persisted in the store at keys.PatKeypair(), so later starts reuse it instead of orphaning every PAT issued so far.

PAT records#

token, err := crypto.GenerateToken()        // "pat_" + 32 random bytes, base64url
crypto.IsToken(s)                           // has the "pat_" prefix?
hash := crypto.HashToken(token)             // sha256 hex — the etcd lookup key
type PatRecord struct {
	UserID             string
	Authorization      string
	Provider           string
	CreatedAt          int64
	SnapshotGroups     []string          // only set for a claims-backed Authorization
	SnapshotAttributes map[string]string // only set for a claims-backed Authorization
}

data, err := crypto.EncryptRecord(pub, record)   // hybrid RSA-OAEP + AES-256-GCM
record, err := crypto.DecryptRecord(priv, data)

A record carries who the token belongs to and which authorizations.<name> resolves it. For an ldap/local backend, roles are always resolved live from the holder’s current group membership at request time — SnapshotGroups/SnapshotAttributes stay nil, nothing is ever written there. The one exception is a claims-backed backend (see config): it has no directory to re-query later by bare user id, so tokenapi captures the caller’s own Groups/Attributes into these two fields at creation time instead, and middleware.resolvePAT reads them back rather than attempting a live lookup that has nowhere to go — the one case a PAT’s roles don’t track a later change in group membership until the token is regenerated. Encryption is hybrid: an AES-256 key seals the JSON record (AES-GCM), and that AES key is itself wrapped with the RSA public key (OAEP) — nobody holding only the etcd data, without the RSA private key, can recover a record.

Self-issued JWTs#

token, err := crypto.MintSelfJWT(key, subject, groups, ttl)

Signs a JWT (RS256/RS384/RS512, picked from key.Algorithm) embedding sub, groups, iat and exp — groups must already be the caller’s real, resolved groups, never an operator-asserted claim. Whether those embedded groups actually matter later depends on the consuming authorizations.<name>: a claims-backed one with from: token reads them straight back out of this JWT; an ldap/local-backed one ignores them entirely and re-resolves live by sub instead (see middleware). key must have a private half (Issuer: true in config); the kid header is set from key.Kid so verification can locate the right key later.

Local signing keys#

keys, err := crypto.LoadSigningKeys(provider.Local)
type SigningKey struct {
	Kid       string
	Algorithm string
	Issuer    bool
	Expiry    int
	Audiences []string
	Private   *rsa.PrivateKey // non-nil only when the PEM held a private key
	Public    *rsa.PublicKey
}

Decodes every entry of an authentifications.<name>.local list. Each entry’s secret is a PEM block — RSA PRIVATE KEY (PKCS1), PRIVATE KEY (PKCS8) or PUBLIC KEY — so a provider can trust a key it never signs with (public-only) alongside ones it does (private). Disabled entries (enabled: false) are skipped entirely.