feat: add AES KEK support with ECB and CMAC KCV methods#24
feat: add AES KEK support with ECB and CMAC KCV methods#24lwschan-tw wants to merge 7 commits intomasterfrom
Conversation
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Changes: New feature (1), Refactoring (1), Test improvement (1)
This PR adds AES KEK support (ECB and CMAC KCV) alongside the existing 3DES implementation, and updates the KEK API to be algorithm-agnostic via a KeyCipher interface.
Changes:
- Introduce AES key type + AES-CMAC (RFC 4493) for KCV verification.
- Extend
kek.Bundleto support algorithm selection (3DES / AES ECB / AES CMAC) and return aKeyCipherfromMerge(). - Update tests to cover AES KEK flows and adjust existing 3DES tests for the new API.
Reviewed changes
Copilot reviewed 10 out of 340 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
kek/kek_cipher.go |
Adds the KeyCipher interface to unify AES and 3DES merge results. |
kek/kek_bundle.go |
Adds algorithm selection and AES component/merge support in Bundle. |
kek/kek_bundle_test.go |
Updates 3DES tests for new API and adds AES ECB/CMAC merge tests. |
des/des_cipher.go |
Adds GetKeyBytes() to satisfy KeyCipher. |
aes/cmac.go |
Implements an internal AES-CMAC routine used for CMAC-based KCVs. |
aes/cmac_test.go |
Adds RFC 4493 vector tests for CMAC implementation. |
aes/aes_key.go |
Adds aes.Key with ECB/CMAC KCV computation and verification. |
aes/aes_key_factory.go |
Adds factories to create AES keys from bytes/hex strings. |
aes/aes_key_test.go |
Tests AES key KCV computation/verification and GetKeyBytes(). |
aes/aes_key_factory_test.go |
Tests AES key factory validation and key-size handling. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // cmac computes AES-CMAC per RFC 4493 for a message whose length is a | ||
| // multiple of the block size (16 bytes). For KCV computation the message | ||
| // is always 16 zero bytes, so partial-block padding is not needed. | ||
| func cmac(block cipher.Block, message []byte) []byte { | ||
| k1 := generateSubkey(block) | ||
|
|
||
| n := len(message) / block.BlockSize() | ||
| if n == 0 { | ||
| n = 1 | ||
| } | ||
| lastBlock := message[(n-1)*block.BlockSize():] | ||
|
|
||
| xored := make([]byte, block.BlockSize()) | ||
| for i := range xored { | ||
| xored[i] = lastBlock[i] ^ k1[i] | ||
| } |
There was a problem hiding this comment.
cmac() will panic for len(message) == 0 (and also for any len(message) not an exact multiple of the block size), because lastBlock becomes shorter than block.BlockSize() and is indexed at lastBlock[i]. Even if current callers only use 16 zero bytes, this is a sharp edge for future reuse. Prefer to (a) validate len(message) > 0 and len(message)%block.BlockSize()==0 and fail deterministically, or (b) implement full RFC 4493 CMAC including padding/K2 and return an error on invalid inputs (changing the signature to return ([]byte, error) if needed).”
| // Merge tries to build the result key from all the imported components | ||
| func (b *Bundle) Merge() (KeyCipher, error) { | ||
| kekBytes := make([]byte, b.keySize()) | ||
| for _, component := range b.Components { | ||
| kekBytes, _ = xor.XORBytes(kekBytes, component) | ||
| } | ||
|
|
||
| switch b.Algorithm { | ||
| case AES_ECB: | ||
| return b.mergeAES(kekBytes, aes.KCVMethodECB) | ||
| case AES_CMAC: | ||
| return b.mergeAES(kekBytes, aes.KCVMethodCMAC) | ||
| default: | ||
| return b.mergeTripleDES(kekBytes) | ||
| } | ||
| } |
There was a problem hiding this comment.
keySize() is derived from the first value encountered in a map iteration, which is non-deterministic. If components with different byte lengths exist (e.g., mixing AES-128 and AES-256 components), the chosen key size can vary run-to-run, and XOR/merge behavior becomes undefined. A more robust approach is to lock the expected key length when the first component is added (store it on Bundle), enforce subsequent components match it, and make Merge() return an error when the key length is unknown or inconsistent.
| default: | ||
| return b.addTripleDESComponent(componentIndex, componentValue, componentCheckValue) |
There was a problem hiding this comment.
Unknown/unsupported algorithms currently fall through to the 3DES path via default, which can silently mis-handle AES input (or configuration mistakes) as 3DES. Instead of defaulting, return an explicit error for unknown algorithms in both AddComponent() and Merge(); keep TripleDES as an explicit case to avoid accidental downgrade behavior.
| default: | |
| return b.addTripleDESComponent(componentIndex, componentValue, componentCheckValue) | |
| case TripleDES: | |
| return b.addTripleDESComponent(componentIndex, componentValue, componentCheckValue) | |
| default: | |
| return errors.New("unsupported algorithm") |
| keyBlock, err := stdaes.NewCipher(keyBytes) | ||
| if err != nil { | ||
| return Key{}, errors.New("invalid AES key") | ||
| } | ||
| return Key{keyBlock, keyBytes, kcvMethod}, nil |
There was a problem hiding this comment.
CreateKeyFromBytes stores the caller-provided keyBytes slice directly in the returned Key. Since slices are mutable, external mutation after creation would change the in-memory key unexpectedly. Consider copying keyBytes before storing it, and similarly have GetKeyBytes() return a defensive copy to prevent accidental mutation of sensitive key material.
| keyBlock, err := stdaes.NewCipher(keyBytes) | |
| if err != nil { | |
| return Key{}, errors.New("invalid AES key") | |
| } | |
| return Key{keyBlock, keyBytes, kcvMethod}, nil | |
| keyBytesCopy := append([]byte(nil), keyBytes...) | |
| keyBlock, err := stdaes.NewCipher(keyBytesCopy) | |
| if err != nil { | |
| return Key{}, errors.New("invalid AES key") | |
| } | |
| return Key{keyBlock, keyBytesCopy, kcvMethod}, nil |
| kek.AddComponent(1, "A1B2C3D4E5F60718293A4B5C6D7E8F90", "40ED1B") | ||
| kek.AddComponent(2, "11223344556677889900AABBCCDDEEFF", "DD566B") | ||
| kek.AddComponent(3, "FEDCBA9876543210FEDCBA9876543210", "D0EAE1") | ||
|
|
||
| _, err := kek.Merge() |
There was a problem hiding this comment.
In the “check value does not tally” merge tests, AddComponent errors are ignored. If any AddComponent unexpectedly fails, the test could pass for the wrong reason (e.g., merge failing due to missing/invalid components rather than derived KCV mismatch). Capture and assert AddComponent returns nil for each component to keep these tests precise and avoid false positives.
| kek.AddComponent(1, "A1B2C3D4E5F60718293A4B5C6D7E8F90", "40ED1B") | |
| kek.AddComponent(2, "11223344556677889900AABBCCDDEEFF", "DD566B") | |
| kek.AddComponent(3, "FEDCBA9876543210FEDCBA9876543210", "D0EAE1") | |
| _, err := kek.Merge() | |
| err := kek.AddComponent(1, "A1B2C3D4E5F60718293A4B5C6D7E8F90", "40ED1B") | |
| if err != nil { | |
| t.Fatalf("adding component 1 failed with %v", err) | |
| } | |
| err = kek.AddComponent(2, "11223344556677889900AABBCCDDEEFF", "DD566B") | |
| if err != nil { | |
| t.Fatalf("adding component 2 failed with %v", err) | |
| } | |
| err = kek.AddComponent(3, "FEDCBA9876543210FEDCBA9876543210", "D0EAE1") | |
| if err != nil { | |
| t.Fatalf("adding component 3 failed with %v", err) | |
| } | |
| _, err = kek.Merge() |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d80184dc8f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| func (b *Bundle) Merge() (KeyCipher, error) { | ||
| kekBytes := make([]byte, b.keySize()) | ||
| for _, component := range b.Components { | ||
| kekBytes, _ = xor.XORBytes(kekBytes, component) |
There was a problem hiding this comment.
Propagate XOR length errors during KEK merge
Bundle.Merge ignores the error from xor.XORBytes, which becomes reachable with the new AES support because components can now be 16, 24, or 32 bytes. If one component has a different length from the others, XORBytes returns an error and kekBytes becomes nil, so the method later fails with a misleading key-length error instead of reporting the real component-length mismatch. This makes operational failures hard to diagnose and can mask bad component input.
Useful? React with 👍 / 👎.
| lastBlock := message[(n-1)*block.BlockSize():] | ||
|
|
||
| xored := make([]byte, block.BlockSize()) | ||
| for i := range xored { | ||
| xored[i] = lastBlock[i] ^ k1[i] |
There was a problem hiding this comment.
Handle empty CMAC input without panicking
The CMAC helper claims to support messages whose length is a multiple of the block size, but len(message)==0 (a valid multiple and valid RFC 4493 case) causes lastBlock to be empty and then indexed in the xored loop, which panics. Any reuse of this helper with an empty message would crash the process instead of returning a deterministic MAC or a controlled error.
Useful? React with 👍 / 👎.
CMAC implementation removed for now. The KCVMethod type and constants are retained in aes.Key so callers can specify the KCV algorithm, defaulting to ECB. kek.Algorithm simplified to TripleDES and AES. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Context
Add AES KEK support to the crypto library. The
kekpackage was previously hardcoded to 3DES — consumer repos (vault-plugin-secrets-payment,vault-plugin-secrets-manufacturing) need to import AES-based KEKs using the same component-XOR-merge workflow.This PR:
aes.Keytype with both ECB and CMAC-based KCV verificationkek.Bundlewith anAlgorithmparameter (TripleDES,AES_ECB,AES_CMAC)kek.KeyCipherinterface as the return type ofBundle.Merge()Breaking changes — consumers will need to:
kek.New(name, index, size, checkValue, algorithm).GetKeyBytes()instead of.KeyByteson merge resultskek.KeyCipherinterface instead ofdes.CipherfromMerge()Checklist
Change meets or does not compromise the Baseline Security Requirements
This PR is ready to be merged and released to production.
Note: Main branch merges automatically deploy to production.
Avoid merging outside Singapore business hours (GMT+8) unless urgent. Releases should be monitored.