From 5032658337da0cbc394e9935fba56f98466e5478 Mon Sep 17 00:00:00 2001 From: Quint Daenen Date: Tue, 2 Jul 2024 20:42:48 +0200 Subject: [PATCH] Add verify func for II delegations. --- certification/certificate.go | 2 +- certification/hash.go | 101 +++++++++++++++++++ certification/hash_test.go | 117 ++++++++++++++++++++++ certification/ii/canister_sig.go | 61 ++++++++++++ certification/ii/canister_sig_test.go | 40 ++++++++ certification/ii/delegation.go | 137 ++++++++++++++++++++++++++ certification/ii/delegation_test.go | 44 +++++++++ pocketic/endpoints_test.go | 2 +- pocketic/pocketic_test.go | 6 +- query.go | 32 +++--- registry/proto/v1/registry.pb.go | 106 ++++++++++---------- request.go | 74 +------------- 12 files changed, 579 insertions(+), 143 deletions(-) create mode 100644 certification/hash.go create mode 100644 certification/hash_test.go create mode 100644 certification/ii/canister_sig.go create mode 100644 certification/ii/canister_sig_test.go create mode 100644 certification/ii/delegation.go create mode 100644 certification/ii/delegation_test.go diff --git a/certification/certificate.go b/certification/certificate.go index 3e8c501..3082615 100644 --- a/certification/certificate.go +++ b/certification/certificate.go @@ -139,7 +139,7 @@ func VerifyCertifiedData( return err } if !bytes.Equal(certificateCertifiedData, certifiedData) { - return fmt.Errorf("certified data does not match") + return fmt.Errorf("certified data does not match: %x != %x", certificateCertifiedData, certifiedData) } return nil } diff --git a/certification/hash.go b/certification/hash.go new file mode 100644 index 0000000..1bbd564 --- /dev/null +++ b/certification/hash.go @@ -0,0 +1,101 @@ +package certification + +import ( + "bytes" + "crypto/sha256" + "fmt" + "math/big" + "sort" + + "github.com/aviate-labs/leb128" + "github.com/fxamacker/cbor/v2" +) + +// HashAny computes the hash of any value. +func HashAny(v any) ([32]byte, error) { + switch v := v.(type) { + case cbor.RawMessage: + var anyValue any + if err := cbor.Unmarshal(v, &anyValue); err != nil { + panic(err) + } + return HashAny(anyValue) + case string: + return sha256.Sum256([]byte(v)), nil + case []byte: + return sha256.Sum256(v), nil + case int64: + bi := big.NewInt(int64(v)) + e, err := leb128.EncodeUnsigned(bi) + if err != nil { + return [32]byte{}, err + } + return sha256.Sum256(e), nil + case uint64: + bi := big.NewInt(int64(v)) + e, err := leb128.EncodeUnsigned(bi) + if err != nil { + return [32]byte{}, err + } + return sha256.Sum256(e), nil + case map[any]any: // cbor maps are not guaranteed to have string keys + kv := make([]KeyValuePair, len(v)) + i := 0 + for k, v := range v { + s, isString := k.(string) + if !isString { + return [32]byte{}, fmt.Errorf("unsupported type %T", k) + } + kv[i] = KeyValuePair{Key: s, Value: v} + i++ + } + return RepresentationIndependentHash(kv) + case map[string]any: + m := make([]KeyValuePair, len(v)) + i := 0 + for k, v := range v { + m[i] = KeyValuePair{Key: k, Value: v} + i++ + } + return RepresentationIndependentHash(m) + case []any: + var hashes []byte + for _, v := range v { + valueHash, err := HashAny(v) + if err != nil { + return [32]byte{}, err + } + hashes = append(hashes, valueHash[:]...) + } + return sha256.Sum256(hashes), nil + default: + return [32]byte{}, fmt.Errorf("unsupported type %T", v) + } +} + +// RepresentationIndependentHash computes the hash of a map in a representation-independent way. +// https://internetcomputer.org/docs/current/references/ic-interface-spec/#hash-of-map +func RepresentationIndependentHash(m []KeyValuePair) ([32]byte, error) { + var hashes [][]byte + for _, kv := range m { + if kv.Value == nil { + continue + } + + keyHash := sha256.Sum256([]byte(kv.Key)) + valueHash, err := HashAny(kv.Value) + if err != nil { + return [32]byte{}, err + } + hashes = append(hashes, append(keyHash[:], valueHash[:]...)) + } + sort.Slice(hashes, func(i, j int) bool { + return bytes.Compare(hashes[i], hashes[j]) == -1 + }) + return sha256.Sum256(bytes.Join(hashes, nil)), nil +} + +type KeyValuePair struct { + Key string + Value any +} diff --git a/certification/hash_test.go b/certification/hash_test.go new file mode 100644 index 0000000..cc7f165 --- /dev/null +++ b/certification/hash_test.go @@ -0,0 +1,117 @@ +package certification + +import ( + "bytes" + "encoding/hex" + "testing" +) + +func TestHashAny(t *testing.T) { + for _, test := range []struct { + name string + v any + want string + }{ + { + name: "array", + v: []any{"a"}, + want: "bf5d3affb73efd2ec6c36ad3112dd933efed63c4e1cbffcfa88e2759c144f2d8", + }, + { + name: "array", + v: []any{"a", "b"}, + want: "e5a01fee14e0ed5c48714f22180f25ad8365b53f9779f79dc4a3d7e93963f94a", + }, + { + name: "array", + v: []any{[]byte{97}, "b"}, + want: "e5a01fee14e0ed5c48714f22180f25ad8365b53f9779f79dc4a3d7e93963f94a", + }, + { + name: "array of arrays", + v: []any{[]any{"a"}}, + want: "eb48bdfa15fc43dbea3aabb1ee847b6e69232c0f0d9705935e50d60cce77877f", + }, + { + name: "array of arrays", + v: []any{[]any{"a", "b"}}, + want: "029fd80ca2dd66e7c527428fc148e812a9d99a5e41483f28892ef9013eee4a19", + }, + { + name: "array of arrays", + v: []any{[]any{"a", "b"}, []byte{97}}, + want: "aec3805593d9ec6df50da070597f73507050ce098b5518d0456876701ada7bb7", + }, + } { + t.Run(test.name, func(t *testing.T) { + got, err := HashAny(test.v) + if err != nil { + t.Fatal(err) + } + want, err := hex.DecodeString(test.want) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got[:], want) { + t.Fatalf("got %x, want %x", got, test.want) + } + }) + } +} + +func TestRepresentationIndependentHash(t *testing.T) { + for _, test := range []struct { + name string + kv []KeyValuePair + want string + }{ + { + name: "key-value map", + kv: []KeyValuePair{ + {Key: "name", Value: "foo"}, + {Key: "message", Value: "Hello World!"}, + {Key: "answer", Value: uint64(42)}, + }, + want: "b0c6f9191e37dceafdfc47fbfc7e9cc95f21c7b985c2f7ba5855015c2a8f13ac", + }, + { + name: "duplicate keys", + kv: []KeyValuePair{ + {Key: "name", Value: "foo"}, + {Key: "name", Value: "bar"}, + {Key: "message", Value: "Hello World!"}, + }, + want: "435f77c9bdeca5dba4a4b8a34e4f732b4311f1fc252ec6d4e8ee475234b170f9", + }, + { + name: "reordered keys", + kv: []KeyValuePair{ + {Key: "name", Value: "bar"}, + {Key: "message", Value: "Hello World!"}, + {Key: "name", Value: "foo"}, + }, + want: "435f77c9bdeca5dba4a4b8a34e4f732b4311f1fc252ec6d4e8ee475234b170f9", + }, + { + name: "bytes", + kv: []KeyValuePair{ + {Key: "bytes", Value: []byte{0x01, 0x02, 0x03, 0x04}}, + }, + want: "546729666d96a712bd94f902a0388e33f9a19a335c35bc3d95b0221a4a574455", + }, + } { + t.Run(test.name, func(t *testing.T) { + got, err := RepresentationIndependentHash(test.kv) + if err != nil { + t.Fatal(err) + } + want, err := hex.DecodeString(test.want) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got[:], want) { + t.Fatalf("got %x, want %x", got, test.want) + } + }) + } +} diff --git a/certification/ii/canister_sig.go b/certification/ii/canister_sig.go new file mode 100644 index 0000000..209352c --- /dev/null +++ b/certification/ii/canister_sig.go @@ -0,0 +1,61 @@ +package verify + +import ( + "bytes" + "fmt" + "github.com/aviate-labs/agent-go/principal" +) + +var ( + CanisterSigPublicKeyDERObjectID = []byte{ + 0x30, 0x0C, 0x06, 0x0A, 0x2B, 0x06, 0x01, + 0x04, 0x01, 0x83, 0xB8, 0x43, 0x01, 0x02, + } + CanisterSigPublicKeyPrefixLength = 19 +) + +type CanisterSigPublicKey struct { + CanisterID principal.Principal + Seed []byte +} + +func CanisterSigPublicKeyFromDER(der []byte) (*CanisterSigPublicKey, error) { + if len(der) < 21 { + return nil, fmt.Errorf("DER data is too short") + } + if !bytes.Equal(der[2:len(CanisterSigPublicKeyDERObjectID)+2], CanisterSigPublicKeyDERObjectID) { + return nil, fmt.Errorf("DER data does not match object ID") + } + canisterIDLength := int(der[CanisterSigPublicKeyPrefixLength]) + if len(der) < CanisterSigPublicKeyPrefixLength+canisterIDLength { + return nil, fmt.Errorf("DER data is too short") + } + offset := CanisterSigPublicKeyPrefixLength + 1 + rawCanisterID := der[offset : offset+canisterIDLength] + offset += canisterIDLength + return &CanisterSigPublicKey{ + CanisterID: principal.Principal{Raw: rawCanisterID}, + Seed: der[offset:], + }, nil +} + +func (s *CanisterSigPublicKey) DER() []byte { + raw := s.Raw() + var der bytes.Buffer + der.WriteByte(0x30) + der.WriteByte(17 + byte(len(raw))) + der.Write(CanisterSigPublicKeyDERObjectID) + der.WriteByte(0x03) + der.WriteByte(1 + byte(len(raw))) + der.WriteByte(0x00) + der.Write(raw) + return der.Bytes() +} + +func (s *CanisterSigPublicKey) Raw() []byte { + var raw bytes.Buffer + raw.WriteByte(byte(len(s.CanisterID.Raw))) + raw.Write(s.CanisterID.Raw) + raw.Write(s.Seed) + return raw.Bytes() +} diff --git a/certification/ii/canister_sig_test.go b/certification/ii/canister_sig_test.go new file mode 100644 index 0000000..88ab230 --- /dev/null +++ b/certification/ii/canister_sig_test.go @@ -0,0 +1,40 @@ +package verify + +import ( + "bytes" + "encoding/hex" + "github.com/aviate-labs/agent-go/principal" + "testing" +) + +var ( + testCanisterID = principal.MustDecode("rwlgt-iiaaa-aaaaa-aaaaa-cai") + testSeed = []byte{42, 72, 44} + testCanisterSigPublicKeyDER, _ = hex.DecodeString("301f300c060a2b0601040183b8430102030f000a000000000000000001012a482c") +) + +func TestCanisterSigPublicKeyFromDER(t *testing.T) { + cspk, err := CanisterSigPublicKeyFromDER(testCanisterSigPublicKeyDER) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(cspk.CanisterID.Raw, testCanisterID.Raw) { + t.Fatalf("expected %x, got %x", testCanisterID.Raw, cspk.CanisterID.Raw) + } + if !bytes.Equal(cspk.Seed, testSeed) { + t.Fatalf("expected %x, got %x", testSeed, cspk.Seed) + } +} + +func TestCanisterSigPublicKey_DER(t *testing.T) { + cspk := CanisterSigPublicKey{ + CanisterID: testCanisterID, + Seed: testSeed, + } + + der := cspk.DER() + if !bytes.Equal(der, testCanisterSigPublicKeyDER) { + t.Fatalf("expected %x, got %x", testCanisterSigPublicKeyDER, der) + } +} diff --git a/certification/ii/delegation.go b/certification/ii/delegation.go new file mode 100644 index 0000000..3913235 --- /dev/null +++ b/certification/ii/delegation.go @@ -0,0 +1,137 @@ +package verify + +import ( + "bytes" + "crypto/sha256" + "encoding/binary" + "encoding/hex" + "encoding/json" + "fmt" + "github.com/aviate-labs/agent-go/certification" + "github.com/aviate-labs/agent-go/certification/hashtree" + "github.com/aviate-labs/agent-go/principal" + "github.com/fxamacker/cbor/v2" +) + +type BEHexUint64 uint64 + +func (b *BEHexUint64) UnmarshalJSON(bytes []byte) error { + var s string + if err := json.Unmarshal(bytes, &s); err != nil { + return err + } + bb, err := hex.DecodeString(s) + if err != nil { + return err + } + *b = BEHexUint64(binary.BigEndian.Uint64(bb)) + return nil +} + +type Delegation struct { + PublicKey HexString `json:"pubkey"` + Expiration BEHexUint64 `json:"expiration"` + Targets []HexString `json:"targets"` +} + +func (d Delegation) SignatureMessage() ([]byte, error) { + kv := []certification.KeyValuePair{ + {Key: "pubkey", Value: []byte(d.PublicKey)}, + {Key: "expiration", Value: uint64(d.Expiration)}, + } + ts := make([]any, len(d.Targets)) + for i, target := range d.Targets { + ts[i] = []byte(target) + } + if 0 < len(ts) { + kv = append(kv, certification.KeyValuePair{Key: "targets", Value: ts}) + } + hash, err := certification.RepresentationIndependentHash(kv) + if err != nil { + return nil, err + } + return append([]byte("\x1aic-request-auth-delegation"), hash[:]...), nil +} + +type DelegationChain struct { + Delegations []SignedDelegation `json:"delegations"` + PublicKey HexString `json:"publicKey"` +} + +func (d DelegationChain) VerifyChallenge( + challenge []byte, + currentTimeNS uint64, + canisterID principal.Principal, + rootPublicKey []byte, +) error { + if len(d.Delegations) != 1 { + return fmt.Errorf("expected exactly one delegation") + } + signedDelegation := d.Delegations[0] + delegation := signedDelegation.Delegation + if !bytes.Equal(challenge, []byte(delegation.PublicKey)) { + return fmt.Errorf("invalid challenge") + } + canisterSig, err := CanisterSigPublicKeyFromDER([]byte(d.PublicKey)) + if err != nil { + return err + } + if !bytes.Equal(canisterSig.CanisterID.Raw, canisterID.Raw) { + return fmt.Errorf("invalid canister ID") + } + if uint64(delegation.Expiration) < currentTimeNS { + return fmt.Errorf("delegation expired") + } + + sig := []byte(signedDelegation.Signature) + message, err := delegation.SignatureMessage() + if err != nil { + return err + } + var wrapper struct { + Certificate []byte `cbor:"certificate"` + Tree hashtree.HashTree `cbor:"tree"` + } + if err := cbor.Unmarshal(sig, &wrapper); err != nil { + return err + } + var certificate certification.Certificate + if err := cbor.Unmarshal(wrapper.Certificate, &certificate); err != nil { + return err + } + tree := wrapper.Tree.Digest() + if err := certification.VerifyCertifiedData( + certificate, + canisterSig.CanisterID, + rootPublicKey, + tree[:], + ); err != nil { + return err + } + seed := sha256.Sum256(canisterSig.Seed) + msg := sha256.Sum256(message) + if _, err := wrapper.Tree.Lookup(hashtree.Label("sig"), seed[:], msg[:]); err != nil { + return err + } + return nil +} + +type HexString string + +func (h *HexString) UnmarshalJSON(bytes []byte) error { + var s string + if err := json.Unmarshal(bytes, &s); err != nil { + return err + } + b, err := hex.DecodeString(s) + if err != nil { + return err + } + *h = HexString(b) + return nil +} + +type SignedDelegation struct { + Delegation Delegation `json:"delegation"` + Signature HexString `json:"signature"` +} diff --git a/certification/ii/delegation_test.go b/certification/ii/delegation_test.go new file mode 100644 index 0000000..d08b74f --- /dev/null +++ b/certification/ii/delegation_test.go @@ -0,0 +1,44 @@ +package verify + +import ( + "encoding/hex" + "encoding/json" + "github.com/aviate-labs/agent-go/certification" + "github.com/aviate-labs/agent-go/principal" + "testing" +) + +func TestVerifyChallenge(t *testing.T) { + challenge, err := hex.DecodeString("e7875e69ce7beda6fc7b6dfbd9b75be1c6f6d5debae3ae1ed7c7f873de1b6f9f75e9e7dcddcf37efaddcdf6f7b69a7b57377b5ddaef87dee386ddd75e39e9cd39d7d77debc79df1b7b469df36eb8e7cef47b4d5cefa7f5df67dbefc73debdf5c") + if err != nil { + t.Fatal(err) + } + delegationJSON := []byte(`{ + "delegations": [ + { + "delegation":{ + "expiration":"17b5b384762bfd21", + "pubkey":"e7875e69ce7beda6fc7b6dfbd9b75be1c6f6d5debae3ae1ed7c7f873de1b6f9f75e9e7dcddcf37efaddcdf6f7b69a7b57377b5ddaef87dee386ddd75e39e9cd39d7d77debc79df1b7b469df36eb8e7cef47b4d5cefa7f5df67dbefc73debdf5c" + }, + "signature":"d9d9f7a26b6365727469666963617465590547d9d9f7a3647472656583018301830183024863616e697374657283018301830183018301830182045820640c48458731be868c750243066312f4e06b2bfde48309a3cfd0617ee3c8f3448301820458204042fb2844db206e1724a248eef393f5cb1d22280f298d948fc18e0a408533438301820458208d3dbc5b1ac807eb4f313b91712db94fdf4a50068207719f1cba37771b2ac8ef83024a000000000060002701018301830183024e6365727469666965645f6461746182035820a61cee2397ab0f006060d4a7bf4a9bef463d5b2381c502a6c66a26b6d088b64d820458206ccd6bb31a54761d4a56e9cfd8cba384d5b8fb47184e8ca13cb70e04f2209ace82045820c64354fe1474e905acdcf09f6569cfb29c305d0b06806908f2da5ee9404726bf820458203de781de0811f5a8469166c594f9433d966f686f4f4065ad9395e30bfac153e282045820cb2a94057004ae336fb52ba39117cf90aaadefe02ddfe9205bcc13c8f6150a0282045820bc1f9b4c54f66eb8fc25381e90641ae59ef87c590186355162a52cb4875242cb8204582001f9f57686d9eb1af846b6ee42c48b02289fe9cf134f84d527a000e65e4d7443820458201c1f10e2904ed9819f3cf7e051c473151700ea5b8038bf1413ba894b3afac4608204582045c96fb30bf784be7d9da2f7e41a2fa93f728bf07829da23acad05006286c269820458204ffce0d4d1e2124180daef5447fe496bbec7ef22b53786138b4acf523453fa75830182045820d5523abdfb2963caffc236cfe5a7f30a832b152c2f827d6acdf79ed5bb9a690e83024474696d65820349a1fa9b83afaae6da17697369676e6174757265583092eaf174a665a296e8968d910ab5a6130fb7deca606a68f5903d8e6a4b64a0fc609b7b7f6a68146e6c51b35e367deb8b6a64656c65676174696f6ea2697375626e65745f6964581d2c55b347ecf2686c83781d6c59d1b43e7b4cba8deb6c1b376107f2cd026b636572746966696361746559026ed9d9f7a2647472656583018204582075d2df1ca388b2596be5564ca726dbcadf77bbc535811734b704a8846153be1383018302467375626e657483018301830183018204582035bc207266aa1f9a1b4eea393efe91ae33ed4ce77069ed8e881d86716adf7b6b830182045820f8c3eae0377ee00859223bf1c6202f5885c4dcdc8fd13b1d48c3c838688919bc83018302581d2c55b347ecf2686c83781d6c59d1b43e7b4cba8deb6c1b376107f2cd02830183024f63616e69737465725f72616e67657382035832d9d9f782824a000000000060000001014a00000000006000ae0101824a00000000006000b001014a00000000006fffff010183024a7075626c69635f6b657982035885308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7c0503020103610090075120778eb21a530a02bcc763e7f4a192933506966af7b54c10a4d2b24de6a86b200e3440bae6267bf4c488d9a11d0472c38c1b6221198f98e4e6882ba38a5a4e3aa5afce899b7f825ed95adfa12629688073556f2747527213e8d73e40ce8204582036f3cd257d90fb38e42597f193a5e031dbd585b6292793bb04db4794803ce06e82045820028fc5e5f70868254e7215e7fc630dbd29eefc3619af17ce231909e1faf97e9582045820ef8995c410ed405731c9b913f67879e3b6a6b4d659d2746db9a6b47d7e70d3d582045820f9a6810df003d2188a807e8370076bd94a996877ec8bd11aa2c4e1358c01c6ab83024474696d65820349e2c9c9e480f6edd917697369676e61747572655830833724e450e6e1c8848118e82b04c5db3964f0869b6fb52af9bdbf3876435a19c798c03b41d5eb5fd39535c4ab24e70464747265658301820458209a7cc9ffcec2242e2e15b45a4e1fb9983c87c5b7e8badb7b92a891b40382f73683024373696783025820c9f3b4b781360e36240c549029e4b0857a6cc31e7230a680e551cab71aae0df38301820458203e26edaf16f66c93c238503a3d2077176e9ce6f0438940679b22cb31a636bfee83025820f49c0d7056981c0f2fdfaf02d219db038e2c448193bbf19642fbf118a8f4739a820340" + } + ], + "publicKey":"303c300c060a2b0601040183b8430102032c000a00000000006000270101f3ffab2278616508ad5ebfa0cb79a21e08dbb7132f6875b95f81e72067f31302" +}`) + expiration := uint64(1708469015156620577) + canisterID := principal.MustDecode("fgte5-ciaaa-aaaad-aaatq-cai") + rootKey, _ := hex.DecodeString(certification.RootKey) + + var dc DelegationChain + if err := json.Unmarshal(delegationJSON, &dc); err != nil { + t.Fatal(err) + } + if err := dc.VerifyChallenge( + challenge, + expiration-42, + canisterID, + rootKey, + ); err != nil { + t.Fatal(err) + } +} diff --git a/pocketic/endpoints_test.go b/pocketic/endpoints_test.go index 99ff83d..7e748a4 100644 --- a/pocketic/endpoints_test.go +++ b/pocketic/endpoints_test.go @@ -16,7 +16,7 @@ func TestEndpoints(t *testing.T) { pocketic.WithApplicationSubnet(), ) if err != nil { - t.Fatal(err) + t.Skipf("skipping test: %v", err) } t.Run("status", func(t *testing.T) { diff --git a/pocketic/pocketic_test.go b/pocketic/pocketic_test.go index ddd9000..7758a19 100644 --- a/pocketic/pocketic_test.go +++ b/pocketic/pocketic_test.go @@ -21,7 +21,7 @@ import ( func TestConcurrentCalls(t *testing.T) { pic, err := pocketic.New(pocketic.WithPollingDelay(10*time.Millisecond, 10*time.Second)) if err != nil { - t.Fatal(err) + t.Skipf("skipping test: %v", err) } var wg sync.WaitGroup for i := 0; i < 10; i++ { @@ -47,7 +47,7 @@ func TestConcurrentCalls(t *testing.T) { func TestCreateCanister(t *testing.T) { pic, err := pocketic.New(pocketic.WithLogger(new(testLogger))) if err != nil { - t.Fatal(err) + t.Skipf("skipping test: %v", err) } canisterID, err := pic.CreateCanister() @@ -66,7 +66,7 @@ func TestHttpGateway(t *testing.T) { pocketic.WithApplicationSubnet(), ) if err != nil { - t.Fatal(err) + t.Skipf("skipping test: %v", err) } endpoint, err := pic.MakeLive(nil) diff --git a/query.go b/query.go index bdc2deb..c61c5cf 100644 --- a/query.go +++ b/query.go @@ -150,12 +150,14 @@ func (q Query) Query(values ...any) error { } switch resp.Status { case "replied": - sig, err := hashOfMap(map[string]any{ - "status": resp.Status, - "reply": resp.Reply, - "timestamp": signature.Timestamp, - "request_id": q.requestID[:], - }) + sig, err := certification.RepresentationIndependentHash( + []certification.KeyValuePair{ + {Key: "status", Value: resp.Status}, + {Key: "reply", Value: resp.Reply}, + {Key: "timestamp", Value: signature.Timestamp}, + {Key: "request_id", Value: q.requestID[:]}, + }, + ) if err != nil { return err } @@ -171,14 +173,16 @@ func (q Query) Query(values ...any) error { if err != nil { return err } - sig, err := hashOfMap(map[string]any{ - "status": resp.Status, - "reject_code": code, - "reject_message": resp.RejectMsg, - "error_code": resp.ErrorCode, - "timestamp": signature.Timestamp, - "request_id": q.requestID[:], - }) + sig, err := certification.RepresentationIndependentHash( + []certification.KeyValuePair{ + {Key: "status", Value: resp.Status}, + {Key: "reject_code", Value: code}, + {Key: "reject_message", Value: resp.RejectMsg}, + {Key: "error_code", Value: resp.ErrorCode}, + {Key: "timestamp", Value: signature.Timestamp}, + {Key: "request_id", Value: q.requestID[:]}, + }, + ) if err != nil { return err } diff --git a/registry/proto/v1/registry.pb.go b/registry/proto/v1/registry.pb.go index 99341d5..08380d2 100644 --- a/registry/proto/v1/registry.pb.go +++ b/registry/proto/v1/registry.pb.go @@ -70,6 +70,56 @@ var file_registry_proto_rawDesc = []byte{ 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } +func file_registry_proto_init() { + if File_registry_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_registry_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ProtoRegistry); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_registry_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ProtoRegistryRecord); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_registry_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_registry_proto_goTypes, + DependencyIndexes: file_registry_proto_depIdxs, + MessageInfos: file_registry_proto_msgTypes, + }.Build() + File_registry_proto = out.File + file_registry_proto_rawDesc = nil + file_registry_proto_goTypes = nil + file_registry_proto_depIdxs = nil +} + func file_registry_proto_rawDescGZIP() []byte { file_registry_proto_rawDescOnce.Do(func() { file_registry_proto_rawDescData = protoimpl.X.CompressGZIP(file_registry_proto_rawDescData) @@ -77,6 +127,8 @@ func file_registry_proto_rawDescGZIP() []byte { return file_registry_proto_rawDescData } +func init() { file_registry_proto_init() } + type ProtoRegistry struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -159,9 +211,7 @@ func (x *ProtoRegistryRecord) GetVersion() uint64 { } return 0 } - func (*ProtoRegistryRecord) ProtoMessage() {} - func (x *ProtoRegistryRecord) ProtoReflect() protoreflect.Message { mi := &file_registry_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { @@ -173,6 +223,7 @@ func (x *ProtoRegistryRecord) ProtoReflect() protoreflect.Message { } return mi.MessageOf(x) } + func (x *ProtoRegistryRecord) Reset() { *x = ProtoRegistryRecord{} if protoimpl.UnsafeEnabled { @@ -184,54 +235,3 @@ func (x *ProtoRegistryRecord) Reset() { func (x *ProtoRegistryRecord) String() string { return protoimpl.X.MessageStringOf(x) } - -func init() { file_registry_proto_init() } -func file_registry_proto_init() { - if File_registry_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_registry_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ProtoRegistry); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_registry_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ProtoRegistryRecord); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_registry_proto_rawDesc, - NumEnums: 0, - NumMessages: 2, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_registry_proto_goTypes, - DependencyIndexes: file_registry_proto_depIdxs, - MessageInfos: file_registry_proto_msgTypes, - }.Build() - File_registry_proto = out.File - file_registry_proto_rawDesc = nil - file_registry_proto_goTypes = nil - file_registry_proto_depIdxs = nil -} diff --git a/request.go b/request.go index 7e952d5..a3e0363 100644 --- a/request.go +++ b/request.go @@ -3,7 +3,6 @@ package agent import ( "bytes" "crypto/sha256" - "fmt" "github.com/aviate-labs/agent-go/certification/hashtree" "github.com/aviate-labs/agent-go/identity" "github.com/aviate-labs/agent-go/principal" @@ -25,32 +24,6 @@ var ( pathsKey = sha256.Sum256([]byte("paths")) ) -func encodeLEB128(i uint64) []byte { - bi := big.NewInt(int64(i)) - e, _ := leb128.EncodeUnsigned(bi) - return e -} - -func hashOfMap(m map[string]any) ([32]byte, error) { - var hashes [][]byte - for k, v := range m { - if v == nil { - continue - } - - keyHash := sha256.Sum256([]byte(k)) - valueHash, err := hashValue(v) - if err != nil { - return [32]byte{}, err - } - hashes = append(hashes, append(keyHash[:], valueHash[:]...)) - } - sort.Slice(hashes, func(i, j int) bool { - return bytes.Compare(hashes[i], hashes[j]) == -1 - }) - return sha256.Sum256(bytes.Join(hashes, nil)), nil -} - func hashPaths(paths [][]hashtree.Label) [32]byte { var hash []byte for _, path := range paths { @@ -65,49 +38,6 @@ func hashPaths(paths [][]hashtree.Label) [32]byte { return sha256.Sum256(hash) } -func hashValue(v any) ([32]byte, error) { - switch v := v.(type) { - case cbor.RawMessage: - var anyValue any - if err := cbor.Unmarshal(v, &anyValue); err != nil { - panic(err) - } - return hashValue(anyValue) - case string: - return sha256.Sum256([]byte(v)), nil - case []byte: - return sha256.Sum256(v), nil - case int64: - return sha256.Sum256(encodeLEB128(uint64(v))), nil - case uint64: - return sha256.Sum256(encodeLEB128(v)), nil - case map[any]any: // cbor maps are not guaranteed to have string keys - m := make(map[string]any, len(v)) - for k, v := range v { - s, isString := k.(string) - if !isString { - return [32]byte{}, fmt.Errorf("unsupported type %T", k) - } - m[s] = v - } - return hashOfMap(m) - case map[string]any: - return hashOfMap(v) - case []any: - var hashes []byte - for _, v := range v { - valueHash, err := hashValue(v) - if err != nil { - return [32]byte{}, err - } - hashes = append(hashes, valueHash[:]...) - } - return sha256.Sum256(hashes), nil - default: - return [32]byte{}, fmt.Errorf("unsupported type %T", v) - } -} - // Request is the request to the agent. // DOCS: https://smartcontracts.org/docs/interface-spec/index.html#http-call type Request struct { @@ -193,7 +123,9 @@ func NewRequestID(req Request) RequestID { hashes = append(hashes, append(senderKey[:], senderHash[:]...)) } if req.IngressExpiry != 0 { - ingressExpiryHash := sha256.Sum256(encodeLEB128(req.IngressExpiry)) + bi := big.NewInt(int64(req.IngressExpiry)) + e, _ := leb128.EncodeUnsigned(bi) + ingressExpiryHash := sha256.Sum256(e) hashes = append(hashes, append(ingressExpiryKey[:], ingressExpiryHash[:]...)) } if len(req.Nonce) != 0 {