Skip to content
Merged
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
8 changes: 7 additions & 1 deletion libs/ur-parse-lib/src/keystone_ur_encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,12 @@ mod tests {
// "PCZS" || batch_version_le || Postcard body.
let response = vec![b'P', b'C', b'Z', b'S', 1, 0, 0, 0, 0];
let request_id = vec![0xaa, 0xbb];
let result = ZcashBatchSigResult::new(request_id.clone(), response.clone());
let firmware_version = [1, 2, 3];
let result = ZcashBatchSigResult::new(
request_id.clone(),
response.clone(),
firmware_version,
);
let cbor: Vec<u8> = result.try_into().unwrap();
let encoded = probe_encode(
&cbor,
Expand All @@ -222,6 +227,7 @@ mod tests {
);
assert_eq!(decoded_result.get_data(), response);
assert_eq!(decoded_result.get_request_id(), request_id);
assert_eq!(decoded_result.get_firmware_version(), &firmware_version);
}

#[test]
Expand Down
226 changes: 184 additions & 42 deletions libs/ur-registry/src/zcash/zcash_batch_sig_result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,17 @@
//! The payload is the opaque output of
//! `pczt::roles::signer::batch::BatchSignResponse::serialize`. The PCZT crate
//! owns the versioned response encoding and its semantic validation; this
//! registry type only provides the outer UR/CBOR envelope and the request id
//! copied from the corresponding signing request.
//! registry type only provides the outer UR/CBOR envelope, the request id
//! copied from the corresponding signing request, and the signing device's
//! firmware version.
//!
//! ```text
//! zcash-batch-sig-result = {
//! 1: bstr, ; serialized PCZT BatchSignResponse
//! 2: bstr, ; request id
//! 3: bstr .size 3 ; raw [major, minor, build] firmware version
//! }
//! ```

use alloc::{string::ToString, vec::Vec};

Expand All @@ -21,16 +30,26 @@ use super::cbor_helpers::{decode_definite_u8_map, reject_duplicate_key, require_

const DATA: u8 = 1;
const REQUEST_ID: u8 = 2;
const FIRMWARE_VERSION: u8 = 3;

#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ZcashBatchSigResult {
data: Bytes,
request_id: Bytes,
firmware_version: [u8; 3],
}

impl ZcashBatchSigResult {
pub fn new(request_id: Bytes, data: Bytes) -> Self {
Self { data, request_id }
/// Builds a batch signature result with the raw firmware build version.
///
/// Each element is the corresponding major, minor, or build component.
/// Display-only version offsets are not applied to this value.
pub fn new(request_id: Bytes, data: Bytes, firmware_version: [u8; 3]) -> Self {
Self {
data,
request_id,
firmware_version,
}
}

pub fn get_data(&self) -> &[u8] {
Expand All @@ -40,11 +59,15 @@ impl ZcashBatchSigResult {
pub fn get_request_id(&self) -> &[u8] {
&self.request_id
}

pub fn get_firmware_version(&self) -> &[u8; 3] {
&self.firmware_version
}
}

impl MapSize for ZcashBatchSigResult {
fn map_size(&self) -> u64 {
2
3
}
}

Expand All @@ -63,6 +86,8 @@ impl<C> minicbor::Encode<C> for ZcashBatchSigResult {
e.map(self.map_size())?;
e.int(Int::from(DATA))?.bytes(&self.data)?;
e.int(Int::from(REQUEST_ID))?.bytes(&self.request_id)?;
e.int(Int::from(FIRMWARE_VERSION))?
.bytes(&self.firmware_version)?;
Ok(())
}
}
Expand All @@ -88,6 +113,14 @@ impl<'b, C> minicbor::Decode<'b, C> for ZcashBatchSigResult {
match key {
DATA => obj.data = d.bytes()?.to_vec(),
REQUEST_ID => obj.request_id = d.bytes()?.to_vec(),
FIRMWARE_VERSION => {
obj.firmware_version = d.bytes()?.try_into().map_err(|_| {
minicbor::decode::Error::message(
"zcash-batch-sig-result firmware version must be exactly 3 bytes",
)
.at(d.position())
})?
}
_ => d.skip()?,
}
Ok(())
Expand All @@ -100,6 +133,12 @@ impl<'b, C> minicbor::Decode<'b, C> for ZcashBatchSigResult {
d,
"missing zcash-batch-sig-result request id",
)?;
require_key(
&seen_keys,
FIRMWARE_VERSION,
d,
"missing zcash-batch-sig-result firmware version",
)?;
Ok(result)
}
}
Expand Down Expand Up @@ -140,92 +179,194 @@ mod tests {
vec![b'P', b'C', b'Z', b'S', 1, 0, 0, 0, 0]
}

fn firmware_version() -> [u8; 3] {
[1, 2, 3]
}

#[test]
fn round_trip() {
let data = empty_batch_response();
let request_id = vec![0xaa, 0xbb];
let result = ZcashBatchSigResult::new(request_id.clone(), data.clone());
let firmware_version = firmware_version();
let result = ZcashBatchSigResult::new(request_id.clone(), data.clone(), firmware_version);

let encoded: Vec<u8> = result.try_into().unwrap();
let decoded = ZcashBatchSigResult::try_from(encoded).unwrap();

assert_eq!(decoded.get_data(), data);
assert_eq!(decoded.get_request_id(), request_id);
assert_eq!(decoded.get_firmware_version(), &firmware_version);
}

#[test]
fn wire_encoding_is_stable() {
let encoded: Vec<u8> = ZcashBatchSigResult::new(vec![0xaa, 0xbb], empty_batch_response())
.try_into()
.unwrap();
let encoded: Vec<u8> =
ZcashBatchSigResult::new(vec![0xaa, 0xbb], empty_batch_response(), firmware_version())
.try_into()
.unwrap();

assert_eq!(hex::encode(encoded), "a2014950435a5301000000000242aabb");
assert_eq!(
hex::encode(encoded),
"a3014950435a5301000000000242aabb0343010203"
);
}

#[test]
fn preserves_empty_data() {
let encoded: Vec<u8> = ZcashBatchSigResult::new(vec![0xaa, 0xbb], vec![])
.try_into()
.unwrap();
let encoded: Vec<u8> =
ZcashBatchSigResult::new(vec![0xaa, 0xbb], vec![], firmware_version())
.try_into()
.unwrap();
let decoded = ZcashBatchSigResult::try_from(encoded).unwrap();

assert!(decoded.get_data().is_empty());
}

#[test]
fn preserves_empty_request_id() {
let encoded: Vec<u8> = ZcashBatchSigResult::new(vec![], empty_batch_response())
.try_into()
.unwrap();
let encoded: Vec<u8> =
ZcashBatchSigResult::new(vec![], empty_batch_response(), firmware_version())
.try_into()
.unwrap();
let decoded = ZcashBatchSigResult::try_from(encoded).unwrap();

assert!(decoded.get_request_id().is_empty());
}

#[test]
fn rejects_missing_data() {
let err = ZcashBatchSigResult::try_from(vec![0xa1, REQUEST_ID, 0x40]).unwrap_err();

assert!(
err.to_string()
.contains("missing zcash-batch-sig-result data")
);
let err = ZcashBatchSigResult::try_from(vec![
0xa2,
REQUEST_ID,
0x40,
FIRMWARE_VERSION,
0x43,
1,
2,
3,
])
.unwrap_err();

assert!(err
.to_string()
.contains("missing zcash-batch-sig-result data"));
}

#[test]
fn rejects_missing_request_id() {
let err = ZcashBatchSigResult::try_from(vec![0xa1, DATA, 0x40]).unwrap_err();
let err =
ZcashBatchSigResult::try_from(vec![0xa2, DATA, 0x40, FIRMWARE_VERSION, 0x43, 1, 2, 3])
.unwrap_err();

assert!(
err.to_string()
.contains("missing zcash-batch-sig-result request id")
);
assert!(err
.to_string()
.contains("missing zcash-batch-sig-result request id"));
}

#[test]
fn rejects_duplicate_keys() {
let err = ZcashBatchSigResult::try_from(vec![0xa2, 0x01, 0x40, 0x01, 0x40]).unwrap_err();
fn rejects_missing_firmware_version() {
let err =
ZcashBatchSigResult::try_from(vec![0xa2, DATA, 0x40, REQUEST_ID, 0x40]).unwrap_err();

assert!(
err.to_string()
.contains("duplicate key in zcash-batch-sig-result map")
);
assert!(err
.to_string()
.contains("missing zcash-batch-sig-result firmware version"));
}

#[test]
fn rejects_indefinite_map() {
let err = ZcashBatchSigResult::try_from(vec![0xbf, DATA, 0x40, REQUEST_ID, 0x40, 0xff])
.unwrap_err();
fn rejects_short_firmware_version() {
let err = ZcashBatchSigResult::try_from(vec![
0xa3,
DATA,
0x40,
REQUEST_ID,
0x40,
FIRMWARE_VERSION,
0x42,
1,
2,
])
.unwrap_err();

assert!(err
.to_string()
.contains("firmware version must be exactly 3 bytes"));
}

assert!(
err.to_string()
.contains("indefinite zcash-batch-sig-result map is unsupported")
);
#[test]
fn rejects_long_firmware_version() {
let err = ZcashBatchSigResult::try_from(vec![
0xa3,
DATA,
0x40,
REQUEST_ID,
0x40,
FIRMWARE_VERSION,
0x44,
1,
2,
3,
4,
])
.unwrap_err();

assert!(err
.to_string()
.contains("firmware version must be exactly 3 bytes"));
}

#[test]
fn rejects_duplicate_firmware_version() {
let err = ZcashBatchSigResult::try_from(vec![
0xa4,
DATA,
0x40,
REQUEST_ID,
0x40,
FIRMWARE_VERSION,
0x43,
1,
2,
3,
FIRMWARE_VERSION,
0x43,
1,
2,
3,
])
.unwrap_err();

assert!(err
.to_string()
.contains("duplicate key in zcash-batch-sig-result map"));
}

#[test]
fn rejects_indefinite_map() {
let err = ZcashBatchSigResult::try_from(vec![
0xbf,
DATA,
0x40,
REQUEST_ID,
0x40,
FIRMWARE_VERSION,
0x43,
1,
2,
3,
0xff,
])
.unwrap_err();

assert!(err
.to_string()
.contains("indefinite zcash-batch-sig-result map is unsupported"));
}

#[test]
fn rejects_trailing_data() {
let result = ZcashBatchSigResult::new(vec![0xaa, 0xbb], empty_batch_response());
let result =
ZcashBatchSigResult::new(vec![0xaa, 0xbb], empty_batch_response(), firmware_version());
let mut encoded: Vec<u8> = result.try_into().unwrap();
encoded.push(0);

Expand All @@ -236,12 +377,13 @@ mod tests {

#[test]
fn skips_unknown_keys() {
let encoded = hex::decode("a3014950435a5301000000000242aabb0982010a").unwrap();
let encoded = hex::decode("a4014950435a5301000000000242aabb03430102030982010a").unwrap();

let decoded = ZcashBatchSigResult::try_from(encoded).unwrap();

assert_eq!(decoded.get_data(), empty_batch_response());
assert_eq!(decoded.get_request_id(), &[0xaa, 0xbb]);
assert_eq!(decoded.get_firmware_version(), &[1, 2, 3]);
}

#[test]
Expand Down
Loading