From 0190b5d680e3ea3fa83f98485ca613b128b7266d Mon Sep 17 00:00:00 2001 From: Patryk Kalinowski Date: Fri, 15 May 2026 17:19:26 +0200 Subject: [PATCH 01/13] C-01: use ephemeral dummy provider certificate --- enclave/attestation_test.go | 4 +- enclave/provider_dummy.go | 103 +++++++++++++++--------------------- 2 files changed, 44 insertions(+), 63 deletions(-) diff --git a/enclave/attestation_test.go b/enclave/attestation_test.go index 62e3800..7128ce2 100644 --- a/enclave/attestation_test.go +++ b/enclave/attestation_test.go @@ -73,7 +73,7 @@ func TestNitroAttestation_Decrypt(t *testing.T) { doc, err := nitro.Parse(params.Recipient.AttestationDocument) require.NoError(t, err) assert.Equal(t, []byte("nonce"), doc.Nonce) - assert.NoError(t, doc.Validate(nitro.WithRootFingerprint("14e8bc5fabb52876f35f122289eaabfa08885837cc7f161149c6d242596258aa"))) + assert.NoError(t, doc.Validate(nitro.WithRootFingerprint(doc.RootCertFingerprint()))) assert.NoError(t, doc.Verify()) assert.Equal(t, types.KeyEncryptionMechanismRsaesOaepSha256, params.Recipient.KeyEncryptionAlgorithm) return &kms.DecryptOutput{CiphertextForRecipient: ciphertextForRecipient}, nil @@ -116,7 +116,7 @@ func TestAttestation_GenerateDataKey(t *testing.T) { doc, err := nitro.Parse(params.Recipient.AttestationDocument) require.NoError(t, err) assert.Equal(t, []byte("nonce"), doc.Nonce) - assert.NoError(t, doc.Validate(nitro.WithRootFingerprint("14e8bc5fabb52876f35f122289eaabfa08885837cc7f161149c6d242596258aa"))) + assert.NoError(t, doc.Validate(nitro.WithRootFingerprint(doc.RootCertFingerprint()))) assert.NoError(t, doc.Verify()) assert.Equal(t, types.KeyEncryptionMechanismRsaesOaepSha256, params.Recipient.KeyEncryptionAlgorithm) return &kms.GenerateDataKeyOutput{ diff --git a/enclave/provider_dummy.go b/enclave/provider_dummy.go index d15c799..955a98e 100644 --- a/enclave/provider_dummy.go +++ b/enclave/provider_dummy.go @@ -10,7 +10,6 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/asn1" - "encoding/pem" "fmt" "io" "math" @@ -23,78 +22,60 @@ import ( "github.com/fxamacker/cbor/v2" ) -var dummyPrivKey = `-----BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAujDWnWEKVYoHUwieLegkzR2K+4z2Fg3uVEwmZ16iRJiYm5TO -ltLN6BSHaLCqreA1bYXXTFlIG10z2+h16fhkCNKzy4yKwjwUdXJlbBivypQers8h -Pwy1l4c+uID/VX5zXG4y7g7aNc0Ude+lzBvydh9vFz5PwupFzY6ok3czI95ODni7 -hn/X/8TBGTyh0eYZu8ehfKy6W9AHbX7D+yL2qebSWWkJBEribptpCcaJi8QPUx9M -HWz8j1j83+M6rnG1FQpLl8VNOO6BXmzb5FNr+6lwEfvwHbht0Azhk0ArMQZ/r0lO -ObAvVDmE2AuudXyWWh5sRrXnXlVitDjTQybQAQIDAQABAoIBAQCYf9Poh0jdkvY4 -zkAwvYkW73GcY3JT0gk4xj5WQC6MHKgyFgm3guXfhqD54GmLjK52DD+xaxciQo5t -OdMKVcYpa9qTh4NHX8oqAA6OIRIqzHLtHv3OFGzPtZhrqkx4C+AU/rV8QnH7ywNN -LYIQ0XsfwNNOqFzP+u49VPFCB0m9v7r7mJxeUXp8PDfdhquFT69hpKwNdpzuIDA7 -kVOG4ATkkPTGp3AmJj9Vrit9ffi+xlbhrNIuBui9Fxo1v5G6VT2uBhXJU22zl1hS -uYWT4rCOwVQaV/TBDj4T8diDxYpnAXvpO8U+WdqLddhUNaYeDym/HPq2cFsN9VdY -9FYiVl4ZAoGBAOWVsrRAWgFTmx99nUwy6XhobSWgZDrCQiSK50VGzblBdVnmMvyW -Q3LmdqtVQUkZLETx7PZXYkvIzMRP4oWGcViBPaSZ/IqX/kF5WJeXWW7Zgl5HEXTk -GaN26xl7yFjQ5l0f++HAwSW485B2GXvMcdp+6n7OfG6Xo1cg8CgWck5TAoGBAM+c -/h03pASGVvUDNNfeDulyxcXR/PZZTt1YMTqeYLmkbkJcIJVa2uTdDmzcEbGDA0eq -ezMDA+omGB+WR7HRe9+vgmz7Ww4BZRhKjvnxRgHlTGYHBsHhYr21fgPteGv/aDi2 -xhAGqyOj1jua8ooqpw8TviYXk6ZbxMNF7eV9KxXbAoGAasEjKaHKuFcyCICWhfoe -ifi02AwuzwvJSci1JYd43a3MbZMXHlCY6HK1t5GbG+xyo1SDRUD42hhy7s3enQwY -5HikO0fHIILwnW1ZfpPH6D2H22LcgSgXq+T+CQl/7ZyloaPfsee5aFsKFqBz1RcJ -0fm1/GTzg1FLiJYuVdWqLTUCgYAaOURHwH1xLN7S9+K22Y+coSimAg4nt8QkZT1i -oBqrmD9tFmHvO5imi92Elo+NknTZmokROnJGIyWs57iKl2FEMdERnvYzYK26UcCZ -hYZIOwRZZs3Ns4BbYg9Ww6oQSiSJ9VwzLgRz7f/ja4DzPsv3NZExEo1N2A2UdMLF -1/eXPQKBgQDSCJ1tWQYVLvjrzJBC5gute7kHf1AhMoIEqpsEvk51JXu7+xN8BMnb -zSwIPR3fSngqLJqGw+Tz5LT3iSsDNVj7EnaHoYvTrxsd2yFYtVmz2fHgnHXBjZmj -AzDn4G6VZ+F11K/sdfuo+1vfgxPendYDkjp0ZtgJc97iBq49Devv1A== ------END RSA PRIVATE KEY-----` -var dummyCert = `-----BEGIN CERTIFICATE----- -MIIDITCCAgmgAwIBAgIBATANBgkqhkiG9w0BAQsFADAyMREwDwYDVQQKEwhTZXF1 -ZW5jZTEdMBsGA1UEAxMUZHVtbXkubml0cm8tZW5jbGF2ZXMwHhcNMjUwNDI0MTM0 -NjA5WhcNMzUwNDI0MTM0NjA5WjAyMREwDwYDVQQKEwhTZXF1ZW5jZTEdMBsGA1UE -AxMUZHVtbXkubml0cm8tZW5jbGF2ZXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw -ggEKAoIBAQC6MNadYQpVigdTCJ4t6CTNHYr7jPYWDe5UTCZnXqJEmJiblM6W0s3o -FIdosKqt4DVthddMWUgbXTPb6HXp+GQI0rPLjIrCPBR1cmVsGK/KlB6uzyE/DLWX -hz64gP9VfnNcbjLuDto1zRR176XMG/J2H28XPk/C6kXNjqiTdzMj3k4OeLuGf9f/ -xMEZPKHR5hm7x6F8rLpb0AdtfsP7Ivap5tJZaQkESuJum2kJxomLxA9TH0wdbPyP -WPzf4zqucbUVCkuXxU047oFebNvkU2v7qXAR+/AduG3QDOGTQCsxBn+vSU45sC9U -OYTYC651fJZaHmxGtedeVWK0ONNDJtABAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIB -hjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSw2hfihIyfiqyiuiuTp3OCt0Sl -8DANBgkqhkiG9w0BAQsFAAOCAQEAl55+EnYlS5/YTQQhZozA/XW7Y9Kt00w9k0Ix -9vXTVeZdzTNR/YKCAzG7ynNjNbdFkhJcqqwKycVOSID0Xz4dWvB6jVukIV6B3W2u -ta/P4SYg4VQ9YzPqF1n1sUzX3OwKOhEcSxQQjvs8ssRaWq9aqEHyxCxuc9BWoqvB -Am9iwrNpmUmlRbFwDOwtICZRbqAf799pOFo1i8WKQc/J5y1KwZCCg3GAEBv8CNQE -vMVH5ygi1fMeQPNg8oWDD+3gP1GmLGMP14kHT/aPyDAHHUMrq7nSgA8SXTC9fihO -sygULgtpiSjKgeg9cTvK9yhz7T0c2CxFgyhUnz4v6uZtQTJK2Q== ------END CERTIFICATE-----` - func DummyProvider(random io.Reader) func() (Session, error) { if random == nil { random = rand.Reader } - return func() (Session, error) { - block, _ := pem.Decode([]byte(dummyPrivKey)) - if block == nil || block.Type != "RSA PRIVATE KEY" { - return nil, fmt.Errorf("invalid PEM block") + + // Generate an ephemeral CA key pair and self-signed certificate once per provider instance. + // This avoids shipping static key material in the source code. + caKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return func() (Session, error) { + return nil, fmt.Errorf("failed to generate CA key: %v", err) + } + } + + serialNumber, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) + if err != nil { + return func() (Session, error) { + return nil, fmt.Errorf("failed to generate serial number: %v", err) } - key, err := x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - return nil, fmt.Errorf("failed to parse dummy private key: %v", err) + } + + caTemplate := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"Dummy"}, + CommonName: "dummy.nitro-enclaves", + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + } + + caCertDER, err := x509.CreateCertificate(rand.Reader, &caTemplate, &caTemplate, &caKey.PublicKey, caKey) + if err != nil { + return func() (Session, error) { + return nil, fmt.Errorf("failed to create CA certificate: %v", err) } + } - certBlock, _ := pem.Decode([]byte(dummyCert)) - caCert, err := x509.ParseCertificate(certBlock.Bytes) - if err != nil { + caCert, err := x509.ParseCertificate(caCertDER) + if err != nil { + return func() (Session, error) { return nil, fmt.Errorf("failed to parse CA certificate: %v", err) } + } + return func() (Session, error) { return &dummySession{ random: random, - privateKey: key, + privateKey: caKey, caCert: caCert, - caCertDER: certBlock.Bytes, + caCertDER: caCertDER, }, nil } } @@ -231,7 +212,7 @@ func (d *dummySession) generateCertificate() ([]byte, *ecdsa.PrivateKey, error) template := x509.Certificate{ SerialNumber: serialNumber, Subject: pkix.Name{ - Organization: []string{"Sequence"}, + Organization: []string{"Dummy"}, CommonName: "dummy.nitro-enclaves", }, NotBefore: time.Now(), From 2eb08ba5f5d5d5633a5610c69e3abf8bb947ec60 Mon Sep 17 00:00:00 2001 From: Patryk Kalinowski Date: Fri, 15 May 2026 17:28:56 +0200 Subject: [PATCH 02/13] H-01: reject empty allowedKeyIDs --- enclave/attestation.go | 10 ++++++++-- enclave/attestation_test.go | 5 +++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/enclave/attestation.go b/enclave/attestation.go index 96939f7..4349573 100644 --- a/enclave/attestation.go +++ b/enclave/attestation.go @@ -50,7 +50,13 @@ func (a *Attestation) Document() []byte { // Decrypt requests a decryption operation from KMS on ciphertext. If the key used to encrypt the // original data is not one of allowedKeyIDs, Decrypt returns an error. +// +// allowedKeyIDs must not be empty — callers must explicitly specify which KMS keys are acceptable. func (a *Attestation) Decrypt(ctx context.Context, ciphertext []byte, allowedKeyIDs []string) ([]byte, error) { + if len(allowedKeyIDs) == 0 { + return nil, fmt.Errorf("allowedKeyIDs must not be empty") + } + params := &kms.DecryptInput{ CiphertextBlob: ciphertext, EncryptionAlgorithm: types.EncryptionAlgorithmSpecSymmetricDefault, @@ -117,8 +123,8 @@ func (a *Attestation) GenerateDataKey(ctx context.Context, keyID string) (*DataK } func keyIsAllowed(key *string, allowedKeys []string) (string, bool) { - if key == nil || len(allowedKeys) == 0 { - return "", true + if key == nil { + return "", false } for _, v := range allowedKeys { diff --git a/enclave/attestation_test.go b/enclave/attestation_test.go index 7128ce2..851a474 100644 --- a/enclave/attestation_test.go +++ b/enclave/attestation_test.go @@ -76,7 +76,8 @@ func TestNitroAttestation_Decrypt(t *testing.T) { assert.NoError(t, doc.Validate(nitro.WithRootFingerprint(doc.RootCertFingerprint()))) assert.NoError(t, doc.Verify()) assert.Equal(t, types.KeyEncryptionMechanismRsaesOaepSha256, params.Recipient.KeyEncryptionAlgorithm) - return &kms.DecryptOutput{CiphertextForRecipient: ciphertextForRecipient}, nil + keyID := "arn:aws:kms:us-east-1:000000000000:key/test-key-id" + return &kms.DecryptOutput{KeyId: &keyID, CiphertextForRecipient: ciphertextForRecipient}, nil }, } @@ -92,7 +93,7 @@ func TestNitroAttestation_Decrypt(t *testing.T) { att, err := e.GetAttestation(context.Background(), []byte("nonce"), []byte("user-data")) require.NoError(t, err) - plaintext, err := att.Decrypt(context.Background(), []byte("ciphertext"), nil) + plaintext, err := att.Decrypt(context.Background(), []byte("ciphertext"), []string{"arn:aws:kms:us-east-1:000000000000:key/test-key-id"}) require.NoError(t, err) assert.Equal(t, expectedPlaintext, plaintext) }() From 2cd9633a075f7b58d4e84dc74726eed7265ec6e1 Mon Sep 17 00:00:00 2001 From: Patryk Kalinowski Date: Fri, 15 May 2026 17:56:45 +0200 Subject: [PATCH 03/13] H-03: enforce quarantine before keys can be cleaned up --- encryption/pool.go | 11 +++++++++ encryption/pool_test.go | 52 ++++++++++++++++++++++++++++++++++------- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/encryption/pool.go b/encryption/pool.go index 4a9284d..ccaf6e4 100644 --- a/encryption/pool.go +++ b/encryption/pool.go @@ -235,8 +235,15 @@ func (p *Pool) RotateKey(ctx context.Context, att *enclave.Attestation, keyRef s return nil } +// cleanupQuarantinePeriod is the minimum time a key must be inactive before it can be deleted. +// This ensures eventual consistency of DynamoDB GSIs has fully propagated before we trust the +// "no references" check. +const cleanupQuarantinePeriod = 24 * time.Hour + // CleanupUnusedKeys removes cipher keys that are no longer used by any encrypted data. // +// Keys must be inactive for at least 24 hours before they are eligible for deletion. +// // It is inefficient and best-effort, not guaranteed to complete in a single pass, as it is // assumed to be called infrequently. It can, however, be retried until the returned count is 0. func (p *Pool) CleanupUnusedKeys(ctx context.Context) (deleted int, err error) { @@ -253,6 +260,10 @@ func (p *Pool) CleanupUnusedKeys(ctx context.Context) (deleted int, err error) { return deleted, fmt.Errorf("list generation key refs: %w", err) } for _, key := range keys { + if key.InactiveSince == nil || time.Since(*key.InactiveSince) < cleanupQuarantinePeriod { + continue + } + isUsedAnywhere := false for _, dataTable := range p.dataTables { isUsed, err := dataTable.ReferencesCipherKeyRef(ctx, key.KeyRef) diff --git a/encryption/pool_test.go b/encryption/pool_test.go index 097488d..09ac66d 100644 --- a/encryption/pool_test.go +++ b/encryption/pool_test.go @@ -6,6 +6,7 @@ import ( "encoding/pem" "errors" "testing" + "time" "github.com/0xsequence/nitrocontrol/enclave" "github.com/0xsequence/nitrocontrol/encryption" @@ -817,6 +818,8 @@ func TestPool_CleanupUnusedKeys(t *testing.T) { privKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) require.NoError(t, err) + pastQuarantine := time.Now().Add(-48 * time.Hour) + t.Run("deletes unused keys", func(t *testing.T) { kms := &MockKMS{} keysTable := &MockKeysTable{} @@ -829,8 +832,8 @@ func TestPool_CleanupUnusedKeys(t *testing.T) { configs := []*encryption.Config{{PoolSize: 10, Threshold: 2, RemoteKeys: map[string]encryption.RemoteKey{}}} inactiveKeys := []*data.CipherKey{ - {Generation: 0, KeyRef: "unused-key-1"}, - {Generation: 0, KeyRef: "unused-key-2"}, + {Generation: 0, KeyRef: "unused-key-1", InactiveSince: &pastQuarantine}, + {Generation: 0, KeyRef: "unused-key-2", InactiveSince: &pastQuarantine}, } keysTable.On("ScanInactive", mock.Anything, (*string)(nil)).Return(inactiveKeys, nil, nil) @@ -861,8 +864,8 @@ func TestPool_CleanupUnusedKeys(t *testing.T) { configs := []*encryption.Config{{PoolSize: 10, Threshold: 2, RemoteKeys: map[string]encryption.RemoteKey{}}} inactiveKeys := []*data.CipherKey{ - {Generation: 0, KeyRef: "used-key"}, - {Generation: 0, KeyRef: "unused-key"}, + {Generation: 0, KeyRef: "used-key", InactiveSince: &pastQuarantine}, + {Generation: 0, KeyRef: "unused-key", InactiveSince: &pastQuarantine}, } keysTable.On("ScanInactive", mock.Anything, (*string)(nil)).Return(inactiveKeys, nil, nil) @@ -893,7 +896,7 @@ func TestPool_CleanupUnusedKeys(t *testing.T) { configs := []*encryption.Config{{PoolSize: 10, Threshold: 2, RemoteKeys: map[string]encryption.RemoteKey{}}} inactiveKeys := []*data.CipherKey{ - {Generation: 0, KeyRef: "key-in-table2"}, + {Generation: 0, KeyRef: "key-in-table2", InactiveSince: &pastQuarantine}, } keysTable.On("ScanInactive", mock.Anything, (*string)(nil)).Return(inactiveKeys, nil, nil) @@ -922,8 +925,8 @@ func TestPool_CleanupUnusedKeys(t *testing.T) { configs := []*encryption.Config{{PoolSize: 10, Threshold: 2, RemoteKeys: map[string]encryption.RemoteKey{}}} - page1Keys := []*data.CipherKey{{Generation: 0, KeyRef: "key-page1"}} - page2Keys := []*data.CipherKey{{Generation: 0, KeyRef: "key-page2"}} + page1Keys := []*data.CipherKey{{Generation: 0, KeyRef: "key-page1", InactiveSince: &pastQuarantine}} + page2Keys := []*data.CipherKey{{Generation: 0, KeyRef: "key-page2", InactiveSince: &pastQuarantine}} cursor := "cursor-1" keysTable.On("ScanInactive", mock.Anything, (*string)(nil)).Return(page1Keys, &cursor, nil) @@ -988,7 +991,7 @@ func TestPool_CleanupUnusedKeys(t *testing.T) { configs := []*encryption.Config{{PoolSize: 10, Threshold: 2, RemoteKeys: map[string]encryption.RemoteKey{}}} - inactiveKeys := []*data.CipherKey{{Generation: 0, KeyRef: "some-key"}} + inactiveKeys := []*data.CipherKey{{Generation: 0, KeyRef: "some-key", InactiveSince: &pastQuarantine}} keysTable.On("ScanInactive", mock.Anything, (*string)(nil)).Return(inactiveKeys, nil, nil) dataTable.On("TableARN").Return("table-1") @@ -1013,7 +1016,7 @@ func TestPool_CleanupUnusedKeys(t *testing.T) { configs := []*encryption.Config{{PoolSize: 10, Threshold: 2, RemoteKeys: map[string]encryption.RemoteKey{}}} - inactiveKeys := []*data.CipherKey{{Generation: 0, KeyRef: "some-key"}} + inactiveKeys := []*data.CipherKey{{Generation: 0, KeyRef: "some-key", InactiveSince: &pastQuarantine}} keysTable.On("ScanInactive", mock.Anything, (*string)(nil)).Return(inactiveKeys, nil, nil) dataTable.On("TableARN").Return("table-1") @@ -1027,4 +1030,35 @@ func TestPool_CleanupUnusedKeys(t *testing.T) { require.ErrorContains(t, err, "delete failed") require.Equal(t, 0, deleted) }) + + t.Run("skips keys still in quarantine", func(t *testing.T) { + kms := &MockKMS{} + keysTable := &MockKeysTable{} + dataTable := &MockEncryptedDataTable{} + + random := &constantReader{value: 0x42} + enc, err := enclave.New(context.Background(), enclave.DummyProvider(random), kms, privKey) + require.NoError(t, err) + + configs := []*encryption.Config{{PoolSize: 10, Threshold: 2, RemoteKeys: map[string]encryption.RemoteKey{}}} + + recentlyRotated := time.Now().Add(-1 * time.Hour) + inactiveKeys := []*data.CipherKey{ + {Generation: 0, KeyRef: "recent-key", InactiveSince: &recentlyRotated}, + {Generation: 0, KeyRef: "old-key", InactiveSince: &pastQuarantine}, + } + + keysTable.On("ScanInactive", mock.Anything, (*string)(nil)).Return(inactiveKeys, nil, nil) + dataTable.On("TableARN").Return("table-1") + dataTable.On("ReferencesCipherKeyRef", mock.Anything, "old-key").Return(false, nil) + keysTable.On("Delete", mock.Anything, "old-key", 0).Return(nil) + + pool := encryption.NewPool(enc, configs, keysTable, []encryption.EncryptedDataTable{dataTable}, nil) + deleted, err := pool.CleanupUnusedKeys(context.Background()) + require.NoError(t, err) + require.Equal(t, 1, deleted) + + keysTable.AssertNotCalled(t, "Delete", mock.Anything, "recent-key", mock.Anything) + keysTable.AssertCalled(t, "Delete", mock.Anything, "old-key", 0) + }) } From ebc54410665dda83c51ac2f4e2e79f05e020b2b8 Mon Sep 17 00:00:00 2001 From: Patryk Kalinowski Date: Fri, 15 May 2026 18:53:13 +0200 Subject: [PATCH 04/13] M-02: add padding validation to aescbc --- aescbc/aescbc.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/aescbc/aescbc.go b/aescbc/aescbc.go index 924c3fb..160fbb5 100644 --- a/aescbc/aescbc.go +++ b/aescbc/aescbc.go @@ -70,6 +70,11 @@ func pkcs7Unpad(data []byte) ([]byte, error) { if padding < 1 || padding > aes.BlockSize { return nil, fmt.Errorf("invalid padding") } + for _, b := range data[len(data)-padding:] { + if b != byte(padding) { + return nil, fmt.Errorf("invalid padding") + } + } return data[:len(data)-padding], nil } From 68de93de0b0f73d2d430d461c1f46c888ba38aaa Mon Sep 17 00:00:00 2001 From: Patryk Kalinowski Date: Fri, 15 May 2026 18:59:41 +0200 Subject: [PATCH 05/13] M-03: enforce shamir decryption threshold --- encryption/pool.go | 4 ++++ encryption/pool_test.go | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/encryption/pool.go b/encryption/pool.go index ccaf6e4..0a5fe97 100644 --- a/encryption/pool.go +++ b/encryption/pool.go @@ -505,6 +505,10 @@ func (p *Pool) combineShares(ctx context.Context, att *enclave.Attestation, conf decryptedShares = append(decryptedShares, decryptedShare) } + if len(decryptedShares) < config.Threshold { + return nil, fmt.Errorf("insufficient shares: need %d, got %d", config.Threshold, len(decryptedShares)) + } + privateKey, err := shamir.Combine(decryptedShares) if err != nil { return nil, err diff --git a/encryption/pool_test.go b/encryption/pool_test.go index 09ac66d..ef1b65a 100644 --- a/encryption/pool_test.go +++ b/encryption/pool_test.go @@ -572,7 +572,7 @@ func TestPool_Decrypt(t *testing.T) { pool := encryption.NewPool(enc, configs, keysTable, nil, nil) plaintext, err := pool.Decrypt(context.Background(), att, "cipherKey4", legacyCiphertext55_v2, []byte("aad")) require.Empty(t, plaintext) - require.ErrorContains(t, err, "combine shares: less than two parts cannot be used to reconstruct the secret") + require.ErrorContains(t, err, "combine shares: insufficient shares: need 2, got 1") }) t.Run("decrypts successfully and migrates key", func(t *testing.T) { From 29fe6303f3b02107e08eee0e3f12e3462c6f3787 Mon Sep 17 00:00:00 2001 From: Patryk Kalinowski Date: Mon, 18 May 2026 12:43:28 +0200 Subject: [PATCH 06/13] M-04: remove redundant io.ReadAll --- attestation/middleware.go | 7 +++---- attestation/userdata.go | 16 ++-------------- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/attestation/middleware.go b/attestation/middleware.go index e7a9d6f..9088bc1 100644 --- a/attestation/middleware.go +++ b/attestation/middleware.go @@ -44,7 +44,7 @@ func Middleware(enc *enclave.Enclave, errorFn func(http.ResponseWriter, error), return context.WithValue(r.Context(), contextKey, att), cancelFunc, nil } - runPostMiddleware := func(w http.ResponseWriter, r *http.Request, body []byte, nonce []byte) (err error) { + runPostMiddleware := func(w http.ResponseWriter, r *http.Request, reqBody []byte, resBody []byte, nonce []byte) (err error) { log := loggerFromContextFn(r.Context()) ctx, span := tracing.Trace(r.Context(), "attestation.Middleware") defer func() { @@ -52,7 +52,7 @@ func Middleware(enc *enclave.Enclave, errorFn func(http.ResponseWriter, error), span.End() }() - userData, err := generateUserData(r, body) + userData, err := generateUserData(r, reqBody, resBody) if err != nil { return err } @@ -109,8 +109,7 @@ func Middleware(enc *enclave.Enclave, errorFn func(http.ResponseWriter, error), next.ServeHTTP(ww, r.WithContext(ctx)) - r.Body = io.NopCloser(bytes.NewBuffer(reqBody)) - if err := runPostMiddleware(ww, r, body.Bytes(), nonce); err != nil { + if err := runPostMiddleware(ww, r, reqBody, body.Bytes(), nonce); err != nil { errorFn(w, err) return } diff --git a/attestation/userdata.go b/attestation/userdata.go index 2b322d0..185279f 100644 --- a/attestation/userdata.go +++ b/attestation/userdata.go @@ -1,11 +1,9 @@ package attestation import ( - "bytes" "crypto/sha256" "encoding/base64" "fmt" - "io" "net/http" ) @@ -24,20 +22,10 @@ func (u *userData) String() string { return fmt.Sprintf("%s/%d:%s", u.Prefix, u.Version, base64.StdEncoding.EncodeToString(u.Hash)) } -func generateUserData(r *http.Request, resBody []byte) ([]byte, error) { +func generateUserData(r *http.Request, reqBody []byte, resBody []byte) ([]byte, error) { hasher := sha256.New() hasher.Write([]byte(r.Method + " " + r.URL.Path + "\n")) - - var reqBody []byte - var err error - if r.Body != nil { - reqBody, err = io.ReadAll(r.Body) - if err != nil { - return nil, fmt.Errorf("failed to read request body: %w", err) - } - r.Body = io.NopCloser(bytes.NewBuffer(reqBody)) - hasher.Write(reqBody) - } + hasher.Write(reqBody) hasher.Write([]byte("\n")) hasher.Write(resBody) hash := hasher.Sum(nil) From e7a64be84aea5cc7fb6c64276f04652bbbe30766 Mon Sep 17 00:00:00 2001 From: Patryk Kalinowski Date: Mon, 18 May 2026 13:10:24 +0200 Subject: [PATCH 07/13] M-06: tracing header customization --- tracing/http_client.go | 2 +- tracing/middleware.go | 58 ++++++++++++++++++++++++++++++++++-------- tracing/span.go | 28 +++++++++++++++++--- 3 files changed, 73 insertions(+), 15 deletions(-) diff --git a/tracing/http_client.go b/tracing/http_client.go index 829363f..d3b32da 100644 --- a/tracing/http_client.go +++ b/tracing/http_client.go @@ -40,7 +40,7 @@ func (c *wrappedClient) Do(req *http.Request) (res *http.Response, err error) { span.SetMetadata(map[string]any{ "http.method": req.Method, - "http.url": req.URL.String(), + "http.url": req.URL.Redacted(), "http.scheme": req.URL.Scheme, "http.query": req.URL.RawQuery, "http.path": req.URL.Path, diff --git a/tracing/middleware.go b/tracing/middleware.go index 3f4a7a0..b6749d6 100644 --- a/tracing/middleware.go +++ b/tracing/middleware.go @@ -9,7 +9,39 @@ import ( "github.com/go-chi/traceid" ) -func Middleware(errorFn func(http.ResponseWriter, error)) func(http.Handler) http.Handler { +const defaultHeaderName = "X-Sequence-Span" + +type middlewareConfig struct { + headerName string + skipStackTrace bool +} + +// MiddlewareOption configures the tracing middleware. +type MiddlewareOption func(*middlewareConfig) + +// WithHeaderName sets the response header name for the serialized span tree. +// Default: "X-Sequence-Span". +func WithHeaderName(name string) MiddlewareOption { + return func(c *middlewareConfig) { + c.headerName = name + } +} + +// WithoutStackTrace prevents stack traces from being captured in span error metadata. +func WithoutStackTrace() MiddlewareOption { + return func(c *middlewareConfig) { + c.skipStackTrace = true + } +} + +func Middleware(errorFn func(http.ResponseWriter, error), opts ...MiddlewareOption) func(http.Handler) http.Handler { + cfg := &middlewareConfig{ + headerName: defaultHeaderName, + } + for _, opt := range opts { + opt(cfg) + } + return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var body bytes.Buffer @@ -17,19 +49,24 @@ func Middleware(errorFn func(http.ResponseWriter, error)) func(http.Handler) htt ww.Tee(&body) ww.Discard() - tid := traceid.FromContext(r.Context()) + reqCtx := r.Context() + if cfg.skipStackTrace { + reqCtx = withTracingConfig(reqCtx, &tracingConfig{skipStackTrace: true}) + } + + tid := traceid.FromContext(reqCtx) ctx, span := Trace( - r.Context(), + reqCtx, r.URL.Path, WithSpanKind(SpanKindServer), WithMetadata(map[string]any{ "sequence.traceid": tid, - "net.host.name": r.Host, - "server.address": r.Host, - "http.method": r.Method, - "http.url": r.URL.String(), - "url.path": r.URL.Path, - "url.query": r.URL.RawQuery, + "net.host.name": r.Host, + "server.address": r.Host, + "http.method": r.Method, + "http.url": r.URL.Redacted(), + "url.path": r.URL.Path, + "url.query": r.URL.RawQuery, }), ) @@ -37,13 +74,14 @@ func Middleware(errorFn func(http.ResponseWriter, error)) func(http.Handler) htt span.SetStatus(ww.Status()) span.End() + spanJSON, err := json.Marshal(span) if err != nil { errorFn(w, err) return } - w.Header().Set("X-Sequence-Span", string(spanJSON)) + w.Header().Set(cfg.headerName, string(spanJSON)) w.WriteHeader(ww.Status()) if _, err := body.WriteTo(w); err != nil { diff --git a/tracing/span.go b/tracing/span.go index b0c1614..f7c2455 100644 --- a/tracing/span.go +++ b/tracing/span.go @@ -28,7 +28,8 @@ type Span struct { Status int `json:"status,omitempty"` Logs []json.RawMessage `json:"logs,omitempty"` - mu sync.Mutex + mu sync.Mutex + skipStackTrace bool } func Trace(ctx context.Context, name string, opts ...func(*Span)) (context.Context, *Span) { @@ -40,6 +41,9 @@ func Trace(ctx context.Context, name string, opts ...func(*Span)) (context.Conte Annotations: make(map[string]string), Logs: make([]json.RawMessage, 0), } + if cfg := getTracingConfig(ctx); cfg != nil { + span.skipStackTrace = cfg.skipStackTrace + } if parent != nil { parent.mu.Lock() parent.Children = append(parent.Children, span) @@ -52,6 +56,20 @@ func Trace(ctx context.Context, name string, opts ...func(*Span)) (context.Conte } type spanKey struct{} +type configKey struct{} + +type tracingConfig struct { + skipStackTrace bool +} + +func withTracingConfig(ctx context.Context, cfg *tracingConfig) context.Context { + return context.WithValue(ctx, configKey{}, cfg) +} + +func getTracingConfig(ctx context.Context) *tracingConfig { + cfg, _ := ctx.Value(configKey{}).(*tracingConfig) + return cfg +} func GetSpan(ctx context.Context) *Span { span, ok := ctx.Value(spanKey{}).(*Span) @@ -85,9 +103,11 @@ func (s *Span) RecordError(err error) { s.Metadata["exception.type"] = typeStr(err) s.Metadata["exception.message"] = err.Error() - stackTrace := make([]byte, 2048) - n := runtime.Stack(stackTrace, false) - s.Metadata["exception.stacktrace"] = string(stackTrace[0:n]) + if !s.skipStackTrace { + stackTrace := make([]byte, 2048) + n := runtime.Stack(stackTrace, false) + s.Metadata["exception.stacktrace"] = string(stackTrace[0:n]) + } } func (s *Span) SetMetadata(attrs map[string]any) { From bbb6192dafe846a16cbd852bc977c9234fb0788a Mon Sep 17 00:00:00 2001 From: Patryk Kalinowski Date: Mon, 18 May 2026 13:19:27 +0200 Subject: [PATCH 08/13] M-08: fix BER tag parse off-by-one panic --- cms/ber.go | 13 +++++-------- cms/cms_test.go | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/cms/ber.go b/cms/ber.go index 829e259..b07aad9 100644 --- a/cms/ber.go +++ b/cms/ber.go @@ -150,14 +150,14 @@ func readObject(ber []byte, offset int) (asn1Object, int, error) { for ber[offset] >= 0x80 { tag = tag*128 + ber[offset] - 0x80 offset++ - if offset > berLen { + if offset >= berLen { return nil, 0, errors.New("ber2der: cannot move offset forward, end of ber data reached") } } // jvehent 20170227: this doesn't appear to be used anywhere... //tag = tag*128 + ber[offset] - 0x80 offset++ - if offset > berLen { + if offset >= berLen { return nil, 0, errors.New("ber2der: cannot move offset forward, end of ber data reached") } } @@ -173,15 +173,15 @@ func readObject(ber []byte, offset int) (asn1Object, int, error) { var length int l := ber[offset] offset++ - if offset > berLen { - return nil, 0, errors.New("ber2der: cannot move offset forward, end of ber data reached") - } indefinite := false if l > 0x80 { numberOfBytes := (int)(l & 0x7F) if numberOfBytes > 4 { // int is only guaranteed to be 32bit return nil, 0, errors.New("ber2der: BER tag length too long") } + if offset+numberOfBytes > berLen { + return nil, 0, errors.New("ber2der: cannot move offset forward, end of ber data reached") + } if numberOfBytes == 4 && (int)(ber[offset]) > 0x7F { return nil, 0, errors.New("ber2der: BER tag length is negative") } @@ -193,9 +193,6 @@ func readObject(ber []byte, offset int) (asn1Object, int, error) { for i := 0; i < numberOfBytes; i++ { length = length*256 + (int)(ber[offset]) offset++ - if offset > berLen { - return nil, 0, errors.New("ber2der: cannot move offset forward, end of ber data reached") - } } } else if l == 0x80 { indefinite = true diff --git a/cms/cms_test.go b/cms/cms_test.go index fde30e0..1ad3596 100644 --- a/cms/cms_test.go +++ b/cms/cms_test.go @@ -52,6 +52,27 @@ cJEGAbCDYhyjvtjBLNy7YDQ1hdmCnqMxg/5AIwUMkvTTRg+qepfboA== } ) +func TestParse_malformedBER(t *testing.T) { + tests := []struct { + name string + input []byte + }{ + {"multi-byte tag truncated", []byte{0x1F, 0x80}}, + {"multi-byte tag no length", []byte{0x1F, 0x01}}, + {"tag only", []byte{0x30}}, + {"long-form length truncated", []byte{0x30, 0x82}}, + {"long-form length partial", []byte{0x30, 0x82, 0x01}}, + {"length exceeds data", []byte{0x30, 0x10, 0x00}}, + {"empty input", []byte{}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := cms.Parse(tt.input) + require.Error(t, err) + }) + } +} + func TestDecodeCiphertextForRecipient(t *testing.T) { block, _ := pem.Decode([]byte(testPrivateKey)) key, err := x509.ParsePKCS1PrivateKey(block.Bytes) From ca9e9f976c52b4f0ac10f9a7215dc5863c5b47bd Mon Sep 17 00:00:00 2001 From: Patryk Kalinowski Date: Mon, 18 May 2026 13:22:54 +0200 Subject: [PATCH 09/13] M-09: fix CMS envelope decryption with non-block-aligned ciphertext --- cms/cms.go | 4 ++++ cms/cms_test.go | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/cms/cms.go b/cms/cms.go index a947a98..3ccdc85 100644 --- a/cms/cms.go +++ b/cms/cms.go @@ -58,6 +58,10 @@ func (ek *EncryptedKey) Decrypt(key *rsa.PrivateKey) ([]byte, error) { return nil, errors.New("pkcs7: encryption algorithm parameters are malformed") } + if len(ek.cipherText) == 0 || len(ek.cipherText)%block.BlockSize() != 0 { + return nil, fmt.Errorf("cms: ciphertext length %d is not a multiple of block size %d", len(ek.cipherText), block.BlockSize()) + } + mode := cipher.NewCBCDecrypter(block, ek.iv) plaintext := make([]byte, len(ek.cipherText)) mode.CryptBlocks(plaintext, ek.cipherText) diff --git a/cms/cms_test.go b/cms/cms_test.go index 1ad3596..336b269 100644 --- a/cms/cms_test.go +++ b/cms/cms_test.go @@ -86,3 +86,17 @@ func TestDecodeCiphertextForRecipient(t *testing.T) { require.Equal(t, plaintextKey, dataKey) } + +func TestDecryptEnvelopedKey_truncatedCiphertext(t *testing.T) { + block, _ := pem.Decode([]byte(testPrivateKey)) + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + require.NoError(t, err) + + ciphertext, err := base64.StdEncoding.DecodeString(testCiphertextString) + require.NoError(t, err) + + // Truncate by one byte to misalign the inner ciphertext off a block boundary + truncated := ciphertext[:len(ciphertext)-1] + _, err = cms.DecryptEnvelopedKey(key, truncated) + require.Error(t, err) +} From 3572d01ac679d3bb6745eacd38ddcb86439805fe Mon Sep 17 00:00:00 2001 From: Patryk Kalinowski Date: Mon, 18 May 2026 13:24:58 +0200 Subject: [PATCH 10/13] M-10: add godoc warning and nil check for aesgcm.Encrypt --- aesgcm/aesgcm.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/aesgcm/aesgcm.go b/aesgcm/aesgcm.go index f8a2368..7151d72 100644 --- a/aesgcm/aesgcm.go +++ b/aesgcm/aesgcm.go @@ -10,7 +10,13 @@ import ( // Encrypt encrypts plaintext using key and random entropy. Key must be a valid AES-256 key with a length of 32 bytes. // The result is a concatenation of nonce (using standard 12-byte nonce size) and the actual ciphertext. +// +// random must be a cryptographically secure random source (e.g. crypto/rand.Reader or an NSM session). +// AES-GCM is catastrophically broken under nonce reuse. func Encrypt(random io.Reader, key []byte, plaintext []byte, additionalData []byte) ([]byte, error) { + if random == nil { + return nil, fmt.Errorf("random source must not be nil") + } if len(key) != 32 { return nil, fmt.Errorf("key must be 32 bytes for AES-256 but was %d", len(key)) } From 371129d3fca9b232094a2b89fdad92fab35ed36f Mon Sep 17 00:00:00 2001 From: Patryk Kalinowski Date: Mon, 18 May 2026 13:32:37 +0200 Subject: [PATCH 11/13] M-11: validate Config --- encryption/config.go | 15 ++++++++-- encryption/config_test.go | 59 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/encryption/config.go b/encryption/config.go index 9f9c797..a9ddfca 100644 --- a/encryption/config.go +++ b/encryption/config.go @@ -2,6 +2,7 @@ package encryption import ( "context" + "fmt" "io" "github.com/0xsequence/nitrocontrol/enclave" @@ -19,7 +20,17 @@ type Config struct { RemoteKeys map[string]RemoteKey } -func NewConfig(poolSize int, threshold int, keys []RemoteKey) *Config { +func NewConfig(poolSize int, threshold int, keys []RemoteKey) (*Config, error) { + if poolSize < 1 { + return nil, fmt.Errorf("poolSize must be at least 1, got %d", poolSize) + } + if threshold < 2 { + return nil, fmt.Errorf("threshold must be at least 2, got %d", threshold) + } + if len(keys) < threshold { + return nil, fmt.Errorf("number of keys (%d) must be at least threshold (%d)", len(keys), threshold) + } + config := &Config{ PoolSize: poolSize, Threshold: threshold, @@ -30,7 +41,7 @@ func NewConfig(poolSize int, threshold int, keys []RemoteKey) *Config { config.RemoteKeys[key.RemoteKeyID()] = key } - return config + return config, nil } func (c *Config) areSharesValid(shares map[string]string) bool { diff --git a/encryption/config_test.go b/encryption/config_test.go index 2c8eb97..f1704fc 100644 --- a/encryption/config_test.go +++ b/encryption/config_test.go @@ -2,12 +2,71 @@ package encryption import ( "bytes" + "context" "errors" "testing" + "github.com/0xsequence/nitrocontrol/enclave" "github.com/stretchr/testify/require" ) +type stubRemoteKey struct{ id string } + +func (k *stubRemoteKey) RemoteKeyID() string { return k.id } +func (k *stubRemoteKey) Encrypt(_ context.Context, _ *enclave.Attestation, _ []byte) (string, error) { + return "", nil +} +func (k *stubRemoteKey) Decrypt(_ context.Context, _ *enclave.Attestation, _ string) ([]byte, error) { + return nil, nil +} + +func TestNewConfig(t *testing.T) { + keys := []RemoteKey{ + &stubRemoteKey{id: "key1"}, + &stubRemoteKey{id: "key2"}, + &stubRemoteKey{id: "key3"}, + } + + t.Run("valid config", func(t *testing.T) { + cfg, err := NewConfig(10, 2, keys) + require.NoError(t, err) + require.Equal(t, 10, cfg.PoolSize) + require.Equal(t, 2, cfg.Threshold) + require.Len(t, cfg.RemoteKeys, 3) + }) + + t.Run("zero pool size", func(t *testing.T) { + _, err := NewConfig(0, 2, keys) + require.ErrorContains(t, err, "poolSize must be at least 1") + }) + + t.Run("negative pool size", func(t *testing.T) { + _, err := NewConfig(-1, 2, keys) + require.ErrorContains(t, err, "poolSize must be at least 1") + }) + + t.Run("threshold too low", func(t *testing.T) { + _, err := NewConfig(10, 1, keys) + require.ErrorContains(t, err, "threshold must be at least 2") + }) + + t.Run("threshold zero", func(t *testing.T) { + _, err := NewConfig(10, 0, keys) + require.ErrorContains(t, err, "threshold must be at least 2") + }) + + t.Run("not enough keys for threshold", func(t *testing.T) { + _, err := NewConfig(10, 3, keys[:2]) + require.ErrorContains(t, err, "number of keys (2) must be at least threshold (3)") + }) + + t.Run("exact threshold keys", func(t *testing.T) { + cfg, err := NewConfig(10, 2, keys[:2]) + require.NoError(t, err) + require.Len(t, cfg.RemoteKeys, 2) + }) +} + func TestConfig_areSharesValid(t *testing.T) { config := &Config{ RemoteKeys: map[string]RemoteKey{ From 44228c71b5a576d11ae65bcf3a59c3f075ab72e5 Mon Sep 17 00:00:00 2001 From: Patryk Kalinowski Date: Mon, 18 May 2026 13:51:00 +0200 Subject: [PATCH 12/13] address review feedback --- enclave/attestation.go | 3 ++ enclave/attestation_test.go | 63 +++++++++++++++++++++++++++++++++++++ enclave/provider_dummy.go | 4 +++ tracing/http_client.go | 1 - tracing/middleware.go | 1 - 5 files changed, 70 insertions(+), 2 deletions(-) diff --git a/enclave/attestation.go b/enclave/attestation.go index 4349573..266a247 100644 --- a/enclave/attestation.go +++ b/enclave/attestation.go @@ -71,6 +71,9 @@ func (a *Attestation) Decrypt(ctx context.Context, ciphertext []byte, allowedKey } // Verify that the key used to decrypt was one of the allowed keys + if out.KeyId == nil { + return nil, fmt.Errorf("KMS response missing KeyId, cannot verify against allowed keys") + } if keyID, ok := keyIsAllowed(out.KeyId, allowedKeyIDs); !ok { return nil, fmt.Errorf("KMS key not allowed for this operation: %q", keyID) } diff --git a/enclave/attestation_test.go b/enclave/attestation_test.go index 851a474..0424bc9 100644 --- a/enclave/attestation_test.go +++ b/enclave/attestation_test.go @@ -101,6 +101,69 @@ func TestNitroAttestation_Decrypt(t *testing.T) { wg.Wait() } +func TestNitroAttestation_Decrypt_nilKeyId(t *testing.T) { + block, _ := pem.Decode([]byte(testPrivateKey)) + privKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) + require.NoError(t, err) + + kmsMock := &mockKMS{ + decrypt: func(params *kms.DecryptInput) (*kms.DecryptOutput, error) { + return &kms.DecryptOutput{KeyId: nil, Plaintext: []byte("plaintext")}, nil + }, + } + + e, err := enclave.New(context.Background(), enclave.DummyProvider(nil), kmsMock, privKey) + require.NoError(t, err) + + att, err := e.GetAttestation(context.Background(), []byte("nonce"), nil) + require.NoError(t, err) + + _, err = att.Decrypt(context.Background(), []byte("ciphertext"), []string{"some-key"}) + require.Error(t, err) + assert.ErrorContains(t, err, "KMS response missing KeyId") +} + +func TestNitroAttestation_Decrypt_emptyAllowedKeys(t *testing.T) { + block, _ := pem.Decode([]byte(testPrivateKey)) + privKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) + require.NoError(t, err) + + kmsMock := &mockKMS{} + + e, err := enclave.New(context.Background(), enclave.DummyProvider(nil), kmsMock, privKey) + require.NoError(t, err) + + att, err := e.GetAttestation(context.Background(), []byte("nonce"), nil) + require.NoError(t, err) + + _, err = att.Decrypt(context.Background(), []byte("ciphertext"), nil) + require.Error(t, err) + assert.ErrorContains(t, err, "allowedKeyIDs must not be empty") +} + +func TestNitroAttestation_Decrypt_wrongKey(t *testing.T) { + block, _ := pem.Decode([]byte(testPrivateKey)) + privKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) + require.NoError(t, err) + + kmsMock := &mockKMS{ + decrypt: func(params *kms.DecryptInput) (*kms.DecryptOutput, error) { + keyID := "arn:aws:kms:us-east-1:000000000000:key/other-key" + return &kms.DecryptOutput{KeyId: &keyID, Plaintext: []byte("plaintext")}, nil + }, + } + + e, err := enclave.New(context.Background(), enclave.DummyProvider(nil), kmsMock, privKey) + require.NoError(t, err) + + att, err := e.GetAttestation(context.Background(), []byte("nonce"), nil) + require.NoError(t, err) + + _, err = att.Decrypt(context.Background(), []byte("ciphertext"), []string{"arn:aws:kms:us-east-1:000000000000:key/expected-key"}) + require.Error(t, err) + assert.ErrorContains(t, err, "KMS key not allowed") +} + func TestAttestation_GenerateDataKey(t *testing.T) { block, _ := pem.Decode([]byte(testPrivateKey)) privKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) diff --git a/enclave/provider_dummy.go b/enclave/provider_dummy.go index 955a98e..8d81a5b 100644 --- a/enclave/provider_dummy.go +++ b/enclave/provider_dummy.go @@ -22,6 +22,10 @@ import ( "github.com/fxamacker/cbor/v2" ) +// DummyProvider returns a Provider that simulates the Nitro Security Module for testing and local development. +// The random parameter controls the entropy source for session reads (used by enclave key generation and +// attestation randomness). It does not affect the CA key pair, which is always generated using crypto/rand +// to ensure cryptographic security. func DummyProvider(random io.Reader) func() (Session, error) { if random == nil { random = rand.Reader diff --git a/tracing/http_client.go b/tracing/http_client.go index d3b32da..c18dc11 100644 --- a/tracing/http_client.go +++ b/tracing/http_client.go @@ -42,7 +42,6 @@ func (c *wrappedClient) Do(req *http.Request) (res *http.Response, err error) { "http.method": req.Method, "http.url": req.URL.Redacted(), "http.scheme": req.URL.Scheme, - "http.query": req.URL.RawQuery, "http.path": req.URL.Path, "http.request_content_length": req.ContentLength, }) diff --git a/tracing/middleware.go b/tracing/middleware.go index b6749d6..cdc4b1f 100644 --- a/tracing/middleware.go +++ b/tracing/middleware.go @@ -66,7 +66,6 @@ func Middleware(errorFn func(http.ResponseWriter, error), opts ...MiddlewareOpti "http.method": r.Method, "http.url": r.URL.Redacted(), "url.path": r.URL.Path, - "url.query": r.URL.RawQuery, }), ) From 92ed3d5eeac710cd79c545693ec2a4e1d40e3c1b Mon Sep 17 00:00:00 2001 From: Patryk Kalinowski Date: Mon, 18 May 2026 14:04:01 +0200 Subject: [PATCH 13/13] address review feedback --- aescbc/aescbc.go | 5 ++++- tracing/middleware.go | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/aescbc/aescbc.go b/aescbc/aescbc.go index 160fbb5..89bc153 100644 --- a/aescbc/aescbc.go +++ b/aescbc/aescbc.go @@ -66,8 +66,11 @@ func Encrypt(random io.Reader, key []byte, plaintext []byte) ([]byte, error) { // PKCS7 unpadding func pkcs7Unpad(data []byte) ([]byte, error) { + if len(data) == 0 { + return nil, fmt.Errorf("invalid padding") + } padding := int(data[len(data)-1]) - if padding < 1 || padding > aes.BlockSize { + if padding < 1 || padding > aes.BlockSize || padding > len(data) { return nil, fmt.Errorf("invalid padding") } for _, b := range data[len(data)-padding:] { diff --git a/tracing/middleware.go b/tracing/middleware.go index cdc4b1f..a8df7db 100644 --- a/tracing/middleware.go +++ b/tracing/middleware.go @@ -23,7 +23,9 @@ type MiddlewareOption func(*middlewareConfig) // Default: "X-Sequence-Span". func WithHeaderName(name string) MiddlewareOption { return func(c *middlewareConfig) { - c.headerName = name + if name != "" { + c.headerName = name + } } }