Skip to content

feat(crypto): add hybrid NIST EC + ML-KEM key wrapping support#3276

Open
sujankota wants to merge 9 commits intomainfrom
claude-nist-ec-ml-kem
Open

feat(crypto): add hybrid NIST EC + ML-KEM key wrapping support#3276
sujankota wants to merge 9 commits intomainfrom
claude-nist-ec-ml-kem

Conversation

@sujankota
Copy link
Copy Markdown
Contributor

@sujankota sujankota commented Apr 8, 2026

Proposed Changes

  • Add hybrid post-quantum key wrapping for three hybrid types: X-Wing (hpqt:xwing), P256+ML-KEM-768 (hpqt:secp256r1-mlkem768), and P384+ML-KEM-1024
    (hpqt:secp384r1-mlkem1024)
  • Implement key generation, PEM serialization, wrap/unwrap for NIST hybrid types in lib/ocrypto
  • Add support across SDK (sdk/tdf.go), experimental SDK (sdk/experimental/tdf/key_access.go), KAS service, and policy/protobuf layers
  • Hardcode all hybrid test keys (X-Wing, P256+ML-KEM-768, P384+ML-KEM-1024) as constants matching existing RSA/EC key patterns
  • Add end-to-end Test_SimpleTDF coverage for all three hybrid types

Checklist

  • I have added or updated unit tests
  • I have added or updated integration tests (if appropriate)
  • I have added or updated documentation

Testing Instructions

Summary by CodeRabbit

  • New Features

    • Added support for three hybrid post‑quantum key algorithms (X‑Wing, P-256+ML‑KEM‑768, P-384+ML‑KEM‑1024) and a new "hybrid-wrapped" key-access type; hybrid-wrapped entries omit ephemeral public keys.
  • Documentation

    • OpenAPI, protobuf docs, and JSON schemas updated to include new algorithm enum values and hybrid-wrapped semantics.
  • Configuration

    • New configuration flag to enable hybrid TDF behavior.
  • Tests

    • Added comprehensive unit and end‑to‑end tests for hybrid key generation, wrap/unwrap, encoding, and algorithm mappings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 8, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds three hybrid post‑quantum key algorithms (X‑Wing, P‑256+ML‑KEM‑768, P‑384+ML‑KEM‑1024) across protobuf/OpenAPI/docs, implements X‑Wing and hybrid NIST crypto in ocrypto, integrates hybrid wrapping/unwrapping into SDK and service layers, and updates configs and extensive tests.

Changes

Cohort / File(s) Summary
Proto & Docs
docs/grpc/index.html, service/kas/kas.proto, service/policy/objects.proto, service/policy/kasregistry/key_access_server_registry.proto
Added three HPQT enum values; updated KeyAccess/ephemeral/session key docs to include hybrid-wrapped.
OpenAPI Specs
docs/openapi/... (multiple files under authorization/, policy/, kas/, policy/*)
Extended policy.Algorithm and policy.KasPublicKeyAlgEnum enums across many OpenAPI specs to include HPQT entries.
Hybrid Crypto Implementation
lib/ocrypto/xwing.go, lib/ocrypto/hybrid_nist.go, lib/ocrypto/*_test.go
New X‑Wing and hybrid NIST (P-256/P-384 + ML‑KEM) implementations: keygen, PEM helpers, encapsulate/decapsulate, wrap/unwrap, HKDF+AES‑GCM, ASN.1 envelopes, and tests.
ocrypto Factory & deps
lib/ocrypto/asym_encryption.go, lib/ocrypto/asym_decryption.go, lib/ocrypto/go.mod
Factory dispatch recognizes hybrid PEM block types; added Hybrid SchemeType and dependency github.com/cloudflare/circl.
SDK TDF & Manifest
sdk/tdf.go, sdk/experimental/tdf/*, sdk/schema/manifest*.json, sdk/basekey.go, sdk/*_test.go
Mapped hybrid key types ↔ policy enums, introduced hybrid-wrapped key type, implemented hybrid wrapping paths (encapsulate → HKDF → AES‑GCM → ASN.1 → base64), and updated tests and manifest schemas.
Service Crypto Integrations
service/internal/security/*.go, service/kas/*, service/kas/access/*, service/kas/access/provider.go
Added algorithm constants, StandardXWing/StandardHybrid wrappers, load/decrypt paths for hybrid keys, HybridTDFEnabled config flag, and rewrap verification for hybrid-wrapped.
Indexing / DB / Policy mappings
service/kas/key_indexer.go, service/pkg/db/marshalHelpers.go, service/policy/db/grant_mappings.go
Added bidirectional mappings for new HPQT enums and string forms (e.g., hpqt:xwing) used by indexing, DB marshal, and grant mappings.
Tests & E2E
many files: lib/ocrypto/*_test.go, sdk/*_test.go, service/*_test.go, sdk/experimental/tdf/writer_test.go, sdk/tdf_hybrid_test.go
Wide test additions/updates for hybrid key generation, PEM round-trips, encapsulation, wrap/unwrap round-trips, TDF writer end‑to‑end flows, conversion mappings, KAS integration, and negative cases.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant SDK as SDK<br/>(tdf.go)
    participant Crypto as Cryptography<br/>(ocrypto)
    participant KAS as KAS Service<br/>(security)
    participant DB as Database

    rect rgba(100, 150, 200, 0.5)
        Note over Client,SDK: Client creates TDF using hybrid KAS public key
        Client->>SDK: CreateTDF(plaintext, policy, KASInfo)
        SDK->>SDK: createKeyAccess(hybrid algorithm, kasPublicKey)
        SDK->>Crypto: Encapsulate(publicKey)
        Crypto-->>SDK: sharedSecret, ciphertext
        SDK->>Crypto: CalculateHKDF(tdfSalt, sharedSecret)
        Crypto-->>SDK: wrapKey
        SDK->>Crypto: AES‑GCM encrypt(wrapKey, DEK)
        Crypto-->>SDK: encryptedDEK
        SDK->>SDK: ASN.1 marshal(HybridWrapped)
        SDK-->>Client: TDF(..., KeyAccess{keyType:"hybrid-wrapped", wrappedKey})
    end

    rect rgba(150, 100, 200, 0.5)
        Note over KAS,DB: KAS unwraps wrapped DEK
        KAS->>KAS: Decrypt(keyDetails, wrappedKey)
        KAS->>Crypto: Decapsulate(ciphertext, privKey)
        Crypto-->>KAS: sharedSecret
        KAS->>Crypto: CalculateHKDF(tdfSalt, sharedSecret) → wrapKey
        KAS->>Crypto: AES‑GCM decrypt(wrapKey, encryptedDEK)
        Crypto-->>KAS: DEK
        KAS-->>Client: return DecryptedKey(DEK)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

size/m

Poem

🐰 I hopped through code with whiskers bright,
I wove X‑Wing secrets in the night.
Curves and KEMs in a hybrid dance,
DEKs wrapped snug with careful chance.
A rabbit cheers: quantum takes flight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main change: addition of hybrid NIST EC + ML-KEM key wrapping support across the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude-nist-ec-ml-kem

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces support for hybrid post-quantum cryptographic key wrapping within the TDF ecosystem. By combining classical elliptic curve cryptography with NIST-standardized ML-KEM algorithms, the changes enhance the platform's security posture against future quantum threats. The implementation includes new cryptographic primitives, updated key management logic, and necessary schema adjustments to support these hybrid schemes across the SDK and KAS services.

Highlights

  • Hybrid Cryptography Support: Added support for hybrid NIST EC + ML-KEM key wrapping, including X-Wing, P-256 + ML-KEM-768, and P-384 + ML-KEM-1024 schemes.
  • Dependency Updates: Introduced the Cloudflare CIRCL library to support post-quantum cryptographic primitives.
  • API and Schema Updates: Updated protobuf definitions, schemas, and SDK logic to recognize and handle the new 'hybrid-wrapped' key type.
  • Infrastructure Integration: Integrated new hybrid key types into the KAS (Key Access Server) and security provider logic for key generation, wrapping, and unwrapping.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Ignored Files
  • Ignored by pattern: docs/openapi/**/* (13)
    • docs/openapi/authorization/authorization.openapi.yaml
    • docs/openapi/authorization/v2/authorization.openapi.yaml
    • docs/openapi/kas/kas.openapi.yaml
    • docs/openapi/policy/actions/actions.openapi.yaml
    • docs/openapi/policy/attributes/attributes.openapi.yaml
    • docs/openapi/policy/kasregistry/key_access_server_registry.openapi.yaml
    • docs/openapi/policy/namespaces/namespaces.openapi.yaml
    • docs/openapi/policy/objects.openapi.yaml
    • docs/openapi/policy/obligations/obligations.openapi.yaml
    • docs/openapi/policy/registeredresources/registered_resources.openapi.yaml
    • docs/openapi/policy/resourcemapping/resource_mapping.openapi.yaml
    • docs/openapi/policy/subjectmapping/subject_mapping.openapi.yaml
    • docs/openapi/policy/unsafe/unsafe.openapi.yaml
  • Ignored by pattern: protocol/**/* (3)
    • protocol/go/kas/kas.pb.go
    • protocol/go/policy/kasregistry/key_access_server_registry.pb.go
    • protocol/go/policy/objects.pb.go
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.


Quantum threats are on the way, Hybrid keys will save the day. EC and KEM now intertwined, Security for all mankind.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions github-actions bot added comp:db DB component comp:policy Policy Configuration ( attributes, subject mappings, resource mappings, kas registry) comp:sdk A software development kit, including library, for client applications and inter-service communicati comp:kas Key Access Server docs Documentation comp:lib:ocrypto size/xl labels Apr 8, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces support for hybrid post-quantum cryptography (PQC) schemes, specifically X-Wing and NIST hybrid (EC + ML-KEM) algorithms, across the ocrypto library, SDK, and KAS service. The changes include new key generation and wrapping logic, protocol buffer updates, and service-level integration for the hybrid-wrapped key type. Review feedback identifies a critical bug in the experimental TDF implementation where all hybrid keys are incorrectly processed as X-Wing, along with a similar issue in the test environment's fake KAS. Additionally, improvements are suggested for documentation accuracy regarding supported hybrid schemes and the modularity of shared cryptographic helper functions.

<td><p>Type of key wrapping used for the data encryption key
Required: Always
Values: &#39;wrapped&#39; (RSA-wrapped for ZTDF), &#39;ec-wrapped&#39; (experimental ECDH-wrapped) </p></td>
Values: &#39;wrapped&#39; (RSA-wrapped for ZTDF), &#39;ec-wrapped&#39; (experimental ECDH-wrapped), &#39;hybrid-wrapped&#39; (experimental X-Wing-wrapped) </p></td>
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.

medium

The description for 'hybrid-wrapped' is misleading as it only mentions "X-Wing-wrapped". This pull request introduces support for multiple hybrid schemes (X-Wing and NIST EC + ML-KEM). To avoid confusion and improve documentation accuracy, please consider making the description more generic to cover all supported hybrid algorithms.

Values: &#39;wrapped&#39; (RSA-wrapped for ZTDF), &#39;ec-wrapped&#39; (experimental ECDH-wrapped), &#39;hybrid-wrapped&#39; (experimental hybrid key wrapping, e.g., X-Wing, NIST EC + ML-KEM) </p></td>

// --- Decryptor ---

func NewP256MLKEM768Decryptor(privateKey []byte) (*HybridNISTDecryptor, error) {
return NewSaltedP256MLKEM768Decryptor(privateKey, defaultXWingSalt(), nil)
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.

medium

This function uses defaultXWingSalt(), which is defined in xwing.go. This creates a dependency on the X-Wing implementation for a generic NIST hybrid scheme and is confusingly named. A similar issue exists for other helper functions like xwingRawToPEM and cloneOrNil that are defined in xwing.go but used here.

To improve modularity and clarity, I recommend moving these generic helper functions to a new shared file (e.g., hybrid_common.go) and renaming them to be algorithm-agnostic (e.g., defaultHybridSalt()).

@sujankota sujankota marked this pull request as ready for review April 9, 2026 18:21
@sujankota sujankota requested review from a team as code owners April 9, 2026 18:21
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
service/pkg/db/marshalHelpers.go (1)

160-165: ⚠️ Potential issue | 🟠 Major

FormatAlg still misses the two ML-KEM algorithm cases.

Line 160 introduces X-Wing, but policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768 and policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024 still hit the unsupported path.

Proposed fix
 	case policy.Algorithm_ALGORITHM_HPQT_XWING:
 		return "hpqt:xwing", nil
+	case policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768:
+		return "hpqt:secp256r1-mlkem768", nil
+	case policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024:
+		return "hpqt:secp384r1-mlkem1024", nil
 	case policy.Algorithm_ALGORITHM_UNSPECIFIED:
 		fallthrough
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/pkg/db/marshalHelpers.go` around lines 160 - 165, FormatAlg is
missing cases for the two ML-KEM algorithms so those enums fall through to the
default unsupported path; add explicit case branches for
policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768 and
policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024 in the switch (alongside
ALGORITHM_HPQT_XWING) and return the correct canonical string identifiers (e.g.,
the ML-KEM names) with nil error so they no longer trigger the default
unsupported-algorithm error in FormatAlg.
service/kas/kas.go (1)

195-201: ⚠️ Potential issue | 🟠 Major

Fallback key discovery is incomplete for hybrid ML-KEM variants.

Line 195 only adds security.AlgorithmHPQTXWing. If deployment keys are only hpqt:secp256r1-mlkem768 or hpqt:secp384r1-mlkem1024, this fallback path may fail to seed defaults.

Proposed fix
-		for _, alg := range []string{security.AlgorithmECP256R1, security.AlgorithmRSA2048, security.AlgorithmHPQTXWing} {
+		for _, alg := range []string{
+			security.AlgorithmECP256R1,
+			security.AlgorithmRSA2048,
+			security.AlgorithmHPQTXWing,
+			security.AlgorithmHPQTSecp256r1MLKEM768,
+			security.AlgorithmHPQTSecp384r1MLKEM1024,
+		} {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/kas/kas.go` around lines 195 - 201, The fallback loop only checks
security.AlgorithmHPQTXWing and misses hybrid ML-KEM variants, causing defaults
to be empty when only hybrid keys like hpqt:secp256r1-mlkem768 or
hpqt:secp384r1-mlkem1024 exist; update the loop around cryptoProvider.FindKID to
also try the hybrid variant identifiers (e.g., the hpqt ML-KEM names used in
your deployment) in addition to security.AlgorithmHPQTXWing, appending any found
KIDs to defaults and retaining the existing l.Warn for misses so
cryptoProvider.FindKID, defaults, and l.Warn continue to be used consistently.
service/policy/db/grant_mappings.go (1)

25-30: ⚠️ Potential issue | 🟠 Major

Add missing ML-KEM algorithm mappings in the switch.

Line 25 adds X-Wing, but algorithms for the NIST EC + ML-KEM variants still fall through to ...UNSPECIFIED, which can silently degrade grant key metadata.

Proposed fix
 	case policy.Algorithm_ALGORITHM_HPQT_XWING:
 		return policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING
+	case policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768:
+		return policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768
+	case policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024:
+		return policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024
 	case policy.Algorithm_ALGORITHM_UNSPECIFIED:
 		return policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/policy/db/grant_mappings.go` around lines 25 - 30, The switch mapping
on policy.Algorithm (seen handling policy.Algorithm_ALGORITHM_HPQT_XWING ->
policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING) is missing cases
for the NIST EC + ML-KEM algorithm variants, causing them to fall through to
KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED; add explicit case branches for each ML-KEM
algorithm constant (the policy.Algorithm_... ML-KEM variants) and return the
corresponding policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_... ML-KEM enum
values instead of letting them default to UNSPECIFIED so grant key metadata is
preserved. Ensure each new case mirrors the existing case style used for
ALGORITHM_HPQT_XWING and the default remains unchanged.
docs/openapi/policy/obligations/obligations.openapi.yaml (1)

511-522: ⚠️ Potential issue | 🟠 Major

OpenAPI enum is missing the two ML‑KEM hybrid values

policy.Algorithm and policy.KasPublicKeyAlgEnum include ...HPQT_XWING but still omit ...HPQT_SECP256R1_MLKEM768 and ...HPQT_SECP384R1_MLKEM1024. That leaves this API contract out of sync with the supported algorithm set and can break generated clients.

Suggested patch
     policy.Algorithm:
       type: string
       title: Algorithm
       enum:
         - ALGORITHM_UNSPECIFIED
         - ALGORITHM_RSA_2048
         - ALGORITHM_RSA_4096
         - ALGORITHM_EC_P256
         - ALGORITHM_EC_P384
         - ALGORITHM_EC_P521
         - ALGORITHM_HPQT_XWING
+        - ALGORITHM_HPQT_SECP256R1_MLKEM768
+        - ALGORITHM_HPQT_SECP384R1_MLKEM1024

@@
     policy.KasPublicKeyAlgEnum:
       type: string
       title: KasPublicKeyAlgEnum
       enum:
         - KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED
         - KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048
         - KAS_PUBLIC_KEY_ALG_ENUM_RSA_4096
         - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1
         - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP384R1
         - KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP521R1
         - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING
+        - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP256R1_MLKEM768
+        - KAS_PUBLIC_KEY_ALG_ENUM_HPQT_SECP384R1_MLKEM1024

Also applies to: 538-549

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/openapi/policy/obligations/obligations.openapi.yaml` around lines 511 -
522, The OpenAPI enums policy.Algorithm and policy.KasPublicKeyAlgEnum are
missing the two ML‑KEM hybrid values; update both enum definitions to include
the entries ...HPQT_SECP256R1_MLKEM768 and ...HPQT_SECP384R1_MLKEM1024 (match
casing/style of existing entries like ALGORITHM_HPQT_XWING) so the API contract
reflects the full supported algorithm set and stays in sync with generated
clients.
docs/openapi/policy/actions/actions.openapi.yaml (1)

196-207: ⚠️ Potential issue | 🟠 Major

Add the two ML-KEM enum values here as well.

policy.Algorithm and policy.KasPublicKeyAlgEnum still stop at X-Wing, but this PR adds ALGORITHM_HPQT_SECP256R1_MLKEM768 and ALGORITHM_HPQT_SECP384R1_MLKEM1024 elsewhere. That leaves this spec unable to describe valid kasKeys[].publicKey.algorithm / KasPublicKey.alg values, and generated clients may reject legitimate server responses.

Also applies to: 223-234

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/openapi/policy/actions/actions.openapi.yaml` around lines 196 - 207, The
OpenAPI enum definitions for policy.Algorithm and policy.KasPublicKeyAlgEnum are
missing the two new ML-KEM values; update both enums to include
ALGORITHM_HPQT_SECP256R1_MLKEM768 and ALGORITHM_HPQT_SECP384R1_MLKEM1024 so that
kasKeys[].publicKey.algorithm / KasPublicKey.alg can represent the new
algorithms; locate the enum blocks named policy.Algorithm and
policy.KasPublicKeyAlgEnum in the YAML and append those two identifiers to their
enum lists (also mirror the same changes around the second block noted at lines
223-234).
docs/openapi/policy/resourcemapping/resource_mapping.openapi.yaml (1)

406-417: ⚠️ Potential issue | 🟠 Major

These shared policy enums are still missing the two ML-KEM variants.

Only X-Wing was added here, but the shared policy.Algorithm / policy.KasPublicKeyAlgEnum definitions also need ALGORITHM_HPQT_SECP256R1_MLKEM768 and ALGORITHM_HPQT_SECP384R1_MLKEM1024. Otherwise this OpenAPI document still understates the supported key algorithms for embedded kasKeys/public-key payloads.

Also applies to: 433-444

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/openapi/policy/resourcemapping/resource_mapping.openapi.yaml` around
lines 406 - 417, The shared policy enum definitions are missing two ML-KEM
variants; update the policy.Algorithm and policy.KasPublicKeyAlgEnum enum lists
to include ALGORITHM_HPQT_SECP256R1_MLKEM768 and
ALGORITHM_HPQT_SECP384R1_MLKEM1024 so the OpenAPI file accurately reflects
supported key algorithms for kasKeys/public-key payloads, and mirror the same
additions in the other identical enum block referenced (the second occurrence
around the kas public-key enums).
docs/openapi/policy/kasregistry/key_access_server_registry.openapi.yaml (1)

527-549: ⚠️ Potential issue | 🟠 Major

KAS registry OpenAPI still blocks the two new NIST hybrid algorithms.

The schema enums stop at X-Wing, and the CreateKey, ListKeys, and RotateKey.NewKey CEL snippets still only admit up to 6. Since the authoritative policy.Algorithm enum now includes values 7 and 8 for the ML-KEM variants, this spec still prevents clients from describing or submitting those new key types.

Also applies to: 1146-1155, 1669-1678, 1931-1940

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/openapi/policy/kasregistry/key_access_server_registry.openapi.yaml`
around lines 527 - 549, The OpenAPI spec and CEL validation are missing the two
new ML-KEM algorithm values (numeric values 7 and 8) causing clients to be
blocked; update the policy.Algorithm and policy.KasPublicKeyAlgEnum enums to
include the two ML-KEM entries (matching the authoritative enum names for the
new ML-KEM variants) and update any CEL expressions used in CreateKey, ListKeys,
and RotateKey.NewKey (and the corresponding enum definitions at the other
locations noted) to allow values 7 and 8 instead of stopping at 6 so the new key
types are accepted.
service/kas/access/publicKey.go (1)

79-86: 🧹 Nitpick | 🔵 Trivial

Misleading comment: "For RSA keys" now also handles HPQT algorithms.

The comment on line 81 states "For RSA keys, return the public key in PKCS8 format" but this case now handles HPQT hybrid algorithms as well. Consider updating the comment to reflect this.

📝 Suggested comment update
 	case security.AlgorithmRSA2048, security.AlgorithmHPQTXWing,
 		security.AlgorithmHPQTSecp256r1MLKEM768, security.AlgorithmHPQTSecp384r1MLKEM1024, "":
-		// For RSA keys, return the public key in PKCS8 format
+		// For RSA and HPQT keys, return the public key in PKCS8 format
 		pem, err = keyDetails.ExportPublicKey(ctx, trust.KeyTypePKCS8)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/kas/access/publicKey.go` around lines 79 - 86, Update the misleading
comment in the switch case that handles security.AlgorithmRSA2048,
security.AlgorithmHPQTXWing, security.AlgorithmHPQTSecp256r1MLKEM768,
security.AlgorithmHPQTSecp384r1MLKEM1024 and "" to accurately reflect that both
RSA and HPQT/hybrid algorithms are covered; edit the comment above the case
(near keyDetails.ExportPublicKey usage) to say something like "For RSA and
HPQT/hybrid keys, return the public key in PKCS8 format" so the comment matches
the handled algorithms and refers to keyDetails.ExportPublicKey for clarity.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/grpc/index.html`:
- Line 6291: Update the description of the 'hybrid-wrapped' value to be
algorithm-agnostic (or list all supported hybrid variants) instead of implying
X-Wing-only; locate the occurrences of the literal 'hybrid-wrapped' in the docs
(e.g., the HTML lines that currently read "hybrid-wrapped (experimental
X-Wing-wrapped)") and replace that phrasing with either a generic description
like "hybrid-wrapped (experimental hybrid KEMs combining classical and
post-quantum/ECDH schemes; algorithm-specific variants may include NIST EC +
ML-KEM, X-Wing, etc.)" or enumerate the supported hybrid variants so integrators
are not misled.
- Around line 4147-4151: The generated docs are missing the ML-KEM enum entries
for KasPublicKeyAlgEnum; add entries for KAS_PUBLIC_KEY_ALG_ENUM_MLKEM768 = 11
and KAS_PUBLIC_KEY_ALG_ENUM_MLKEM1024 = 12 alongside the existing
KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING (10) in the KasPublicKeyAlgEnum section so
the documentation matches service/policy/objects.proto (symbols:
KasPublicKeyAlgEnum, KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING,
KAS_PUBLIC_KEY_ALG_ENUM_MLKEM768, KAS_PUBLIC_KEY_ALG_ENUM_MLKEM1024).

In `@docs/openapi/authorization/v2/authorization.openapi.yaml`:
- Line 178: The OpenAPI enums that currently list ALGORITHM_HPQT_XWING need the
two ML-KEM variants added so API contracts cover the full hybrid family; update
both enums that contain ALGORITHM_HPQT_XWING to also include
ALGORITHM_HPQT_MLKEM and ALGORITHM_HPQT_MLKEM_AES (or your repository’s exact
ML-KEM symbol names) so NIST EC + ML-KEM variants are present alongside
ALGORITHM_HPQT_XWING.

In `@sdk/basekey_test.go`:
- Around line 96-100: Add test cases for the two new ML-KEM mappings by updating
the table-driven tests in TestGetKasKeyAlg and TestFormatAlg and the round-trip
cases so they include entries for HybridSecp256r1MLKEM768Key and
HybridSecp384r1MLKEM1024Key; specifically, add rows mapping
string(HybridSecp256r1MLKEM768Key) ->
policy.Algorithm_ALGORITHM_ML_KEM_SECP256R1_768 (or the exact enum used) and
string(HybridSecp384r1MLKEM1024Key) ->
policy.Algorithm_ALGORITHM_ML_KEM_SECP384R1_1024 in the same style as the
existing "hybrid xwing" entry, and include them in the round-trip assertion
table so TestGetKasKeyAlg, TestFormatAlg and the round-trip checks validate the
new conversions.

In `@sdk/granter.go`:
- Around line 505-510: FormatAlg is missing cases for
ALGORITHM_HPQT_SECP256R1_MLKEM768 and ALGORITHM_HPQT_SECP384R1_MLKEM1024,
causing those algs to fall through to the default error; add explicit switch
cases in FormatAlg that return the corresponding ocrypto constants
(ocrypto.HybridSecp256r1MLKEM768Key and ocrypto.HybridSecp384r1MLKEM1024Key)
similar to the existing HPQT_XWING case, and update reverseAlgMap in
service/pkg/db/marshalHelpers_test.go to include mappings from
ALGORITHM_HPQT_SECP256R1_MLKEM768 -> ocrypto.HybridSecp256r1MLKEM768Key and
ALGORITHM_HPQT_SECP384R1_MLKEM1024 -> ocrypto.HybridSecp384r1MLKEM1024Key so
tests cover these new variants.

In `@sdk/schema/manifest-lax.schema.json`:
- Line 55: The enum that currently lists ["wrapped", "hybrid-wrapped", "remote"]
must include "ec-wrapped" to preserve compatibility with existing EC manifests;
update the enum array in sdk/schema/manifest-lax.schema.json (the enum
containing "wrapped" / "hybrid-wrapped" / "remote") to also include "ec-wrapped"
so the schema accepts EC-style manifests used by SDK key access generation.

In `@sdk/tdf_test.go`:
- Around line 2919-2920: The two KAS fixture entries for "https://d.kas/" and
"https://e.kas/" advertise "rsa:2048" while their key material variables
(mockECPrivateKey1/mockECPublicKey1 and mockECPrivateKey2/mockECPublicKey2) are
EC keys; update those two entries in sdk/tdf_test.go to the correct EC algorithm
identifier (e.g., "ec:secp256r1" or the project's canonical EC string) so the
advertised algorithm matches the PEM, and re-run tests to ensure no other
fixtures rely on "rsa:2048"; this keeps KASInfo.Algorithm consistent with the
key material.
- Around line 3198-3230: The hybrid rewrap branch creates an asymmetric
encryptor via ocrypto.FromPublicPEM(bodyData.GetClientPublicKey()) and encrypts
the symmetricKey into entityWrappedKey but never sets resp.SessionPublicKey;
update this branch to mirror the ec-wrapped path by extracting the
ephemeral/session public key from the asymmetric encryptor (the same value the
ec-wrapped branch sets) and assign it to resp.SessionPublicKey after creating
asymEncrypt and before returning, so EC client keys receive the session public
key needed to unwrap the returned DEK.

In `@sdk/tdf.go`:
- Line 46: The comment notes duplicated hybrid wrap logic between kHybridWrapped
handling in createKeyAccess/generateWrapKeyWithHybrid and wrapKeyWithHybrid in
sdk/experimental/tdf/key_access.go; refactor by extracting the shared hybrid
key-wrap logic into a single helper function (e.g., NewHybridWrapKey or
wrapKeyHybridShared) placed in a common package accessible to both stable and
experimental code, update createKeyAccess to call that new helper instead of
generateWrapKeyWithHybrid, remove or forward the duplicate wrapKeyWithHybrid to
the shared helper, and ensure kHybridWrapped constant and error handling are
preserved so behavior remains unchanged.

In `@service/internal/security/standard_crypto.go`:
- Around line 502-543: The cases for StandardXWingCrypto and
StandardHybridCrypto risk confusing variable shadowing around err and
privateKey; extract each decryption path into small helper methods (e.g.,
decryptXWing using XWingPrivateKeyFromPem + XWingUnwrapDEK, decryptP256MLKEM768
using P256MLKEM768PrivateKeyFromPem + P256MLKEM768UnwrapDEK,
decryptP384MLKEM1024 using P384MLKEM1024PrivateKeyFromPem +
P384MLKEM1024UnwrapDEK) and call those from the switch, so parsing and unwrap
happen in isolated functions and no local err shadowing occurs in
StandardXWingCrypto / StandardHybridCrypto.

In `@service/policy/kasregistry/key_access_server_registry.proto`:
- Around line 403-407: The CEL validators for the key_algorithm field (validator
id "key_algorithm_defined") restrict allowed values to [1..6], but the Algorithm
enum now includes values 7 and 8 (ALGORITHM_HPQT_SECP256R1_MLKEM768 and
ALGORITHM_HPQT_SECP384R1_MLKEM1024), causing Create/List/Rotate key requests
with ML‑KEM algorithms to fail; update the expression in the key_algorithm
validators (the field named key_algorithm in key_access_server_registry.proto)
to include 7 and 8 (e.g., change "this in [1, 2, 3, 4, 5, 6]" to "this in [1, 2,
3, 4, 5, 6, 7, 8]") for all occurrences (including the other two validator
instances referenced in the comment) so the new enum values pass validation.

---

Outside diff comments:
In `@docs/openapi/policy/actions/actions.openapi.yaml`:
- Around line 196-207: The OpenAPI enum definitions for policy.Algorithm and
policy.KasPublicKeyAlgEnum are missing the two new ML-KEM values; update both
enums to include ALGORITHM_HPQT_SECP256R1_MLKEM768 and
ALGORITHM_HPQT_SECP384R1_MLKEM1024 so that kasKeys[].publicKey.algorithm /
KasPublicKey.alg can represent the new algorithms; locate the enum blocks named
policy.Algorithm and policy.KasPublicKeyAlgEnum in the YAML and append those two
identifiers to their enum lists (also mirror the same changes around the second
block noted at lines 223-234).

In `@docs/openapi/policy/kasregistry/key_access_server_registry.openapi.yaml`:
- Around line 527-549: The OpenAPI spec and CEL validation are missing the two
new ML-KEM algorithm values (numeric values 7 and 8) causing clients to be
blocked; update the policy.Algorithm and policy.KasPublicKeyAlgEnum enums to
include the two ML-KEM entries (matching the authoritative enum names for the
new ML-KEM variants) and update any CEL expressions used in CreateKey, ListKeys,
and RotateKey.NewKey (and the corresponding enum definitions at the other
locations noted) to allow values 7 and 8 instead of stopping at 6 so the new key
types are accepted.

In `@docs/openapi/policy/obligations/obligations.openapi.yaml`:
- Around line 511-522: The OpenAPI enums policy.Algorithm and
policy.KasPublicKeyAlgEnum are missing the two ML‑KEM hybrid values; update both
enum definitions to include the entries ...HPQT_SECP256R1_MLKEM768 and
...HPQT_SECP384R1_MLKEM1024 (match casing/style of existing entries like
ALGORITHM_HPQT_XWING) so the API contract reflects the full supported algorithm
set and stays in sync with generated clients.

In `@docs/openapi/policy/resourcemapping/resource_mapping.openapi.yaml`:
- Around line 406-417: The shared policy enum definitions are missing two ML-KEM
variants; update the policy.Algorithm and policy.KasPublicKeyAlgEnum enum lists
to include ALGORITHM_HPQT_SECP256R1_MLKEM768 and
ALGORITHM_HPQT_SECP384R1_MLKEM1024 so the OpenAPI file accurately reflects
supported key algorithms for kasKeys/public-key payloads, and mirror the same
additions in the other identical enum block referenced (the second occurrence
around the kas public-key enums).

In `@service/kas/access/publicKey.go`:
- Around line 79-86: Update the misleading comment in the switch case that
handles security.AlgorithmRSA2048, security.AlgorithmHPQTXWing,
security.AlgorithmHPQTSecp256r1MLKEM768,
security.AlgorithmHPQTSecp384r1MLKEM1024 and "" to accurately reflect that both
RSA and HPQT/hybrid algorithms are covered; edit the comment above the case
(near keyDetails.ExportPublicKey usage) to say something like "For RSA and
HPQT/hybrid keys, return the public key in PKCS8 format" so the comment matches
the handled algorithms and refers to keyDetails.ExportPublicKey for clarity.

In `@service/kas/kas.go`:
- Around line 195-201: The fallback loop only checks security.AlgorithmHPQTXWing
and misses hybrid ML-KEM variants, causing defaults to be empty when only hybrid
keys like hpqt:secp256r1-mlkem768 or hpqt:secp384r1-mlkem1024 exist; update the
loop around cryptoProvider.FindKID to also try the hybrid variant identifiers
(e.g., the hpqt ML-KEM names used in your deployment) in addition to
security.AlgorithmHPQTXWing, appending any found KIDs to defaults and retaining
the existing l.Warn for misses so cryptoProvider.FindKID, defaults, and l.Warn
continue to be used consistently.

In `@service/pkg/db/marshalHelpers.go`:
- Around line 160-165: FormatAlg is missing cases for the two ML-KEM algorithms
so those enums fall through to the default unsupported path; add explicit case
branches for policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768 and
policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024 in the switch (alongside
ALGORITHM_HPQT_XWING) and return the correct canonical string identifiers (e.g.,
the ML-KEM names) with nil error so they no longer trigger the default
unsupported-algorithm error in FormatAlg.

In `@service/policy/db/grant_mappings.go`:
- Around line 25-30: The switch mapping on policy.Algorithm (seen handling
policy.Algorithm_ALGORITHM_HPQT_XWING ->
policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_HPQT_XWING) is missing cases
for the NIST EC + ML-KEM algorithm variants, causing them to fall through to
KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED; add explicit case branches for each ML-KEM
algorithm constant (the policy.Algorithm_... ML-KEM variants) and return the
corresponding policy.KasPublicKeyAlgEnum_KAS_PUBLIC_KEY_ALG_ENUM_... ML-KEM enum
values instead of letting them default to UNSPECIFIED so grant key metadata is
preserved. Ensure each new case mirrors the existing case style used for
ALGORITHM_HPQT_XWING and the default remains unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 19596a14-4638-47ad-be5a-be8062ddd675

📥 Commits

Reviewing files that changed from the base of the PR and between ca46c63 and 6eb01e6.

⛔ Files ignored due to path filters (4)
  • lib/ocrypto/go.sum is excluded by !**/*.sum
  • protocol/go/kas/kas.pb.go is excluded by !**/*.pb.go
  • protocol/go/policy/kasregistry/key_access_server_registry.pb.go is excluded by !**/*.pb.go
  • protocol/go/policy/objects.pb.go is excluded by !**/*.pb.go
📒 Files selected for processing (50)
  • docs/grpc/index.html
  • docs/openapi/authorization/authorization.openapi.yaml
  • docs/openapi/authorization/v2/authorization.openapi.yaml
  • docs/openapi/kas/kas.openapi.yaml
  • docs/openapi/policy/actions/actions.openapi.yaml
  • docs/openapi/policy/attributes/attributes.openapi.yaml
  • docs/openapi/policy/kasregistry/key_access_server_registry.openapi.yaml
  • docs/openapi/policy/namespaces/namespaces.openapi.yaml
  • docs/openapi/policy/objects.openapi.yaml
  • docs/openapi/policy/obligations/obligations.openapi.yaml
  • docs/openapi/policy/registeredresources/registered_resources.openapi.yaml
  • docs/openapi/policy/resourcemapping/resource_mapping.openapi.yaml
  • docs/openapi/policy/subjectmapping/subject_mapping.openapi.yaml
  • docs/openapi/policy/unsafe/unsafe.openapi.yaml
  • lib/ocrypto/asym_decryption.go
  • lib/ocrypto/asym_encryption.go
  • lib/ocrypto/ec_key_pair.go
  • lib/ocrypto/go.mod
  • lib/ocrypto/hybrid_nist.go
  • lib/ocrypto/hybrid_nist_test.go
  • lib/ocrypto/xwing.go
  • lib/ocrypto/xwing_test.go
  • sdk/basekey.go
  • sdk/basekey_test.go
  • sdk/experimental/tdf/key_access.go
  • sdk/experimental/tdf/key_access_test.go
  • sdk/experimental/tdf/keysplit/attributes.go
  • sdk/experimental/tdf/keysplit/attributes_test.go
  • sdk/granter.go
  • sdk/schema/manifest-lax.schema.json
  • sdk/schema/manifest.schema.json
  • sdk/tdf.go
  • sdk/tdf_hybrid_test.go
  • sdk/tdf_test.go
  • service/internal/security/basic_manager.go
  • service/internal/security/crypto_provider.go
  • service/internal/security/in_process_provider.go
  • service/internal/security/standard_crypto.go
  • service/kas/access/provider.go
  • service/kas/access/publicKey.go
  • service/kas/access/rewrap.go
  • service/kas/kas.go
  • service/kas/kas.proto
  • service/kas/key_indexer.go
  • service/kas/key_indexer_test.go
  • service/pkg/db/marshalHelpers.go
  • service/pkg/db/marshalHelpers_test.go
  • service/policy/db/grant_mappings.go
  • service/policy/kasregistry/key_access_server_registry.proto
  • service/policy/objects.proto
👮 Files not reviewed due to content moderation or server errors (8)
  • service/kas/access/rewrap.go
  • docs/openapi/policy/unsafe/unsafe.openapi.yaml
  • service/kas/key_indexer_test.go
  • docs/openapi/policy/namespaces/namespaces.openapi.yaml
  • service/kas/key_indexer.go
  • docs/openapi/policy/subjectmapping/subject_mapping.openapi.yaml
  • service/internal/security/crypto_provider.go
  • docs/openapi/authorization/authorization.openapi.yaml

<td><p>Type of key wrapping used for the data encryption key
Required: Always
Values: &#39;wrapped&#39; (RSA-wrapped for ZTDF), &#39;ec-wrapped&#39; (experimental ECDH-wrapped) </p></td>
Values: &#39;wrapped&#39; (RSA-wrapped for ZTDF), &#39;ec-wrapped&#39; (experimental ECDH-wrapped), &#39;hybrid-wrapped&#39; (experimental X-Wing-wrapped) </p></td>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Broaden hybrid wrapping wording beyond X-Wing-only phrasing.

Line 6291 and Line 6676 describe hybrid-wrapped as X-Wing-based only. Given this PR’s hybrid NIST EC + ML-KEM scope, these descriptions should be algorithm-agnostic (or enumerate all supported hybrid variants) to avoid misleading integrators.

📝 Suggested wording update
-Values: 'wrapped' (RSA-wrapped for ZTDF), 'ec-wrapped' (experimental ECDH-wrapped), 'hybrid-wrapped' (experimental X-Wing-wrapped)
+Values: 'wrapped' (RSA-wrapped for ZTDF), 'ec-wrapped' (experimental ECDH-wrapped), 'hybrid-wrapped' (experimental hybrid PQ-wrapped, e.g., X-Wing and NIST EC+ML-KEM variants)
-Optional: Empty for RSA-based or X-Wing-based ZTDF (key_type="wrapped" or key_type="hybrid-wrapped")
+Optional: Empty for RSA-based or hybrid PQ-wrapped ZTDF (key_type="wrapped" or key_type="hybrid-wrapped")

Also applies to: 6676-6676

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/grpc/index.html` at line 6291, Update the description of the
'hybrid-wrapped' value to be algorithm-agnostic (or list all supported hybrid
variants) instead of implying X-Wing-only; locate the occurrences of the literal
'hybrid-wrapped' in the docs (e.g., the HTML lines that currently read
"hybrid-wrapped (experimental X-Wing-wrapped)") and replace that phrasing with
either a generic description like "hybrid-wrapped (experimental hybrid KEMs
combining classical and post-quantum/ECDH schemes; algorithm-specific variants
may include NIST EC + ML-KEM, X-Wing, etc.)" or enumerate the supported hybrid
variants so integrators are not misled.

Comment on lines +2919 to +2920
{"https://d.kas/", mockECPrivateKey1, mockECPublicKey1, "e1", "rsa:2048"},
{"https://e.kas/", mockECPrivateKey2, mockECPublicKey2, defaultKID, "rsa:2048"},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix the advertised algorithm for the EC KAS fixtures.

https://d.kas/ and https://e.kas/ serve EC key material, but they are still registered as "rsa:2048". That makes the test harness internally inconsistent and can hide regressions in any path that starts trusting KASInfo.Algorithm instead of inferring the key type from the PEM.

Suggested fix
-		{"https://d.kas/", mockECPrivateKey1, mockECPublicKey1, "e1", "rsa:2048"},
-		{"https://e.kas/", mockECPrivateKey2, mockECPublicKey2, defaultKID, "rsa:2048"},
+		{"https://d.kas/", mockECPrivateKey1, mockECPublicKey1, "e1", string(ocrypto.EC256Key)},
+		{"https://e.kas/", mockECPrivateKey2, mockECPublicKey2, defaultKID, string(ocrypto.EC256Key)},
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{"https://d.kas/", mockECPrivateKey1, mockECPublicKey1, "e1", "rsa:2048"},
{"https://e.kas/", mockECPrivateKey2, mockECPublicKey2, defaultKID, "rsa:2048"},
{"https://d.kas/", mockECPrivateKey1, mockECPublicKey1, "e1", string(ocrypto.EC256Key)},
{"https://e.kas/", mockECPrivateKey2, mockECPublicKey2, defaultKID, string(ocrypto.EC256Key)},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/tdf_test.go` around lines 2919 - 2920, The two KAS fixture entries for
"https://d.kas/" and "https://e.kas/" advertise "rsa:2048" while their key
material variables (mockECPrivateKey1/mockECPublicKey1 and
mockECPrivateKey2/mockECPublicKey2) are EC keys; update those two entries in
sdk/tdf_test.go to the correct EC algorithm identifier (e.g., "ec:secp256r1" or
the project's canonical EC string) so the advertised algorithm matches the PEM,
and re-run tests to ensure no other fixtures rely on "rsa:2048"; this keeps
KASInfo.Algorithm consistent with the key material.

Comment on lines +3198 to +3230
case "hybrid-wrapped":
kasPrivateKey := strings.ReplaceAll(f.privateKey, "\n\t", "\n")
if kao.GetKid() != "" && kao.GetKid() != f.KID {
lk, ok := f.legakeys[kaoReq.GetKeyAccessObject().GetKid()]
f.s.Require().True(ok, "unable to find key [%s]", kao.GetKid())
kasPrivateKey = strings.ReplaceAll(lk.private, "\n\t", "\n")
}

var symmetricKey []byte
switch ocrypto.KeyType(f.Algorithm) { //nolint:exhaustive // only handle hybrid types
case ocrypto.HybridSecp256r1MLKEM768Key:
privateKey, err := ocrypto.P256MLKEM768PrivateKeyFromPem([]byte(kasPrivateKey))
f.s.Require().NoError(err, "failed to extract P256+ML-KEM-768 private key from PEM")
symmetricKey, err = ocrypto.P256MLKEM768UnwrapDEK(privateKey, wrappedKey)
f.s.Require().NoError(err, "failed to unwrap P256+ML-KEM-768 wrapped key")
case ocrypto.HybridSecp384r1MLKEM1024Key:
privateKey, err := ocrypto.P384MLKEM1024PrivateKeyFromPem([]byte(kasPrivateKey))
f.s.Require().NoError(err, "failed to extract P384+ML-KEM-1024 private key from PEM")
symmetricKey, err = ocrypto.P384MLKEM1024UnwrapDEK(privateKey, wrappedKey)
f.s.Require().NoError(err, "failed to unwrap P384+ML-KEM-1024 wrapped key")
case ocrypto.HybridXWingKey:
privateKey, err := ocrypto.XWingPrivateKeyFromPem([]byte(kasPrivateKey))
f.s.Require().NoError(err, "failed to extract X-Wing private key from PEM")
symmetricKey, err = ocrypto.XWingUnwrapDEK(privateKey, wrappedKey)
f.s.Require().NoError(err, "failed to unwrap X-Wing wrapped key")
default:
f.s.Require().Failf("unsupported hybrid algorithm", "algorithm: %s", f.Algorithm)
}

asymEncrypt, err := ocrypto.FromPublicPEM(bodyData.GetClientPublicKey())
f.s.Require().NoError(err, "ocrypto.FromPublicPEM failed")
entityWrappedKey, err = asymEncrypt.Encrypt(symmetricKey)
f.s.Require().NoError(err, "ocrypto.encrypt failed")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Populate SessionPublicKey when hybrid rewrap targets an EC client key.

This branch re-encrypts with FromPublicPEM(...) and never sets resp.SessionPublicKey. If the client public key in the rewrap request is EC, the reader won't have the ephemeral session key it needs to unwrap the returned DEK. The ec-wrapped path in the same method already handles this correctly.

Suggested fix
-				asymEncrypt, err := ocrypto.FromPublicPEM(bodyData.GetClientPublicKey())
-				f.s.Require().NoError(err, "ocrypto.FromPublicPEM failed")
-				entityWrappedKey, err = asymEncrypt.Encrypt(symmetricKey)
-				f.s.Require().NoError(err, "ocrypto.encrypt failed")
+				asymEncrypt, err := ocrypto.FromPublicPEMWithSalt(bodyData.GetClientPublicKey(), tdfSalt(), nil)
+				f.s.Require().NoError(err, "ocrypto.FromPublicPEMWithSalt failed")
+				if e, ok := asymEncrypt.(ocrypto.ECEncryptor); ok {
+					resp.SessionPublicKey, err = e.PublicKeyInPemFormat()
+					f.s.Require().NoError(err, "unable to serialize ephemeral key")
+				}
+				entityWrappedKey, err = asymEncrypt.Encrypt(symmetricKey)
+				f.s.Require().NoError(err, "ocrypto.encrypt failed")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case "hybrid-wrapped":
kasPrivateKey := strings.ReplaceAll(f.privateKey, "\n\t", "\n")
if kao.GetKid() != "" && kao.GetKid() != f.KID {
lk, ok := f.legakeys[kaoReq.GetKeyAccessObject().GetKid()]
f.s.Require().True(ok, "unable to find key [%s]", kao.GetKid())
kasPrivateKey = strings.ReplaceAll(lk.private, "\n\t", "\n")
}
var symmetricKey []byte
switch ocrypto.KeyType(f.Algorithm) { //nolint:exhaustive // only handle hybrid types
case ocrypto.HybridSecp256r1MLKEM768Key:
privateKey, err := ocrypto.P256MLKEM768PrivateKeyFromPem([]byte(kasPrivateKey))
f.s.Require().NoError(err, "failed to extract P256+ML-KEM-768 private key from PEM")
symmetricKey, err = ocrypto.P256MLKEM768UnwrapDEK(privateKey, wrappedKey)
f.s.Require().NoError(err, "failed to unwrap P256+ML-KEM-768 wrapped key")
case ocrypto.HybridSecp384r1MLKEM1024Key:
privateKey, err := ocrypto.P384MLKEM1024PrivateKeyFromPem([]byte(kasPrivateKey))
f.s.Require().NoError(err, "failed to extract P384+ML-KEM-1024 private key from PEM")
symmetricKey, err = ocrypto.P384MLKEM1024UnwrapDEK(privateKey, wrappedKey)
f.s.Require().NoError(err, "failed to unwrap P384+ML-KEM-1024 wrapped key")
case ocrypto.HybridXWingKey:
privateKey, err := ocrypto.XWingPrivateKeyFromPem([]byte(kasPrivateKey))
f.s.Require().NoError(err, "failed to extract X-Wing private key from PEM")
symmetricKey, err = ocrypto.XWingUnwrapDEK(privateKey, wrappedKey)
f.s.Require().NoError(err, "failed to unwrap X-Wing wrapped key")
default:
f.s.Require().Failf("unsupported hybrid algorithm", "algorithm: %s", f.Algorithm)
}
asymEncrypt, err := ocrypto.FromPublicPEM(bodyData.GetClientPublicKey())
f.s.Require().NoError(err, "ocrypto.FromPublicPEM failed")
entityWrappedKey, err = asymEncrypt.Encrypt(symmetricKey)
f.s.Require().NoError(err, "ocrypto.encrypt failed")
case "hybrid-wrapped":
kasPrivateKey := strings.ReplaceAll(f.privateKey, "\n\t", "\n")
if kao.GetKid() != "" && kao.GetKid() != f.KID {
lk, ok := f.legakeys[kaoReq.GetKeyAccessObject().GetKid()]
f.s.Require().True(ok, "unable to find key [%s]", kao.GetKid())
kasPrivateKey = strings.ReplaceAll(lk.private, "\n\t", "\n")
}
var symmetricKey []byte
switch ocrypto.KeyType(f.Algorithm) { //nolint:exhaustive // only handle hybrid types
case ocrypto.HybridSecp256r1MLKEM768Key:
privateKey, err := ocrypto.P256MLKEM768PrivateKeyFromPem([]byte(kasPrivateKey))
f.s.Require().NoError(err, "failed to extract P256+ML-KEM-768 private key from PEM")
symmetricKey, err = ocrypto.P256MLKEM768UnwrapDEK(privateKey, wrappedKey)
f.s.Require().NoError(err, "failed to unwrap P256+ML-KEM-768 wrapped key")
case ocrypto.HybridSecp384r1MLKEM1024Key:
privateKey, err := ocrypto.P384MLKEM1024PrivateKeyFromPem([]byte(kasPrivateKey))
f.s.Require().NoError(err, "failed to extract P384+ML-KEM-1024 private key from PEM")
symmetricKey, err = ocrypto.P384MLKEM1024UnwrapDEK(privateKey, wrappedKey)
f.s.Require().NoError(err, "failed to unwrap P384+ML-KEM-1024 wrapped key")
case ocrypto.HybridXWingKey:
privateKey, err := ocrypto.XWingPrivateKeyFromPem([]byte(kasPrivateKey))
f.s.Require().NoError(err, "failed to extract X-Wing private key from PEM")
symmetricKey, err = ocrypto.XWingUnwrapDEK(privateKey, wrappedKey)
f.s.Require().NoError(err, "failed to unwrap X-Wing wrapped key")
default:
f.s.Require().Failf("unsupported hybrid algorithm", "algorithm: %s", f.Algorithm)
}
asymEncrypt, err := ocrypto.FromPublicPEMWithSalt(bodyData.GetClientPublicKey(), tdfSalt(), nil)
f.s.Require().NoError(err, "ocrypto.FromPublicPEMWithSalt failed")
if e, ok := asymEncrypt.(ocrypto.ECEncryptor); ok {
resp.SessionPublicKey, err = e.PublicKeyInPemFormat()
f.s.Require().NoError(err, "unable to serialize ephemeral key")
}
entityWrappedKey, err = asymEncrypt.Encrypt(symmetricKey)
f.s.Require().NoError(err, "ocrypto.encrypt failed")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/tdf_test.go` around lines 3198 - 3230, The hybrid rewrap branch creates
an asymmetric encryptor via ocrypto.FromPublicPEM(bodyData.GetClientPublicKey())
and encrypts the symmetricKey into entityWrappedKey but never sets
resp.SessionPublicKey; update this branch to mirror the ec-wrapped path by
extracting the ephemeral/session public key from the asymmetric encryptor (the
same value the ec-wrapped branch sets) and assign it to resp.SessionPublicKey
after creating asymEncrypt and before returning, so EC client keys receive the
session public key needed to unwrap the returned DEK.

kKeySize = 32
kWrapped = "wrapped"
kECWrapped = "ec-wrapped"
kHybridWrapped = "hybrid-wrapped"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

LGTM! Hybrid key wrapping integration in TDF creation.

The implementation correctly:

  • Adds the kHybridWrapped constant
  • Extends createKeyAccess to handle hybrid key types before EC/RSA
  • Provides generateWrapKeyWithXWing and generateWrapKeyWithHybrid helpers with proper error handling

There's structural similarity between generateWrapKeyWithHybrid here and wrapKeyWithHybrid in sdk/experimental/tdf/key_access.go. If the experimental package becomes stable, consider consolidating these into a shared helper to reduce duplication.

Also applies to: 678-686, 773-814

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/tdf.go` at line 46, The comment notes duplicated hybrid wrap logic
between kHybridWrapped handling in createKeyAccess/generateWrapKeyWithHybrid and
wrapKeyWithHybrid in sdk/experimental/tdf/key_access.go; refactor by extracting
the shared hybrid key-wrap logic into a single helper function (e.g.,
NewHybridWrapKey or wrapKeyHybridShared) placed in a common package accessible
to both stable and experimental code, update createKeyAccess to call that new
helper instead of generateWrapKeyWithHybrid, remove or forward the duplicate
wrapKeyWithHybrid to the shared helper, and ensure kHybridWrapped constant and
error handling are preserved so behavior remains unchanged.

Comment on lines +502 to +543
case StandardXWingCrypto:
if len(ephemeralPublicKey) > 0 {
return nil, errors.New("ephemeral public key should not be provided for X-Wing decryption")
}

privateKey, err := ocrypto.XWingPrivateKeyFromPem([]byte(key.xwingPrivateKeyPem))
if err != nil {
return nil, fmt.Errorf("failed to parse X-Wing private key: %w", err)
}

rawKey, err = ocrypto.XWingUnwrapDEK(privateKey, ciphertext)
if err != nil {
return nil, fmt.Errorf("failed to decrypt with X-Wing: %w", err)
}

case StandardHybridCrypto:
if len(ephemeralPublicKey) > 0 {
return nil, errors.New("ephemeral public key should not be provided for hybrid decryption")
}

switch key.Algorithm {
case AlgorithmHPQTSecp256r1MLKEM768:
privateKey, err := ocrypto.P256MLKEM768PrivateKeyFromPem([]byte(key.hybridPrivateKeyPem))
if err != nil {
return nil, fmt.Errorf("failed to parse P256-MLKEM768 private key: %w", err)
}
rawKey, err = ocrypto.P256MLKEM768UnwrapDEK(privateKey, ciphertext)
if err != nil {
return nil, fmt.Errorf("failed to decrypt with P256-MLKEM768: %w", err)
}
case AlgorithmHPQTSecp384r1MLKEM1024:
privateKey, err := ocrypto.P384MLKEM1024PrivateKeyFromPem([]byte(key.hybridPrivateKeyPem))
if err != nil {
return nil, fmt.Errorf("failed to parse P384-MLKEM1024 private key: %w", err)
}
rawKey, err = ocrypto.P384MLKEM1024UnwrapDEK(privateKey, ciphertext)
if err != nil {
return nil, fmt.Errorf("failed to decrypt with P384-MLKEM1024: %w", err)
}
default:
return nil, fmt.Errorf("unsupported hybrid algorithm [%s]", key.Algorithm)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Variable shadowing may hide errors in hybrid decryption paths.

In the StandardXWingCrypto and StandardHybridCrypto cases, there's a potential issue with variable shadowing:

rawKey, err = ocrypto.XWingUnwrapDEK(privateKey, ciphertext)

The err variable on line 512 shadows the err declared on line 507, which means if XWingPrivateKeyFromPem fails but err is reassigned, the original error could be lost. However, looking more closely, the current flow returns early on error, so this is actually safe.

The code flow is correct because:

  1. privateKey, err := ... - if this errors, it returns immediately
  2. rawKey, err = ... - this uses assignment, not declaration

Recommendation: Consider using more explicit error variable names or extracting each hybrid case into a helper function for clarity.

♻️ Optional refactor for clarity

Consider extracting each hybrid decrypt case into a helper method:

func (s *StandardCrypto) decryptXWing(key StandardXWingCrypto, ciphertext []byte) ([]byte, error) {
    privateKey, err := ocrypto.XWingPrivateKeyFromPem([]byte(key.xwingPrivateKeyPem))
    if err != nil {
        return nil, fmt.Errorf("failed to parse X-Wing private key: %w", err)
    }
    return ocrypto.XWingUnwrapDEK(privateKey, ciphertext)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/internal/security/standard_crypto.go` around lines 502 - 543, The
cases for StandardXWingCrypto and StandardHybridCrypto risk confusing variable
shadowing around err and privateKey; extract each decryption path into small
helper methods (e.g., decryptXWing using XWingPrivateKeyFromPem +
XWingUnwrapDEK, decryptP256MLKEM768 using P256MLKEM768PrivateKeyFromPem +
P256MLKEM768UnwrapDEK, decryptP384MLKEM1024 using P384MLKEM1024PrivateKeyFromPem
+ P384MLKEM1024UnwrapDEK) and call those from the switch, so parsing and unwrap
happen in isolated functions and no local err shadowing occurs in
StandardXWingCrypto / StandardHybridCrypto.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
sdk/experimental/tdf/key_access.go (1)

221-233: ⚠️ Potential issue | 🟠 Major

EC key wrapping uses XOR instead of AES-GCM - breaks interoperability with stable SDK.

The experimental wrapKeyWithEC function uses XOR-based key wrapping (line 230: wrapped[i] = symKey[i] ^ wrapKey[i]), while the stable SDK's generateWrapKeyWithEC uses AES-GCM encryption. TDFs created with the experimental SDK using EC keys will not be decryptable by the stable SDK, and vice versa.

The hybrid key wrapping implementation correctly uses AES-GCM (line 298), matching the stable SDK pattern. Align wrapKeyWithEC to use AES-GCM for consistency and interoperability:

Expected change

Replace the XOR wrapping logic with AES-GCM:

gcm, err := ocrypto.NewAESGcm(wrapKey)
if err != nil {
    return "", "", "", fmt.Errorf("failed to create AES-GCM: %w", err)
}
wrapped, err := gcm.Encrypt(symKey)
if err != nil {
    return "", "", "", fmt.Errorf("failed to encrypt: %w", err)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/experimental/tdf/key_access.go` around lines 221 - 233, The wrapKeyWithEC
function currently XORs symKey with wrapKey (wrapped[i] = symKey[i] ^
wrapKey[i]) which breaks interoperability with the stable SDK; replace that XOR
logic in wrapKeyWithEC with AES-GCM encryption like the hybrid path and the
stable generateWrapKeyWithEC uses: create an AES-GCM instance from wrapKey
(using ocrypto.NewAESGcm or equivalent), call its Encrypt on symKey to produce
wrapped, and propagate any errors with wrapped/ephemeralPubKey error returns
(e.g., wrap failure -> return "", "", "", fmt.Errorf("failed to create AES-GCM:
%w", err) or "failed to encrypt: %w"). Ensure the function still returns
Base64Encode(wrapped), the same wrapper type "eccWrapped", and the
ephemeralPubKey on success.
♻️ Duplicate comments (1)
sdk/tdf.go (1)

774-836: 🧹 Nitpick | 🔵 Trivial

Implementation is correct; consider consolidating with experimental package.

The hybrid wrapping logic follows sound cryptographic practices:

  • KEM encapsulation → HKDF key derivation → AES-GCM encryption → ASN.1 marshaling

However, this function is structurally identical to wrapKeyWithHybrid in sdk/experimental/tdf/key_access.go. When the experimental package stabilizes, extract the shared logic into a common helper to reduce maintenance burden.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/tdf.go` around lines 774 - 836, The generateWrapKeyWithHybrid function
duplicates logic found in wrapKeyWithHybrid
(sdk/experimental/tdf/key_access.go); consolidate by extracting the common
KEM→HKDF→AES-GCM→ASN.1 flow into a shared helper (e.g., shared package or util
function) that both generateWrapKeyWithHybrid and wrapKeyWithHybrid call,
keeping unique parsing/algorithm-dispatching in each caller (referencing
generateWrapKeyWithHybrid, wrapKeyWithHybrid, and the ocrypto
encapsulate/KDF/AESGcm/ASN.1 steps) so maintenance happens in one place once the
experimental package stabilizes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/ocrypto/hybrid_nist.go`:
- Around line 97-105: Add a short clarifying comment around the mlkemDecapsulate
implementation (the anonymous function assigned to mlkemDecapsulate that calls
sk.DecapsulateTo) stating that CIRCL's DecapsulateTo implements "implicit
rejection" and therefore does not return an error on incorrect ciphertext, and
that ciphertext authenticity is provided by the subsequent AES-GCM decryption
step rather than by ML-KEM; place the comment next to the DecapsulateTo call so
future readers understand the design choice and where tampering is detected.

---

Outside diff comments:
In `@sdk/experimental/tdf/key_access.go`:
- Around line 221-233: The wrapKeyWithEC function currently XORs symKey with
wrapKey (wrapped[i] = symKey[i] ^ wrapKey[i]) which breaks interoperability with
the stable SDK; replace that XOR logic in wrapKeyWithEC with AES-GCM encryption
like the hybrid path and the stable generateWrapKeyWithEC uses: create an
AES-GCM instance from wrapKey (using ocrypto.NewAESGcm or equivalent), call its
Encrypt on symKey to produce wrapped, and propagate any errors with
wrapped/ephemeralPubKey error returns (e.g., wrap failure -> return "", "", "",
fmt.Errorf("failed to create AES-GCM: %w", err) or "failed to encrypt: %w").
Ensure the function still returns Base64Encode(wrapped), the same wrapper type
"eccWrapped", and the ephemeralPubKey on success.

---

Duplicate comments:
In `@sdk/tdf.go`:
- Around line 774-836: The generateWrapKeyWithHybrid function duplicates logic
found in wrapKeyWithHybrid (sdk/experimental/tdf/key_access.go); consolidate by
extracting the common KEM→HKDF→AES-GCM→ASN.1 flow into a shared helper (e.g.,
shared package or util function) that both generateWrapKeyWithHybrid and
wrapKeyWithHybrid call, keeping unique parsing/algorithm-dispatching in each
caller (referencing generateWrapKeyWithHybrid, wrapKeyWithHybrid, and the
ocrypto encapsulate/KDF/AESGcm/ASN.1 steps) so maintenance happens in one place
once the experimental package stabilizes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7e652f77-7128-4434-a346-54a1bb0cd243

📥 Commits

Reviewing files that changed from the base of the PR and between 6eb01e6 and 9668f24.

📒 Files selected for processing (7)
  • lib/ocrypto/ec_key_pair.go
  • lib/ocrypto/hybrid_nist.go
  • lib/ocrypto/hybrid_nist_test.go
  • lib/ocrypto/xwing.go
  • lib/ocrypto/xwing_test.go
  • sdk/experimental/tdf/key_access.go
  • sdk/tdf.go

Comment on lines +97 to +105
mlkemDecapsulate: func(privKey, ciphertext []byte) ([]byte, error) {
var sk mlkem768.PrivateKey
if err := sk.Unpack(privKey); err != nil {
return nil, fmt.Errorf("mlkem768 private key unpack: %w", err)
}
ss := make([]byte, mlkem768.SharedKeySize)
sk.DecapsulateTo(ss, ciphertext)
return ss, nil
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

ML-KEM DecapsulateTo does not indicate decapsulation failure.

CIRCL's DecapsulateTo (lines 103, 134) doesn't return an error—it implements "implicit rejection" per the ML-KEM spec, meaning wrong ciphertext produces a pseudorandom output rather than failing. This is by design for IND-CCA2 security, but the calling code relies on subsequent AES-GCM decryption to detect tampering.

This is correct cryptographic behavior, but consider adding a comment explaining that authentication is provided by AES-GCM, not ML-KEM decapsulation.

📝 Suggested documentation
 	mlkemDecapsulate: func(privKey, ciphertext []byte) ([]byte, error) {
 		var sk mlkem768.PrivateKey
 		if err := sk.Unpack(privKey); err != nil {
 			return nil, fmt.Errorf("mlkem768 private key unpack: %w", err)
 		}
 		ss := make([]byte, mlkem768.SharedKeySize)
+		// DecapsulateTo implements implicit rejection per ML-KEM spec.
+		// Authentication is provided by subsequent AES-GCM decryption.
 		sk.DecapsulateTo(ss, ciphertext)
 		return ss, nil
 	},

Also applies to: 128-136

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ocrypto/hybrid_nist.go` around lines 97 - 105, Add a short clarifying
comment around the mlkemDecapsulate implementation (the anonymous function
assigned to mlkemDecapsulate that calls sk.DecapsulateTo) stating that CIRCL's
DecapsulateTo implements "implicit rejection" and therefore does not return an
error on incorrect ciphertext, and that ciphertext authenticity is provided by
the subsequent AES-GCM decryption step rather than by ML-KEM; place the comment
next to the DecapsulateTo call so future readers understand the design choice
and where tampering is detected.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/ocrypto/hybrid_nist_test.go`:
- Around line 197-227: The tests TestP256MLKEM768Encapsulate and
TestP384MLKEM1024Encapsulate currently only check hybridCt is non-empty; change
those assertions to validate the exact ciphertext length using the protocol
constants (P256MLKEM768CiphertextSize and P384MLKEM1024CiphertextSize) against
the hybridCt slice (and prefer require/ assert that checks size, e.g.,
require.Equal/ assert.Len) so the tests fail on size regressions instead of just
non-empty values.
- Around line 87-111: The tests TestP256MLKEM768WrapUnwrapWrongKeyFails and
TestP384MLKEM1024WrapUnwrapWrongKeyFails currently accept any error; update each
to assert the specific unwrap failure by replacing the generic require.Error
check with a check that the returned error from P256MLKEM768UnwrapDEK /
P384MLKEM1024UnwrapDEK contains the string "AES-GCM decrypt failed" (e.g., use
require.ErrorContains(t, err, "AES-GCM decrypt failed") or an equivalent
assertion) so the wrong-key path is validated explicitly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6213bab2-4c80-4aae-9340-efd652fbb3d5

📥 Commits

Reviewing files that changed from the base of the PR and between 9668f24 and c28eb46.

📒 Files selected for processing (2)
  • lib/ocrypto/hybrid_nist_test.go
  • lib/ocrypto/xwing_test.go

Comment on lines +87 to +111
func TestP256MLKEM768WrapUnwrapWrongKeyFails(t *testing.T) {
keyPair, err := NewP256MLKEM768KeyPair()
require.NoError(t, err)
wrongKeyPair, err := NewP256MLKEM768KeyPair()
require.NoError(t, err)

wrapped, err := P256MLKEM768WrapDEK(keyPair.publicKey, []byte("top secret dek"))
require.NoError(t, err)

_, err = P256MLKEM768UnwrapDEK(wrongKeyPair.privateKey, wrapped)
require.Error(t, err)
}

func TestP384MLKEM1024WrapUnwrapWrongKeyFails(t *testing.T) {
keyPair, err := NewP384MLKEM1024KeyPair()
require.NoError(t, err)
wrongKeyPair, err := NewP384MLKEM1024KeyPair()
require.NoError(t, err)

wrapped, err := P384MLKEM1024WrapDEK(keyPair.publicKey, []byte("top secret dek"))
require.NoError(t, err)

_, err = P384MLKEM1024UnwrapDEK(wrongKeyPair.privateKey, wrapped)
require.Error(t, err)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Tighten wrong-key negative assertions to validate the expected failure path.

These tests currently pass on any error, including unrelated parse/size failures. Assert that the error contains "AES-GCM decrypt failed" (the unwrap path in lib/ocrypto/hybrid_nist.go wraps wrong-key failures with that message), so regressions don’t get masked.

Suggested patch
 func TestP256MLKEM768WrapUnwrapWrongKeyFails(t *testing.T) {
@@
 	_, err = P256MLKEM768UnwrapDEK(wrongKeyPair.privateKey, wrapped)
 	require.Error(t, err)
+	assert.Contains(t, err.Error(), "AES-GCM decrypt failed")
 }
@@
 func TestP384MLKEM1024WrapUnwrapWrongKeyFails(t *testing.T) {
@@
 	_, err = P384MLKEM1024UnwrapDEK(wrongKeyPair.privateKey, wrapped)
 	require.Error(t, err)
+	assert.Contains(t, err.Error(), "AES-GCM decrypt failed")
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestP256MLKEM768WrapUnwrapWrongKeyFails(t *testing.T) {
keyPair, err := NewP256MLKEM768KeyPair()
require.NoError(t, err)
wrongKeyPair, err := NewP256MLKEM768KeyPair()
require.NoError(t, err)
wrapped, err := P256MLKEM768WrapDEK(keyPair.publicKey, []byte("top secret dek"))
require.NoError(t, err)
_, err = P256MLKEM768UnwrapDEK(wrongKeyPair.privateKey, wrapped)
require.Error(t, err)
}
func TestP384MLKEM1024WrapUnwrapWrongKeyFails(t *testing.T) {
keyPair, err := NewP384MLKEM1024KeyPair()
require.NoError(t, err)
wrongKeyPair, err := NewP384MLKEM1024KeyPair()
require.NoError(t, err)
wrapped, err := P384MLKEM1024WrapDEK(keyPair.publicKey, []byte("top secret dek"))
require.NoError(t, err)
_, err = P384MLKEM1024UnwrapDEK(wrongKeyPair.privateKey, wrapped)
require.Error(t, err)
}
func TestP256MLKEM768WrapUnwrapWrongKeyFails(t *testing.T) {
keyPair, err := NewP256MLKEM768KeyPair()
require.NoError(t, err)
wrongKeyPair, err := NewP256MLKEM768KeyPair()
require.NoError(t, err)
wrapped, err := P256MLKEM768WrapDEK(keyPair.publicKey, []byte("top secret dek"))
require.NoError(t, err)
_, err = P256MLKEM768UnwrapDEK(wrongKeyPair.privateKey, wrapped)
require.Error(t, err)
require.Contains(t, err.Error(), "AES-GCM decrypt failed")
}
func TestP384MLKEM1024WrapUnwrapWrongKeyFails(t *testing.T) {
keyPair, err := NewP384MLKEM1024KeyPair()
require.NoError(t, err)
wrongKeyPair, err := NewP384MLKEM1024KeyPair()
require.NoError(t, err)
wrapped, err := P384MLKEM1024WrapDEK(keyPair.publicKey, []byte("top secret dek"))
require.NoError(t, err)
_, err = P384MLKEM1024UnwrapDEK(wrongKeyPair.privateKey, wrapped)
require.Error(t, err)
require.Contains(t, err.Error(), "AES-GCM decrypt failed")
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ocrypto/hybrid_nist_test.go` around lines 87 - 111, The tests
TestP256MLKEM768WrapUnwrapWrongKeyFails and
TestP384MLKEM1024WrapUnwrapWrongKeyFails currently accept any error; update each
to assert the specific unwrap failure by replacing the generic require.Error
check with a check that the returned error from P256MLKEM768UnwrapDEK /
P384MLKEM1024UnwrapDEK contains the string "AES-GCM decrypt failed" (e.g., use
require.ErrorContains(t, err, "AES-GCM decrypt failed") or an equivalent
assertion) so the wrong-key path is validated explicitly.

Comment on lines +197 to +227
func TestP256MLKEM768Encapsulate(t *testing.T) {
keyPair, err := NewP256MLKEM768KeyPair()
require.NoError(t, err)

pubKey, err := keyPair.PublicKeyInPemFormat()
require.NoError(t, err)

pubKeyRaw, err := P256MLKEM768PubKeyFromPem([]byte(pubKey))
require.NoError(t, err)

combinedSecret, hybridCt, err := P256MLKEM768Encapsulate(pubKeyRaw)
require.NoError(t, err)
assert.NotEmpty(t, combinedSecret)
assert.NotEmpty(t, hybridCt)
}

func TestP384MLKEM1024Encapsulate(t *testing.T) {
keyPair, err := NewP384MLKEM1024KeyPair()
require.NoError(t, err)

pubKey, err := keyPair.PublicKeyInPemFormat()
require.NoError(t, err)

pubKeyRaw, err := P384MLKEM1024PubKeyFromPem([]byte(pubKey))
require.NoError(t, err)

combinedSecret, hybridCt, err := P384MLKEM1024Encapsulate(pubKeyRaw)
require.NoError(t, err)
assert.NotEmpty(t, combinedSecret)
assert.NotEmpty(t, hybridCt)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Validate encapsulation ciphertext sizes, not just non-empty values.

assert.NotEmpty can miss size regressions. Use explicit ciphertext size assertions (P256MLKEM768CiphertextSize / P384MLKEM1024CiphertextSize) to enforce protocol invariants.

Suggested patch
 func TestP256MLKEM768Encapsulate(t *testing.T) {
@@
 	combinedSecret, hybridCt, err := P256MLKEM768Encapsulate(pubKeyRaw)
 	require.NoError(t, err)
 	assert.NotEmpty(t, combinedSecret)
-	assert.NotEmpty(t, hybridCt)
+	assert.Len(t, hybridCt, P256MLKEM768CiphertextSize)
 }
@@
 func TestP384MLKEM1024Encapsulate(t *testing.T) {
@@
 	combinedSecret, hybridCt, err := P384MLKEM1024Encapsulate(pubKeyRaw)
 	require.NoError(t, err)
 	assert.NotEmpty(t, combinedSecret)
-	assert.NotEmpty(t, hybridCt)
+	assert.Len(t, hybridCt, P384MLKEM1024CiphertextSize)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ocrypto/hybrid_nist_test.go` around lines 197 - 227, The tests
TestP256MLKEM768Encapsulate and TestP384MLKEM1024Encapsulate currently only
check hybridCt is non-empty; change those assertions to validate the exact
ciphertext length using the protocol constants (P256MLKEM768CiphertextSize and
P384MLKEM1024CiphertextSize) against the hybridCt slice (and prefer require/
assert that checks size, e.g., require.Equal/ assert.Len) so the tests fail on
size regressions instead of just non-empty values.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
lib/ocrypto/hybrid_nist.go (1)

97-104: 🧹 Nitpick | 🔵 Trivial

Clarify CIRCL's implicit-rejection behavior next to DecapsulateTo.

DecapsulateTo intentionally does not fail on bad ciphertext, so a short note that tampering is detected by the later AES-GCM decrypt would prevent future “missing error handling” cleanups here.

Also applies to: 128-135

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ocrypto/hybrid_nist.go` around lines 97 - 104, Add a short clarifying
comment next to the call to sk.DecapsulateTo in mlkemDecapsulate (and the
analogous block at lines 128–135) stating that CIRCL's DecapsulateTo
intentionally does not return an error on malformed ciphertext
(implicit-rejection) and that tampering is detected later by the AES-GCM
decryption step; reference mlkem768.PrivateKey, DecapsulateTo and the subsequent
AES-GCM decrypt to make it clear why no error is returned here and to prevent
future reviewers from trying to add error handling at this callsite.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/ocrypto/hybrid_nist.go`:
- Around line 307-320: Rename the scheme-specific helper defaultXWingSalt() to a
scheme-agnostic defaultHybridSalt() and update all callers (e.g.,
NewP256MLKEM768Decryptor, NewSaltedP256MLKEM768Decryptor,
NewP384MLKEM1024Decryptor, NewSaltedP384MLKEM1024Decryptor and the other
occurrences noted) to call defaultHybridSalt() instead; keep the implementation
identical (or make defaultHybridSalt() delegate to defaultXWingSalt() and then
remove the old name) so behavior is unchanged but the helper name is generic for
all hybrid NIST constructors.

In `@sdk/experimental/tdf/writer_test.go`:
- Around line 892-980: The tests testHybridXWingFlow, testHybridP256MLKEM768Flow
and testHybridP384MLKEM1024Flow currently only assert a non-empty WrappedKey;
update each to call validateManifestSchema(t, result.Manifest) and then perform
a full round-trip unwrap using the generated keyPair's private key (from
ocrypto.NewXWingKeyPair / NewP256MLKEM768KeyPair / NewP384MLKEM1024KeyPair) and
the unwrap implementation in lib/ocrypto/hybrid_nist.go to decrypt
result.Manifest.KeyAccessObjs[0].WrappedKey, asserting the unwrapped CEK equals
the expected content-encryption key used by the writer (or that decrypting the
encrypted segment recovers the original bytes); keep NewWriter, WriteSegment and
Finalize usage but add these schema and unwrap assertions to ensure writer and
hybrid_nist unwrap are compatible.
- Around line 883-890: The helper createTestAttributeWithAlgorithm currently
no-ops when the nested grants/KAS-keys are missing which hides test setup bugs;
change its signature to accept t *testing.T (e.g.,
createTestAttributeWithAlgorithm(t *testing.T, fqn, kasURL, kid string, alg
policy.Algorithm, pem string)) and replace the silent branch with a fail-fast
assertion (t.Fatalf or t.Fatalff) if value.GetGrants() is empty or
value.GetGrants()[0].GetKasKeys() is empty, then proceed to set
PublicKey.Algorithm and PublicKey.Pem; update all callers to pass the testing.T.

---

Duplicate comments:
In `@lib/ocrypto/hybrid_nist.go`:
- Around line 97-104: Add a short clarifying comment next to the call to
sk.DecapsulateTo in mlkemDecapsulate (and the analogous block at lines 128–135)
stating that CIRCL's DecapsulateTo intentionally does not return an error on
malformed ciphertext (implicit-rejection) and that tampering is detected later
by the AES-GCM decryption step; reference mlkem768.PrivateKey, DecapsulateTo and
the subsequent AES-GCM decrypt to make it clear why no error is returned here
and to prevent future reviewers from trying to add error handling at this
callsite.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ddf776b3-70e8-4011-b2bd-5a5f433f2483

📥 Commits

Reviewing files that changed from the base of the PR and between c28eb46 and 8156e84.

📒 Files selected for processing (6)
  • lib/ocrypto/hybrid_nist.go
  • sdk/basekey_test.go
  • sdk/experimental/tdf/writer_test.go
  • service/pkg/db/marshalHelpers.go
  • service/pkg/db/marshalHelpers_test.go
  • service/policy/kasregistry/key_access_server_registry.proto

Comment on lines +307 to +320
func NewP256MLKEM768Decryptor(privateKey []byte) (*HybridNISTDecryptor, error) {
return NewSaltedP256MLKEM768Decryptor(privateKey, defaultXWingSalt(), nil)
}

func NewSaltedP256MLKEM768Decryptor(privateKey, salt, info []byte) (*HybridNISTDecryptor, error) {
return newHybridNISTDecryptor(&p256mlkem768Params, privateKey, salt, info)
}

func NewP384MLKEM1024Decryptor(privateKey []byte) (*HybridNISTDecryptor, error) {
return NewSaltedP384MLKEM1024Decryptor(privateKey, defaultXWingSalt(), nil)
}

func NewSaltedP384MLKEM1024Decryptor(privateKey, salt, info []byte) (*HybridNISTDecryptor, error) {
return newHybridNISTDecryptor(&p384mlkem1024Params, privateKey, salt, info)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Extract a scheme-agnostic default salt helper.

These NIST paths now depend on defaultXWingSalt(). Keeping the X-Wing name here makes the default look scheme-specific and makes a future salt change easy to apply to only one hybrid family by mistake. Please rename/extract this as something like defaultHybridSalt().

Also applies to: 340-353, 522-525

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ocrypto/hybrid_nist.go` around lines 307 - 320, Rename the
scheme-specific helper defaultXWingSalt() to a scheme-agnostic
defaultHybridSalt() and update all callers (e.g., NewP256MLKEM768Decryptor,
NewSaltedP256MLKEM768Decryptor, NewP384MLKEM1024Decryptor,
NewSaltedP384MLKEM1024Decryptor and the other occurrences noted) to call
defaultHybridSalt() instead; keep the implementation identical (or make
defaultHybridSalt() delegate to defaultXWingSalt() and then remove the old name)
so behavior is unchanged but the helper name is generic for all hybrid NIST
constructors.

Comment on lines +883 to +890
func createTestAttributeWithAlgorithm(fqn, kasURL, kid string, alg policy.Algorithm, pem string) *policy.Value {
value := createTestAttribute(fqn, kasURL, kid)
if len(value.GetGrants()) > 0 && len(value.GetGrants()[0].GetKasKeys()) > 0 {
value.GetGrants()[0].GetKasKeys()[0].PublicKey.Algorithm = alg
value.GetGrants()[0].GetKasKeys()[0].PublicKey.Pem = pem
}
return value
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Make the fixture helper fail fast on missing grants.

Silently returning the original RSA fixture when the nested grant/KAS-key shape changes makes setup bugs harder to diagnose. In test code this helper should assert the expected shape instead of no-op'ing.

🧪 Tighten the helper
 func createTestAttributeWithAlgorithm(fqn, kasURL, kid string, alg policy.Algorithm, pem string) *policy.Value {
 	value := createTestAttribute(fqn, kasURL, kid)
-	if len(value.GetGrants()) > 0 && len(value.GetGrants()[0].GetKasKeys()) > 0 {
-		value.GetGrants()[0].GetKasKeys()[0].PublicKey.Algorithm = alg
-		value.GetGrants()[0].GetKasKeys()[0].PublicKey.Pem = pem
-	}
+	if len(value.GetGrants()) == 0 || len(value.GetGrants()[0].GetKasKeys()) == 0 || value.GetGrants()[0].GetKasKeys()[0].PublicKey == nil {
+		panic("createTestAttributeWithAlgorithm: missing KAS key fixture")
+	}
+	value.GetGrants()[0].GetKasKeys()[0].PublicKey.Algorithm = alg
+	value.GetGrants()[0].GetKasKeys()[0].PublicKey.Pem = pem
 	return value
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/experimental/tdf/writer_test.go` around lines 883 - 890, The helper
createTestAttributeWithAlgorithm currently no-ops when the nested
grants/KAS-keys are missing which hides test setup bugs; change its signature to
accept t *testing.T (e.g., createTestAttributeWithAlgorithm(t *testing.T, fqn,
kasURL, kid string, alg policy.Algorithm, pem string)) and replace the silent
branch with a fail-fast assertion (t.Fatalf or t.Fatalff) if value.GetGrants()
is empty or value.GetGrants()[0].GetKasKeys() is empty, then proceed to set
PublicKey.Algorithm and PublicKey.Pem; update all callers to pass the testing.T.

Comment on lines +892 to +980
func testHybridXWingFlow(t *testing.T) {
ctx := t.Context()

keyPair, err := ocrypto.NewXWingKeyPair()
require.NoError(t, err)
pubPEM, err := keyPair.PublicKeyInPemFormat()
require.NoError(t, err)

writer, err := NewWriter(ctx)
require.NoError(t, err)

_, err = writer.WriteSegment(ctx, 0, []byte("hybrid xwing test data"))
require.NoError(t, err)

attributes := []*policy.Value{
createTestAttributeWithAlgorithm(
"https://example.com/attr/Classification/value/Secret",
testKAS1, "xwing-kid",
policy.Algorithm_ALGORITHM_HPQT_XWING, pubPEM,
),
}
result, err := writer.Finalize(ctx, WithAttributeValues(attributes))
require.NoError(t, err)
assert.NotEmpty(t, result.Data)

keyAccess := result.Manifest.KeyAccessObjs[0]
assert.Equal(t, "hybrid-wrapped", keyAccess.KeyType)
assert.NotEmpty(t, keyAccess.WrappedKey)
}

func testHybridP256MLKEM768Flow(t *testing.T) {
ctx := t.Context()

keyPair, err := ocrypto.NewP256MLKEM768KeyPair()
require.NoError(t, err)
pubPEM, err := keyPair.PublicKeyInPemFormat()
require.NoError(t, err)

writer, err := NewWriter(ctx)
require.NoError(t, err)

_, err = writer.WriteSegment(ctx, 0, []byte("hybrid p256 mlkem768 test data"))
require.NoError(t, err)

attributes := []*policy.Value{
createTestAttributeWithAlgorithm(
"https://example.com/attr/Classification/value/Secret",
testKAS1, "p256mlkem768-kid",
policy.Algorithm_ALGORITHM_HPQT_SECP256R1_MLKEM768, pubPEM,
),
}
result, err := writer.Finalize(ctx, WithAttributeValues(attributes))
require.NoError(t, err)
assert.NotEmpty(t, result.Data)

keyAccess := result.Manifest.KeyAccessObjs[0]
assert.Equal(t, "hybrid-wrapped", keyAccess.KeyType)
assert.NotEmpty(t, keyAccess.WrappedKey)
}

func testHybridP384MLKEM1024Flow(t *testing.T) {
ctx := t.Context()

keyPair, err := ocrypto.NewP384MLKEM1024KeyPair()
require.NoError(t, err)
pubPEM, err := keyPair.PublicKeyInPemFormat()
require.NoError(t, err)

writer, err := NewWriter(ctx)
require.NoError(t, err)

_, err = writer.WriteSegment(ctx, 0, []byte("hybrid p384 mlkem1024 test data"))
require.NoError(t, err)

attributes := []*policy.Value{
createTestAttributeWithAlgorithm(
"https://example.com/attr/Classification/value/Secret",
testKAS1, "p384mlkem1024-kid",
policy.Algorithm_ALGORITHM_HPQT_SECP384R1_MLKEM1024, pubPEM,
),
}
result, err := writer.Finalize(ctx, WithAttributeValues(attributes))
require.NoError(t, err)
assert.NotEmpty(t, result.Data)

keyAccess := result.Manifest.KeyAccessObjs[0]
assert.Equal(t, "hybrid-wrapped", keyAccess.KeyType)
assert.NotEmpty(t, keyAccess.WrappedKey)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

These hybrid flows stop before the new read path.

Right now they only prove that the writer-side mapping emitted a non-empty WrappedKey. A bad hybrid-wrapped manifest shape or a wrap/unwrap incompatibility in lib/ocrypto/hybrid_nist.go would still pass here. Please add validateManifestSchema(t, result.Manifest) and a round-trip unwrap assertion with the matching generated private key.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sdk/experimental/tdf/writer_test.go` around lines 892 - 980, The tests
testHybridXWingFlow, testHybridP256MLKEM768Flow and testHybridP384MLKEM1024Flow
currently only assert a non-empty WrappedKey; update each to call
validateManifestSchema(t, result.Manifest) and then perform a full round-trip
unwrap using the generated keyPair's private key (from ocrypto.NewXWingKeyPair /
NewP256MLKEM768KeyPair / NewP384MLKEM1024KeyPair) and the unwrap implementation
in lib/ocrypto/hybrid_nist.go to decrypt
result.Manifest.KeyAccessObjs[0].WrappedKey, asserting the unwrapped CEK equals
the expected content-encryption key used by the writer (or that decrypting the
encrypted segment recovers the original bytes); keep NewWriter, WriteSegment and
Finalize usage but add these schema and unwrap assertions to ensure writer and
hybrid_nist unwrap are compatible.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
docs/grpc/index.html (1)

6315-6315: ⚠️ Potential issue | 🟠 Major

Broaden hybrid-wrapped wording beyond X-Wing-only phrasing.

Line 6315 and Line 6700 still describe hybrid wrapping as X-Wing-specific, which is inaccurate now that NIST EC + ML-KEM variants are supported too. Please make this algorithm-agnostic (or enumerate all supported hybrid variants).
Because this HTML is generated, the source comments in service/kas/kas.proto should be updated and docs regenerated.

📝 Suggested wording update
-Values: 'wrapped' (RSA-wrapped for ZTDF), 'ec-wrapped' (experimental ECDH-wrapped), 'hybrid-wrapped' (experimental X-Wing-wrapped)
+Values: 'wrapped' (RSA-wrapped for ZTDF), 'ec-wrapped' (experimental ECDH-wrapped), 'hybrid-wrapped' (experimental hybrid PQ-wrapped, e.g., X-Wing and NIST EC+ML-KEM variants)
-Optional: Empty for RSA-based or X-Wing-based ZTDF (key_type="wrapped" or key_type="hybrid-wrapped")
+Optional: Empty for RSA-based or hybrid PQ-wrapped ZTDF (key_type="wrapped" or key_type="hybrid-wrapped")

Also applies to: 6700-6700

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/grpc/index.html` at line 6315, The docs still describe the
'hybrid-wrapped' value as X-Wing-specific; update the source comment in
service/kas/kas.proto where the enum/value 'hybrid-wrapped' (and any surrounding
description for the Values list) is documented to make the wording
algorithm-agnostic (e.g., "hybrid-wrapped (hybrid KEM combining classical EC and
PQ/ML-KEM variants such as X-Wing, NIST EC+ML-KEM, etc.)" or enumerate supported
hybrids), then regenerate the HTML/docs so lines referencing 'hybrid-wrapped'
(shown in generated docs at the two occurrences) reflect the new, broadened
wording.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@docs/grpc/index.html`:
- Line 6315: The docs still describe the 'hybrid-wrapped' value as
X-Wing-specific; update the source comment in service/kas/kas.proto where the
enum/value 'hybrid-wrapped' (and any surrounding description for the Values
list) is documented to make the wording algorithm-agnostic (e.g.,
"hybrid-wrapped (hybrid KEM combining classical EC and PQ/ML-KEM variants such
as X-Wing, NIST EC+ML-KEM, etc.)" or enumerate supported hybrids), then
regenerate the HTML/docs so lines referencing 'hybrid-wrapped' (shown in
generated docs at the two occurrences) reflect the new, broadened wording.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7ff68288-35c0-4752-945c-722dde3d092b

📥 Commits

Reviewing files that changed from the base of the PR and between 8156e84 and 2f0e5d1.

📒 Files selected for processing (13)
  • docs/grpc/index.html
  • docs/openapi/authorization/authorization.openapi.yaml
  • docs/openapi/authorization/v2/authorization.openapi.yaml
  • docs/openapi/policy/actions/actions.openapi.yaml
  • docs/openapi/policy/attributes/attributes.openapi.yaml
  • docs/openapi/policy/kasregistry/key_access_server_registry.openapi.yaml
  • docs/openapi/policy/namespaces/namespaces.openapi.yaml
  • docs/openapi/policy/objects.openapi.yaml
  • docs/openapi/policy/obligations/obligations.openapi.yaml
  • docs/openapi/policy/registeredresources/registered_resources.openapi.yaml
  • docs/openapi/policy/resourcemapping/resource_mapping.openapi.yaml
  • docs/openapi/policy/subjectmapping/subject_mapping.openapi.yaml
  • docs/openapi/policy/unsafe/unsafe.openapi.yaml

@@ -93,13 +93,24 @@ func encrypt(cmd *cobra.Command, args []string) error {
}

func keyTypeForKeyType(alg string) (ocrypto.KeyType, error) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Make a new method, ParseKeyType, in lib/ocrypto, that performs this and returns an error as below, but call it 'unrecognized' instead of 'unsupported'

EC SchemeType = "ec-wrapped"
RSA SchemeType = "wrapped"
EC SchemeType = "ec-wrapped"
Hybrid SchemeType = "hybrid-wrapped"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we need to create this? Can we just use 'wrapped'?

toolchain go1.25.8

require (
github.com/cloudflare/circl v1.6.3
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we use crypto/mlkem? Are you using this for xwing?

Alternative: Filippo's implementation uses builtin mlkem:
https://github.com/FiloSottile/mlkem768/blob/2e7bebc7d88d185c7e886b447d036c810a458a01/xwing/xwing.go

Comment on lines +39 to +43
P384MLKEM1024ECPublicKeySize = 97 // uncompressed P-384 point
P384MLKEM1024ECPrivateKeySize = 48 // P-384 scalar
P384MLKEM1024MLKEMPubKeySize = 1568 // mlkem1024.PublicKeySize
P384MLKEM1024MLKEMPrivKeySize = 3168 // mlkem1024.PrivateKeySize
P384MLKEM1024MLKEMCtSize = 1568 // mlkem1024.CiphertextSize
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

these names are confusing. They seem to only exist to satisfy the linter? IMO these will be much more clear inlined at the site of their use.

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

Labels

comp:db DB component comp:kas Key Access Server comp:lib:ocrypto comp:policy Policy Configuration ( attributes, subject mappings, resource mappings, kas registry) comp:sdk A software development kit, including library, for client applications and inter-service communicati docs Documentation size/xl

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants