Export CPS-style key-material API for batch decrypt/derive/signened indices#688
Export CPS-style key-material API for batch decrypt/derive/signened indices#688disassembler wants to merge 1 commit into
Conversation
Callers can now decrypt the root key once with withDecryptedKeyMaterial, then batch-derive children via deriveKeyMaterial and sign via signWithKeyMaterial, amortising the expensive Argon2id KDF over many operations. Secret key material stays in sodium_malloc'd memory for the duration of the callback and is zeroed on return.
| deriveKeyMaterial :: | ||
| DerivationScheme -> | ||
| KeyMaterial Validated -> | ||
| DerivationIndex -> | ||
| (KeyMaterial Validated -> IO (Either XPrvError a)) -> | ||
| IO (Either XPrvError a) | ||
| deriveKeyMaterial = legacyDerivePrivate |
There was a problem hiding this comment.
Instead of adding a new function, I'd recommend simply renaming legacyDerivePrivate -> deriveKeyMaterial, since legacyDerivePrivate was a local implementation detail and was not previously exported
| KeyMaterial Validated -> | ||
| msg -> | ||
| IO (Either XPrvError Signature) | ||
| signWithKeyMaterial keyMaterial msg = |
There was a problem hiding this comment.
This is a copy and paste of most of the body of the encryptedSign function.
DRY
| encryptedChainCode, | ||
|
|
||
| -- * CPS-style key-material API | ||
| KeyMaterial, |
There was a problem hiding this comment.
There was a plan to rename KeyMaterial to Ext[ended]KeyMaterial, mentioned in #679
Since now we want to expose it, it makes sense to rename it now.
|
|
||
| -- | Extract the 32-byte public key from in-memory key material. | ||
| keyMaterialPublicBytes :: KeyMaterial Validated -> ByteString | ||
| keyMaterialPublicBytes = publicKeyByteString . kmPublicKey |
There was a problem hiding this comment.
PublicKey is exposed together with various tooling. So, it would be better to expose a separate accessor for PUblicKey and users can then use publicKeyByteString, publicKeyByteArray or otherexposed function that operate on PublicKey.
|
Closed in favor of more complete #689 |
Description
Background
cardano-crypto-walletis a new Haskell package in cardano-base that providesauthenticated encryption and HD key derivation for wallet root keys. It replaces
the dependency on the older
cardano-cryptoC library for wallet key operations.The envelope format ("v2") stores each root key as a CBOR-encoded structure
containing:
(128 MiB memory, 3 iterations, parallelism 4)
(authenticated but not encrypted)
The plaintext secret key is held exclusively in
sodium_malloc'd memory(
MLockedSizedBytes) for its entire lifetime — the allocator marks the pagesnon-swappable (
mlock) and zeroes them on free. It never touches the GC heap.CPS-style key-material API (this PR)
Motivation
Every call to
encryptedSignorencryptedDerivePrivatetoday runs the fullArgon2id KDF — by design, this costs ~128 MiB of memory and ~300 ms of CPU on
typical hardware. That is appropriate for a single interactive passphrase check,
but it is prohibitively expensive when a wallet needs to derive a batch of child
keys or sign a batch of transactions from the same root.
The existing high-level API (
encryptedSign,encryptedDerivePrivate) alsore-encrypts after every operation, meaning each call is:
For a batch of N operations, that is N Argon2id invocations and N
encrypt/decrypt round-trips.
New exports
Intended usage pattern
N operations, 1 KDF invocation.
Security analysis
Secret key lifecycle
KeyMaterial Validatedwraps aSecretKey, which is a newtype overMLockedSizedBytes 64. The locked-memory guarantees are:sodium_malloc/mlsbNew(a libsodium allocator backed bymlock-ed pages with guard pages on both sides).MLockedSizedBytesis a stable pointer intoC-allocated memory; Haskell's garbage collector cannot relocate it.
mlockprevents the OS from paging theregion out.
mlsbFinalizecallssodium_free, which zeroes theregion before releasing it.
KeyMaterialconstructors are not exported. The only way to obtain aKeyMaterial Validatedis throughwithDecryptedKeyMaterialorderiveKeyMaterial. Both are CPS: the callback receives the key material andthe bracket around
mlsbNew/mlsbFinalizeguarantees the memory is zeroed whenthe callback returns — whether normally or via exception.
Why CPS is more secure than returning
KeyMaterialIf
withDecryptedKeyMaterialreturnedIO (Either XPrvError (KeyMaterial Validated))instead of taking a callback, the caller could let theKeyMaterialvalue escape into a lazy data structure, be captured by a closure, or be held
alive across a GC cycle. In that model the locked memory lives until the
KeyMaterial's finalizer fires — which in Haskell is non-deterministic.With CPS, the secret key's lifetime is lexically bounded by the callback.
The compiler and runtime cannot extend it beyond that scope, and the bracket
ensures cleanup even in the presence of asynchronous exceptions.
Validatedphantom typeKeyMaterialcarries a phantom type parameter drawn fromtype data Validity = Validated | Unchecked.Validatedis only produced byvalidateKeyMaterial,which calls the C function
cardano_crypto_wallet_validateto verify that the32-byte public key in the material is the correct Ed25519 public key for the
64-byte secret key. This check prevents a confused-deputy attack where a
maliciously crafted
EncryptedKeycould cause signing under a key the user didnot intend.
Uncheckedis an internal-only constructor; it is not exported. Callers canonly ever interact with
KeyMaterial Validated.encryptedDerivePublicandunsafePerformIOThe function uses
unsafePerformIOto call the C derivation function, whichrequires pointers. This is safe because:
(PublicKey, ChainCode, DerivationIndex, DerivationScheme), it always returns the same result.PinnedSizedBytesvalues it reads are immutable for the duration ofthe call —
psbUseAsCPtrprovides a stable pointer into pinned memory thatcannot be moved or mutated by concurrent Haskell threads.
to freshly allocated output buffers that are immediately consumed.
childIndex >= 0x80000000) is a pure check thatshort-circuits before any I/O.
What is NOT provided here
withDecryptedKeyMaterial. This package never stores a passphrase or derivedwrapping key between calls. Passphrase retention policy is the caller's
responsibility.
already-constructed
EncryptedKeyvalues.KeyMaterialvalues should not be shared across threads.The locked-memory allocator is thread-safe for allocation/deallocation, but
concurrent reads of the same
SecretKeypointer are untested and unnecessary —derive per-thread if needed.
Version
cardano-crypto-wallet 0.2.0.0. The breaking changes in this version are:encryptedDerivePublicreturn type changed from(PublicKey, ChainCode)toEither XPrvError (PublicKey, ChainCode)(Fix encryptedDerivePublic to return Either instead of throwing on har… #687).XPrvHardenedDerivationUnsupportedconstructor added toXPrvError.The new CPS API is additive and does not break existing callsites.
Checklist
CHANGELOG.mdfor the affected packages.New section is never added with the code changes. (See RELEASING.md)
.cabalandCHANGELOG.mdfiles according to theversioning process.
.cabalfiles for all affected packages are updated.If you change the bounds in a cabal file, that package itself must have a version increase. (See RELEASING.md)