Skip to content

[DRAFT] PQC xwing (gemini generated) - #3212

Closed
dmihalcik-virtru wants to merge 4 commits into
mainfrom
post-quantum-hybrid-gemini-2026-03-dm
Closed

[DRAFT] PQC xwing (gemini generated)#3212
dmihalcik-virtru wants to merge 4 commits into
mainfrom
post-quantum-hybrid-gemini-2026-03-dm

Conversation

@dmihalcik-virtru

Copy link
Copy Markdown
Member
  • feat(kas): add ml-kem key wrapping support
  • feat(kas): add ml-kem-1024 support
  • feat(kas): add support for Hybrid X-Wing (X25519 + ML-KEM-768)

Proposed Changes

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

@coderabbitai

coderabbitai Bot commented Mar 26, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 857644ff-9a11-43fc-bc00-01a0deaeabcc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch post-quantum-hybrid-gemini-2026-03-dm

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.

@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 Mar 26, 2026
@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 post-quantum cryptographic support to the system by implementing ML-KEM and Hybrid X-Wing key wrapping. These changes involve updating the core cryptographic library, extending the KAS (Key Access Server) protocols, and modifying the SDK to handle these new algorithm types during key generation, encryption, and decryption processes. The update ensures the system is prepared for quantum-resistant key exchange while maintaining compatibility with existing workflows.

Highlights

  • ML-KEM Support: Added support for ML-KEM-768 and ML-KEM-1024 key wrapping, including necessary crypto primitives and decapsulation logic.
  • Hybrid X-Wing Support: Implemented support for Hybrid X-Wing (X25519 + ML-KEM-768) key wrapping, enabling quantum-resistant hybrid key exchange.
  • API and Schema Updates: Updated gRPC documentation, protobuf definitions, and SDK manifest schemas to include the new ML-KEM and Hybrid algorithm types.
  • Crypto Provider Refactoring: Refactored internal security providers to support the new key types, replacing RSA-specific logic with a more generic PrivateKeyCrypto interface.

🧠 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.


The quantum threat is drawing near, But lattice math will calm our fear. With X-Wing flight and KEM in hand, We guard the keys across the land.

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.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request implements support for Post-Quantum Cryptography (PQC) algorithms, including ML-KEM (768 and 1024) and Hybrid X-Wing (X25519 + ML-KEM-768), across the crypto library, SDK, and KAS service. The feedback identifies critical bugs where multiple "%w" verbs are used in "fmt.Errorf" calls, which is invalid in Go. Furthermore, several suggestions are provided to reduce extensive code duplication in the new encryption, decryption, and key formatting logic by extracting shared helper functions and consolidating identical case blocks.

Comment on lines +68 to +71
decap1024, err1024 := mlkem.NewDecapsulationKey1024(block.Bytes)
if err1024 != nil {
return nil, fmt.Errorf("mlkem.NewDecapsulationKey1024 failed after mlkem.NewDecapsulationKey768 failed: %w / %w", err, err1024)
}

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.

critical

The use of multiple %w format specifiers in fmt.Errorf is not valid. In Go 1.20+ this is a compile-time error, and in older versions it can lead to a runtime panic. To combine multiple errors while preserving their types for inspection, you should use errors.Join.

Suggested change
decap1024, err1024 := mlkem.NewDecapsulationKey1024(block.Bytes)
if err1024 != nil {
return nil, fmt.Errorf("mlkem.NewDecapsulationKey1024 failed after mlkem.NewDecapsulationKey768 failed: %w / %w", err, err1024)
}
decap1024, err1024 := mlkem.NewDecapsulationKey1024(block.Bytes)
if err1024 != nil {
return nil, fmt.Errorf("mlkem.NewDecapsulationKey failed for both 768 and 1024: %w", errors.Join(err, err1024))
}

Comment on lines +188 to +191
encap1024, err1024 := mlkem.NewEncapsulationKey1024(block.Bytes)
if err1024 != nil {
return nil, fmt.Errorf("mlkem.NewEncapsulationKey1024 failed after mlkem.NewEncapsulationKey768 failed: %w / %w", err, err1024)
}

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.

critical

The use of multiple %w format specifiers in fmt.Errorf is not valid. In Go 1.20+ this is a compile-time error, and in older versions it can lead to a runtime panic. To combine multiple errors while preserving their types for inspection, you should use errors.Join.

Suggested change
encap1024, err1024 := mlkem.NewEncapsulationKey1024(block.Bytes)
if err1024 != nil {
return nil, fmt.Errorf("mlkem.NewEncapsulationKey1024 failed after mlkem.NewEncapsulationKey768 failed: %w / %w", err, err1024)
}
encap1024, err1024 := mlkem.NewEncapsulationKey1024(block.Bytes)
if err1024 != nil {
return nil, fmt.Errorf("mlkem.NewEncapsulationKey failed for both 768 and 1024: %w", errors.Join(err, err1024))
}

Comment on lines +250 to +285
func (d MLKEMDecryptor768) DecryptWithEphemeralKey(data, ephemeral []byte) ([]byte, error) {
if d.decap == nil {
return nil, errors.New("mlkem decapsulation key is nil")
}
if len(ephemeral) == 0 {
return nil, errors.New("ciphertext encapsulation is required for ML-KEM decryption")
}

sharedSecret, err := d.decap.Decapsulate(ephemeral)
if err != nil {
return nil, fmt.Errorf("mlkem.Decapsulate failed: %w", err)
}

block, err := aes.NewCipher(sharedSecret)
if err != nil {
return nil, fmt.Errorf("aes.NewCipher failure: %w", err)
}

gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("cipher.NewGCM failure: %w", err)
}

nonceSize := gcm.NonceSize()
if len(data) < nonceSize {
return nil, errors.New("ciphertext too short")
}

nonce, ciphertext := data[:nonceSize], data[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, fmt.Errorf("gcm.Open failure: %w", err)
}

return plaintext, 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

There is significant code duplication between MLKEMDecryptor768.DecryptWithEphemeralKey, MLKEMDecryptor1024.DecryptWithEphemeralKey, and HybridXWingDecryptorWrapper.DecryptWithEphemeralKey. The core AES-GCM decryption logic is identical in all of them.

To improve maintainability and reduce redundancy, I suggest extracting this common logic into a private helper function.

For example, you could create a function decryptAESGCM:

func decryptAESGCM(data, sharedSecret []byte) ([]byte, error) {
	block, err := aes.NewCipher(sharedSecret)
	if err != nil {
		return nil, fmt.Errorf("aes.NewCipher failure: %w", err)
	}

	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, fmt.Errorf("cipher.NewGCM failure: %w", err)
	}

	nonceSize := gcm.NonceSize()
	if len(data) < nonceSize {
		return nil, errors.New("ciphertext too short")
	}

	nonce, ciphertext := data[:nonceSize], data[nonceSize:]
	plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
	if err != nil {
		return nil, fmt.Errorf("gcm.Open failure: %w", err)
	}

	return plaintext, nil
}

Then, this function and the other similar ones can be simplified to:

func (d MLKEMDecryptor768) DecryptWithEphemeralKey(data, ephemeral []byte) ([]byte, error) {
	if d.decap == nil {
		return nil, errors.New("mlkem decapsulation key is nil")
	}
	if len(ephemeral) == 0 {
		return nil, errors.New("ciphertext encapsulation is required for ML-KEM decryption")
	}

	sharedSecret, err := d.decap.Decapsulate(ephemeral)
	if err != nil {
		return nil, fmt.Errorf("mlkem.Decapsulate failed: %w", err)
	}

	return decryptAESGCM(data, sharedSecret)
}

This change would apply to MLKEMDecryptor1024.DecryptWithEphemeralKey and HybridXWingDecryptorWrapper.DecryptWithEphemeralKey as well.

Comment on lines +390 to +407
func (e MLKEMEncryptor768) Encrypt(data []byte) ([]byte, error) {
block, err := aes.NewCipher(e.sharedSecret)
if err != nil {
return nil, fmt.Errorf("aes.NewCipher failed: %w", err)
}

gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("cipher.NewGCM failed: %w", err)
}

nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, fmt.Errorf("nonce generation failed: %w", err)
}

return gcm.Seal(nonce, nonce, data, nil), 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

There is significant code duplication between the Encrypt methods for MLKEMEncryptor768, MLKEMEncryptor1024, and HybridXWingEncryptorWrapper. The core AES-GCM encryption logic is identical.

To improve maintainability, I suggest extracting this common logic into a private helper function.

For example, you could create a function encryptAESGCM:

func encryptAESGCM(data, sharedSecret []byte) ([]byte, error) {
	block, err := aes.NewCipher(sharedSecret)
	if err != nil {
		return nil, fmt.Errorf("aes.NewCipher failed: %w", err)
	}

	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, fmt.Errorf("cipher.NewGCM failed: %w", err)
	}

	nonce := make([]byte, gcm.NonceSize()) 
	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
		return nil, fmt.Errorf("nonce generation failed: %w", err)
	}

	return gcm.Seal(nonce, nonce, data, nil), nil
}

Then, this Encrypt method can be simplified to:

func (e MLKEMEncryptor768) Encrypt(data []byte) ([]byte, error) {
	return encryptAESGCM(data, e.sharedSecret)
}

This change would apply to MLKEMEncryptor1024.Encrypt and HybridXWingEncryptorWrapper.Encrypt as well.

Comment on lines +242 to +268
func (keyPair MLKEMKeyPair) PrivateKeyInPemFormat() (string, error) {
if keyPair.PrivateKey == nil {
return "", errors.New("failed to generate PEM formatted private key")
}

privateKeyPEM := pem.EncodeToMemory(
&pem.Block{
Type: "MLKEM DECAPSULATION KEY",
Bytes: keyPair.PrivateKey.Bytes(),
},
)
return string(privateKeyPEM), nil
}

func (keyPair MLKEMKeyPair) PublicKeyInPemFormat() (string, error) {
if keyPair.PrivateKey == nil {
return "", errors.New("failed to generate PEM formatted public key")
}

publicKeyPEM := pem.EncodeToMemory(
&pem.Block{
Type: "MLKEM ENCAPSULATOR",
Bytes: keyPair.PrivateKey.EncapsulationKey().Bytes(),
},
)
return string(publicKeyPEM), 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

The PrivateKeyInPemFormat and PublicKeyInPemFormat methods for MLKEMKeyPair and MLKEM1024KeyPair are nearly identical, leading to code duplication.

To improve maintainability, you could use helper functions that operate on an interface. Both *mlkem.DecapsulationKey768 and *mlkem.DecapsulationKey1024 have Bytes() and EncapsulationKey().Bytes() methods, so they can satisfy a common interface.

Example refactoring:

type mlkemPrivateKey interface {
    Bytes() []byte
    EncapsulationKey() interface {
        Bytes() []byte
    }
}

func mlkemPrivateKeyInPemFormat(key mlkemPrivateKey) (string, error) {
    if key == nil {
        return "", errors.New("failed to generate PEM formatted private key")
    }
    privateKeyPEM := pem.EncodeToMemory(&pem.Block{
        Type:  "MLKEM DECAPSULATION KEY",
        Bytes: key.Bytes(),
    })
    return string(privateKeyPEM), nil
}

func mlkemPublicKeyInPemFormat(key mlkemPrivateKey) (string, error) {
    if key == nil {
        return "", errors.New("failed to generate PEM formatted public key")
    }
    publicKeyPEM := pem.EncodeToMemory(&pem.Block{
        Type:  "MLKEM ENCAPSULATOR",
        Bytes: key.EncapsulationKey().Bytes(),
    })
    return string(publicKeyPEM), nil
}

// Then the methods become one-liners:
func (keyPair MLKEMKeyPair) PrivateKeyInPemFormat() (string, error) {
    return mlkemPrivateKeyInPemFormat(keyPair.PrivateKey)
}

func (keyPair MLKEMKeyPair) PublicKeyInPemFormat() (string, error) {
    return mlkemPublicKeyInPemFormat(keyPair.PrivateKey)
}

This would make the code more DRY and easier to maintain.

Comment thread sdk/tdf.go
Comment on lines +783 to +819
func generateWrapKeyWithMLKEM(publicKey string, symKey []byte) (string, string, error) {
publicKeyEncryptor, err := ocrypto.FromPublicPEM(publicKey)
if err != nil {
return "", "", fmt.Errorf("generateWrapKeyWithMLKEM: ocrypto.FromPublicPEM failed:%w", err)
}

wrappedKey, err := publicKeyEncryptor.Encrypt(symKey)
if err != nil {
return "", "", fmt.Errorf("generateWrapKeyWithMLKEM: encrypt failed:%w", err)
}

encapsulatedKey := publicKeyEncryptor.EphemeralKey()
if len(encapsulatedKey) == 0 {
return "", "", errors.New("generateWrapKeyWithMLKEM: encapsulated key missing")
}

return string(ocrypto.Base64Encode(wrappedKey)), string(ocrypto.Base64Encode(encapsulatedKey)), nil
}

func generateWrapKeyWithHybrid(publicKey string, symKey []byte) (string, string, error) {
publicKeyEncryptor, err := ocrypto.FromPublicPEM(publicKey)
if err != nil {
return "", "", fmt.Errorf("generateWrapKeyWithHybrid: ocrypto.FromPublicPEM failed:%w", err)
}

wrappedKey, err := publicKeyEncryptor.Encrypt(symKey)
if err != nil {
return "", "", fmt.Errorf("generateWrapKeyWithHybrid: encrypt failed:%w", err)
}

encapsulatedKey := publicKeyEncryptor.EphemeralKey()
if len(encapsulatedKey) == 0 {
return "", "", errors.New("generateWrapKeyWithHybrid: encapsulated key missing")
}

return string(ocrypto.Base64Encode(wrappedKey)), string(ocrypto.Base64Encode(encapsulatedKey)), 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

The functions generateWrapKeyWithMLKEM and generateWrapKeyWithHybrid are almost identical. The only difference is the name used in the error message. This code duplication can be avoided by merging them into a single function that takes the algorithm name as a parameter for error reporting.

For example, a single function generateWrapKeyWithKEM could be created:

func generateWrapKeyWithKEM(publicKey string, symKey []byte, name string) (string, string, error) {
	publicKeyEncryptor, err := ocrypto.FromPublicPEM(publicKey)
	if err != nil {
		return "", "", fmt.Errorf("generateWrapKeyWithKEM for %s: ocrypto.FromPublicPEM failed:%w", name, err)
	}

	wrappedKey, err := publicKeyEncryptor.Encrypt(symKey)
	if err != nil {
		return "", "", fmt.Errorf("generateWrapKeyWithKEM for %s: encrypt failed:%w", name, err)
	}

	encapsulatedKey := publicKeyEncryptor.EphemeralKey()
	if len(encapsulatedKey) == 0 {
		return "", "", fmt.Errorf("generateWrapKeyWithKEM for %s: encapsulated key missing", name)
	}

	return string(ocrypto.Base64Encode(wrappedKey)), string(ocrypto.Base64Encode(encapsulatedKey)), nil
}

You can then call this from createKeyAccess for both ML-KEM and Hybrid cases, passing the appropriate name.

Comment on lines +100 to +119
case ocrypto.MLKEM768Key, ocrypto.MLKEM1024Key:
plaintext, err := decrypter.DecryptWithEphemeralKey(ciphertext, ephemeralPublicKey)
if err != nil {
return nil, fmt.Errorf("failed to decrypt with ML-KEM: %w", err)
}
protectedKey, err := ocrypto.NewAESProtectedKey(plaintext)
if err != nil {
return nil, fmt.Errorf("failed to create protected key: %w", err)
}
return protectedKey, nil
case ocrypto.HybridXWingKey:
plaintext, err := decrypter.DecryptWithEphemeralKey(ciphertext, ephemeralPublicKey)
if err != nil {
return nil, fmt.Errorf("failed to decrypt with Hybrid X-Wing: %w", err)
}
protectedKey, err := ocrypto.NewAESProtectedKey(plaintext)
if err != nil {
return nil, fmt.Errorf("failed to create protected key: %w", err)
}
return protectedKey, 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

The logic inside the case blocks for MLKEM768Key, MLKEM1024Key and HybridXWingKey is identical. You can combine these cases to reduce code duplication and improve readability.

Suggested change
case ocrypto.MLKEM768Key, ocrypto.MLKEM1024Key:
plaintext, err := decrypter.DecryptWithEphemeralKey(ciphertext, ephemeralPublicKey)
if err != nil {
return nil, fmt.Errorf("failed to decrypt with ML-KEM: %w", err)
}
protectedKey, err := ocrypto.NewAESProtectedKey(plaintext)
if err != nil {
return nil, fmt.Errorf("failed to create protected key: %w", err)
}
return protectedKey, nil
case ocrypto.HybridXWingKey:
plaintext, err := decrypter.DecryptWithEphemeralKey(ciphertext, ephemeralPublicKey)
if err != nil {
return nil, fmt.Errorf("failed to decrypt with Hybrid X-Wing: %w", err)
}
protectedKey, err := ocrypto.NewAESProtectedKey(plaintext)
if err != nil {
return nil, fmt.Errorf("failed to create protected key: %w", err)
}
return protectedKey, nil
case ocrypto.MLKEM768Key, ocrypto.MLKEM1024Key, ocrypto.HybridXWingKey:
plaintext, err := decrypter.DecryptWithEphemeralKey(ciphertext, ephemeralPublicKey)
if err != nil {
return nil, fmt.Errorf("failed to decrypt with KEM: %w", err)
}
protectedKey, err := ocrypto.NewAESProtectedKey(plaintext)
if err != nil {
return nil, fmt.Errorf("failed to create protected key: %w", err)
}
return protectedKey, nil

Comment on lines +658 to +701
case string(ocrypto.MLKEM):
ephemeralCiphertext := kao.GetKeyAccessObject().GetEphemeralPublicKey()
if ephemeralCiphertext == "" {
p.Logger.WarnContext(ctx, "missing encapsulated key for ml-kem rewrap")
failedKAORewrap(results, kao, err400("bad request"))
continue
}

encapsulatedKey, err := ocrypto.Base64Decode([]byte(ephemeralCiphertext))
if err != nil {
p.Logger.WarnContext(ctx, "failed to decode encapsulated key for ml-kem rewrap", slog.Any("error", err))
failedKAORewrap(results, kao, err400("bad request"))
continue
}

kid := trust.KeyIdentifier(kao.GetKeyAccessObject().GetKid())
dek, err = p.KeyDelegator.Decrypt(ctx, kid, kao.GetKeyAccessObject().GetWrappedKey(), encapsulatedKey)
if err != nil {
p.Logger.WarnContext(ctx, "failed to decrypt ML-KEM key", slog.Any("error", err))
failedKAORewrap(results, kao, err400("bad request"))
continue
}
case string(ocrypto.Hybrid):
ephemeralCiphertext := kao.GetKeyAccessObject().GetEphemeralPublicKey()
if ephemeralCiphertext == "" {
p.Logger.WarnContext(ctx, "missing encapsulated key for hybrid rewrap")
failedKAORewrap(results, kao, err400("bad request"))
continue
}

encapsulatedKey, err := ocrypto.Base64Decode([]byte(ephemeralCiphertext))
if err != nil {
p.Logger.WarnContext(ctx, "failed to decode encapsulated key for hybrid rewrap", slog.Any("error", err))
failedKAORewrap(results, kao, err400("bad request"))
continue
}

kid := trust.KeyIdentifier(kao.GetKeyAccessObject().GetKid())
dek, err = p.KeyDelegator.Decrypt(ctx, kid, kao.GetKeyAccessObject().GetWrappedKey(), encapsulatedKey)
if err != nil {
p.Logger.WarnContext(ctx, "failed to decrypt hybrid key", slog.Any("error", err))
failedKAORewrap(results, kao, err400("bad request"))
continue
}

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 logic for handling ocrypto.MLKEM and ocrypto.Hybrid key types is identical. These two case blocks can be combined to reduce code duplication and improve maintainability.

case string(ocrypto.MLKEM), string(ocrypto.Hybrid):
    ephemeralCiphertext := kao.GetKeyAccessObject().GetEphemeralPublicKey()
    if ephemeralCiphertext == "" {
        p.Logger.WarnContext(ctx, "missing encapsulated key for KEM rewrap")
        failedKAORewrap(results, kao, err400("bad request"))
        continue
    }

    encapsulatedKey, err := ocrypto.Base64Decode([]byte(ephemeralCiphertext))
    if err != nil {
        p.Logger.WarnContext(ctx, "failed to decode encapsulated key for KEM rewrap", slog.Any("error", err))
        failedKAORewrap(results, kao, err400("bad request"))
        continue
    }

    kid := trust.KeyIdentifier(kao.GetKeyAccessObject().GetKid())
    dek, err = p.KeyDelegator.Decrypt(ctx, kid, kao.GetKeyAccessObject().GetWrappedKey(), encapsulatedKey)
    if err != nil {
        p.Logger.WarnContext(ctx, "failed to decrypt KEM key", slog.Any("error", err))
        failedKAORewrap(results, kao, err400("bad request"))
        continue
    }

Signed-off-by: David Mihalcik <dmihalcik@virtru.com>
Signed-off-by: David Mihalcik <dmihalcik@virtru.com>
This commit introduces support for the Hybrid X-Wing KEM scheme as defined in
draft-connolly-cfrg-xwing-kem-10.

- Added HybridXWingKey (hpqt:xwing) to lib/ocrypto.
- Implemented HybridXWingEncryptor/Decryptor using ASN.1 for composite keys and ciphertexts.
- Integrated hybrid scheme support into KAS rewrap and Policy service.
- Added 'hybrid' scheme type to SDK and manifest schema.
- Added comprehensive unit tests for Hybrid X-Wing round-trips.

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
@dmihalcik-virtru
dmihalcik-virtru force-pushed the post-quantum-hybrid-gemini-2026-03-dm branch from 5802096 to 493092f Compare April 9, 2026 12:30
@github-actions

github-actions Bot commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

⚠️ Govulncheck found vulnerabilities ⚠️

The following modules have known vulnerabilities:

  • examples
  • sdk
  • lib/fixtures

See the workflow run for details.

@github-actions

github-actions Bot commented Apr 9, 2026

Copy link
Copy Markdown
Contributor
Benchmark results, click to expand

Benchmark authorization.GetDecisions Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 184.240212ms

Benchmark authorization.v2.GetMultiResourceDecision Results:

Metric Value
Approved Decision Requests 1000
Denied Decision Requests 0
Total Time 90.659503ms

Benchmark Statistics

Name № Requests Avg Duration Min Duration Max Duration

Bulk Benchmark Results

Metric Value
Total Decrypts 100
Successful Decrypts 100
Failed Decrypts 0
Total Time 410.368708ms
Throughput 243.68 requests/second

TDF3 Benchmark Results:

Metric Value
Total Requests 5000
Successful Requests 5000
Failed Requests 0
Concurrent Requests 50
Total Time 42.95366732s
Average Latency 427.969708ms
Throughput 116.40 requests/second

@dmihalcik-virtru

Copy link
Copy Markdown
Member Author

Merged in #3276

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 pqc size/xl

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant