Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions sdk/experimental/tdf/keysplit/xor_splitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,21 @@ func (x *XORSplitter) GenerateSplits(_ context.Context, attrs []*policy.Value, d
// 4. Collect all public keys from assignments
allKeys := collectAllPublicKeys(assignments)

// 5. Merge the default KAS public key if not already present.
// Attribute grants may reference the default KAS URL without including the public key
// (e.g., legacy grants with only a URI). The default KAS key fills this gap.
if x.config.defaultKAS != nil && x.config.defaultKAS.GetPublicKey() != nil {
kasURL := x.config.defaultKAS.GetKasUri()
if _, exists := allKeys[kasURL]; !exists {
allKeys[kasURL] = KASPublicKey{
URL: kasURL,
KID: x.config.defaultKAS.GetPublicKey().GetKid(),
PEM: x.config.defaultKAS.GetPublicKey().GetPem(),
Algorithm: formatAlgorithm(x.config.defaultKAS.GetPublicKey().GetAlgorithm()),
}
}
}
Comment thread
pflynn-virtru marked this conversation as resolved.

slog.Debug("completed key split generation",
slog.Int("num_splits", len(splits)),
slog.Int("num_kas_keys", len(allKeys)))
Expand Down
70 changes: 70 additions & 0 deletions sdk/experimental/tdf/keysplit/xor_splitter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -526,3 +526,73 @@ func TestXORSplitter_ComplexScenarios(t *testing.T) {
assert.True(t, found, "Should find split with multiple KAS URLs")
})
}

// TestXORSplitter_DefaultKASMergedForURIOnlyGrant is a regression test
// ensuring that when an attribute grant references a KAS URL without
// embedding the public key (URI-only legacy grant), the default KAS's
// full public key info is merged into the result. Without the merge fix
// in GenerateSplits, collectAllPublicKeys returns an incomplete map and
// key wrapping fails.
func TestXORSplitter_DefaultKASMergedForURIOnlyGrant(t *testing.T) {
defaultKAS := &policy.SimpleKasKey{
KasUri: kasUs,
PublicKey: &policy.SimpleKasPublicKey{
Algorithm: policy.Algorithm_ALGORITHM_RSA_2048,
Kid: "default-kid",
Pem: mockRSAPublicKey1,
},
}
splitter := NewXORSplitter(WithDefaultKAS(defaultKAS))

dek := make([]byte, 32)
_, err := rand.Read(dek)
require.NoError(t, err)

// Create an attribute whose grant references kasUs by URI only (no KasKeys).
attr := createMockValue("https://test.com/attr/level/value/secret", "", "", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF)
attr.Grants = []*policy.KeyAccessServer{
{Uri: kasUs}, // URI-only, no embedded public key
}

result, err := splitter.GenerateSplits(t.Context(), []*policy.Value{attr}, dek)
require.NoError(t, err)
require.NotNil(t, result)

// The default KAS public key must be merged into the result.
require.Contains(t, result.KASPublicKeys, kasUs, "default KAS key should be merged for URI-only grant")
pubKey := result.KASPublicKeys[kasUs]
assert.Equal(t, "default-kid", pubKey.KID)
assert.Equal(t, mockRSAPublicKey1, pubKey.PEM)
assert.Equal(t, "rsa:2048", pubKey.Algorithm)
}

// TestXORSplitter_DefaultKASDoesNotOverwriteExistingKey verifies that when
// an attribute grant already embeds a full public key for the same KAS URL
// as the default, the grant's key is preserved and not overwritten.
func TestXORSplitter_DefaultKASDoesNotOverwriteExistingKey(t *testing.T) {
defaultKAS := &policy.SimpleKasKey{
KasUri: kasUs,
PublicKey: &policy.SimpleKasPublicKey{
Algorithm: policy.Algorithm_ALGORITHM_RSA_2048,
Kid: "default-kid",
Pem: mockRSAPublicKey1,
},
}
splitter := NewXORSplitter(WithDefaultKAS(defaultKAS))

dek := make([]byte, 32)
_, err := rand.Read(dek)
require.NoError(t, err)

// Create an attribute with a fully-embedded grant for the same KAS URL
// but with a different KID.
attr := createMockValue("https://test.com/attr/level/value/secret", kasUs, "grant-kid", policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF)

result, err := splitter.GenerateSplits(t.Context(), []*policy.Value{attr}, dek)
require.NoError(t, err)
require.NotNil(t, result)

require.Contains(t, result.KASPublicKeys, kasUs)
pubKey := result.KASPublicKeys[kasUs]
assert.Equal(t, "grant-kid", pubKey.KID, "grant's key should not be overwritten by default KAS")
}
6 changes: 5 additions & 1 deletion sdk/experimental/tdf/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,11 @@ func (w *Writer) WriteSegment(ctx context.Context, index int, data []byte) (*Seg
if err != nil {
return nil, err
}
segmentSig, err := calculateSignature(segmentCipher, w.dek, w.segmentIntegrityAlgorithm, false) // Don't ever hex encode new tdf's
// Hash must cover nonce + cipher to match the standard SDK reader's verification.
// The standard SDK's Encrypt() returns nonce prepended to cipher and hashes that;
// EncryptInPlace() returns them separately, so we must concatenate for hashing.
segmentData := append(nonce, segmentCipher...) //nolint:gocritic // nonce cap == len, so always allocates
segmentSig, err := calculateSignature(segmentData, w.dek, w.segmentIntegrityAlgorithm, false)
if err != nil {
return nil, err
}
Expand Down
Loading
Loading