Cryptography · Key Management
Designing a Key Service Interface That Cannot Be Misused
Why a key store should expose operations rather than keys, which parameters belong in a request, and how context binding and audit change the trust model.
A key store that returns key bytes to its callers is a store whose security is the sum of every caller’s security. One service that logs its configuration, one developer who prints a variable, one crash that dumps memory, and the key is out. A key service inverts the relationship: callers send plaintext and receive ciphertext, or send ciphertext and receive plaintext, and the key never appears in the caller’s address space. That inversion is the entire design, and everything else in the interface follows from accepting it.
Operations, not material
The primitive is a request that names an operation, a key, and a payload. The service resolves the key, checks that the operation is permitted for that key and that caller, performs it, and returns the result. Encrypt, Decrypt, Sign, Verify, Wrap, Unwrap, Derive — each is a separate operation with its own authorization.
The interface should not expose the key identifier as the only thing that selects a key. A caller who can name any key can request any operation on any key, and the authorization check becomes the only barrier. Naming a purpose instead, resolved by the service to a key through a policy, means a caller cannot reach a key it was not meant to use even if it learns the identifier: the request asks for “encrypt for tenant 4471’s object store”, and the service decides which key and which version satisfy that.
The distinction between wrapping and encryption deserves care, because conflating them is a common interface defect. A wrap operation produces a wrapped key bound to a context; an encrypt operation produces ciphertext bound to a nonce. AES Key Wrap, from RFC 3394, is deterministic and has no nonce, so a service that offers “encrypt” over key material and lets the caller supply a nonce creates the risk of nonce reuse under a master key. Offering Wrap as a distinct operation removes the parameter the caller would otherwise have to get right.
The same reasoning applies to decryption-oracle risk. An interface that decrypts arbitrary ciphertext under a long-lived key without binding the ciphertext to its context hands the caller an unrestricted decryption oracle for that key. If the caller is compromised, the key’s protection is only as strong as the caller’s authorization. Binding the context — the record identifier, the key version, the purpose — into the associated data means a ciphertext stolen from one record does not decrypt as another, and the oracle’s reach is limited to the caller’s own data.
What belongs in a request
A request that is easy to misuse omits parameters the service could supply. Four are worth enforcing.
The first is the key version. The service should choose the current version for encryption and accept older versions for decryption, rather than letting the caller pin a version. A caller that pins a version for encryption is a caller that can keep using a retired key, and the rotation machinery loses its force.
The second is the nonce, if the mode needs one. AES-GCM with a 96-bit nonce has a collision budget: the probability of a repeat grows with the square of the number of messages under one key, and the standard’s guidance limits messages per key accordingly. A service that generates nonces internally, and refuses to accept caller-supplied ones, removes the entire class of nonce-reuse failures. For deterministic modes the service can derive the nonce from a counter it owns.
The third is the associated data. Whatever identifies the record should be an explicit parameter, and the service should refuse a request without one for operations where it matters. Making it optional means the default path is the unprotected one.
The fourth is the algorithm. The service chooses. A caller that specifies a mode can specify a weak one, and an interface that accepts the parameter invites the request. Where the caller genuinely needs a choice, the permitted set should be a small enumeration tied to key type, and the mapping from “I want authenticated encryption for this record” to the concrete mechanism should live in the service.
Encrypt(key_ref, plaintext, aad) -> ciphertext, key_version
Decrypt(key_ref, ciphertext, aad) -> plaintext
Wrap(key_ref, key_material, context) -> wrapped
Unwrap(key_ref, wrapped, context) -> key_material
Rotate(key_ref) -> new_version
Retire(key_ref, version) -> status
The Unwrap operation is the dangerous one, and its presence should be deliberate. A caller with unwrap permission has, in effect, the ability to obtain data keys in plaintext and can therefore decrypt anything those keys protect, whatever the caller’s other permissions say. Services that can avoid exporting the operation do so by offering decrypt over the payload instead, and limit unwrap to the narrow set of components that genuinely need a key in their own memory, such as a storage node performing bulk encryption locally.
Authorization is part of the cryptographic argument
The security of the service rests on the claim that a caller can only perform operations consistent with its role. That claim is enforced by whatever authenticates the caller and checks the request, and it fails in ways that are invisible from the outside.
The first failure is a permission that is broader than the caller’s actual need. A service that can decrypt any record in a tenant can read the whole tenant, and that is the correct grant for a storage backend and the wrong grant for a reporting job. The permission model has to distinguish them, which means the key reference in a request must carry enough context for the check to be precise.
The second is confused deputy. A component that holds broad permissions and forwards requests from less privileged callers must validate those requests, or the less privileged caller inherits the broad permissions. The classic shape is a proxy that accepts a key reference from a request body and passes it through; the proxy must resolve the reference against the authenticated caller rather than trusting the parameter.
The third is the missing negative test. Authorization that is never exercised in its denying direction is authorization that may not exist. A service whose tests only confirm that authorized operations succeed has not established that unauthorized ones fail, and misconfigured policies are permissive far more often than restrictive.
The audit log is part of the same argument. Each operation should record the caller identity, the key reference, the version, the operation, the timestamp, and a correlation identifier that ties it to the request that caused it. The log makes abuse detectable after the fact, which is the only control that remains once a caller is compromised and acting within its permissions. Operations that are individually authorized but anomalous in volume — a pattern of unwraps across many key references — are exactly what the log exists to reveal, which means the recording must be detailed enough to support that analysis and durable enough not to be lost with the caller.
Rate, bulk, and the shape of the dependency
Making the key service central makes it a dependency, and its latency and throughput become the system’s.
The first design question is whether it does bulk work. If the service performs encryption of payloads, it must handle the full data rate, which is expensive and usually unnecessary: a service that wraps a data key and lets the storage node encrypt under it moves the bulk work back to where the data is. That is the envelope pattern, and its cost is that the data key lives in the storage node’s memory, outside the service’s protection.
The second is caching. Data keys and wrapped keys can be cached, but the cache must be invalidated when the key is rotated or destroyed, and the invalidation must reach every holder. A service that cannot signal its callers has to bound the cache lifetime instead, which converts the signal into a worst-case delay.
The third is availability. A key service that is unreachable stops every operation that needs it, including decryption of data the system has locally. Multiplexing across regions or replicas helps only if the replicas hold the same keys, which brings the replication of key material back into the design. Replication by wrapping the key under a transport key held by each replica preserves the non-extractability of the original, at the cost of a second key that must itself be protected — the same trade described in HSM key boundaries.
Where the interface ends and the caller’s duty begins
The service can generate keys, enforce usage, bind context, rotate, and destroy. It cannot make the caller handle the results correctly. A caller that stores a returned plaintext in a log, or that keeps a data key beyond the operation it was unwrapped for, has undone the design without violating it.
Two obligations are therefore worth stating in the interface itself. The first is a bound on how long a returned plaintext may be held, expressed as a lease that the service can shorten, with the understanding that a caller ignoring the lease is a correctness bug rather than a security guarantee. The second is a requirement that callers present the context they will use later for decryption; the service records it at wrap time and the caller cannot silently change it.
What the interface genuinely guarantees is narrower than it appears. It guarantees that keys are not returned to callers that are only authorized for ciphertext operations, that deleted keys cannot be used, and that every operation that touched a key left a record. Those are properties of the service and its enforcement, and they are worth having precisely because they are checkable. The properties of the data — that it is encrypted with a sound mode, that the plaintext was not copied elsewhere, that the caller’s authorisation matched its behaviour — remain outside the boundary and are the subject of protecting key material in memory and at rest.
References
- NIST SP 800-57 Part 1 Rev. 5: Recommendation for Key Management — NIST
- NIST SP 800-38D: Recommendation for Block Cipher Modes of Operation: Galois/Counter Mode (GCM) and GMAC — NIST
- RFC 5869: HMAC-based Extract-and-Expand Key Derivation Function (HKDF) — IETF
- RFC 3394: Advanced Encryption Standard (AES) Key Wrap Algorithm — IETF