Skip to content

Commit

Permalink
Merge pull request #14 from tuminfei/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
tuminfei authored Jul 21, 2023
2 parents 2d68cd2 + ce01b68 commit eb442dc
Show file tree
Hide file tree
Showing 15 changed files with 568 additions and 31 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ gemspec

gem 'base32', '~> 0.3.4'
gem 'bitcoin-ruby', '~> 0.0.20'
gem 'bls12-381', '~> 0.3.0'
gem 'byebug', '~> 11.1', '>= 11.1.3'
gem 'cbor', '~> 0.5.9.6'
gem 'ctf-party', '~> 2.3'
Expand Down
26 changes: 16 additions & 10 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
PATH
remote: .
specs:
ic_agent (0.1.4)
ic_agent (0.2.0)
base32 (~> 0.3.4)
bitcoin-ruby (~> 0.0.20)
bls12-381 (~> 0.3.0)
cbor (~> 0.5.9.6)
ctf-party (~> 2.3)
ecdsa (~> 1.2)
Expand All @@ -23,6 +24,8 @@ GEM
eventmachine
ffi
scrypt
bls12-381 (0.3.0)
h2c (~> 0.2.0)
byebug (11.1.3)
cbor (0.5.9.6)
coderay (1.1.3)
Expand All @@ -34,21 +37,23 @@ GEM
ecdsa (1.2.0)
ed25519 (1.3.0)
eventmachine (1.2.7)
faraday (2.7.4)
faraday (2.7.10)
faraday-net_http (>= 2.0, < 3.1)
ruby2_keywords (>= 0.0.4)
faraday-net_http (3.0.2)
ffi (1.15.5)
ffi-compiler (1.0.1)
ffi (>= 1.0.0)
rake
i18n (1.12.0)
h2c (0.2.0)
ecdsa (~> 1.2.0)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
json (2.6.3)
leb128 (1.0.0)
method_source (1.0.0)
mini_portile2 (2.8.2)
pkg-config (1.5.1)
mini_portile2 (2.8.4)
pkg-config (1.5.2)
polyglot (0.3.5)
pry (0.14.2)
coderay (~> 1.1)
Expand All @@ -62,19 +67,19 @@ GEM
rspec-core (~> 3.12.0)
rspec-expectations (~> 3.12.0)
rspec-mocks (~> 3.12.0)
rspec-core (3.12.1)
rspec-core (3.12.2)
rspec-support (~> 3.12.0)
rspec-expectations (3.12.2)
rspec-expectations (3.12.3)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-mocks (3.12.5)
rspec-mocks (3.12.6)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-support (3.12.0)
rspec-support (3.12.1)
ruby-enum (0.9.0)
i18n
ruby2_keywords (0.0.5)
rubytree (2.0.0)
rubytree (2.0.2)
json (~> 2.0, > 2.3.1)
rubyzip (2.3.2)
scrypt (3.0.7)
Expand All @@ -88,6 +93,7 @@ PLATFORMS
DEPENDENCIES
base32 (~> 0.3.4)
bitcoin-ruby (~> 0.0.20)
bls12-381 (~> 0.3.0)
byebug (~> 11.1, >= 11.1.3)
cbor (~> 0.5.9.6)
ctf-party (~> 2.3)
Expand Down
2 changes: 2 additions & 0 deletions ic_agent.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Gem::Specification.new do |spec|
spec.metadata['homepage_uri'] = spec.homepage
spec.metadata['source_code_uri'] = spec.homepage
spec.metadata['changelog_uri'] = spec.homepage
spec.metadata['documentation_uri'] = 'https://tuminfei.github.io/ic_agent.github.com/'

# Specify which files should be added to the gem when it is released.
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
Expand All @@ -33,6 +34,7 @@ Gem::Specification.new do |spec|
# spec.add_dependency "example-gem", "~> 1.0"
spec.add_dependency 'base32', '~> 0.3.4'
spec.add_dependency 'bitcoin-ruby', '~> 0.0.20'
spec.add_dependency 'bls12-381', '~> 0.3.0'
spec.add_dependency 'cbor', '~> 0.5.9.6'
spec.add_dependency 'ctf-party', '~> 2.3'
spec.add_dependency 'ecdsa', '~> 1.2'
Expand Down
9 changes: 7 additions & 2 deletions lib/ic_agent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,15 @@ module IcAgent
class Error < StandardError; end
class ValueError < StandardError; end
class TypeError < StandardError; end
class AgentError < StandardError; end
class BaseException < StandardError; end

IC_REQUEST_DOMAIN_SEPARATOR = "\x0Aic-request".freeze
IC_ROOT_KEY = "\x4E\x9A\xF9\x9F\x06\x13\x26\x81\xE7\xD2\x55\x2A\x26\x17\x98\x51\xE9\xC3\x79\xB3\xC7\xBE\x88\x27\xB8\x35\x17\xFC\x84\x4E\x4C\x4F".freeze
IC_REQUEST_DOMAIN_SEPARATOR = "\x0Aic-request"
IC_ROOT_KEY = "\x4E\x9A\xF9\x9F\x06\x13\x26\x81\xE7\xD2\x55\x2A\x26\x17\x98\x51\xE9\xC3\x79\xB3\xC7\xBE\x88\x27\xB8\x35\x17\xFC\x84\x4E\x4C\x4F"
IC_PUBKEY_ED_DER_HEAD = '302a300506032b6570032100'
IC_PUBKEY_SECP_DER_HERD = '3056301006072a8648ce3d020106052b8104000a034200'
DEFAULT_POLL_TIMEOUT_SECS = 60
IC_STATE_ROOT_DOMAIN_SEPARATOR = "\ric-state-root".str2hex
BLS_KEY_LENGTH = 96
BLS_DER_PREFIX = '308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7c05030201036100'
end
153 changes: 149 additions & 4 deletions lib/ic_agent/agent.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
require 'cbor'
require 'bls'
require 'ctf_party'
require 'bitcoin'

module IcAgent
class Request
# Signs a request with an identity's signature and encodes it using CBOR.
#
# @param req [Hash] The request to be signed.
# @param iden [Identity] The identity used for signing.
# @return [Array] The request ID and the encoded signed request.
def self.sign_request(req, iden)
req_id = IcAgent::Utils.to_request_id(req)
msg = IcAgent::IC_REQUEST_DOMAIN_SEPARATOR + req_id
Expand All @@ -26,6 +33,13 @@ def self.sign_request(req, iden)
class Agent
attr_accessor :identity, :client, :ingress_expiry, :root_key, :nonce_factory

# Initializes a new IC agent.
#
# @param identity [Identity] The identity associated with the agent.
# @param client [Client] The client used for communication with the IC network.
# @param nonce_factory [NonceFactory] The factory for generating nonces.
# @param ingress_expiry [Integer] The expiration time for ingress requests.
# @param root_key [String] The IC root key used for verification.
def initialize(identity, client, nonce_factory = nil, ingress_expiry = 300, root_key = IcAgent::IC_ROOT_KEY)
@identity = identity
@client = client
Expand All @@ -34,14 +48,25 @@ def initialize(identity, client, nonce_factory = nil, ingress_expiry = 300, root
@nonce_factory = nonce_factory
end

# Retrieves the principal associated with the agent's identity.
#
# @return [Principal] The principal associated with the agent.
def get_principal
@identity.sender
end

# Calculates the expiration date for ingress requests.
#
# @return [Integer] The expiration date in nanoseconds.
def get_expiry_date
((Time.now.to_i + @ingress_expiry) * 10**9).to_i
end

# Sends a query request to a canister and decodes the response using CBOR.
#
# @param canister_id [String] The ID of the target canister.
# @param data [Hash] The data to be sent in the query request.
# @return [Object] The decoded response from the canister.
def query_endpoint(canister_id, data)
ret = @client.query(canister_id, data)
decode_ret = nil
Expand All @@ -54,16 +79,34 @@ def query_endpoint(canister_id, data)
decode_ret
end

# Calls a method on a canister and returns the request ID.
#
# @param canister_id [String] The ID of the target canister.
# @param request_id [String] The ID of the request.
# @param data [Hash] The data to be sent in the call request.
# @return [String] The request ID.
def call_endpoint(canister_id, request_id, data)
@client.call(canister_id, request_id, data)
request_id
end

# Reads the state of a canister.
#
# @param canister_id [String] The ID of the target canister.
# @param data [Hash] The data to be sent in the read state request.
# @return [Object] The response from the canister.
def read_state_endpoint(canister_id, data)
result = @client.read_state(canister_id, data)
result
@client.read_state(canister_id, data)
end

# Sends a raw query request to a canister and handles the response.
#
# @param canister_id [String] The ID of the target canister.
# @param method_name [String] The name of the method to be called.
# @param arg [String] The argument to be passed to the method.
# @param return_type [Object] The expected type of the return value.
# @param effective_canister_id [String] The effective canister ID (optional).
# @return [Object] The decoded response from the canister.
def query_raw(canister_id, method_name, arg, return_type = nil, effective_canister_id = nil)
req_canister_id = canister_id.is_a?(String) ? Principal.from_str(canister_id).bytes : canister_id.bytes
req = {
Expand Down Expand Up @@ -92,6 +135,15 @@ def query_raw(canister_id, method_name, arg, return_type = nil, effective_canist
end
end

# Sends a raw update request to a canister and handles the response.
#
# @param canister_id [String] The ID of the target canister.
# @param method_name [String] The name of the method to be called.
# @param arg [String] The argument to be passed to the method.
# @param return_type [Object] The expected type of the return value.
# @param effective_canister_id [String] The effective canister ID (optional).
# @param kwargs [Hash] Additional keyword arguments.
# @return [Object] The decoded response from the canister.
def update_raw(canister_id, method_name, arg, return_type = nil, effective_canister_id = nil, **kwargs)
req_canister_id = canister_id.is_a?(String) ? Principal.from_str(canister_id).bytes : canister_id.bytes
req = {
Expand Down Expand Up @@ -120,7 +172,13 @@ def update_raw(canister_id, method_name, arg, return_type = nil, effective_canis
end
end

def read_state_raw(canister_id, paths)
# Sends a raw read state request to a canister and handles the response.
#
# @param canister_id [String] The ID of the target canister.
# @param paths [Array] The paths to read from the canister's state.
# @param [TrueClass] bls_verify
# @return [Object] The decoded response from the canister.
def read_state_raw(canister_id, paths, bls_verify = true)
req = {
'request_type' => 'read_state',
'sender' => @identity.sender.bytes,
Expand All @@ -140,16 +198,33 @@ def read_state_raw(canister_id, paths)
rescue StandardError
raise ValueError, "Unable to decode cbor value: #{ret}"
end
CBOR.decode(d.value['certificate'])
cert = CBOR.decode(d.value['certificate'])

if bls_verify
verify(cert, canister_id) ? cert : false
else
cert
end
end

# Retrieves the status and certificate of a request from a canister.
#
# @param canister_id [String] The ID of the target canister.
# @param req_id [String] The ID of the request.
# @return [Array] The status and certificate of the request.
def request_status_raw(canister_id, req_id)
paths = [['request_status', req_id]]
cert = read_state_raw(canister_id, paths)
status = IcAgent::Certificate.lookup(['request_status', req_id, 'status'], cert)
[status, cert]
end

# Polls a canister for the status of a request.
#
# @param canister_id [String] The ID of the target canister.
# @param req_id [String] The ID of the request.
# @param delay [Integer] The delay between each poll attempt (in seconds).
# @param timeout [Integer] The maximum timeout for polling.
def poll(canister_id, req_id, delay = 1, timeout = IcAgent::DEFAULT_POLL_TIMEOUT_SECS)
status = nil
cert = nil
Expand All @@ -172,5 +247,75 @@ def poll(canister_id, req_id, delay = 1, timeout = IcAgent::DEFAULT_POLL_TIMEOUT
[status, _]
end
end

def verify(cert, canister_id)
signature_hex = IcAgent::Certificate.signature(cert).str2hex
tree = IcAgent::Certificate.tree(cert)
delegation = IcAgent::Certificate.delegation(cert)
root_hash = IcAgent::Certificate.reconstruct(tree).str2hex
msg = IcAgent::IC_STATE_ROOT_DOMAIN_SEPARATOR + root_hash
der_key = check_delegation(delegation, canister_id, true)
public_key_hash = extract_der(der_key).str2hex

public_key = BLS::PointG2.from_hex(public_key_hash)
signature = BLS::PointG1.from_hex(signature_hex)
BLS.verify(signature, msg, public_key)
end

def check_delegation(delegation, effective_canister_id, disable_range_check)
return @root_key unless delegation

begin
cert = CBOR.decode(delegation['certificate'])
rescue CBOR::MalformedFormatError => e
raise TypeError, "certificate CBOR::MalformedFormatError: #{delegation['certificate']}"
end

path = ['subnet', delegation['subnet_id'], 'canister_ranges']
canister_range = IcAgent::Certificate.lookup(path, cert)

begin
ranges = []
ranges_json = CBOR.decode(canister_range).values[1]

ranges_json.each do |range_json|
range = {}
range['low'] = Principal.from_hex(range_json[0])
range['high'] = Principal.from_hex(range_json[1])
ranges << range
end

if !disable_range_check && !principal_is_within_ranges(effective_canister_id, ranges)
raise AgentError 'certificate CERTIFICATE_NOT_AUTHORIZED'
end
rescue Exception => e
raise AgentError "certificate INVALID_CBOR_DATA, canister_range: #{canister_range.to_s}"
end

path = ['subnet', delegation['subnet_id'], 'public_key']
IcAgent::Certificate.lookup(path, cert)
end

def principal_is_within_ranges(principal, ranges)
ranges.each do |range|
return true if range['low'].lt_eq(principal) && range['high'].gt_eq(principal)
end
false
end

def extract_der(der_buf)
bls_der_prefix = OpenSSL::BN.from_hex(IcAgent::BLS_DER_PREFIX).to_s(2)
expected_length = bls_der_prefix.bytesize + IcAgent::BLS_KEY_LENGTH
if der_buf.bytesize != expected_length
raise TypeError, "BLS DER-encoded public key must be #{expected_length} bytes long"
end

prefix = der_buf.byteslice(0, bls_der_prefix.bytesize)
if prefix != bls_der_prefix
raise TypeError, "BLS DER-encoded public key is invalid. Expect the following prefix: #{bls_der_prefix}, but get #{prefix}"
end

der_buf.byteslice(bls_der_prefix.bytesize..-1)
end
end
end
Loading

0 comments on commit eb442dc

Please sign in to comment.