Documentation
¶
Overview ¶
Package keys holds the two kinds of key this system needs and the interfaces a deployment plugs its own behind.
A Provider gives the symmetric keys that turn an identifier into a keyed pseudonym. There is one key per tenant and per purpose, which is what stops two copies of the same event being joined on a person: the security copy and the billing copy of one sign-in carry different pseudonyms for the same subject, and nothing short of both keys relates them.
A Signer gives the asymmetric key seals are signed with.
Keys are never rotated. Rotation would break linkability across time for one person, which is the property the security copy is kept for. They are destroyed instead, and destroying one is how a subject's records become unlinkable: the pseudonyms remain, and nothing can recompute them.
Index ¶
- Constants
- Variables
- func IsPseudonym(v string) bool
- func ParseECPublic(publicKeyPEM []byte) (*ecdsa.PublicKey, error)
- func Verify(publicKeyPEM, message, signature []byte) error
- type JWTLogin
- type KMSSigner
- type Local
- func (l *Local) Close() error
- func (l *Local) Destroy(_ context.Context, tenant string, purpose Purpose) error
- func (l *Local) DirectoryID() (string, error)
- func (l *Local) Open(_ context.Context, tenant string, purpose Purpose, sealed []byte) ([]byte, error)
- func (l *Local) Pseudonym(_ context.Context, tenant string, purpose Purpose, identifier string) (string, error)
- func (l *Local) Seal(_ context.Context, tenant string, purpose Purpose, plaintext []byte) ([]byte, error)
- type LocalSigner
- type Provider
- type Purpose
- type Sealer
- type Signer
- type Transit
- func (t *Transit) Close() error
- func (t *Transit) Destroy(ctx context.Context, tenant string, purpose Purpose) error
- func (t *Transit) Open(ctx context.Context, tenant string, purpose Purpose, sealed []byte) ([]byte, error)
- func (t *Transit) Pseudonym(ctx context.Context, tenant string, purpose Purpose, identifier string) (string, error)
- func (t *Transit) Seal(ctx context.Context, tenant string, purpose Purpose, plaintext []byte) ([]byte, error)
- type TransitSigner
Constants ¶
const PseudonymPrefix = "ps_"
PseudonymPrefix marks a value as a keyed pseudonym rather than an identifier anything else would recognise. A reader who does not know that costs an investigation its first hour.
Variables ¶
var ErrDestroyed = errors.New("keys: the key has been destroyed")
ErrDestroyed is returned when a key has been destroyed. Asking for a pseudonym under it is a fault in the caller, not a reason to mint a new key: minting one would give a person a second, unrelated identity in the same trail.
Functions ¶
func IsPseudonym ¶
IsPseudonym reports whether a value was produced by this package.
func ParseECPublic ¶ added in v0.7.0
ParseECPublic reads a PEM public key that must be an ECDSA key on P-384, the key of a seal. It names what it found when it is anything else.
Types ¶
type JWTLogin ¶
type JWTLogin struct {
// Mount is the auth mount, e.g. jwt-devel: one per cluster, inside the
// environment's namespace.
Mount string
// Role is the role on that mount.
Role string
// TokenFile holds the JWT. It is read at every login, because the kubelet
// replaces a projected token before it expires.
TokenFile string
}
JWTLogin is how a workload signs in to OpenBAO without a stored secret: it presents a JWT — normally its projected service-account token — to a JWT auth mount, under a role that binds the token's subject and audience and names the policies the session gets. It is the way the estate's workloads already reach OpenBAO, and it means there is no long-lived token anywhere to leak or rotate.
type KMSSigner ¶
type KMSSigner struct {
Client *kms.Client
// KeyID is the key's ARN, ID or alias. It is also what the operator's
// log names as the signer.
Key string
// contains filtered or unexported fields
}
KMSSigner signs seals with an AWS KMS asymmetric key.
It is the answer to the local signer's stated limit: whoever can write the archive can also sign for what they wrote. Here the private key never leaves KMS, the writer's role has no kms:Sign on it, and only the notary's role does — so writing the archive and vouching for it are different privileges, held by different identities, with every signature in KMS's own log.
The key is an ECC_NIST_P384 SIGN_VERIFY key, which is what ES384 is: a seal is signed ECDSA_SHA_384. The message is hashed here, with SHA-384, and KMS is given the digest (MessageType DIGEST): a JWS signing input is larger than the 4 KiB KMS takes whole once a seal carries its meters, and a digest is the same signature either way. Verify hashes the same way, so an auditor needs only the public half this exports. An ECC_NIST_P256 key still signs (ECDSA_SHA_256), for the callers that predate seals.
type Local ¶
type Local struct {
// Root wraps the data keys. Without one a random root is generated and
// nothing survives the process, which is fine for a test and for nothing
// else.
Root []byte
// Dir holds the wrapped keys. Empty means memory only.
Dir string
// contains filtered or unexported fields
}
Local keeps wrapped data keys on the local filesystem, or in memory when no directory is given.
It is for tests, for a single-node deployment, and for the conformance suite, which has to run without a cloud. A deployment that must survive the loss of one machine wants the KMS or transit provider instead, because a key that exists in one place is a trail that can be made unreadable by one disk failing — which is not erasure, but loss.
func NewLocal ¶
NewLocal returns a provider wrapping its keys under root. A root of the wrong length is refused rather than stretched, because a silently weakened key is worse than none.
func (*Local) DirectoryID ¶
DirectoryID is this key directory's identity, minted the first time any writer asks and read back ever after.
Data keys are random, so a directory is not interchangeable with another one holding the same root: two writers that do not share one mint different keys for the same tenant, and a directory lost and recreated re-keys every tenant. Neither is visible from inside one writer. The identity is what lets a shared store notice: every writer registers it there, and one that brings a different directory is refused. See postgres.BindKeyDirectory.
func (*Local) Open ¶
func (l *Local) Open(_ context.Context, tenant string, purpose Purpose, sealed []byte) ([]byte, error)
Open implements Sealer.
func (*Local) Pseudonym ¶
func (l *Local) Pseudonym(_ context.Context, tenant string, purpose Purpose, identifier string) (string, error)
Pseudonym implements Provider.
func (*Local) Seal ¶
func (l *Local) Seal(_ context.Context, tenant string, purpose Purpose, plaintext []byte) ([]byte, error)
Seal implements Sealer.
The sealing key is derived from the data key rather than being the data key: one key used both for pseudonyms (HMAC) and for encryption (AES-GCM) would be one key serving two algorithms, which is the kind of reuse that turns an attack on one into an attack on both.
type LocalSigner ¶
type LocalSigner struct {
// contains filtered or unexported fields
}
LocalSigner signs with a key on this machine.
It is for tests, for the conformance suite, and for a deployment small enough that the signing key living beside the archive is an accepted risk. It is worth being plain about that risk: whoever can write the archive can also sign a seal for what they wrote, so a local signer proves that objects have not changed since they were signed and not that the operator did not choose what to sign.
func LoadLocalSigner ¶
func LoadLocalSigner(id string, pemBytes []byte) (*LocalSigner, error)
LoadLocalSigner reads a private key in PEM form: PKCS#8 (ed25519, P-256 or P-384) or SEC 1 ("EC PRIVATE KEY", as `openssl ecparam -genkey` writes it).
func LoadLocalSignerFile ¶
func LoadLocalSignerFile(id, path string) (*LocalSigner, error)
LoadLocalSignerFile reads a private key from a file.
func NewLocalP384 ¶ added in v0.7.0
func NewLocalP384(id string) (*LocalSigner, error)
NewLocalP384 returns a signer holding a generated P-384 key: what signs a seal (ES384).
func NewLocalSigner ¶
func NewLocalSigner(id string) (*LocalSigner, error)
NewLocalSigner returns a signer holding a generated ed25519 key. It is the key of the signers that predate seals; a seal is signed with NewLocalP384.
func (*LocalSigner) PrivateKey ¶
func (s *LocalSigner) PrivateKey() ([]byte, error)
PrivateKey returns the signing key in PEM form (PKCS#8), so a test or a small deployment can keep it somewhere and load it again.
type Provider ¶
type Provider interface {
// Pseudonym returns the pseudonym of an identifier under a tenant's key for
// a purpose, creating the key if there is none.
Pseudonym(ctx context.Context, tenant string, purpose Purpose, identifier string) (string, error)
// Destroy removes a key. Pseudonyms computed under it stay in the archive
// and can never be recomputed, which is what erasure means here.
Destroy(ctx context.Context, tenant string, purpose Purpose) error
// Close releases whatever the provider holds.
Close() error
}
Provider gives keyed pseudonyms and destroys the keys behind them.
type Purpose ¶
type Purpose string
Purpose is what a key is used for. A profile's name is the usual purpose, so that each copy of a record pseudonymises under a key of its own.
type Sealer ¶
type Sealer interface {
// Seal returns the sealed form of plaintext.
Seal(ctx context.Context, tenant string, purpose Purpose, plaintext []byte) ([]byte, error)
// Open returns the plaintext, or ErrDestroyed when the key is gone.
Open(ctx context.Context, tenant string, purpose Purpose, sealed []byte) ([]byte, error)
}
Sealer keeps something readable only under a tenant's key for a purpose.
It is what makes a pseudonym reversible for the cases the law requires, and only for them: the identifier behind a pseudonym is sealed under the same key the pseudonym came from, so the one holder able to open it is whoever holds that key, and destroying the key — erasure — makes every sealed identifier unreadable along with it. Nothing about resolving survives an erasure.
It is a separate interface because a provider that cannot seal is still a provider; a deployment with one simply cannot resolve.
type Signer ¶
type Signer interface {
// Sign returns a signature over a message.
Sign(ctx context.Context, message []byte) ([]byte, error)
// PublicKey returns the verifying half, PEM encoded.
PublicKey(ctx context.Context) ([]byte, error)
// KeyID names the key for an operator's log. What a seal names as its
// signer is the key's RFC 7638 thumbprint, which is computed from the public
// half and so cannot disagree with it.
KeyID() string
}
Signer signs what the notary vouches for: a seal, and the statements about the keys that sign them.
A seal is signed ES384: the key is P-384, the signature is ECDSA over a SHA-384 of the message, and Sign returns it ASN.1 encoded, as KMS and transit do. The JWS wants the two integers side by side; internal/seal turns one into the other. An older key type (ed25519, P-256) still signs and verifies here, for the callers that predate seals.
The private half never has to leave the provider that holds it: a deployment signs with a managed key and publishes only the public half. What an auditor needs is that public half and a copy of the verifier, and then the chain can be checked without trusting the operator, which is the whole point of signing it.
type Transit ¶
type Transit struct {
// Address is the server, e.g. https://openbao.example:8200.
Address string
// Mount is where the transit engine is mounted. Empty means "transit".
Mount string
// Prefix starts every key's name. Empty means "audit".
Prefix string
// Namespace is the OpenBAO namespace the engine lives in — in the estate,
// the environment's. Empty is the root namespace.
Namespace string
// CAFile is a PEM bundle trusted beside the system roots, for a server
// whose certificate comes from a private chain.
CAFile string
// One way to authenticate: Login, which signs in with the pod's projected
// service-account token and needs no stored secret; or Token; or
// TokenFile, read on every call, for a token something else keeps renewed.
Login *JWTLogin
Token string
TokenFile string
HTTP *http.Client
// contains filtered or unexported fields
}
Transit keeps every tenant's key for every purpose in an OpenBAO (or Vault) transit engine, and asks the engine for each pseudonym.
The key material never leaves the engine: a pseudonym is the engine's HMAC of the identifier under the key, and a sealed identifier is its encryption. That removes both faults of Local at once. Every replica asks the same engine, so they cannot mint different keys for one tenant, and there is no directory to lose. What a deployment gives up is a round trip per pseudonym.
A key is named <prefix>.<purpose>.<tenant>, so that the engine's policy — not this code — decides who may use which purpose: a role granted transit/hmac/audit.security.* can pseudonymise for the security profile and for nothing else. See docs/how-to/configure-openbao-keys.md for the policies.
Keys are never rotated, as everywhere in this package, and every call is pinned to the key's first version so that a rotation by somebody else changes nothing. Destroy is what ends a key: it rotates it once and trims the first version away. The key's name stays behind, holding a version nothing uses, which is what keeps a later call from creating a fresh key under the old name and handing the same person a second identity — the marker Local writes beside its files, kept by the engine instead.
func NewTransit ¶
NewTransit returns a provider after checking that it can reach the engine with the credentials it was given — a writer that finds out on its first record has already accepted a batch it cannot pseudonymise.
func (*Transit) Destroy ¶
Destroy implements Provider. It needs rights on the key itself that the writer's role must not have; it is the CLI's, run by whoever may erase.
The steps are ordered so that stopping between any two leaves nothing usable that should not be: the first version is refused before it is trimmed, and trimmed before the call returns.
func (*Transit) Open ¶
func (t *Transit) Open(ctx context.Context, tenant string, purpose Purpose, sealed []byte) ([]byte, error)
Open implements Sealer.
func (*Transit) Pseudonym ¶
func (t *Transit) Pseudonym(ctx context.Context, tenant string, purpose Purpose, identifier string) (string, error)
Pseudonym implements Provider.
func (*Transit) Seal ¶
func (t *Transit) Seal(ctx context.Context, tenant string, purpose Purpose, plaintext []byte) ([]byte, error)
Seal implements Sealer. The engine encrypts under the same key the pseudonym came from; it keeps the HMAC key of a version apart from its encryption key, so this is not one key serving two algorithms.
type TransitSigner ¶
type TransitSigner struct {
// Address is the server, e.g. https://openbao.example:8200.
Address string
// Mount is where the transit engine is mounted. Empty means "transit".
Mount string
// Key is the transit key's name.
Key string
// Namespace is the OpenBAO namespace the engine lives in — in the estate,
// the environment's. Empty is the root namespace.
Namespace string
// CAFile is a PEM bundle trusted beside the system roots, for a server
// whose certificate comes from a private chain.
CAFile string
// One way to authenticate: Login, which signs in with the pod's projected
// service-account token and needs no stored secret; or Token; or
// TokenFile, read on every call, for a token something else keeps renewed.
Login *JWTLogin
Token string
TokenFile string
HTTP *http.Client
// contains filtered or unexported fields
}
TransitSigner signs seals with an OpenBAO (or Vault) transit key.
Like the KMS signer it keeps the private half out of the archive's reach: the key lives in the transit engine, the notary's policy may call transit/sign on it, and the writer's may not. It is the signer for a deployment whose secrets live in OpenBAO rather than a cloud's KMS.
The key is an ecdsa-p384 transit key, which is what ES384 is: transit hashes the message with SHA-384 and returns the signature ASN.1 encoded, as the other signers do. An ed25519 key still signs the message itself, for the callers that predate seals. Either way the signature is checked by the same Verify as a local key, with only the public half this exports.
A transit key can be rotated, and transit signs with the newest version unless told otherwise. So the signer reads the key's latest version once and pins every signature to it: the public half it exports and the signatures it makes always belong together, and KeyID names the version, so a verifier can tell which public half a seal wants after a rotation.
func NewTransitSigner ¶
func NewTransitSigner(ctx context.Context, s *TransitSigner) (*TransitSigner, error)
NewTransitSigner returns a signer that has already read its key.
Loading first matters: a seal names its signer before it is signed, and the name carries the key version, which is only known once the key has been read. It also means a key of the wrong type, or a token that cannot read it, stops the job before any window is sealed.
func (*TransitSigner) KeyID ¶
func (s *TransitSigner) KeyID() string
KeyID implements Signer. It names the version, so that after a rotation a verifier can tell which public half a seal was signed with.