Skip to content

feat: add AES KEK support with ECB and CMAC KCV methods#24

Closed
lwschan-tw wants to merge 7 commits intomasterfrom
add-zmk-aes
Closed

feat: add AES KEK support with ECB and CMAC KCV methods#24
lwschan-tw wants to merge 7 commits intomasterfrom
add-zmk-aes

Conversation

@lwschan-tw
Copy link
Copy Markdown
Contributor

@lwschan-tw lwschan-tw commented Apr 22, 2026

Context

Add AES KEK support to the crypto library. The kek package 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:

  • Adds aes.Key type with both ECB and CMAC-based KCV verification
  • Implements AES-CMAC per RFC 4493 (no external dependency)
  • Extends kek.Bundle with an Algorithm parameter (TripleDES, AES_ECB, AES_CMAC)
  • Introduces kek.KeyCipher interface as the return type of Bundle.Merge()

Breaking changes — consumers will need to:

  1. Pass algorithm to kek.New(name, index, size, checkValue, algorithm)
  2. Use .GetKeyBytes() instead of .KeyBytes on merge results
  3. Handle kek.KeyCipher interface instead of des.Cipher from Merge()

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.

lwschan-tw and others added 6 commits April 22, 2026 11:23
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>
Copilot AI review requested due to automatic review settings April 22, 2026 05:15
@lwschan-tw lwschan-tw requested a review from a team as a code owner April 22, 2026 05:15
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.Bundle to support algorithm selection (3DES / AES ECB / AES CMAC) and return a KeyCipher from Merge().
  • 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.

Comment thread aes/cmac.go Outdated
Comment on lines +19 to +34
// 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]
}
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

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).”

Copilot uses AI. Check for mistakes.
Comment thread kek/kek_bundle.go
Comment on lines +100 to +115
// 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)
}
}
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment thread kek/kek_bundle.go
Comment on lines +71 to +72
default:
return b.addTripleDESComponent(componentIndex, componentValue, componentCheckValue)
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
default:
return b.addTripleDESComponent(componentIndex, componentValue, componentCheckValue)
case TripleDES:
return b.addTripleDESComponent(componentIndex, componentValue, componentCheckValue)
default:
return errors.New("unsupported algorithm")

Copilot uses AI. Check for mistakes.
Comment thread aes/aes_key_factory.go
Comment on lines +25 to +29
keyBlock, err := stdaes.NewCipher(keyBytes)
if err != nil {
return Key{}, errors.New("invalid AES key")
}
return Key{keyBlock, keyBytes, kcvMethod}, nil
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment thread kek/kek_bundle_test.go
Comment on lines +155 to +159
kek.AddComponent(1, "A1B2C3D4E5F60718293A4B5C6D7E8F90", "40ED1B")
kek.AddComponent(2, "11223344556677889900AABBCCDDEEFF", "DD566B")
kek.AddComponent(3, "FEDCBA9876543210FEDCBA9876543210", "D0EAE1")

_, err := kek.Merge()
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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()

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 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".

Comment thread kek/kek_bundle.go
func (b *Bundle) Merge() (KeyCipher, error) {
kekBytes := make([]byte, b.keySize())
for _, component := range b.Components {
kekBytes, _ = xor.XORBytes(kekBytes, component)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment thread aes/cmac.go Outdated
Comment on lines +29 to +33
lastBlock := message[(n-1)*block.BlockSize():]

xored := make([]byte, block.BlockSize())
for i := range xored {
xored[i] = lastBlock[i] ^ k1[i]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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>
@lwschan-tw lwschan-tw closed this Apr 22, 2026
@lwschan-tw lwschan-tw deleted the add-zmk-aes branch April 22, 2026 05:31
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.

2 participants