Skip to content

Commit

Permalink
Add verify func for II delegations.
Browse files Browse the repository at this point in the history
  • Loading branch information
q-uint committed Jul 2, 2024
1 parent 786e084 commit f611f93
Show file tree
Hide file tree
Showing 12 changed files with 579 additions and 143 deletions.
2 changes: 1 addition & 1 deletion certification/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
101 changes: 101 additions & 0 deletions certification/hash.go
Original file line number Diff line number Diff line change
@@ -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
}
117 changes: 117 additions & 0 deletions certification/hash_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
61 changes: 61 additions & 0 deletions certification/ii/canister_sig.go
Original file line number Diff line number Diff line change
@@ -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()
}
40 changes: 40 additions & 0 deletions certification/ii/canister_sig_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading

0 comments on commit f611f93

Please sign in to comment.