Skip to content

Add KeyWrapper/KeyUnwrapper traits for SP 800-38F key wrap (#150) - #160

Draft
roy-basmacier wants to merge 9 commits into
bcgit:feature/key-rework-memoryfrom
roy-basmacier:feature/150-key-wrap
Draft

roy-basmacier wants to merge 9 commits into
bcgit:feature/key-rework-memoryfrom
roy-basmacier:feature/150-key-wrap

Conversation

@roy-basmacier

Copy link
Copy Markdown

Adds the core API for the key-wrap algorithms of NIST SP 800-38F (KW, KWP, TKW; RFC 3394 / RFC 5649), following the discussion on #150 and the conventions of the cipher traits on this branch. Traits only: an AES-KW/KWP implementation over ElectronicCodeBook in bouncycastle-modes is to follow once the API is agreed.

core/traits.rs:

  • KeyWrapper<KEK_LEN> and KeyUnwrapper<KEK_LEN> as separate traits, so a policy can allow unwrapping existing ciphertexts while forbidding new wrappings, as with the *Encryptor/*Decryptor pairs.
  • Fixed-length one-shots wrap_key/unwrap_key are provided over the required wrap_key_out/unwrap_key_out. Implementations must reject invalid (KEY_LEN, CT_LEN) pairs with an inline const assertion, so a wrong length is a compile error rather than a runtime Err.
  • Runtime-length wrap_out/unwrap_out cover KWP over arbitrary data, sized by wrap_out_len (exact) and unwrap_out_max_len (upper bound), mirroring encrypt_out_len/decrypt_out_max_len. OutputBufferTooSmall is checked before any work is done.
  • Wrap input is plain bytes, since wrapping strips KeyMaterial metadata. Unwrap output is a Secret, so it is zeroized on drop without claiming a KeyType or provenance.
  • Every unwrap authenticity failure (wrong KEK, tampering, ICV mismatch, bad padding, wrong recovered length) is the same DecryptionFailed, per SP 800-38F Algorithm 6 steps 4, 7 and 8; output buffers are zeroized on error.
  • The KEK must be a SymmetricCipherKey at least as strong as MAX_SECURITY_STRENGTH. App. A.2's advice that the KEK should be as strong as the wrapped key is documented as the caller's responsibility.

core/errors.rs:

  • SymmetricCipherError::InvalidInputLength, for plaintext or ciphertext lengths the algorithm is not defined on (variable-length APIs only).

core-test-framework:

  • New key_wrap module: TestFrameworkKeyWrap::test() exercises round trips, determinism, the length helpers, buffer-size and input-length errors, tamper and wrong-KEK rejection, output zeroization, and KEK type and strength checks. test_kat() is ready for the RFC 3394/5649 vectors.
  • Checked against an in-file mock (not cryptography) with KWP's length behaviour. Adds a bouncycastle-utils dependency for Secret.

Assisted-by: Claude:claude-opus-5-5

@ounsworth ounsworth added this to the 0.1.4alpha milestone Oct 24, 2026
royb and others added 7 commits October 27, 2026 03:51
Adds the core API for the key-wrap algorithms of NIST SP 800-38F (KW, KWP,
TKW; RFC 3394 / RFC 5649), following the discussion on bcgit#150 and the
conventions of the cipher traits on this branch. Traits only: an AES-KW/KWP
implementation over ElectronicCodeBook in bouncycastle-modes is to follow
once the API is agreed.

core/traits.rs:
- KeyWrapper<KEK_LEN> and KeyUnwrapper<KEK_LEN> as separate traits, so a
  policy can allow unwrapping existing ciphertexts while forbidding new
  wrappings, as with the *Encryptor/*Decryptor pairs.
- Fixed-length one-shots wrap_key/unwrap_key are provided over the required
  wrap_key_out/unwrap_key_out. Implementations must reject invalid
  (KEY_LEN, CT_LEN) pairs with an inline const assertion, so a wrong length
  is a compile error rather than a runtime Err.
- Runtime-length wrap_out/unwrap_out cover KWP over arbitrary data, sized by
  wrap_out_len (exact) and unwrap_out_max_len (upper bound), mirroring
  encrypt_out_len/decrypt_out_max_len. OutputBufferTooSmall is checked
  before any work is done.
- Wrap input is plain bytes, since wrapping strips KeyMaterial metadata.
  Unwrap output is a Secret, so it is zeroized on drop without claiming a
  KeyType or provenance.
- Every unwrap authenticity failure (wrong KEK, tampering, ICV mismatch,
  bad padding, wrong recovered length) is the same DecryptionFailed, per
  SP 800-38F Algorithm 6 steps 4, 7 and 8; output buffers are zeroized on
  error.
- The KEK must be a SymmetricCipherKey at least as strong as
  MAX_SECURITY_STRENGTH. App. A.2's advice that the KEK *should* be as
  strong as the wrapped key is documented as the caller's responsibility.

core/errors.rs:
- SymmetricCipherError::InvalidInputLength, for plaintext or ciphertext
  lengths the algorithm is not defined on (variable-length APIs only).

core-test-framework:
- New key_wrap module: TestFrameworkKeyWrap::test() exercises round trips,
  determinism, the length helpers, buffer-size and input-length errors,
  tamper and wrong-KEK rejection, output zeroization, and KEK type and
  strength checks. test_kat() is ready for the RFC 3394/5649 vectors.
- Checked against an in-file mock (not cryptography) with KWP's length
  behaviour. Adds a bouncycastle-utils dependency for Secret.

Assisted-by: Claude:claude-opus-5-5
Implements the KeyWrapper / KeyUnwrapper traits from the previous commit
for the two 128-bit-block key-wrap algorithms of NIST SP 800-38F, KW
(Sec 6.2; RFC 3394) and KWP (Sec 6.3; RFC 5649), generically over any
ElectronicCodeBook permutation, and publishes the AES instantiations.

modes:
- kw.rs: Kw<P, KEK_LEN>. The wrapping function W and its inverse
  (Algorithms 1 and 2) run in place on the caller's buffer, visiting the
  registers in the order the spec's shift register would present them,
  which RFC 3394 Sec 2.2.1 gives as the equivalent index-based form; the
  one block of scratch is a Secret. KW-AE/AD are Algorithms 3 and 4.
- kwp.rs: Kwp<P, KEK_LEN>. KWP-AE/AD are Algorithms 5 and 6, including
  the single-block path for plaintexts of at most one semiblock. The ICV,
  length-field and padding checks of Algorithm 6 steps 4, 7 and 8 are all
  made, combined bitwise, and reported as the one DecryptionFailed.
- The fixed-length API rejects invalid (KEY_LEN, CT_LEN) pairs at compile
  time via const assertions on the Table 1 limits; the run-time API
  returns InvalidInputLength outside them. Output buffers are zeroized on
  every failure. TKW (TDEA) is not included: it needs a 64-bit-block
  engine (PR bcgit#139).

aes:
- kw.rs: AES_KW_128/192/256 and AES_KWP_128/192/256 aliases, with the
  RFC 3394 Sec 4.1 and RFC 5649 7-octet vectors as doctests. No OIDs yet,
  in line with the other AES mode aliases.

tests:
- modes/tests/kw_tests.rs: all six RFC 3394 Sec 4 vectors and both
  RFC 5649 Sec 6 examples via TestFrameworkKeyWrap::test_kat, the shared
  framework for every AES key length at 26 payload lengths (every padding
  length of the single-block case included), and pins for the properties
  that distinguish KW, KWP and a raw block encryption.
- modes/tests/acvp_kw_tests.rs: the NIST ACVP-AES-KW and ACVP-AES-KWP
  sets from bc-test-data, 1800 cases each, through both APIs, including
  the 171 + 178 forged ciphertexts that must be rejected.
- aes/tests/kw_alias_tests.rs: the aliases name the right permutation and
  pass the framework.

benches: KW/KWP wrap and unwrap of a 256-bit key, and KWP over 4 KiB.

cli: aes{128,192,256}-kw and -kwp with wrap|unwrap, reading all of stdin
(key wrap is one-shot), sharing key loading with the other AES commands;
cli/tests/aes_kw_cli_tests.rs replays the RFC vectors in both directions
and pins the length rules, the fail-closed unwrap and the help text.

Assisted-by: Claude:claude-fable-5-1
…KeyWrapMode/KeyUnwrapMode and the KeyWrappingKey/KeyUnwrappingKey roles

The traits from PR bcgit#160 took the KEK as a KeyMaterial on every call. On the key path the KEK is
validated once, when the key object is made, so the pair becomes worker traits on the shape of
MAC: `&self` methods over a worker that borrows the keyed permutation, with no key parameter and
no KeyMaterialError. The mode traits (hazmat) hand the worker out from a key state, and the role
traits forward to them with the one-shots provided, as the MAC roles do. The length helpers take
`&self` so the role can forward them through a worker.

Assisted-by: Claude:claude-mythos-5-1
… roles and its workers

`test` and `test_kat` take the key type and build it from the seed through FromKeyMaterial; the KEK
type and strength checks leave, since the construction suite covers them, and the wrong-KEK case
is a second key. The mock becomes a key with a borrowed worker.

Assisted-by: Claude:claude-mythos-5-1
… the keyed permutation

The two structs from PR bcgit#160 held a PhantomData and keyed the permutation on every call. They
become workers on the shape of Cmac: `Kw<'k, P>` over a `&'k P`, with `KW` / `KWP` as the mode
markers a key is bound to, their key state the permutation alone, and KeyWrapMode / KeyUnwrapMode
handing the worker out. ModeNames gains KW_ALG_NAME and KWP_ALG_NAME. The toy key gets the two
roles, and the structural tests run over it; the framework's tamper checks need diffusion, so
they run in `aes`.

Assisted-by: Claude:claude-mythos-5-1
… aliases name the workers

The key gets the two key-wrap roles, forwarded to the mode as the others are, and the internal
permutations get their KW and KWP names. The aliases become `Kw<'k, AES128Internal>` and friends,
the shape of AES_CMAC_128. Tests: the RFC 3394 and RFC 5649 vectors and the shared key-wrap
framework through the keys, the ACVP KW and KWP sets from bc-test-data (1800 cases each, with
the forgeries counted), the construction suite, and the alias names alongside the other aliases.
The modes bench gains the KW/KWP measurements the old modes crate's bench had.

Assisted-by: Claude:claude-mythos-5-1
…ild key objects

The six commands load `AES*Key<KW>` / `AES*Key<KWP>` through load_key_object and call the key's
one-shots. The direction takes the CipherDirection flag every other cipher command takes, so
`-d encrypt` wraps and `-d decrypt` unwraps, instead of PR bcgit#160's positional wrap|unwrap. The Rust
subprocess test becomes cli/tests/test_aes_kw.sh on the shell harness the other commands use,
with the same cases: the RFC vectors both ways, framing, the length rules, rejection with no
output, and key loading.

Assisted-by: Claude:claude-mythos-5-1
@dghgit
dghgit force-pushed the feature/150-key-wrap branch from 339868e to 80c25ff Compare October 26, 2026 18:39

@ounsworth ounsworth left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this +65,000 lines, changing 354 files?

I suspect there's a bit branching issue here. Once we finish the release/0.1.3alpha, and fork a new release/0.1.4alpha branch and point this at it, maybe the diff will get smaller?

# Conflicts:
#	crypto/aes/src/key.rs
#	crypto/cipher/tests/common/toy_key.rs
#	crypto/core/src/hazmat/mod.rs
#	crypto/core/src/hazmat/mode.rs
Kw::from_perm and Kwp::from_perm were pub, which the key-rework-memory merge
just made the odd ones out: 534d182 took every other worker's bare
constructor to pub(crate) under QUALITY_AND_STYLE's hazmat rule, since the
key object is the route and the mode impls that call them live in this
crate. The two follow. The doc examples build their KEK through the test
suite's toy key, as the other modes' now do, and kw_tests compares the key
route against the hazmat key-wrap mode traits rather than the constructors.

Assisted-by: Claude:claude-mythos-5-1
@dghgit
dghgit changed the base branch from feature/simple-ciphers to feature/key-rework-memory October 31, 2026 05:55
@dghgit
dghgit self-requested a review October 31, 2026 13:22

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants