Skip to content
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
30 changes: 30 additions & 0 deletions extmod/foundation-rust/include/foundation.h
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,18 @@ typedef struct {
bool has_passport_firmware_version;
} UR_PassportResponse;

/**
* A Casa `crypto-account` containing both supported registration keys.
*/
typedef struct {
uint32_t master_fingerprint;
uint64_t network;
uint8_t root_key_data[33];
uint8_t root_chain_code[32];
uint8_t casa_key_data[33];
uint8_t casa_chain_code[32];
} UR_CryptoAccount;

/**
* A uniform resource.
*/
Expand All @@ -445,6 +457,10 @@ typedef enum {
* Passport custom `x-passport-response`.
*/
PassportResponse,
/**
* Casa wallet-registration `crypto-account`.
*/
CryptoAccount,
} UR_Value_Tag;

typedef struct {
Expand All @@ -471,6 +487,9 @@ typedef struct {
struct {
UR_PassportResponse passport_response;
};
struct {
UR_CryptoAccount crypto_account;
};
};
} UR_Value;

Expand Down Expand Up @@ -649,6 +668,17 @@ void ur_registry_new_derived_key(UR_Value *value,
const UR_Keypath *origin,
uint32_t parent_fingerprint);

/**
* Create the Casa wallet-registration `crypto-account` UR.
*/
void ur_registry_new_crypto_account(UR_Value *value,
const uint8_t (*root_key_data)[33],
const uint8_t (*root_chain_code)[32],
const uint8_t (*casa_key_data)[33],
const uint8_t (*casa_chain_code)[32],
uint32_t master_fingerprint,
uint64_t network);

/**
* Create a new `psbt` UR.
*/
Expand Down
22 changes: 14 additions & 8 deletions extmod/foundation-rust/src/ur/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,21 +99,27 @@ pub unsafe extern "C" fn ur_encoder_start(
value: &UR_Value,
max_chars: usize,
) {
// SAFETY: The UR_Value can contain some raw pointers which need to be
// accessed in order to convert it to a `ur::registry::BaseValue` which
// is then encoded below, so the pointers lifetime only need to be valid
// for the scope of this function.
let value = unsafe { value.to_value() };

// SAFETY: This code assumes that runs on a single thread.
let message = unsafe { &mut *ptr::addr_of_mut!(UR_ENCODER_MESSAGE) };

message.clear();
let mut e = Encoder::new(Writer(message));
value.encode(&mut e, &mut ()).expect("Couldn't encode UR");
let ur_type = match value {
UR_Value::CryptoAccount(account) => {
account.encode(&mut e, &mut ()).expect("Couldn't encode UR");
crate::ur::registry::UR_CryptoAccount::UR_TYPE
}
_ => {
// SAFETY: Other UR values may contain pointers which remain valid
// for this call, as required by this function's contract.
let value = unsafe { value.to_value() };
value.encode(&mut e, &mut ()).expect("Couldn't encode UR");
value.ur_type()
}
};

encoder.inner.start(
value.ur_type(),
ur_type,
message,
max_fragment_len(UR_MAX_TYPE, usize::MAX, max_chars),
);
Expand Down
163 changes: 163 additions & 0 deletions extmod/foundation-rust/src/ur/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use foundation_urtypes::{
value,
value::Value,
};
use minicbor::{data::Tag, encode::Write, Encode, Encoder};

use uuid::Uuid;

Expand All @@ -38,6 +39,8 @@ pub enum UR_Value {
PassportRequest(UR_PassportRequest),
/// Passport custom `x-passport-response`.
PassportResponse(UR_PassportResponse),
/// Casa wallet-registration `crypto-account`.
CryptoAccount(UR_CryptoAccount),
}

impl UR_Value {
Expand Down Expand Up @@ -90,6 +93,9 @@ impl UR_Value {
Value::Psbt(buf)
}
UR_Value::HDKey(v) => Value::HDKey(v.into()),
UR_Value::CryptoAccount(_) => panic!(
"CryptoAccount is encoded directly. Should be unreachable"
),
UR_Value::PassportRequest(_) => panic!(
"Not implemented as it isn't needed. Should be unreachable"
),
Expand All @@ -98,6 +104,99 @@ impl UR_Value {
}
}

/// A Casa `crypto-account` containing both supported registration keys.
#[derive(Debug)]
#[repr(C)]
pub struct UR_CryptoAccount {
pub master_fingerprint: u32,
pub network: u64,
pub root_key_data: [u8; 33],
pub root_chain_code: [u8; 32],
pub casa_key_data: [u8; 33],
pub casa_chain_code: [u8; 32],
}

impl UR_CryptoAccount {
pub const UR_TYPE: &'static str = "crypto-account";

const TAG_CRYPTO_OUTPUT: Tag = Tag::new(308);
const TAG_SCRIPT_HASH: Tag = Tag::new(400);
const TAG_WITNESS_PUBLIC_KEY_HASH: Tag = Tag::new(404);
const TAG_HDKEY_LEGACY: Tag = Tag::new(303);
const TAG_KEYPATH_LEGACY: Tag = Tag::new(304);
const TAG_COIN_INFO_LEGACY: Tag = Tag::new(305);

fn encode_output<W: Write>(
&self,
e: &mut Encoder<W>,
key_data: &[u8; 33],
chain_code: &[u8; 32],
casa_key: bool,
) -> Result<(), minicbor::encode::Error<W::Error>> {
e.tag(Self::TAG_CRYPTO_OUTPUT)?
.tag(Self::TAG_SCRIPT_HASH)?
.tag(Self::TAG_WITNESS_PUBLIC_KEY_HASH)?
.tag(Self::TAG_HDKEY_LEGACY)?
.map(if casa_key { 5 } else { 4 })?
.u8(3)?
.bytes(key_data)?
.u8(4)?
.bytes(chain_code)?
.u8(5)?
.tag(Self::TAG_COIN_INFO_LEGACY)?;

if self.network == UR_NETWORK_MAINNET as u64 {
e.map(0)?;
} else {
e.map(1)?.u8(2)?.u64(self.network)?;
}

e.u8(6)?.tag(Self::TAG_KEYPATH_LEGACY)?.map(3)?.u8(1)?;
if casa_key {
e.array(2)?.u32(45)?.bool(true)?;
} else {
e.array(0)?;
}
e.u8(2)?
.u32(self.master_fingerprint)?
.u8(3)?
.u8(u8::from(casa_key))?;

if casa_key {
e.u8(8)?.u32(self.master_fingerprint)?;
}

Ok(())
}
}

impl<C> Encode<C> for UR_CryptoAccount {
fn encode<W: Write>(
&self,
e: &mut Encoder<W>,
_ctx: &mut C,
) -> Result<(), minicbor::encode::Error<W::Error>> {
e.map(2)?
.u8(1)?
.u32(self.master_fingerprint)?
.u8(2)?
.array(2)?;
self.encode_output(
e,
&self.root_key_data,
&self.root_chain_code,
false,
)?;
self.encode_output(
e,
&self.casa_key_data,
&self.casa_chain_code,
true,
)?;
Ok(())
}
}

/// A `hdkey`.
#[repr(C)]
pub enum UR_HDKey {
Expand Down Expand Up @@ -445,6 +544,27 @@ pub extern "C" fn ur_registry_new_derived_key(
}));
}

/// Create the Casa wallet-registration `crypto-account` UR.
#[no_mangle]
pub extern "C" fn ur_registry_new_crypto_account(
value: &mut UR_Value,
root_key_data: &[u8; 33],
root_chain_code: &[u8; 32],
casa_key_data: &[u8; 33],
casa_chain_code: &[u8; 32],
master_fingerprint: u32,
network: u64,
) {
*value = UR_Value::CryptoAccount(UR_CryptoAccount {
master_fingerprint,
network,
root_key_data: *root_key_data,
root_chain_code: *root_chain_code,
casa_key_data: *casa_key_data,
casa_chain_code: *casa_chain_code,
});
}

/// Create a new `psbt` UR.
#[no_mangle]
pub extern "C" fn ur_registry_new_psbt(
Expand Down Expand Up @@ -476,3 +596,46 @@ pub extern "C" fn ur_registry_new_passport_response(
has_passport_firmware_version: true,
})
}

#[cfg(test)]
mod tests {
use super::*;
use minicbor::encode::write::Cursor;

#[test]
fn casa_crypto_account_wire_format_is_pinned() {
let account = UR_CryptoAccount {
master_fingerprint: 0x1234_5678,
network: UR_NETWORK_MAINNET as u64,
root_key_data: [2; 33],
root_chain_code: [3; 32],
casa_key_data: [4; 33],
casa_chain_code: [5; 32],
};
let mut output = Cursor::new([0u8; 256]);
account
.encode(&mut Encoder::new(&mut output), &mut ())
.unwrap();

let expected = concat!(
"a2011a123456780282d90134d90190d90194d9012fa40358210202020202020202020202020202020202020202",
"020202020202020202020202020458200303030303030303030303030303030303030303030303030303030303",
"03030305d90131a006d90130a30180021a123456780300d90134d90190d90194d9012fa5035821040404040404",
"040404040404040404040404040404040404040404040404040404045820050505050505050505050505050505",
"050505050505050505050505050505050505d90131a006d90130a30182182df5021a123456780301081a123456",
"78",
);
let encoded = &output.get_ref()[..output.position()];
assert_eq!(encoded.len() * 2, expected.len());
for (actual, expected) in
encoded.iter().zip(expected.as_bytes().chunks_exact(2))
{
let nibble = |byte| match byte {
b'0'..=b'9' => byte - b'0',
b'a'..=b'f' => byte - b'a' + 10,
_ => unreachable!(),
};
assert_eq!(*actual, nibble(expected[0]) << 4 | nibble(expected[1]));
}
}
}
65 changes: 65 additions & 0 deletions extmod/foundation/modfoundation-ur.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ STATIC void mod_foundation_ur_Value_print(const mp_print_t *print,
case HDKey:
mp_print_str(print, "UR_Value::HDKey");
break;
case CryptoAccount:
mp_print_str(print, "UR_Value::CryptoAccount");
break;
case Psbt:
mp_print_str(print, "UR_Value::Psbt");
break;
Expand Down Expand Up @@ -427,6 +430,67 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_KW(mod_foundation_ur_new_derived_key_obj,
1,
mod_foundation_ur_new_derived_key);

/// def new_crypto_account(root_key_data,
/// root_chain_code,
/// casa_key_data,
/// casa_chain_code,
/// master_fingerprint,
/// network) -> Value:
/// """
/// Create Casa's two-key wallet-registration payload.
/// """
STATIC mp_obj_t mod_foundation_ur_new_crypto_account(size_t n_args,
const mp_obj_t *pos_args,
mp_map_t *kw_args)
{
mp_buffer_info_t root_key_data = {0};
mp_buffer_info_t root_chain_code = {0};
mp_buffer_info_t casa_key_data = {0};
mp_buffer_info_t casa_chain_code = {0};
UR_Value value = {0};

static const mp_arg_t allowed_args[] = {
{ MP_QSTR_root_key_data, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} },
{ MP_QSTR_root_chain_code, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} },
{ MP_QSTR_casa_key_data, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} },
{ MP_QSTR_casa_chain_code, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} },
{ MP_QSTR_master_fingerprint, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} },
{ MP_QSTR_network, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL} },
};

mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
mp_arg_parse_all(n_args, pos_args, kw_args,
MP_ARRAY_SIZE(allowed_args), allowed_args, args);

mp_get_buffer_raise(args[0].u_obj, &root_key_data, MP_BUFFER_READ);
mp_get_buffer_raise(args[1].u_obj, &root_chain_code, MP_BUFFER_READ);
mp_get_buffer_raise(args[2].u_obj, &casa_key_data, MP_BUFFER_READ);
mp_get_buffer_raise(args[3].u_obj, &casa_chain_code, MP_BUFFER_READ);

if (root_key_data.len != 33 || casa_key_data.len != 33) {
mp_raise_msg(&mp_type_ValueError,
MP_ERROR_TEXT("key data should be 33 bytes"));
}
if (root_chain_code.len != 32 || casa_chain_code.len != 32) {
mp_raise_msg(&mp_type_ValueError,
MP_ERROR_TEXT("chain code should be 32 bytes"));
}

ur_registry_new_crypto_account(
&value,
root_key_data.buf,
root_chain_code.buf,
casa_key_data.buf,
casa_chain_code.buf,
mp_obj_int_get_uint_checked(args[4].u_obj),
mp_obj_int_get_uint_checked(args[5].u_obj));

return MP_OBJ_FROM_PTR(mod_foundation_ur_Value_new(&value));
}
STATIC MP_DEFINE_CONST_FUN_OBJ_KW(mod_foundation_ur_new_crypto_account_obj,
4,
mod_foundation_ur_new_crypto_account);

/// def new_psbt(data: bytes) -> Value:
/// """
/// """
Expand Down Expand Up @@ -684,6 +748,7 @@ STATIC const mp_rom_map_elem_t mod_foundation_ur_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR_PassportRequest), MP_ROM_PTR(&mod_foundation_ur_PassportRequest_type)},
{MP_ROM_QSTR(MP_QSTR_new_bytes), MP_ROM_PTR(&mod_foundation_ur_new_bytes_obj)},
{MP_ROM_QSTR(MP_QSTR_new_derived_key), MP_ROM_PTR(&mod_foundation_ur_new_derived_key_obj)},
{MP_ROM_QSTR(MP_QSTR_new_crypto_account), MP_ROM_PTR(&mod_foundation_ur_new_crypto_account_obj)},
{MP_ROM_QSTR(MP_QSTR_new_psbt), MP_ROM_PTR(&mod_foundation_ur_new_psbt_obj)},
{MP_ROM_QSTR(MP_QSTR_new_passport_response), MP_ROM_PTR(&mod_foundation_ur_new_passport_response_obj)},

Expand Down
Loading