Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

certificate verify #56

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions ic/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from .identity import *
from .constants import *
from .utils import to_request_id
from .certificate import lookup
from .certificate import Certificate


def sign_request(req, iden):
Expand Down Expand Up @@ -106,26 +106,28 @@ def request_status_raw(self, canister_id, req_id):
['request_status'.encode(), req_id],
]
cert = self.read_state_raw(canister_id, paths)
status = lookup(['request_status'.encode(), req_id, 'status'.encode()], cert)
c = Certificate(cert, self)
c.verify()
status = c.lookup(['request_status'.encode(), req_id, 'status'.encode()])
if (status == None):
return status, cert
return status, c
else:
return status.decode(), cert
return status.decode(), c

def poll(self, canister_id, req_id, delay=1, timeout=float('inf')):
status = None
for _ in wait(delay, timeout):
status, cert = self.request_status_raw(canister_id, req_id)
status, c = self.request_status_raw(canister_id, req_id)
if status == 'replied' or status == 'done' or status == 'rejected':
break

if status == 'replied':
path = ['request_status'.encode(), req_id, 'reply'.encode()]
res = lookup(path, cert)
res = c.lookup(path)
return status, res
elif status == 'rejected':
path = ['request_status'.encode(), req_id, 'reject_message'.encode()]
msg = lookup(path, cert)
msg = c.lookup(path)
return status, msg
else:
return status, _
39 changes: 39 additions & 0 deletions ic/bls.py

Large diffs are not rendered by default.

82 changes: 80 additions & 2 deletions ic/certificate.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@
find_label(l, _) = Unknown
'''
from enum import Enum
from .utils import blsVerify
import leb128
import hashlib
import cbor2

class NodeId(Enum):
Empty = 0
Expand All @@ -58,8 +62,6 @@ class NodeId(Enum):
Leaf = 3
Pruned = 4

def lookup(path, cert):
return lookup_path(path, cert['tree'])

def lookup_path(path, tree):
offset = 0
Expand Down Expand Up @@ -95,6 +97,82 @@ def find_label(l, trees):
if l == p :
return t[2]

def domain_seq(s: str):
return bytes(leb128.u.encode(len(s)) + s.encode())


def reconstruct(tree):
if tree[0] == NodeId.Empty.value:
return hashlib.sha256(domain_seq('ic-hashtree-empty')).digest()
elif tree[0] == NodeId.Pruned.value:
return tree[1]
elif tree[0] == NodeId.Leaf.value:
return hashlib.sha256(domain_seq('ic-hashtree-leaf') + tree[1]).digest()
elif tree[0] == NodeId.Labeled.value:
res = reconstruct(tree[2])
return hashlib.sha256(domain_seq('ic-hashtree-labeled') + tree[1] + res).digest()
elif tree[0] == NodeId.Fork.value:
res1 = reconstruct(tree[1])
res2 = reconstruct(tree[2])
return hashlib.sha256(domain_seq('ic-hashtree-fork') + res1 + res2).digest()
else:
raise "unreachable"

def extract_der(der: bytes):
der_prefix = bytes.fromhex('308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7c05030201036100')
key_len = 96
expectedLength = len(der_prefix) + key_len
if (len(der) != expectedLength):
raise f"BLS DER-encoded public key must be {expectedLength} bytes long"
prefix = der[:len(der_prefix)]
if(prefix != der_prefix):
raise f"BLS DER-encoded public key is invalid. Expect the following prefix: {der_prefix}, but get {prefix}"
return der[len(der_prefix):]


class Certificate(object):
def __init__(self, cert, agent) -> None:
super().__init__()
self.cert = cert
self.agent = agent
self.root_key = None
self.verified = False

def lookup(self, path):
if not self.verified:
raise "Cannot lookup unverified certificate. Call 'verify()' first."
return lookup_path(path, self.cert['tree'])

def verify(self):
rootHash = reconstruct(self.cert.get('tree'))
derKey = self._checkDelegation(self.cert.get('delegation'))
sig = self.cert['signature']
key = extract_der(derKey)
msg = domain_seq('ic-state-root') + rootHash
res = blsVerify(key, sig, msg)
self.verified = res
return res


def _checkDelegation(self, delegation=None):
if delegation is None:
if self.root_key is None:
if self.agent.root_key:
self.root_key = self.agent.root_key
return self.root_key
else:
raise 'Agent does not have a rootKey.'
return self.root_key
cert = Certificate(cbor2.loads(delegation['certificate']), self.agent)
if not (cert.verify()):
raise 'fail to verify delegation certificate'

lookup = cert.lookup(['subnet', delegation['subnet_id'], 'public_key'])
if not lookup:
subnet = hex(delegation['subnet_id'])
raise f'Could not find subnet key for subnet 0x{subnet}'
return lookup

if __name__=='__main__':
tree = [1, [4, b'W\xb4\x1b\x00\xc9x\xc0\xcb\\\xf4\xb6\xa1\xbbE\\\x9fr\xe2\x1a8\xd2bE\x14\x11\xab:\xb5\x1b`\x98\x9d'], [1, [4, b'\xac>_\x80\xeb.$\x9c\x00\xbc\x12\xce&!^\xa8,i\x08\xaeH\x8e\x9ce9\x87\xbahGPo\xe6'], [2, b'time', [3, b'\xd2\xac\xd3\x8a\xfc\xa0\xd0\xe0\x16']]]]
tree2 = [1, [4, b'5J\xe2\x98A\x8d5\xc8\xe6\x94V\xc9\x90\x87\x00\xc9:\xe1\xb3i\x91fS\xc0udD\x19mQ\x1c\x85'], [1, [4, b'\xac>_\x80\xeb.$\x9c\x00\xbc\x12\xce&!^\xa8,i\x08\xaeH\x8e\x9ce9\x87\xbahGPo\xe6'], [2, b'time', [3, b'\xe7\xfc\xcf\x90\x87\x85\xd0\xe0\x16']]]]
Expand Down
22 changes: 16 additions & 6 deletions ic/system_state.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
from .agent import Agent
from .certificate import lookup
from .certificate import Certificate
from .principal import Principal
import leb128
import cbor2

def time(agent: Agent, canister_id: str) -> int:
cert = agent.read_state_raw(canister_id, [["time".encode()]])
timestamp = lookup(["time".encode()], cert)
c = Certificate(cert, agent)
c.verify()
timestamp = c.lookup(["time".encode()])
return leb128.u.decode(timestamp)

def subnet_public_key(agent: Agent, canister_id: str, subnet_id: str) -> str:
path = ["subnet".encode(), Principal.from_str(subnet_id).bytes, "public_key".encode()]
cert = agent.read_state_raw(canister_id, [path])
pubkey = lookup(path, cert)
c = Certificate(cert, agent)
c.verify()
pubkey = c.lookup(path)
return pubkey.hex()

def subnet_canister_ranges(agent: Agent, canister_id: str, subnet_id: str) -> list[list[Principal]]:
path = ["subnet".encode(), Principal.from_str(subnet_id).bytes, "canister_ranges".encode()]
cert = agent.read_state_raw(canister_id, [path])
ranges = lookup(path, cert)
c = Certificate(cert, agent)
c.verify()
ranges = c.lookup(path)
return list(
map(lambda range:
list(map(lambda item: Principal(bytes=item), range)),
Expand All @@ -28,11 +34,15 @@ def subnet_canister_ranges(agent: Agent, canister_id: str, subnet_id: str) -> li
def canister_module_hash(agent: Agent, canister_id: str) -> str:
path = ["canister".encode(), Principal.from_str(canister_id).bytes, "module_hash".encode()]
cert = agent.read_state_raw(canister_id, [path])
module_hash = lookup(path, cert)
c = Certificate(cert, agent)
c.verify()
module_hash = c.lookup(path)
return module_hash.hex()

def canister_controllers(agent: Agent, canister_id: str) -> list[Principal]:
path = ["canister".encode(), Principal.from_str(canister_id).bytes, "controllers".encode()]
cert = agent.read_state_raw(canister_id, [path])
controllers = lookup(path, cert)
c = Certificate(cert, agent)
c.verify()
controllers = c.lookup(path)
return list(map(lambda item: Principal(bytes=item), cbor2.loads(controllers)))
17 changes: 16 additions & 1 deletion ic/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import re
import leb128
import hashlib
from .bls import bls_init, load, bls_verify

def encode_list(l):
ret = b''
Expand Down Expand Up @@ -47,3 +47,18 @@ def to_request_id(d):
vec.append(h_k + h_v)
s = b''.join(sorted(vec))
return hashlib.sha256(s).digest()

verify = None

def blsVerify(
pk,
sig,
msg
):
global verify
if verify == None:
load()
if bls_init() != 0:
raise "Can not initialize BLS"
verify = lambda pk1, sig1, msg1: bls_verify(sig1, msg1, pk1) == 0
return verify(pk, sig, msg)
4 changes: 3 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
'leb128>=1.0.4',
'waiter>=1.2',
'antlr4-python3-runtime==4.9.3',
'mnemonic==0.20'
'mnemonic==0.20',
'wasmer==1.1.0',
'wasmer_compiler_cranelift==1.1.0'
],
py_modules = ['ic'],
package_dir = { 'ic': "ic" },
Expand Down
3 changes: 2 additions & 1 deletion test_readstate.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@
print(ret)

ret = canister_controllers(ag, "sxhuu-qqaaa-aaaai-qbbcq-cai")
print(ret)
print(ret)