Skip to content

Commit

Permalink
Add node list endpoint.
Browse files Browse the repository at this point in the history
  • Loading branch information
q-uint committed May 21, 2024
1 parent bc4c645 commit 785acb8
Show file tree
Hide file tree
Showing 19 changed files with 2,804 additions and 2,093 deletions.
9 changes: 6 additions & 3 deletions agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,9 @@ func (a Agent) QueryProto(canisterID principal.Principal, methodName string, in,
if err != nil {
return err
}
if len(payload) == 0 {
payload = []byte{}
}
_, data, err := a.sign(Request{
Type: RequestTypeQuery,
Sender: a.Sender(),
Expand Down Expand Up @@ -393,11 +396,11 @@ func (a Agent) RequestStatus(ecID principal.Principal, requestID RequestID) ([]b
if err := cbor.Unmarshal(c, &state); err != nil {
return nil, nil, err
}
cert, err := certification.New(ecID, a.rootKey[len(a.rootKey)-96:], c)
if err != nil {
var certificate certification.Certificate
if err := cbor.Unmarshal(c, &certificate); err != nil {
return nil, nil, err
}
if err := cert.Verify(); err != nil {
if err := certification.VerifyCertificate(certificate, ecID, a.rootKey); err != nil {
return nil, nil, err
}
node, err := hashtree.DeserializeNode(state["tree"].([]any))
Expand Down
218 changes: 149 additions & 69 deletions certification/certificate.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package certification

import (
"bytes"
"encoding/asn1"
"fmt"
"slices"

Expand All @@ -11,103 +13,170 @@ import (
"github.com/fxamacker/cbor/v2"
)

// Cert is a certificate gets returned by the IC.
type Cert struct {
// Tree is the certificate tree.
Tree hashtree.HashTree `cbor:"tree"`
// Signature is the signature of the certificate tree.
Signature []byte `cbor:"signature"`
// Delegation is the delegation of the certificate.
Delegation *Delegation `cbor:"delegation"`
func PublicKeyFromDER(der []byte) (*bls.PublicKey, error) {
var seq asn1.RawValue
if _, err := asn1.Unmarshal(der, &seq); err != nil {
return nil, err
}
if seq.Tag != asn1.TagSequence {
return nil, fmt.Errorf("invalid tag: %d", seq.Tag)
}
var idSeq asn1.RawValue
rest, err := asn1.Unmarshal(seq.Bytes, &idSeq)
if err != nil {
return nil, err
}
var bs asn1.BitString
if _, err := asn1.Unmarshal(rest, &bs); err != nil {
return nil, err
}
if bs.BitLength != 96*8 {
return nil, fmt.Errorf("invalid bit string length: %d", bs.BitLength)
}
var algoId asn1.ObjectIdentifier
seqRest, err := asn1.Unmarshal(idSeq.Bytes, &algoId)
if err != nil {
return nil, err
}
if !algoId.Equal(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 44668, 5, 3, 1, 2, 1}) {
return nil, fmt.Errorf("invalid algorithm identifier: %v", algoId)
}
var curveID asn1.ObjectIdentifier
if _, err := asn1.Unmarshal(seqRest, &curveID); err != nil {
return nil, err
}
if !curveID.Equal(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 44668, 5, 3, 2, 1}) {
return nil, fmt.Errorf("invalid curve identifier: %v", curveID)
}
return bls.PublicKeyFromBytes(bs.Bytes)
}

// Certificate is a certificate that gets returned by the IC and can be used to verify the state root based on the root
// key and canister ID.
type Certificate struct {
Cert Cert
RootKey []byte
CanisterID principal.Principal
func PublicKeyToDER(publicKey []byte) ([]byte, error) {
if len(publicKey) != 96 {
return nil, fmt.Errorf("invalid public key length: %d", len(publicKey))
}
return asn1.Marshal([]any{
[]any{
asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 44668, 5, 3, 1, 2, 1}, // algorithm identifier
asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 44668, 5, 3, 2, 1}, // curve identifier
},
asn1.BitString{
Bytes: publicKey,
BitLength: len(publicKey) * 8,
},
})
}

// New creates a new certificate.
func New(canisterID principal.Principal, rootKey []byte, certificate []byte) (*Certificate, error) {
var cert Cert
if err := cbor.Unmarshal(certificate, &cert); err != nil {
return nil, err
func VerifyCertificate(
certificate Certificate,
canisterID principal.Principal,
rootPublicKey []byte,
) error {
publicKey, err := PublicKeyFromDER(rootPublicKey)
if err != nil {
return err
}
key := publicKey
if certificate.Delegation != nil {
delegation := certificate.Delegation
k, err := verifyDelegationCertificate(
delegation.Certificate,
delegation.SubnetId,
publicKey,
canisterID,
)
if err != nil {
return err
}
key = k
}
return &Certificate{
Cert: cert,
RootKey: rootKey,
CanisterID: canisterID,
}, nil
return verifyCertificateSignature(certificate, key)
}

// Verify verifies the certificate.
func (c Certificate) Verify() error {
signature, err := bls.SignatureFromBytes(c.Cert.Signature)
if err != nil {
func VerifyCertifiedData(
certificate Certificate,
canisterID principal.Principal,
rootPublicKey []byte,
certifiedData []byte,
) error {
if err := VerifyCertificate(certificate, canisterID, rootPublicKey); err != nil {
return err
}
publicKey, err := c.getPublicKey()
certificateCertifiedData, err := certificate.Tree.Lookup(
hashtree.Label("canister"),
canisterID.Raw,
hashtree.Label("certified_data"),
)
if err != nil {
return err
}
rootHash := c.Cert.Tree.Digest()
if !bytes.Equal(certificateCertifiedData, certifiedData) {
return fmt.Errorf("certified data does not match")
}
return nil
}

func verifyCertificateSignature(certificate Certificate, publicKey *bls.PublicKey) error {
rootHash := certificate.Tree.Digest()
message := append(hashtree.DomainSeparator("ic-state-root"), rootHash[:]...)
if !signature.Verify(publicKey, string(message)) {
signature, err := bls.SignatureFromBytes(certificate.Signature)
if err != nil {
return err
}
if !signature.VerifyByte(publicKey, message) {
return fmt.Errorf("signature verification failed")
}
return nil
}

// getPublicKey checks the delegation and returns the public key.
func (c Certificate) getPublicKey() (*bls.PublicKey, error) {
if c.Cert.Delegation == nil {
return bls.PublicKeyFromBytes(c.RootKey)
func verifyDelegationCertificate(
certificate Certificate,
subnetID principal.Principal,
rootPublicKey *bls.PublicKey,
canisterID principal.Principal,
) (*bls.PublicKey, error) {
if certificate.Delegation != nil {
return nil, fmt.Errorf("multiple delegations are not supported")
}
if err := verifyCertificateSignature(certificate, rootPublicKey); err != nil {
return nil, err
}

cert := c.Cert.Delegation
canisterRanges, err := cert.Certificate.Cert.Tree.Lookup(
hashtree.Label("subnet"), cert.SubnetId.Raw, hashtree.Label("canister_ranges"),
rawRanges, err := certificate.Tree.Lookup(
hashtree.Label("subnet"),
subnetID.Raw,
hashtree.Label("canister_ranges"),
)
if err != nil {
return nil, fmt.Errorf("no canister ranges found for subnet %s: %w", cert.SubnetId, err)
}
var rawRanges [][][]byte
if err := cbor.Unmarshal(canisterRanges, &rawRanges); err != nil {
return nil, err
}

var inRange bool
for _, pair := range rawRanges {
if len(pair) != 2 {
return nil, fmt.Errorf("invalid range: %v", pair)
}
if slices.Compare(pair[0], c.CanisterID.Raw) <= 0 && slices.Compare(c.CanisterID.Raw, pair[1]) <= 0 {
inRange = true
break
}
var canisterRanges canisterRanges
if err := cbor.Unmarshal(rawRanges, &canisterRanges); err != nil {
return nil, err
}
if !inRange {
return nil, fmt.Errorf("canister %s is not in range", c.CanisterID)
if !canisterRanges.InRange(canisterID) {
return nil, fmt.Errorf("canister %s is not in range", canisterID)
}

publicKey, err := cert.Certificate.Cert.Tree.Lookup(
hashtree.Label("subnet"), cert.SubnetId.Raw, hashtree.Label("public_key"),
rawPublicKey, err := certificate.Tree.Lookup(
hashtree.Label("subnet"),
subnetID.Raw,
hashtree.Label("public_key"),
)
if err != nil {
return nil, fmt.Errorf("no public key found for subnet %s: %w", cert.SubnetId, err)
}

if len(publicKey) != len(derPrefix)+96 {
return nil, fmt.Errorf("invalid public key length: %d", len(publicKey))
}

if slices.Compare(publicKey[:len(derPrefix)], derPrefix) != 0 {
return nil, fmt.Errorf("invalid public key prefix: %s", publicKey[:len(derPrefix)])
return nil, err
}
return PublicKeyFromDER(rawPublicKey)
}

return bls.PublicKeyFromBytes(publicKey[len(derPrefix):])
// Certificate is a certificate gets returned by the IC.
type Certificate struct {
// Tree is the certificate tree.
Tree hashtree.HashTree `cbor:"tree"`
// Signature is the signature of the certificate tree.
Signature []byte `cbor:"signature"`
// Delegation is the delegation of the certificate.
Delegation *Delegation `cbor:"delegation"`
}

// Delegation is a delegation of a certificate.
Expand All @@ -122,18 +191,18 @@ type Delegation struct {

// UnmarshalCBOR unmarshals a delegation.
func (d *Delegation) UnmarshalCBOR(bytes []byte) error {
var m map[string]any
var m map[string][]byte
if err := cbor.Unmarshal(bytes, &m); err != nil {
return err
}
for k, v := range m {
switch k {
case "subnet_id":
d.SubnetId = principal.Principal{
Raw: v.([]byte),
Raw: v,
}
case "certificate":
if err := cbor.Unmarshal(v.([]byte), &d.Certificate.Cert); err != nil {
if err := cbor.Unmarshal(v, &d.Certificate); err != nil {
return err
}
default:
Expand All @@ -142,3 +211,14 @@ func (d *Delegation) UnmarshalCBOR(bytes []byte) error {
}
return nil
}

type canisterRanges [][][]byte

func (c canisterRanges) InRange(canisterID principal.Principal) bool {
for _, pair := range c {
if slices.Compare(pair[0], canisterID.Raw) <= 0 && slices.Compare(canisterID.Raw, pair[1]) <= 0 {
return true
}
}
return false
}
48 changes: 38 additions & 10 deletions certification/certificate_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package certification_test
package certification

import (
"encoding/hex"
"github.com/aviate-labs/agent-go/certification"
"github.com/aviate-labs/agent-go/principal"
"github.com/fxamacker/cbor/v2"
"testing"
)

Expand All @@ -16,23 +16,51 @@ func TestSampleCert(t *testing.T) {
"00000000002FFFFF0101",
} {
t.Run(s, func(t *testing.T) {
c, err := certification.New(
rawCertificate := hexToBytes(SampleCert)
var certificate Certificate
if err := cbor.Unmarshal(rawCertificate, &certificate); err != nil {
t.Fatal(err)
}
if err := VerifyCertificate(
certificate,
principal.Principal{
Raw: hexToBytes(s),
},
hexToBytes(certification.RootKey),
hexToBytes(SampleCert),
)
if err != nil {
hexToBytes(RootKey),
); err != nil {
t.Fatal(err)
}
if err := c.Verify(); err != nil {
t.Error(err)
}
})
}
}

func TestSubnetDelegateTestVector(t *testing.T) {
rawCertificate, err := hex.DecodeString("D9D9F7A3647472656583018301830183024863616E6973746572830183018301830183024A000000000010000001018301830183024E6365727469666965645F6461746182035820619D02453B55BA8EA01DA1D26DF7083644EBE84A97AFC8D4ECE7F82453548EAA82045820D598630C2C94E80F8EDD451F3B7E942ACE5680B60FB5897A96A466D2F8FCF6F882045820FD96014A4A0368DBD8BD4BD05806C0F8C6BDFBDFBE6F6182B9E4963F18ADD55B8204582072379FD63D4B0A8E7D0C9F87316613DC1ADED56B7311F83213FF213F8B802F1C820458208079C8D69F2C1813E63B2488CD1C7D0BAA73ECE24AEAAD48348D7A1F9B8E868F82045820F7251F6708258AA9E995EE4A923E615908D40E9D4C57DFA84640B77B76D016D0820458205D38A89DDE470A2252C2F4060C42AD6EBCBE5C689CA68241C2858D1149B13386820458200BDF772535F123B48C553D3AE0B6B464277B8E2DDF887CDFE5F54B595352BD658204582055A1F078E350F151DDDE43AEDFBAC8647AC4B392F7141917824EFB1A956B79DB8301820458205C9ABFBA1DFE4D188D30474AF0A1BB796D8EFA15353973A0EEC3427675789C1783024474696D6582034980EFCBB1A5FEF5E616697369676E61747572655830A754B5AE47F254B23420F17E71265CDAF64D34BA002BA88578FDF3CD9B2436FE5A3B7A2245E6DCF4A574169EB72DD4DF6A64656C65676174696F6EA2697375626E65745F6964581DCF9D54E35F653FD0FD4FF05E9C020923B719429C9A3139BEAAF4871B026B6365727469666963617465590199D9D9F7A26474726565830182045820E82F4E336F033E15D337975AF1617E1030F4C6F9F53FCF89A48E861F75C5C98483018302467375626E6574830182045820FB286DDA6CB7FE72AF261F5C896D51CBF82F233679907A1CB7E7299B44F5E23D8302581DCF9D54E35F653FD0FD4FF05E9C020923B719429C9A3139BEAAF4871B02830183024F63616E69737465725F72616E6765738203581BD9D9F781824A000000000010000001014A00000000001FFFFF010183024A7075626C69635F6B657982035885308182301D060D2B0601040182DC7C0503010201060C2B0601040182DC7C0503020103610088EB824FC43459023B08806C56AC224EDEA54AF7A656D96F6E909906B7442AC65A60D2C0B831425B0376674430E48F1D09658CD3F86BDD8607199401422C8B641C43F58740F52B497136E70B62522AEF12A6DB95ECBA58123D44D9B2E852B40883024474696D6582034987A5E7EB83F2E3E416697369676E6174757265583091EC641476446FFA0AB613BE624664BFEC32F7AC20B7E943EA7DACE1B7247101CA5B3CD6DFF38E6276BD6A7AF6C0587F")
if err != nil {
t.Fatal(err)
}
rootPubKey, err := hex.DecodeString("308182301D060D2B0601040182DC7C0503010201060C2B0601040182DC7C05030201036100923A67B791270CD8F5320212AE224377CF407D3A8A2F44F11FED5915A97EE67AD0E90BC382A44A3F14C363AD2006640417B4BBB3A304B97088EC6B4FC87A25558494FC239B47E129260232F79973945253F5036FD520DDABD1E2DE57ABFB40CB")
if err != nil {
t.Fatal(err)
}
certifiedData := []byte{
97, 157, 2, 69, 59, 85, 186, 142, 160, 29, 161, 210, 109, 247, 8, 54, 68, 235, 232, 74,
151, 175, 200, 212, 236, 231, 248, 36, 83, 84, 142, 170,
}
var certificate Certificate
if err := cbor.Unmarshal(rawCertificate, &certificate); err != nil {
t.Fatal(err)
}
if err := VerifyCertifiedData(
certificate,
principal.MustDecode("5v3p4-iyaaa-aaaaa-qaaaa-cai"),
rootPubKey,
certifiedData,
); err != nil {
t.Fatal(err)
}
}

func hexToBytes(s string) []byte {
b, _ := hex.DecodeString(s)
return b
Expand Down
Loading

0 comments on commit 785acb8

Please sign in to comment.