Skip to content

Commit 6ff7414

Browse files
feat(sdk): Expose policy binding hash from Nano. (#2857)
### Proposed Changes 1.) Expose `PolicyBinding` method that returns a implementation of the new `PolicyBind` interface. 2.) Create new `PolicyBind` interface with methods `String()` and `Verify()`. DSPX-1875 ### 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 (cherry picked from commit 5221cf4)
1 parent ebdc8bf commit 6ff7414

File tree

2 files changed

+241
-11
lines changed

2 files changed

+241
-11
lines changed

sdk/nanotdf.go

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"crypto/elliptic"
77
"crypto/sha256"
88
"encoding/binary"
9+
"encoding/hex"
910
"encoding/json"
1011
"errors"
1112
"fmt"
@@ -72,6 +73,24 @@ type NanoTDFHeader struct {
7273
ecdsaPolicyBindingS []byte
7374
}
7475

76+
type ecdsaPolicyBinding struct {
77+
r []byte
78+
s []byte
79+
ephemeralPubKey []byte
80+
digest []byte
81+
curve elliptic.Curve
82+
}
83+
84+
type gmacPolicyBinding struct {
85+
binding []byte
86+
digest []byte
87+
}
88+
89+
type PolicyBind interface {
90+
Verify() (bool, error)
91+
fmt.Stringer
92+
}
93+
7594
func NewNanoTDFHeaderFromReader(reader io.Reader) (NanoTDFHeader, uint32, error) {
7695
header := NanoTDFHeader{}
7796
var size uint32
@@ -230,25 +249,59 @@ func (header *NanoTDFHeader) ECCurve() (elliptic.Curve, error) {
230249
}
231250

232251
func (header *NanoTDFHeader) VerifyPolicyBinding() (bool, error) {
233-
curve, err := ocrypto.GetECCurveFromECCMode(header.bindCfg.eccMode)
252+
policyBind, err := header.PolicyBinding()
253+
if err != nil {
254+
return false, err
255+
}
256+
return policyBind.Verify()
257+
}
258+
259+
func (b *ecdsaPolicyBinding) Verify() (bool, error) {
260+
ephemeralECDSAPublicKey, err := ocrypto.UncompressECPubKey(b.curve, b.ephemeralPubKey)
234261
if err != nil {
235262
return false, err
236263
}
237264

265+
return ocrypto.VerifyECDSASig(b.digest,
266+
b.r,
267+
b.s,
268+
ephemeralECDSAPublicKey), nil
269+
}
270+
271+
func (b *ecdsaPolicyBinding) String() string {
272+
return string(ocrypto.SHA256AsHex(append(b.r, b.s...)))
273+
}
274+
275+
func (b *gmacPolicyBinding) Verify() (bool, error) {
276+
bindingToCheck := b.digest[len(b.digest)-kNanoTDFGMACLength:]
277+
return bytes.Equal(bindingToCheck, b.binding), nil
278+
}
279+
280+
func (b *gmacPolicyBinding) String() string {
281+
return hex.EncodeToString(b.binding)
282+
}
283+
284+
func (header *NanoTDFHeader) PolicyBinding() (PolicyBind, error) {
238285
digest := ocrypto.CalculateSHA256(header.PolicyBody)
286+
239287
if header.IsEcdsaBindingEnabled() {
240-
ephemeralECDSAPublicKey, err := ocrypto.UncompressECPubKey(curve, header.EphemeralKey)
288+
curve, err := ocrypto.GetECCurveFromECCMode(header.bindCfg.eccMode)
241289
if err != nil {
242-
return false, err
290+
return nil, err
243291
}
244-
245-
return ocrypto.VerifyECDSASig(digest,
246-
header.ecdsaPolicyBindingR,
247-
header.ecdsaPolicyBindingS,
248-
ephemeralECDSAPublicKey), nil
249-
}
250-
binding := digest[len(digest)-kNanoTDFGMACLength:]
251-
return bytes.Equal(binding, header.gmacPolicyBinding), nil
292+
return &ecdsaPolicyBinding{
293+
r: header.ecdsaPolicyBindingR,
294+
s: header.ecdsaPolicyBindingS,
295+
ephemeralPubKey: header.EphemeralKey,
296+
curve: curve,
297+
digest: digest,
298+
}, nil
299+
}
300+
301+
return &gmacPolicyBinding{
302+
binding: header.gmacPolicyBinding,
303+
digest: digest,
304+
}, nil
252305
}
253306

254307
// ============================================================================================================

sdk/nanotdf_test.go

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"crypto/rand"
88
"crypto/x509"
99
"encoding/gob"
10+
"encoding/hex"
1011
"encoding/json"
1112
"encoding/pem"
1213
"errors"
@@ -934,6 +935,182 @@ func (s *NanoSuite) Test_NanoTDF_Obligations() {
934935
}
935936
}
936937

938+
func (s *NanoSuite) Test_PolicyBinding_GMAC() {
939+
// Create test policy data
940+
policyData := []byte(`{"body":{"dataAttributes":["https://example.com/attr/classification/value/secret"]}}`)
941+
942+
// Create GMAC binding - need to simulate having GMAC at end of the digest
943+
gmacBytes := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}
944+
// Append GMAC to the digest to simulate real scenario
945+
policyData = append(policyData, gmacBytes...)
946+
947+
digest := ocrypto.CalculateSHA256(policyData)
948+
949+
// For testing, we will use the last bytes as the GMAC binding
950+
gmacBytes = digest[len(digest)-len(gmacBytes):]
951+
952+
binding := &gmacPolicyBinding{
953+
binding: gmacBytes,
954+
digest: digest,
955+
}
956+
957+
// Test String function
958+
s.Require().Equal(hex.EncodeToString(gmacBytes), binding.String(), "GMAC hash should return binding data directly")
959+
960+
// Test Verify function - should pass with correct binding
961+
valid, err := binding.Verify()
962+
s.Require().NoError(err)
963+
s.Require().True(valid, "GMAC binding should be valid when binding matches digest suffix")
964+
965+
// Test Verify function with wrong binding - should fail
966+
wrongBinding := &gmacPolicyBinding{
967+
binding: []byte{0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01},
968+
digest: digest,
969+
}
970+
valid, err = wrongBinding.Verify()
971+
s.Require().NoError(err)
972+
s.Require().False(valid, "GMAC binding should be invalid when binding doesn't match digest suffix")
973+
}
974+
975+
func (s *NanoSuite) Test_PolicyBinding_ECDSA() {
976+
// Create a test ECDSA key pair
977+
keyPair, err := ocrypto.NewECKeyPair(ocrypto.ECCModeSecp256r1)
978+
s.Require().NoError(err)
979+
980+
// Create test policy data
981+
policyData := []byte(`{"body":{"dataAttributes":["https://example.com/attr/classification/value/secret"]}}`)
982+
digest := ocrypto.CalculateSHA256(policyData)
983+
984+
// Sign the digest
985+
r, sBytes, err := ocrypto.ComputeECDSASig(digest, keyPair.PrivateKey)
986+
s.Require().NoError(err)
987+
988+
// Get the public key in compressed format
989+
compressedPubKey, err := ocrypto.CompressedECPublicKey(ocrypto.ECCModeSecp256r1, keyPair.PrivateKey.PublicKey)
990+
s.Require().NoError(err)
991+
992+
binding := &ecdsaPolicyBinding{
993+
r: r,
994+
s: sBytes,
995+
ephemeralPubKey: compressedPubKey,
996+
digest: digest,
997+
curve: keyPair.PrivateKey.Curve,
998+
}
999+
1000+
// Test String function
1001+
expectedHash := string(ocrypto.SHA256AsHex(append(r, sBytes...)))
1002+
s.Require().NotEmpty(binding.String(), "Hash should not be empty")
1003+
s.Require().Equal(expectedHash, binding.String(), "ECDSA hash should be SHA256 of r||s")
1004+
1005+
// Test Verify function - should pass with correct signature
1006+
valid, err := binding.Verify()
1007+
s.Require().NoError(err)
1008+
s.Require().True(valid, "ECDSA binding should be valid with correct signature")
1009+
1010+
// Test Verify function with wrong signature - should fail
1011+
invalidR := make([]byte, 32)
1012+
invalidS := make([]byte, 32)
1013+
for i := range invalidR {
1014+
invalidR[i] = byte(i)
1015+
invalidS[i] = byte(i + 10)
1016+
}
1017+
1018+
wrongBinding := &ecdsaPolicyBinding{
1019+
r: invalidR,
1020+
s: invalidS,
1021+
ephemeralPubKey: compressedPubKey,
1022+
digest: digest,
1023+
curve: keyPair.PrivateKey.Curve,
1024+
}
1025+
1026+
valid, err = wrongBinding.Verify()
1027+
s.Require().NoError(err)
1028+
s.Require().False(valid, "ECDSA binding should be invalid with wrong signature")
1029+
}
1030+
1031+
func (s *NanoSuite) Test_NanoTDFHeader_VerifyPolicyBinding() {
1032+
s.Run("ECDSA Policy Binding Verification", func() {
1033+
// Create a test ECDSA key pair
1034+
keyPair, err := ocrypto.NewECKeyPair(ocrypto.ECCModeSecp256r1)
1035+
s.Require().NoError(err)
1036+
1037+
// Create test policy data
1038+
policyData := []byte(`{"body":{"dataAttributes":["https://example.com/attr/classification/value/secret"]}}`)
1039+
digest := ocrypto.CalculateSHA256(policyData)
1040+
1041+
// Sign the digest
1042+
r, sBytes, err := ocrypto.ComputeECDSASig(digest, keyPair.PrivateKey)
1043+
s.Require().NoError(err)
1044+
1045+
// Get compressed public key
1046+
compressedPubKey, err := ocrypto.CompressedECPublicKey(ocrypto.ECCModeSecp256r1, keyPair.PrivateKey.PublicKey)
1047+
s.Require().NoError(err)
1048+
1049+
// Create header with ECDSA binding
1050+
header := &NanoTDFHeader{
1051+
bindCfg: bindingConfig{
1052+
useEcdsaBinding: true,
1053+
eccMode: ocrypto.ECCModeSecp256r1,
1054+
},
1055+
PolicyBody: policyData,
1056+
EphemeralKey: compressedPubKey,
1057+
ecdsaPolicyBindingR: r,
1058+
ecdsaPolicyBindingS: sBytes,
1059+
}
1060+
1061+
// Test VerifyPolicyBinding method
1062+
valid, err := header.VerifyPolicyBinding()
1063+
s.Require().NoError(err)
1064+
s.Require().True(valid, "ECDSA policy binding should be valid")
1065+
})
1066+
1067+
s.Run("GMAC Policy Binding Verification", func() {
1068+
// Create test policy data
1069+
policyData := []byte(`{"body":{"dataAttributes":["https://example.com/attr/classification/value/secret"]}}`)
1070+
1071+
// Create GMAC binding - need to simulate having GMAC at end of the digest
1072+
gmacBytes := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}
1073+
// Append GMAC to the digest to simulate real scenario
1074+
policyData = append(policyData, gmacBytes...)
1075+
1076+
digest := ocrypto.CalculateSHA256(policyData)
1077+
1078+
// For testing, we will use the last bytes as the GMAC binding
1079+
gmacBytes = digest[len(digest)-len(gmacBytes):]
1080+
1081+
// Create header with GMAC binding
1082+
header := &NanoTDFHeader{
1083+
bindCfg: bindingConfig{
1084+
useEcdsaBinding: false,
1085+
},
1086+
PolicyBody: policyData,
1087+
gmacPolicyBinding: gmacBytes,
1088+
}
1089+
1090+
// Test VerifyPolicyBinding method
1091+
valid, err := header.VerifyPolicyBinding()
1092+
s.Require().NoError(err)
1093+
s.Require().True(valid, "GMAC hash should match")
1094+
})
1095+
1096+
s.Run("Policy Binding Creation Error", func() {
1097+
// Create header with invalid ECC mode to trigger error in PolicyBinding()
1098+
header := &NanoTDFHeader{
1099+
bindCfg: bindingConfig{
1100+
useEcdsaBinding: true,
1101+
eccMode: 255, // Invalid ECC mode
1102+
},
1103+
PolicyBody: []byte("test"),
1104+
}
1105+
1106+
// Test VerifyPolicyBinding method with error case
1107+
valid, err := header.VerifyPolicyBinding()
1108+
s.Require().Error(err)
1109+
s.Require().False(valid)
1110+
s.Require().Contains(err.Error(), "unsupported nanoTDF ecc mode", "Error should be related to curve/ECC mode")
1111+
})
1112+
}
1113+
9371114
// Helper function to create real NanoTDF data for testing
9381115
func (s *NanoSuite) createRealNanoTDF(sdk *SDK) ([]byte, error) {
9391116
// Read the test file content

0 commit comments

Comments
 (0)