Skip to content

Commit f611f93

Browse files
committed
Add verify func for II delegations.
1 parent 786e084 commit f611f93

File tree

12 files changed

+579
-143
lines changed

12 files changed

+579
-143
lines changed

certification/certificate.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ func VerifyCertifiedData(
139139
return err
140140
}
141141
if !bytes.Equal(certificateCertifiedData, certifiedData) {
142-
return fmt.Errorf("certified data does not match")
142+
return fmt.Errorf("certified data does not match: %x != %x", certificateCertifiedData, certifiedData)
143143
}
144144
return nil
145145
}

certification/hash.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package certification
2+
3+
import (
4+
"bytes"
5+
"crypto/sha256"
6+
"fmt"
7+
"math/big"
8+
"sort"
9+
10+
"github.com/aviate-labs/leb128"
11+
"github.com/fxamacker/cbor/v2"
12+
)
13+
14+
// HashAny computes the hash of any value.
15+
func HashAny(v any) ([32]byte, error) {
16+
switch v := v.(type) {
17+
case cbor.RawMessage:
18+
var anyValue any
19+
if err := cbor.Unmarshal(v, &anyValue); err != nil {
20+
panic(err)
21+
}
22+
return HashAny(anyValue)
23+
case string:
24+
return sha256.Sum256([]byte(v)), nil
25+
case []byte:
26+
return sha256.Sum256(v), nil
27+
case int64:
28+
bi := big.NewInt(int64(v))
29+
e, err := leb128.EncodeUnsigned(bi)
30+
if err != nil {
31+
return [32]byte{}, err
32+
}
33+
return sha256.Sum256(e), nil
34+
case uint64:
35+
bi := big.NewInt(int64(v))
36+
e, err := leb128.EncodeUnsigned(bi)
37+
if err != nil {
38+
return [32]byte{}, err
39+
}
40+
return sha256.Sum256(e), nil
41+
case map[any]any: // cbor maps are not guaranteed to have string keys
42+
kv := make([]KeyValuePair, len(v))
43+
i := 0
44+
for k, v := range v {
45+
s, isString := k.(string)
46+
if !isString {
47+
return [32]byte{}, fmt.Errorf("unsupported type %T", k)
48+
}
49+
kv[i] = KeyValuePair{Key: s, Value: v}
50+
i++
51+
}
52+
return RepresentationIndependentHash(kv)
53+
case map[string]any:
54+
m := make([]KeyValuePair, len(v))
55+
i := 0
56+
for k, v := range v {
57+
m[i] = KeyValuePair{Key: k, Value: v}
58+
i++
59+
}
60+
return RepresentationIndependentHash(m)
61+
case []any:
62+
var hashes []byte
63+
for _, v := range v {
64+
valueHash, err := HashAny(v)
65+
if err != nil {
66+
return [32]byte{}, err
67+
}
68+
hashes = append(hashes, valueHash[:]...)
69+
}
70+
return sha256.Sum256(hashes), nil
71+
default:
72+
return [32]byte{}, fmt.Errorf("unsupported type %T", v)
73+
}
74+
}
75+
76+
// RepresentationIndependentHash computes the hash of a map in a representation-independent way.
77+
// https://internetcomputer.org/docs/current/references/ic-interface-spec/#hash-of-map
78+
func RepresentationIndependentHash(m []KeyValuePair) ([32]byte, error) {
79+
var hashes [][]byte
80+
for _, kv := range m {
81+
if kv.Value == nil {
82+
continue
83+
}
84+
85+
keyHash := sha256.Sum256([]byte(kv.Key))
86+
valueHash, err := HashAny(kv.Value)
87+
if err != nil {
88+
return [32]byte{}, err
89+
}
90+
hashes = append(hashes, append(keyHash[:], valueHash[:]...))
91+
}
92+
sort.Slice(hashes, func(i, j int) bool {
93+
return bytes.Compare(hashes[i], hashes[j]) == -1
94+
})
95+
return sha256.Sum256(bytes.Join(hashes, nil)), nil
96+
}
97+
98+
type KeyValuePair struct {
99+
Key string
100+
Value any
101+
}

certification/hash_test.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package certification
2+
3+
import (
4+
"bytes"
5+
"encoding/hex"
6+
"testing"
7+
)
8+
9+
func TestHashAny(t *testing.T) {
10+
for _, test := range []struct {
11+
name string
12+
v any
13+
want string
14+
}{
15+
{
16+
name: "array",
17+
v: []any{"a"},
18+
want: "bf5d3affb73efd2ec6c36ad3112dd933efed63c4e1cbffcfa88e2759c144f2d8",
19+
},
20+
{
21+
name: "array",
22+
v: []any{"a", "b"},
23+
want: "e5a01fee14e0ed5c48714f22180f25ad8365b53f9779f79dc4a3d7e93963f94a",
24+
},
25+
{
26+
name: "array",
27+
v: []any{[]byte{97}, "b"},
28+
want: "e5a01fee14e0ed5c48714f22180f25ad8365b53f9779f79dc4a3d7e93963f94a",
29+
},
30+
{
31+
name: "array of arrays",
32+
v: []any{[]any{"a"}},
33+
want: "eb48bdfa15fc43dbea3aabb1ee847b6e69232c0f0d9705935e50d60cce77877f",
34+
},
35+
{
36+
name: "array of arrays",
37+
v: []any{[]any{"a", "b"}},
38+
want: "029fd80ca2dd66e7c527428fc148e812a9d99a5e41483f28892ef9013eee4a19",
39+
},
40+
{
41+
name: "array of arrays",
42+
v: []any{[]any{"a", "b"}, []byte{97}},
43+
want: "aec3805593d9ec6df50da070597f73507050ce098b5518d0456876701ada7bb7",
44+
},
45+
} {
46+
t.Run(test.name, func(t *testing.T) {
47+
got, err := HashAny(test.v)
48+
if err != nil {
49+
t.Fatal(err)
50+
}
51+
want, err := hex.DecodeString(test.want)
52+
if err != nil {
53+
t.Fatal(err)
54+
}
55+
if !bytes.Equal(got[:], want) {
56+
t.Fatalf("got %x, want %x", got, test.want)
57+
}
58+
})
59+
}
60+
}
61+
62+
func TestRepresentationIndependentHash(t *testing.T) {
63+
for _, test := range []struct {
64+
name string
65+
kv []KeyValuePair
66+
want string
67+
}{
68+
{
69+
name: "key-value map",
70+
kv: []KeyValuePair{
71+
{Key: "name", Value: "foo"},
72+
{Key: "message", Value: "Hello World!"},
73+
{Key: "answer", Value: uint64(42)},
74+
},
75+
want: "b0c6f9191e37dceafdfc47fbfc7e9cc95f21c7b985c2f7ba5855015c2a8f13ac",
76+
},
77+
{
78+
name: "duplicate keys",
79+
kv: []KeyValuePair{
80+
{Key: "name", Value: "foo"},
81+
{Key: "name", Value: "bar"},
82+
{Key: "message", Value: "Hello World!"},
83+
},
84+
want: "435f77c9bdeca5dba4a4b8a34e4f732b4311f1fc252ec6d4e8ee475234b170f9",
85+
},
86+
{
87+
name: "reordered keys",
88+
kv: []KeyValuePair{
89+
{Key: "name", Value: "bar"},
90+
{Key: "message", Value: "Hello World!"},
91+
{Key: "name", Value: "foo"},
92+
},
93+
want: "435f77c9bdeca5dba4a4b8a34e4f732b4311f1fc252ec6d4e8ee475234b170f9",
94+
},
95+
{
96+
name: "bytes",
97+
kv: []KeyValuePair{
98+
{Key: "bytes", Value: []byte{0x01, 0x02, 0x03, 0x04}},
99+
},
100+
want: "546729666d96a712bd94f902a0388e33f9a19a335c35bc3d95b0221a4a574455",
101+
},
102+
} {
103+
t.Run(test.name, func(t *testing.T) {
104+
got, err := RepresentationIndependentHash(test.kv)
105+
if err != nil {
106+
t.Fatal(err)
107+
}
108+
want, err := hex.DecodeString(test.want)
109+
if err != nil {
110+
t.Fatal(err)
111+
}
112+
if !bytes.Equal(got[:], want) {
113+
t.Fatalf("got %x, want %x", got, test.want)
114+
}
115+
})
116+
}
117+
}

certification/ii/canister_sig.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package verify
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"github.com/aviate-labs/agent-go/principal"
7+
)
8+
9+
var (
10+
CanisterSigPublicKeyDERObjectID = []byte{
11+
0x30, 0x0C, 0x06, 0x0A, 0x2B, 0x06, 0x01,
12+
0x04, 0x01, 0x83, 0xB8, 0x43, 0x01, 0x02,
13+
}
14+
CanisterSigPublicKeyPrefixLength = 19
15+
)
16+
17+
type CanisterSigPublicKey struct {
18+
CanisterID principal.Principal
19+
Seed []byte
20+
}
21+
22+
func CanisterSigPublicKeyFromDER(der []byte) (*CanisterSigPublicKey, error) {
23+
if len(der) < 21 {
24+
return nil, fmt.Errorf("DER data is too short")
25+
}
26+
if !bytes.Equal(der[2:len(CanisterSigPublicKeyDERObjectID)+2], CanisterSigPublicKeyDERObjectID) {
27+
return nil, fmt.Errorf("DER data does not match object ID")
28+
}
29+
canisterIDLength := int(der[CanisterSigPublicKeyPrefixLength])
30+
if len(der) < CanisterSigPublicKeyPrefixLength+canisterIDLength {
31+
return nil, fmt.Errorf("DER data is too short")
32+
}
33+
offset := CanisterSigPublicKeyPrefixLength + 1
34+
rawCanisterID := der[offset : offset+canisterIDLength]
35+
offset += canisterIDLength
36+
return &CanisterSigPublicKey{
37+
CanisterID: principal.Principal{Raw: rawCanisterID},
38+
Seed: der[offset:],
39+
}, nil
40+
}
41+
42+
func (s *CanisterSigPublicKey) DER() []byte {
43+
raw := s.Raw()
44+
var der bytes.Buffer
45+
der.WriteByte(0x30)
46+
der.WriteByte(17 + byte(len(raw)))
47+
der.Write(CanisterSigPublicKeyDERObjectID)
48+
der.WriteByte(0x03)
49+
der.WriteByte(1 + byte(len(raw)))
50+
der.WriteByte(0x00)
51+
der.Write(raw)
52+
return der.Bytes()
53+
}
54+
55+
func (s *CanisterSigPublicKey) Raw() []byte {
56+
var raw bytes.Buffer
57+
raw.WriteByte(byte(len(s.CanisterID.Raw)))
58+
raw.Write(s.CanisterID.Raw)
59+
raw.Write(s.Seed)
60+
return raw.Bytes()
61+
}

certification/ii/canister_sig_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package verify
2+
3+
import (
4+
"bytes"
5+
"encoding/hex"
6+
"github.com/aviate-labs/agent-go/principal"
7+
"testing"
8+
)
9+
10+
var (
11+
testCanisterID = principal.MustDecode("rwlgt-iiaaa-aaaaa-aaaaa-cai")
12+
testSeed = []byte{42, 72, 44}
13+
testCanisterSigPublicKeyDER, _ = hex.DecodeString("301f300c060a2b0601040183b8430102030f000a000000000000000001012a482c")
14+
)
15+
16+
func TestCanisterSigPublicKeyFromDER(t *testing.T) {
17+
cspk, err := CanisterSigPublicKeyFromDER(testCanisterSigPublicKeyDER)
18+
if err != nil {
19+
t.Fatal(err)
20+
}
21+
22+
if !bytes.Equal(cspk.CanisterID.Raw, testCanisterID.Raw) {
23+
t.Fatalf("expected %x, got %x", testCanisterID.Raw, cspk.CanisterID.Raw)
24+
}
25+
if !bytes.Equal(cspk.Seed, testSeed) {
26+
t.Fatalf("expected %x, got %x", testSeed, cspk.Seed)
27+
}
28+
}
29+
30+
func TestCanisterSigPublicKey_DER(t *testing.T) {
31+
cspk := CanisterSigPublicKey{
32+
CanisterID: testCanisterID,
33+
Seed: testSeed,
34+
}
35+
36+
der := cspk.DER()
37+
if !bytes.Equal(der, testCanisterSigPublicKeyDER) {
38+
t.Fatalf("expected %x, got %x", testCanisterSigPublicKeyDER, der)
39+
}
40+
}

0 commit comments

Comments
 (0)