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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
-->

## Head
- Added Unchained as a multisig Connect Wallet option
- Added Coconut Wallet as a single-sig Connect Wallet option
- Improved self-send transaction information formatting (PASS1-638)
- Added the key manager extension, compatible with BIP85 and Nostr (PASS1-24)
Expand Down
20 changes: 18 additions & 2 deletions extmod/foundation-rust/include/foundation.h
Original file line number Diff line number Diff line change
Expand Up @@ -614,13 +614,29 @@ void ur_encoder_start(UR_Encoder *encoder,
const UR_Value *value,
size_t max_chars);

/**
* Start the encoder with an already CBOR-encoded Uniform Resource.
*
* # Safety
*
* `ur_type` and `message` must be valid for reads of their respective
* lengths for the duration of this call. The caller is responsible for
* ensuring that `message` contains well-formed CBOR.
*/
bool ur_encoder_start_raw(UR_Encoder *encoder,
const uint8_t *ur_type,
size_t ur_type_len,
const uint8_t *message,
size_t message_len,
size_t max_chars);

/**
* Returns the UR corresponding to the next fountain encoded part.
*
* # Safety
*
* This function must not be called if `ur_encoder_start` was not called to
* start the encoder. Or if the data used to start the encoder is freed.
* `ur` and `ur_len` must be valid for writes. If the encoder has not been
* started successfully, this function returns an empty string.
*
* # Return Value
*
Expand Down
132 changes: 129 additions & 3 deletions extmod/foundation-rust/src/ur/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

//! Encoder.

use core::{ffi::c_char, fmt::Write, ptr};
use core::{ffi::c_char, fmt::Write, ptr, slice, str};

use foundation_ur::{max_fragment_len, HeaplessEncoder};
use minicbor::{Encode, Encoder};
Expand Down Expand Up @@ -55,6 +55,7 @@ pub const UR_ENCODER_MAX_MESSAGE_LEN: usize = UR_DECODER_MAX_MESSAGE_LEN;
#[cfg_attr(dtcm, link_section = ".dtcm")]
pub static mut UR_ENCODER: UR_Encoder = UR_Encoder {
inner: HeaplessEncoder::new(),
started: false,
};

/// cbindgen:ignore
Expand All @@ -69,6 +70,12 @@ static mut UR_ENCODER_STRING: heapless::Vec<u8, UR_ENCODER_MAX_STRING> =
static mut UR_ENCODER_MESSAGE: heapless::Vec<u8, UR_ENCODER_MAX_MESSAGE_LEN> =
heapless::Vec::new();

/// cbindgen:ignore
#[used]
#[cfg_attr(sram4, link_section = ".sram4")]
static mut UR_ENCODER_TYPE: heapless::String<{ UR_MAX_TYPE.len() }> =
heapless::String::new();

/// Uniform Resource encoder.
pub struct UR_Encoder {
inner: HeaplessEncoder<
Expand All @@ -77,6 +84,7 @@ pub struct UR_Encoder {
UR_ENCODER_MAX_FRAGMENT_LEN,
UR_ENCODER_MAX_SEQUENCE_COUNT,
>,
started: bool,
}

/// Start the encoder.
Expand Down Expand Up @@ -117,14 +125,71 @@ pub unsafe extern "C" fn ur_encoder_start(
message,
max_fragment_len(UR_MAX_TYPE, usize::MAX, max_chars),
);
encoder.started = true;
}

/// Start the encoder with an already CBOR-encoded Uniform Resource.
///
/// # Safety
///
/// `ur_type` and `message` must be valid for reads of their respective
/// lengths for the duration of this call. The caller is responsible for
/// ensuring that `message` contains well-formed CBOR.
#[no_mangle]
pub unsafe extern "C" fn ur_encoder_start_raw(
encoder: &mut UR_Encoder,
ur_type: *const u8,
ur_type_len: usize,
message: *const u8,
message_len: usize,
max_chars: usize,
) -> bool {
// A rejected value must not leave the previously encoded UR available.
encoder.started = false;

let ur_type = unsafe { slice::from_raw_parts(ur_type, ur_type_len) };
let message = unsafe { slice::from_raw_parts(message, message_len) };

let Ok(ur_type) = str::from_utf8(ur_type) else {
return false;
};
if ur_type.is_empty()
|| ur_type.len() > UR_MAX_TYPE.len()
|| !ur_type
.bytes()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == b'-')
{
return false;
}

let encoder_type = unsafe { &mut *ptr::addr_of_mut!(UR_ENCODER_TYPE) };
encoder_type.clear();
if encoder_type.push_str(ur_type).is_err() {
return false;
}

let encoder_message =
unsafe { &mut *ptr::addr_of_mut!(UR_ENCODER_MESSAGE) };
encoder_message.clear();
if encoder_message.extend_from_slice(message).is_err() {
return false;
}

encoder.inner.start(
encoder_type.as_str(),
encoder_message,
max_fragment_len(UR_MAX_TYPE, usize::MAX, max_chars),
);
encoder.started = true;
true
}

/// Returns the UR corresponding to the next fountain encoded part.
///
/// # Safety
///
/// This function must not be called if `ur_encoder_start` was not called to
/// start the encoder. Or if the data used to start the encoder is freed.
/// `ur` and `ur_len` must be valid for writes. If the encoder has not been
/// started successfully, this function returns an empty string.
///
/// # Return Value
///
Expand All @@ -140,6 +205,13 @@ pub unsafe extern "C" fn ur_encoder_next_part(
ur: *mut *const c_char,
ur_len: *mut usize,
) {
if !encoder.started {
static EMPTY: &[u8] = b"\0";
*ur = EMPTY.as_ptr() as *const c_char;
*ur_len = 0;
return;
}

let part = encoder.inner.next_part();

let buf = unsafe { &mut *ptr::addr_of_mut!(UR_ENCODER_STRING) };
Expand All @@ -163,3 +235,57 @@ impl<'a, const N: usize> minicbor::encode::Write for Writer<'a, N> {

#[derive(Debug)]
struct EndOfSlice;

#[cfg(test)]
mod tests {
use super::*;
use foundation_ur::{HeaplessDecoder, UR};

#[test]
fn raw_encoder_preserves_the_ur_type() {
// Encoder FFI storage is process-global, so keep its lifecycle in one test.
let ur_type = b"crypto-hdkey";
let message = b"\xa0";

unsafe {
let encoder = &mut *ptr::addr_of_mut!(UR_ENCODER);
assert!(ur_encoder_start_raw(
encoder,
ur_type.as_ptr(),
ur_type.len(),
message.as_ptr(),
message.len(),
UR_ENCODER_MAX_STRING,
));

let mut encoded = ptr::null();
let mut encoded_len = 0;
ur_encoder_next_part(encoder, &mut encoded, &mut encoded_len);
let encoded =
slice::from_raw_parts(encoded as *const u8, encoded_len);
assert!(encoded.starts_with(b"ur:crypto-hdkey/"));

let encoded = str::from_utf8(encoded).unwrap();
let ur = UR::parse(encoded).unwrap();
let mut decoder: HeaplessDecoder<16, 2, 32, 8, 8, 16> =
HeaplessDecoder::new();
decoder.receive(ur).unwrap();
assert!(decoder.is_complete());
assert_eq!(decoder.message().unwrap().unwrap(), message);

assert!(!ur_encoder_start_raw(
encoder,
b"CRYPTO-HDKEY".as_ptr(),
b"CRYPTO-HDKEY".len(),
message.as_ptr(),
message.len(),
UR_ENCODER_MAX_STRING,
));

let mut encoded = ptr::null();
let mut encoded_len = usize::MAX;
ur_encoder_next_part(encoder, &mut encoded, &mut encoded_len);
assert_eq!(encoded_len, 0);
}
}
}
77 changes: 71 additions & 6 deletions extmod/foundation/modfoundation-ur.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
/// package: foundation.ur

STATIC const mp_obj_type_t mod_foundation_ur_Value_type;
STATIC const mp_obj_type_t mod_foundation_ur_RawValue_type;
STATIC const mp_obj_type_t mod_foundation_ur_CoinType_type;
STATIC const mp_obj_type_t mod_foundation_ur_CoinInfo_type;
STATIC const mp_obj_type_t mod_foundation_ur_Keypath_type;
Expand Down Expand Up @@ -178,6 +179,30 @@ STATIC const mp_obj_type_t mod_foundation_ur_Value_type = {
.locals_dict = (mp_obj_dict_t *)&mod_foundation_ur_Value_locals_dict,
};

/// class RawValue:
/// """
/// A Uniform Resource whose payload is already CBOR encoded.
/// """
typedef struct _mp_obj_RawValue_t {
mp_obj_base_t base;
mp_obj_t ur_type;
mp_obj_t cbor;
} mp_obj_RawValue_t;

STATIC void mod_foundation_ur_RawValue_print(const mp_print_t *print,
mp_obj_t o_in,
mp_print_kind_t kind) {
(void)o_in;
(void)kind;
mp_print_str(print, "UR_RawValue");
}

STATIC const mp_obj_type_t mod_foundation_ur_RawValue_type = {
{ &mp_type_type },
.name = MP_QSTR_RawValue,
.print = mod_foundation_ur_RawValue_print,
};

/// class CoinType:
/// """
/// """
Expand Down Expand Up @@ -347,6 +372,30 @@ STATIC mp_obj_t mod_foundation_ur_new_bytes(mp_obj_t data_in)
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_foundation_ur_new_bytes_obj,
mod_foundation_ur_new_bytes);

/// def new_raw(ur_type: str, cbor: bytes) -> RawValue:
/// """
/// Create a Uniform Resource from an already CBOR-encoded payload.
/// """
STATIC mp_obj_t mod_foundation_ur_new_raw(mp_obj_t ur_type_in,
mp_obj_t cbor_in)
{
if (!mp_obj_is_str(ur_type_in)) {
mp_raise_msg(&mp_type_ValueError,
MP_ERROR_TEXT("ur_type should be a string"));
}

mp_buffer_info_t cbor = {0};
mp_get_buffer_raise(cbor_in, &cbor, MP_BUFFER_READ);

mp_obj_RawValue_t *o = m_new_obj(mp_obj_RawValue_t);
o->base.type = &mod_foundation_ur_RawValue_type;
o->ur_type = ur_type_in;
o->cbor = cbor_in;
return MP_OBJ_FROM_PTR(o);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_foundation_ur_new_raw_obj,
mod_foundation_ur_new_raw);

/// def new_derived_key(key_data=None,
/// is_private=False,
/// chain_code=None,
Expand Down Expand Up @@ -519,15 +568,30 @@ STATIC mp_obj_t mod_foundation_ur_encoder_start(mp_obj_t value_in,
mp_obj_Value_t *value = NULL;
mp_int_t max_fragment_len = 0;

if (!mp_obj_is_type(value_in, &mod_foundation_ur_Value_type)) {
max_fragment_len = mp_obj_int_get_uint_checked(max_fragment_len_in);

if (mp_obj_is_type(value_in, &mod_foundation_ur_Value_type)) {
value = MP_OBJ_TO_PTR(value_in);
ur_encoder_start(&UR_ENCODER, &value->value, max_fragment_len);
} else if (mp_obj_is_type(value_in, &mod_foundation_ur_RawValue_type)) {
mp_obj_RawValue_t *raw = MP_OBJ_TO_PTR(value_in);
mp_buffer_info_t cbor = {0};
GET_STR_DATA_LEN(raw->ur_type, ur_type, ur_type_len);
mp_get_buffer_raise(raw->cbor, &cbor, MP_BUFFER_READ);

if (!ur_encoder_start_raw(&UR_ENCODER,
ur_type,
ur_type_len,
cbor.buf,
cbor.len,
max_fragment_len)) {
mp_raise_msg(&mp_type_ValueError,
MP_ERROR_TEXT("invalid raw Uniform Resource"));
}
} else {
mp_raise_msg(&mp_type_ValueError, MP_ERROR_TEXT("invalid type for value"));
return mp_const_none;
}

value = MP_OBJ_TO_PTR(value_in);
max_fragment_len = mp_obj_int_get_uint_checked(max_fragment_len_in);
ur_encoder_start(&UR_ENCODER, &value->value, max_fragment_len);

return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_foundation_ur_encoder_start_obj,
Expand Down Expand Up @@ -683,6 +747,7 @@ STATIC const mp_rom_map_elem_t mod_foundation_ur_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR_Keypath), MP_ROM_PTR(&mod_foundation_ur_Keypath_type)},
{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_raw), MP_ROM_PTR(&mod_foundation_ur_new_raw_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_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
1 change: 1 addition & 0 deletions ports/stm32/boards/Passport/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@
'wallets/vault.py',
'wallets/keeper.py',
'wallets/theya.py',
'wallets/unchained.py',
'wallets/zeus.py'))

# Extensions
Expand Down
4 changes: 4 additions & 0 deletions ports/stm32/boards/Passport/modules/tests/test_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ def test_ui(test):

def test_foundation(test):
assert test('foundation.py') == b'OK'


def test_unchained(test):
assert test('unchained.py') == b'OK'
7 changes: 7 additions & 0 deletions ports/stm32/boards/Passport/modules/tests/unit/foundation.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ def should_fail(f):
foundation.sha256('this is the message', digest)
assert digest == bytearray(b'\x131qZ\x1f\xfd\x04\xe6`\x04\x93\x1a\x8d\xbc6U\xebJR>\xd5\xece\xecm\x1c\xed\x93x+\xd3\xbd') # nopep8

raw_ur = foundation.ur.new_raw('crypto-hdkey', b'\xa0')
foundation.ur.encoder_start(raw_ur, 535)
assert foundation.ur.encoder_next_part().startswith('ur:crypto-hdkey/')

bad_raw_ur = foundation.ur.new_raw('CRYPTO-HDKEY', b'\xa0')
should_fail(lambda: foundation.ur.encoder_start(bad_raw_ur, 535))

SAMPLE_HOR_RES = 10
SAMPLE_VER_RES = 10
SAMPLE_IMG = [0x04, 0x28, 0x40, 0x01, 0xfb, 0x05, 0xfa, 0x05, 0xfb, 0x05, 0xfa, 0x05,
Expand Down
Loading