Skip to content

Commit f51cc5b

Browse files
committed
feat: add canonical_cbor_into_vec
1 parent bc1c7e7 commit f51cc5b

File tree

11 files changed

+132
-48
lines changed

11 files changed

+132
-48
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ members = ["rs/ic_auth_types", "rs/ic_auth_verifier"]
33
resolver = "2"
44

55
[workspace.package]
6-
version = "0.4.8"
6+
version = "0.5.0"
77
edition = "2024"
88
repository = "https://github.com/ldclabs/ic-auth"
99
keywords = ["auth", "identity", "deeplink", "icp", "canister"]

rs/ic_auth_types/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ license.workspace = true
1212
[dependencies]
1313
base64 = { workspace = true }
1414
candid = { workspace = true }
15+
ciborium = { workspace = true }
1516
serde = { workspace = true }
1617
serde_bytes = { workspace = true }
1718
xid = { workspace = true, optional = true }
1819

1920
[dev-dependencies]
20-
ciborium = { workspace = true }
2121
hex = { workspace = true }
2222
serde_json = { workspace = true }
2323

rs/ic_auth_types/src/bytes.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ impl Display for ByteBufB64 {
148148

149149
impl Debug for ByteBufB64 {
150150
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
151-
write!(f, "ByteBufB64({})", self)
151+
write!(f, "ByteBufB64({self})")
152152
}
153153
}
154154

@@ -160,7 +160,7 @@ impl<const N: usize> Display for ByteArrayB64<N> {
160160

161161
impl<const N: usize> Debug for ByteArrayB64<N> {
162162
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
163-
write!(f, "ByteArrayB64<{}>({})", N, self)
163+
write!(f, "ByteArrayB64<{N}>({self})")
164164
}
165165
}
166166

@@ -470,15 +470,15 @@ mod tests {
470470
b: [1, 2, 3, 4].into(),
471471
};
472472

473-
println!("{:?}", t);
473+
println!("{t:?}");
474474
// Test { a: ByteBufB64(AQIDBA==), b: ByteArrayB64<4>(AQIDBA==) }
475475
assert_eq!(format!("{}", t.a), "AQIDBA==");
476476
assert_eq!(format!("{}", t.b), "AQIDBA==");
477477
assert_eq!(format!("{:?}", t.a), "ByteBufB64(AQIDBA==)");
478478
assert_eq!(format!("{:?}", t.b), "ByteArrayB64<4>(AQIDBA==)");
479479

480480
let data = serde_json::to_string(&t).unwrap();
481-
println!("{}", data);
481+
println!("{data}");
482482
assert_eq!(data, r#"{"a":"AQIDBA==","b":"AQIDBA=="}"#);
483483
let t1: Test = serde_json::from_str(&data).unwrap();
484484
assert_eq!(t, t1);

rs/ic_auth_types/src/cbor.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// https://github.com/ldclabs/ciborium/blob/main/ciborium/src/value/canonical.rs
3+
4+
use ciborium::value::Value;
5+
use core::cmp::Ordering;
6+
use serde::ser;
7+
8+
/// Serializes an object as CBOR into a new Vec<u8>
9+
pub fn cbor_into_vec<T: ?Sized + ser::Serialize>(value: &T) -> Result<Vec<u8>, String> {
10+
let mut data = Vec::new();
11+
ciborium::into_writer(&value, &mut data).map_err(|err| format!("{err:?}"))?;
12+
Ok(data)
13+
}
14+
15+
/// Serializes an object as CBOR into a new Vec<u8> using RFC 8949 Deterministic Encoding.
16+
pub fn canonical_cbor_into_vec<T: ?Sized + ser::Serialize>(value: &T) -> Result<Vec<u8>, String> {
17+
let value = Value::serialized(value).map_err(|err| format!("{err:?}"))?;
18+
19+
let value = canonical_value(value);
20+
let mut data = Vec::new();
21+
ciborium::into_writer(&value, &mut data).map_err(|err| format!("{err:?}"))?;
22+
Ok(data)
23+
}
24+
25+
/// Manually serialize values to compare them.
26+
fn serialized_canonical_cmp(v1: &Value, v2: &Value) -> Ordering {
27+
// There is an optimization to be done here, but it would take a lot more code
28+
// and using mixing keys, Arrays or Maps as CanonicalValue is probably not the
29+
// best use of this type as it is meant mainly to be used as keys.
30+
31+
let mut bytes1 = Vec::new();
32+
let _ = ciborium::into_writer(v1, &mut bytes1);
33+
let mut bytes2 = Vec::new();
34+
let _ = ciborium::into_writer(v2, &mut bytes2);
35+
36+
match bytes1.len().cmp(&bytes2.len()) {
37+
Ordering::Equal => bytes1.cmp(&bytes2),
38+
x => x,
39+
}
40+
}
41+
42+
fn cmp_value(v1: &Value, v2: &Value) -> Ordering {
43+
use Value::*;
44+
45+
match (v1, v2) {
46+
(Integer(i), Integer(o)) => {
47+
// Because of the first rule above, two numbers might be in a different
48+
// order than regular i128 comparison. For example, 10 < -1 in
49+
// canonical ordering, since 10 serializes to `0x0a` and -1 to `0x20`,
50+
// and -1 < -1000 because of their lengths.
51+
i.canonical_cmp(o)
52+
}
53+
(Text(s), Text(o)) => match s.len().cmp(&o.len()) {
54+
Ordering::Equal => s.cmp(o),
55+
x => x,
56+
},
57+
(Bool(s), Bool(o)) => s.cmp(o),
58+
(Null, Null) => Ordering::Equal,
59+
(Tag(t, v), Tag(ot, ov)) => match Value::from(*t).partial_cmp(&Value::from(*ot)) {
60+
Some(Ordering::Equal) | None => match v.partial_cmp(ov) {
61+
Some(x) => x,
62+
None => serialized_canonical_cmp(v1, v2),
63+
},
64+
Some(x) => x,
65+
},
66+
(_, _) => serialized_canonical_cmp(v1, v2),
67+
}
68+
}
69+
70+
fn canonical_value(value: Value) -> Value {
71+
match value {
72+
Value::Map(entries) => {
73+
let mut canonical_entries: Vec<(Value, Value)> = entries
74+
.into_iter()
75+
.map(|(k, v)| (canonical_value(k), canonical_value(v)))
76+
.collect();
77+
78+
// Sort entries based on the canonical comparison of their keys.
79+
// cmp_value (defined in this file) implements RFC 8949 key sorting.
80+
canonical_entries.sort_by(|(k1, _), (k2, _)| cmp_value(k1, k2));
81+
82+
Value::Map(canonical_entries)
83+
}
84+
Value::Array(elements) => {
85+
let canonical_elements: Vec<Value> =
86+
elements.into_iter().map(canonical_value).collect();
87+
Value::Array(canonical_elements)
88+
}
89+
Value::Tag(tag, inner_value) => {
90+
// The tag itself is a u64; its representation is handled by the serializer.
91+
// The inner value must be in canonical form.
92+
Value::Tag(tag, Box::new(canonical_value(*inner_value)))
93+
}
94+
// Other Value variants (Integer, Bytes, Text, Bool, Null, Float)
95+
// are considered "canonical" in their structure.
96+
_ => value,
97+
}
98+
}

rs/ic_auth_types/src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ use candid::{CandidType, Principal};
22
use serde::{Deserialize, Serialize};
33

44
mod bytes;
5+
mod cbor;
56
mod xid;
67

78
pub use bytes::*;
9+
pub use cbor::*;
810
pub use xid::*;
9-
1011
/// A delegation from one key to another.
1112
///
1213
/// If key A signs a delegation containing key B, then key B may be used to
@@ -121,7 +122,7 @@ mod tests {
121122
};
122123

123124
let data = serde_json::to_string(&d).unwrap();
124-
println!("{}", data);
125+
println!("{data}");
125126
assert_eq!(
126127
data,
127128
r#"{"pubkey":"AQIDBA==","expiration":99,"targets":["aaaaa-aa"]}"#

rs/ic_auth_types/src/xid.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ impl FromStr for Xid {
6868
}
6969

7070
if let Some(c) = s.chars().find(|&c| !matches!(c, '0'..='9' | 'a'..='v')) {
71-
return Err(format!("Invalid character: {}", c));
71+
return Err(format!("Invalid character: {c}"));
7272
}
7373

7474
let bs = s.as_bytes();
@@ -187,7 +187,7 @@ impl Display for Xid {
187187

188188
impl Debug for Xid {
189189
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190-
write!(f, "Xid({})", self)
190+
write!(f, "Xid({self})")
191191
}
192192
}
193193

@@ -362,7 +362,7 @@ mod tests {
362362
]);
363363
assert_eq!(xid.to_string(), "9m4e2mr0ui3e8a215n4g");
364364

365-
assert_eq!(format!("{:?}", xid), "Xid(9m4e2mr0ui3e8a215n4g)");
365+
assert_eq!(format!("{xid:?}"), "Xid(9m4e2mr0ui3e8a215n4g)");
366366
}
367367

368368
#[test]
@@ -372,7 +372,7 @@ mod tests {
372372
principal: Principal::anonymous(),
373373
};
374374
let data = serde_json::to_string(&t).unwrap();
375-
println!("{}", data);
375+
println!("{data}");
376376
assert_eq!(
377377
data,
378378
r#"{"thread":"00000000000000000000","principal":"2vxsx-fae"}"#

rs/ic_auth_verifier/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ categories.workspace = true
1010
license.workspace = true
1111

1212
[dependencies]
13-
ic_auth_types = { path = "../ic_auth_types", version = "0.4" }
13+
ic_auth_types = { path = "../ic_auth_types", version = "0.5" }
1414
candid = { workspace = true }
1515
ciborium = { workspace = true }
1616
serde = { workspace = true }

rs/ic_auth_verifier/src/asn1.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ fn public_key_bytes(key_part: ASN1Block) -> Result<Vec<u8>, String> {
147147
}
148148
Ok(key_bytes)
149149
} else {
150-
Err(format!("Expected BitString, got {:?}", key_part))
150+
Err(format!("Expected BitString, got {key_part:?}"))
151151
}
152152
}
153153

@@ -159,13 +159,13 @@ mod tests {
159159
fn test_user_public_key_from_der() {
160160
let data = hex::decode("303C300C060A2B0601040183B8430102032C000A0000000000000007010116FB513D360579FA1102D36E3BC8D53FB966F3AC9F717842B2B54C227582D786").unwrap();
161161
let (algo, pk) = user_public_key_from_der(&data).unwrap();
162-
println!("Algorithm: {:?}", algo);
162+
println!("Algorithm: {algo:?}");
163163
assert_eq!(algo, Algorithm::IcCanisterSignature);
164164
assert_eq!(pk.len(), 43);
165165

166166
let data = hex::decode("302A300506032B65700321004258A79844B5BC3089D6467A2CA67DA33EAB4A96ADCD93E72349B75C0A4C5219").unwrap();
167167
let (algo, pk) = user_public_key_from_der(&data).unwrap();
168-
println!("Algorithm: {:?}", algo);
168+
println!("Algorithm: {algo:?}");
169169
assert_eq!(algo, Algorithm::Ed25519);
170170
assert_eq!(pk.len(), 32);
171171
}

rs/ic_auth_verifier/src/deeplink.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
use ciborium::{from_reader, into_writer};
2-
use ic_auth_types::{ByteBufB64, SignedDelegationCompact};
1+
use ciborium::from_reader;
2+
use ic_auth_types::{ByteBufB64, SignedDelegationCompact, canonical_cbor_into_vec};
33
use serde::{Deserialize, Serialize, de::DeserializeOwned};
44
use std::str::FromStr;
55

@@ -73,9 +73,9 @@ where
7373
}
7474

7575
if let Some(payload) = &self.payload {
76-
let mut data = ByteBufB64(Vec::new());
77-
into_writer(payload, &mut data.0).expect("Failed to serialize payload to CBOR");
78-
url.set_fragment(Some(data.to_string().as_str()));
76+
let data =
77+
canonical_cbor_into_vec(payload).expect("Failed to serialize payload to CBOR");
78+
url.set_fragment(Some(ByteBufB64(data).to_string().as_str()));
7979
}
8080

8181
url

rs/ic_auth_verifier/src/envelope.rs

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ use base64::{
33
engine::general_purpose::{URL_SAFE, URL_SAFE_NO_PAD},
44
};
55
use candid::{CandidType, Principal};
6-
use ciborium::{from_reader, into_writer};
6+
use ciborium::from_reader;
77
use http::header::{AUTHORIZATION, HeaderMap, HeaderName};
8-
use ic_auth_types::{ByteBufB64, DelegationCompact, SignedDelegation, SignedDelegationCompact};
8+
use ic_auth_types::{
9+
ByteBufB64, DelegationCompact, SignedDelegation, SignedDelegationCompact,
10+
canonical_cbor_into_vec,
11+
};
912
use ic_canister_sig_creation::delegation_signature_msg;
1013
use serde::{Deserialize, Serialize};
1114

@@ -160,9 +163,7 @@ impl SignedEnvelope {
160163
/// # Returns
161164
/// * `Vec<u8>` - The CBOR-encoded binary representation of the envelope
162165
pub fn to_bytes(&self) -> Vec<u8> {
163-
let mut buf = vec![];
164-
into_writer(self, &mut buf).expect("failed to encode SignedEnvelope");
165-
buf
166+
canonical_cbor_into_vec(&self).expect("failed to encode SignedEnvelope")
166167
}
167168

168169
/// Decodes a SignedEnvelope from its binary representation.
@@ -459,7 +460,7 @@ impl SignedEnvelope {
459460
headers.insert(
460461
&HEADER_IC_AUTH_DELEGATION,
461462
URL_SAFE_NO_PAD
462-
.encode(to_cbor_bytes(&delegations))
463+
.encode(canonical_cbor_into_vec(&delegations)?)
463464
.parse()
464465
.map_err(|err| {
465466
format!("insert {HEADER_IC_AUTH_DELEGATION} header failed: {err}")
@@ -587,22 +588,6 @@ pub fn extract_user(headers: &HeaderMap) -> Principal {
587588
}
588589
}
589590

590-
/// Encodes an object into CBOR binary format.
591-
///
592-
/// # Arguments
593-
/// * `obj` - The object to encode, which must implement the Serialize trait
594-
///
595-
/// # Returns
596-
/// * `Vec<u8>` - The CBOR-encoded binary representation of the object
597-
///
598-
/// # Panics
599-
/// * If encoding fails
600-
pub fn to_cbor_bytes(obj: &impl Serialize) -> Vec<u8> {
601-
let mut buf: Vec<u8> = Vec::new();
602-
into_writer(obj, &mut buf).expect("failed to encode in CBOR format");
603-
buf
604-
}
605-
606591
/// Decodes base64url-encoded data.
607592
///
608593
/// This function handles both padded and unpadded base64url data.

0 commit comments

Comments
 (0)