-
Notifications
You must be signed in to change notification settings - Fork 33
feat(platform): add ML-KEM-768 client-side encapsulation to Go SDK (DSPX-2399) #3486
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,11 +22,12 @@ type ECCMode uint8 | |
| type KeyType string | ||
|
|
||
| const ( | ||
| RSA2048Key KeyType = "rsa:2048" | ||
| RSA4096Key KeyType = "rsa:4096" | ||
| EC256Key KeyType = "ec:secp256r1" | ||
| EC384Key KeyType = "ec:secp384r1" | ||
| EC521Key KeyType = "ec:secp521r1" | ||
| RSA2048Key KeyType = "rsa:2048" | ||
| RSA4096Key KeyType = "rsa:4096" | ||
| EC256Key KeyType = "ec:secp256r1" | ||
| EC384Key KeyType = "ec:secp384r1" | ||
| EC521Key KeyType = "ec:secp521r1" | ||
| MLKem768Key KeyType = "mlkem:768" | ||
| ) | ||
|
|
||
| const ( | ||
|
|
@@ -91,6 +92,10 @@ func IsRSAKeyType(kt KeyType) bool { | |
| } | ||
| } | ||
|
|
||
| func IsMLKEMKeyType(kt KeyType) bool { | ||
| return kt == MLKem768Key | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| } | ||
|
|
||
| // GetECCurveFromECCMode return elliptic curve from ecc mode | ||
| func GetECCurveFromECCMode(mode ECCMode) (elliptic.Curve, error) { | ||
| var c elliptic.Curve | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,33 @@ | ||||||||||
| package ocrypto | ||||||||||
|
|
||||||||||
| import ( | ||||||||||
| "crypto/mlkem" | ||||||||||
| "encoding/pem" | ||||||||||
| "fmt" | ||||||||||
| ) | ||||||||||
|
|
||||||||||
| const ( | ||||||||||
| // MLKem768CiphertextSize is the byte length of an ML-KEM-768 ciphertext. | ||||||||||
| MLKem768CiphertextSize = 1088 | ||||||||||
| // MLKem768PublicKeySize is the byte length of an ML-KEM-768 encapsulation key. | ||||||||||
| MLKem768PublicKeySize = 1184 | ||||||||||
|
|
||||||||||
| mlkem768PEMType = "ML-KEM-768 PUBLIC KEY" | ||||||||||
| ) | ||||||||||
|
Comment on lines
+10
to
+16
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is better to use the constants provided by the const (
// MLKEM768CiphertextSize is the byte length of an ML-KEM-768 ciphertext.
MLKEM768CiphertextSize = mlkem.CiphertextSize768
// MLKEM768PublicKeySize is the byte length of an ML-KEM-768 encapsulation key.
MLKEM768PublicKeySize = mlkem.EncapsulationKeySize768
MLKEM768PEMType = "ML-KEM-768 PUBLIC KEY"
) |
||||||||||
|
|
||||||||||
| // MLKEMPublicKeyFromPEM parses an ML-KEM-768 encapsulation key from a PEM block | ||||||||||
| // with type "ML-KEM-768 PUBLIC KEY". | ||||||||||
| func MLKEMPublicKeyFromPEM(pemData []byte) (*mlkem.EncapsulationKey768, error) { | ||||||||||
| block, _ := pem.Decode(pemData) | ||||||||||
| if block == nil { | ||||||||||
| return nil, fmt.Errorf("failed to decode PEM block for ML-KEM-768 public key") | ||||||||||
| } | ||||||||||
| if block.Type != mlkem768PEMType { | ||||||||||
| return nil, fmt.Errorf("unexpected PEM type %q, expected %q", block.Type, mlkem768PEMType) | ||||||||||
|
Comment on lines
+25
to
+26
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update these lines to use the renamed and exported PEM type constant.
Suggested change
|
||||||||||
| } | ||||||||||
| key, err := mlkem.NewEncapsulationKey768(block.Bytes) | ||||||||||
| if err != nil { | ||||||||||
| return nil, fmt.Errorf("failed to parse ML-KEM-768 encapsulation key: %w", err) | ||||||||||
| } | ||||||||||
| return key, nil | ||||||||||
| } | ||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,8 +3,10 @@ | |||||||||||||||||
| package tdf | ||||||||||||||||||
|
|
||||||||||||||||||
| import ( | ||||||||||||||||||
| "crypto/mlkem" | ||||||||||||||||||
| "crypto/rand" | ||||||||||||||||||
| "encoding/json" | ||||||||||||||||||
| "encoding/pem" | ||||||||||||||||||
| "strings" | ||||||||||||||||||
| "testing" | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -473,3 +475,63 @@ func TestTdfSalt(t *testing.T) { | |||||||||||||||||
| assert.NotEmpty(t, salt1, "Salt should not be empty") | ||||||||||||||||||
| }) | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // mlkem768TestPublicKeyPEM generates a fresh ML-KEM-768 key pair and returns | ||||||||||||||||||
| // the public key as a PEM block with type "ML-KEM-768 PUBLIC KEY". | ||||||||||||||||||
| func mlkem768TestPublicKeyPEM(t *testing.T) (string, *mlkem.DecapsulationKey768) { | ||||||||||||||||||
| t.Helper() | ||||||||||||||||||
| dk, err := mlkem.GenerateKey768() | ||||||||||||||||||
| require.NoError(t, err) | ||||||||||||||||||
| pubKeyBytes := dk.EncapsulationKey().Bytes() | ||||||||||||||||||
| block := &pem.Block{Type: "ML-KEM-768 PUBLIC KEY", Bytes: pubKeyBytes} | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||||||
| return string(pem.EncodeToMemory(block)), dk | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| func TestWrapKeyWithMLKEM(t *testing.T) { | ||||||||||||||||||
| t.Run("wraps key and produces correct wire format", func(t *testing.T) { | ||||||||||||||||||
| pubKeyPEM, dk := mlkem768TestPublicKeyPEM(t) | ||||||||||||||||||
|
|
||||||||||||||||||
| symKey := make([]byte, 32) | ||||||||||||||||||
| _, err := rand.Read(symKey) | ||||||||||||||||||
| require.NoError(t, err) | ||||||||||||||||||
|
|
||||||||||||||||||
| wrappedB64, err := wrapKeyWithMLKEM(pubKeyPEM, symKey) | ||||||||||||||||||
| require.NoError(t, err) | ||||||||||||||||||
| assert.NotEmpty(t, wrappedB64) | ||||||||||||||||||
|
|
||||||||||||||||||
| payload, err := ocrypto.Base64Decode([]byte(wrappedB64)) | ||||||||||||||||||
| require.NoError(t, err) | ||||||||||||||||||
| assert.Greater(t, len(payload), ocrypto.MLKem768CiphertextSize, "payload must include ciphertext + wrapped DEK") | ||||||||||||||||||
|
|
||||||||||||||||||
| ciphertext := payload[:ocrypto.MLKem768CiphertextSize] | ||||||||||||||||||
| wrappedDEK := payload[ocrypto.MLKem768CiphertextSize:] | ||||||||||||||||||
|
Comment on lines
+504
to
+507
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update these references to use the renamed
Suggested change
|
||||||||||||||||||
|
|
||||||||||||||||||
| sharedKey, err := dk.Decapsulate(ciphertext) | ||||||||||||||||||
| require.NoError(t, err) | ||||||||||||||||||
|
|
||||||||||||||||||
| gcm, err := ocrypto.NewAESGcm(sharedKey) | ||||||||||||||||||
| require.NoError(t, err) | ||||||||||||||||||
|
|
||||||||||||||||||
| recoveredDEK, err := gcm.Decrypt(wrappedDEK) | ||||||||||||||||||
| require.NoError(t, err) | ||||||||||||||||||
| assert.Equal(t, symKey, recoveredDEK, "recovered DEK must match original") | ||||||||||||||||||
| }) | ||||||||||||||||||
|
|
||||||||||||||||||
| t.Run("buildKeyAccessObjects uses wrapped key type with no ephemeral key", func(t *testing.T) { | ||||||||||||||||||
| pubKeyPEM, _ := mlkem768TestPublicKeyPEM(t) | ||||||||||||||||||
| splitResult := createTestSplitResult(testKAS1URL, pubKeyPEM, "mlkem:768") | ||||||||||||||||||
|
|
||||||||||||||||||
| keyAccessList, err := buildKeyAccessObjects(splitResult, []byte(testPolicyJSON), "") | ||||||||||||||||||
| require.NoError(t, err) | ||||||||||||||||||
| require.Len(t, keyAccessList, 1) | ||||||||||||||||||
|
|
||||||||||||||||||
| ka := keyAccessList[0] | ||||||||||||||||||
| assert.Equal(t, "wrapped", ka.KeyType, "ML-KEM KAO must use 'wrapped' key type") | ||||||||||||||||||
| assert.Empty(t, ka.EphemeralPublicKey, "ML-KEM KAO must not set ephemeralPublicKey") | ||||||||||||||||||
| assert.NotEmpty(t, ka.WrappedKey) | ||||||||||||||||||
|
|
||||||||||||||||||
| payload, err := ocrypto.Base64Decode([]byte(ka.WrappedKey)) | ||||||||||||||||||
| require.NoError(t, err) | ||||||||||||||||||
| assert.Greater(t, len(payload), ocrypto.MLKem768CiphertextSize) | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||||||
| }) | ||||||||||||||||||
| } | ||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In Go, acronyms like ML-KEM should be consistently cased (e.g.,
MLKEM). This constant should be renamed toMLKEM768Keyto match the casing used in the helper functionIsMLKEMKeyTypeand other parts of the PR.