Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
75 changes: 64 additions & 11 deletions sdk/nanotdf.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"crypto/elliptic"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -72,6 +73,24 @@ type NanoTDFHeader struct {
ecdsaPolicyBindingS []byte
}

type ecdsaPolicyBinding struct {
r []byte
s []byte
ephemeralPubKey []byte
digest []byte
curve elliptic.Curve
}

type gmacPolicyBinding struct {
binding []byte
digest []byte
}

type PolicyBind interface {
Verify() (bool, error)
fmt.Stringer
}

func NewNanoTDFHeaderFromReader(reader io.Reader) (NanoTDFHeader, uint32, error) {
header := NanoTDFHeader{}
var size uint32
Expand Down Expand Up @@ -230,25 +249,59 @@ func (header *NanoTDFHeader) ECCurve() (elliptic.Curve, error) {
}

func (header *NanoTDFHeader) VerifyPolicyBinding() (bool, error) {
curve, err := ocrypto.GetECCurveFromECCMode(header.bindCfg.eccMode)
policyBind, err := header.PolicyBinding()
if err != nil {
return false, err
}
return policyBind.Verify()
}

func (b *ecdsaPolicyBinding) Verify() (bool, error) {
ephemeralECDSAPublicKey, err := ocrypto.UncompressECPubKey(b.curve, b.ephemeralPubKey)
if err != nil {
return false, err
}

return ocrypto.VerifyECDSASig(b.digest,
b.r,
b.s,
ephemeralECDSAPublicKey), nil
}

func (b *ecdsaPolicyBinding) String() string {
return string(ocrypto.SHA256AsHex(append(b.r, b.s...)))
}

func (b *gmacPolicyBinding) Verify() (bool, error) {
bindingToCheck := b.digest[len(b.digest)-kNanoTDFGMACLength:]
return bytes.Equal(bindingToCheck, b.binding), nil
}

func (b *gmacPolicyBinding) String() string {
return hex.EncodeToString(b.binding)
}

func (header *NanoTDFHeader) PolicyBinding() (PolicyBind, error) {
digest := ocrypto.CalculateSHA256(header.PolicyBody)

if header.IsEcdsaBindingEnabled() {
ephemeralECDSAPublicKey, err := ocrypto.UncompressECPubKey(curve, header.EphemeralKey)
curve, err := ocrypto.GetECCurveFromECCMode(header.bindCfg.eccMode)
if err != nil {
return false, err
return nil, err
}

return ocrypto.VerifyECDSASig(digest,
header.ecdsaPolicyBindingR,
header.ecdsaPolicyBindingS,
ephemeralECDSAPublicKey), nil
}
binding := digest[len(digest)-kNanoTDFGMACLength:]
return bytes.Equal(binding, header.gmacPolicyBinding), nil
return &ecdsaPolicyBinding{
r: header.ecdsaPolicyBindingR,
s: header.ecdsaPolicyBindingS,
ephemeralPubKey: header.EphemeralKey,
curve: curve,
digest: digest,
}, nil
}

return &gmacPolicyBinding{
binding: header.gmacPolicyBinding,
digest: digest,
}, nil
}

// ============================================================================================================
Expand Down
177 changes: 177 additions & 0 deletions sdk/nanotdf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"crypto/rand"
"crypto/x509"
"encoding/gob"
"encoding/hex"
"encoding/json"
"encoding/pem"
"errors"
Expand Down Expand Up @@ -934,6 +935,182 @@ func (s *NanoSuite) Test_NanoTDF_Obligations() {
}
}

func (s *NanoSuite) Test_PolicyBinding_GMAC() {
// Create test policy data
policyData := []byte(`{"body":{"dataAttributes":["https://example.com/attr/classification/value/secret"]}}`)

// Create GMAC binding - need to simulate having GMAC at end of the digest
gmacBytes := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}
// Append GMAC to the digest to simulate real scenario
policyData = append(policyData, gmacBytes...)

digest := ocrypto.CalculateSHA256(policyData)

// For testing, we will use the last bytes as the GMAC binding
gmacBytes = digest[len(digest)-len(gmacBytes):]

binding := &gmacPolicyBinding{
binding: gmacBytes,
digest: digest,
}

// Test String function
s.Require().Equal(hex.EncodeToString(gmacBytes), binding.String(), "GMAC hash should return binding data directly")

// Test Verify function - should pass with correct binding
valid, err := binding.Verify()
s.Require().NoError(err)
s.Require().True(valid, "GMAC binding should be valid when binding matches digest suffix")

// Test Verify function with wrong binding - should fail
wrongBinding := &gmacPolicyBinding{
binding: []byte{0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01},
digest: digest,
}
valid, err = wrongBinding.Verify()
s.Require().NoError(err)
s.Require().False(valid, "GMAC binding should be invalid when binding doesn't match digest suffix")
}

func (s *NanoSuite) Test_PolicyBinding_ECDSA() {
// Create a test ECDSA key pair
keyPair, err := ocrypto.NewECKeyPair(ocrypto.ECCModeSecp256r1)
s.Require().NoError(err)

// Create test policy data
policyData := []byte(`{"body":{"dataAttributes":["https://example.com/attr/classification/value/secret"]}}`)
digest := ocrypto.CalculateSHA256(policyData)

// Sign the digest
r, sBytes, err := ocrypto.ComputeECDSASig(digest, keyPair.PrivateKey)
s.Require().NoError(err)

// Get the public key in compressed format
compressedPubKey, err := ocrypto.CompressedECPublicKey(ocrypto.ECCModeSecp256r1, keyPair.PrivateKey.PublicKey)
s.Require().NoError(err)

binding := &ecdsaPolicyBinding{
r: r,
s: sBytes,
ephemeralPubKey: compressedPubKey,
digest: digest,
curve: keyPair.PrivateKey.Curve,
}

// Test String function
expectedHash := string(ocrypto.SHA256AsHex(append(r, sBytes...)))
s.Require().NotEmpty(binding.String(), "Hash should not be empty")
s.Require().Equal(expectedHash, binding.String(), "ECDSA hash should be SHA256 of r||s")

// Test Verify function - should pass with correct signature
valid, err := binding.Verify()
s.Require().NoError(err)
s.Require().True(valid, "ECDSA binding should be valid with correct signature")

// Test Verify function with wrong signature - should fail
invalidR := make([]byte, 32)
invalidS := make([]byte, 32)
for i := range invalidR {
invalidR[i] = byte(i)
invalidS[i] = byte(i + 10)
}

wrongBinding := &ecdsaPolicyBinding{
r: invalidR,
s: invalidS,
ephemeralPubKey: compressedPubKey,
digest: digest,
curve: keyPair.PrivateKey.Curve,
}

valid, err = wrongBinding.Verify()
s.Require().NoError(err)
s.Require().False(valid, "ECDSA binding should be invalid with wrong signature")
}

func (s *NanoSuite) Test_NanoTDFHeader_VerifyPolicyBinding() {
s.Run("ECDSA Policy Binding Verification", func() {
// Create a test ECDSA key pair
keyPair, err := ocrypto.NewECKeyPair(ocrypto.ECCModeSecp256r1)
s.Require().NoError(err)

// Create test policy data
policyData := []byte(`{"body":{"dataAttributes":["https://example.com/attr/classification/value/secret"]}}`)
digest := ocrypto.CalculateSHA256(policyData)

// Sign the digest
r, sBytes, err := ocrypto.ComputeECDSASig(digest, keyPair.PrivateKey)
s.Require().NoError(err)

// Get compressed public key
compressedPubKey, err := ocrypto.CompressedECPublicKey(ocrypto.ECCModeSecp256r1, keyPair.PrivateKey.PublicKey)
s.Require().NoError(err)

// Create header with ECDSA binding
header := &NanoTDFHeader{
bindCfg: bindingConfig{
useEcdsaBinding: true,
eccMode: ocrypto.ECCModeSecp256r1,
},
PolicyBody: policyData,
EphemeralKey: compressedPubKey,
ecdsaPolicyBindingR: r,
ecdsaPolicyBindingS: sBytes,
}

// Test VerifyPolicyBinding method
valid, err := header.VerifyPolicyBinding()
s.Require().NoError(err)
s.Require().True(valid, "ECDSA policy binding should be valid")
})

s.Run("GMAC Policy Binding Verification", func() {
// Create test policy data
policyData := []byte(`{"body":{"dataAttributes":["https://example.com/attr/classification/value/secret"]}}`)

// Create GMAC binding - need to simulate having GMAC at end of the digest
gmacBytes := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}
// Append GMAC to the digest to simulate real scenario
policyData = append(policyData, gmacBytes...)

digest := ocrypto.CalculateSHA256(policyData)

// For testing, we will use the last bytes as the GMAC binding
gmacBytes = digest[len(digest)-len(gmacBytes):]

// Create header with GMAC binding
header := &NanoTDFHeader{
bindCfg: bindingConfig{
useEcdsaBinding: false,
},
PolicyBody: policyData,
gmacPolicyBinding: gmacBytes,
}

// Test VerifyPolicyBinding method
valid, err := header.VerifyPolicyBinding()
s.Require().NoError(err)
s.Require().True(valid, "GMAC hash should match")
})

s.Run("Policy Binding Creation Error", func() {
// Create header with invalid ECC mode to trigger error in PolicyBinding()
header := &NanoTDFHeader{
bindCfg: bindingConfig{
useEcdsaBinding: true,
eccMode: 255, // Invalid ECC mode
},
PolicyBody: []byte("test"),
}

// Test VerifyPolicyBinding method with error case
valid, err := header.VerifyPolicyBinding()
s.Require().Error(err)
s.Require().False(valid)
s.Require().Contains(err.Error(), "unsupported nanoTDF ecc mode", "Error should be related to curve/ECC mode")
})
}

// Helper function to create real NanoTDF data for testing
func (s *NanoSuite) createRealNanoTDF(sdk *SDK) ([]byte, error) {
// Read the test file content
Expand Down
2 changes: 1 addition & 1 deletion service/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ require (
github.com/opentdf/platform/lib/identifier v0.2.0
github.com/opentdf/platform/lib/ocrypto v0.7.0
github.com/opentdf/platform/protocol/go v0.13.0
github.com/opentdf/platform/sdk v0.10.0
github.com/opentdf/platform/sdk v0.10.1
github.com/pressly/goose/v3 v3.24.3
github.com/spf13/cobra v1.9.1
github.com/spf13/viper v1.20.1
Expand Down
4 changes: 2 additions & 2 deletions service/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,8 @@ github.com/opentdf/platform/lib/ocrypto v0.7.0 h1:uBZJXisuXU3V8681aP8FVMJkyWBrwW
github.com/opentdf/platform/lib/ocrypto v0.7.0/go.mod h1:sYhoBL1bQYgQVSSNpxU13RsrE5JAk8BABT1hfr9L3j8=
github.com/opentdf/platform/protocol/go v0.13.0 h1:vrOOHyhYDPzJgNenz/1g0M5nWtkOYKkPggMNHKzeMcs=
github.com/opentdf/platform/protocol/go v0.13.0/go.mod h1:GRycoDGDxaz91sOvGZFWVEKJLluZFg2wM3NJmhucDHo=
github.com/opentdf/platform/sdk v0.10.0 h1:OU0pdAnEkcpvLHKZQsynkJJ4lOnVPIBP13W2+9jR80Y=
github.com/opentdf/platform/sdk v0.10.0/go.mod h1:EIh7cTBrtKfjav+5WXA1PZQYjI+fJtTeSXl6Ibw79Bw=
github.com/opentdf/platform/sdk v0.10.1 h1:kBrTK48xle7mdGc+atlr4kDh94f6kVj+0OB76K8rozI=
github.com/opentdf/platform/sdk v0.10.1/go.mod h1:+yaTi/c/GWHZPPmO27sq2s7Tcb2P/USkK8LuW1krhI8=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
Expand Down
33 changes: 21 additions & 12 deletions service/kas/access/rewrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,9 @@ type kaoResult struct {
EphemeralPublicKey []byte
RequiredObligations []string

// Only populated for Nano auditing, since policy is encrypted
KeyID string
// Only populated for Nano auditing
KeyID string
PolicyBinding string
}

// From policy ID to KAO ID to result
Expand Down Expand Up @@ -901,11 +902,12 @@ func (p *Provider) nanoTDFRewrap(ctx context.Context, requests []*kaspb.Unsigned
}

auditEventParams := audit.RewrapAuditEventParams{
Policy: kasPolicy,
IsSuccess: access,
TDFFormat: "Nano",
Algorithm: req.GetAlgorithm(),
KeyID: kaoInfo.KeyID,
Policy: kasPolicy,
IsSuccess: access,
TDFFormat: "Nano",
Algorithm: req.GetAlgorithm(),
KeyID: kaoInfo.KeyID,
PolicyBinding: kaoInfo.PolicyBinding,
}

if !access {
Expand Down Expand Up @@ -986,9 +988,15 @@ func (p *Provider) verifyNanoRewrapRequests(ctx context.Context, req *kaspb.Unsi
}

// check the policy binding
verify, err := header.VerifyPolicyBinding()
binding, err := header.PolicyBinding()
if err != nil {
failedKAORewrap(results, kao, fmt.Errorf("failed to retrieve policy binding: %w", err))
return nil, results
}

verify, err := binding.Verify()
if err != nil {
failedKAORewrap(results, kao, fmt.Errorf("failed to verify policy binding: %w", err))
failedKAORewrap(results, kao, fmt.Errorf("error verifying policy binding: %w", err))
return nil, results
}

Expand All @@ -997,9 +1005,10 @@ func (p *Provider) verifyNanoRewrapRequests(ctx context.Context, req *kaspb.Unsi
return nil, results
}
results[kao.GetKeyAccessObjectId()] = kaoResult{
ID: kao.GetKeyAccessObjectId(),
DEK: symmetricKey,
KeyID: kid,
ID: kao.GetKeyAccessObjectId(),
DEK: symmetricKey,
KeyID: kid,
PolicyBinding: binding.String(),
}
return policy, results
}
Expand Down
Loading