Skip to content

Commit 5cd299f

Browse files
committed
feat(rust/signed-doc): add type for Key ID
* update example + tidy up metadata module
1 parent 13b7ac7 commit 5cd299f

File tree

9 files changed

+286
-8
lines changed

9 files changed

+286
-8
lines changed

rust/signed_doc/examples/mk_signed_doc.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use std::{
66
fs::{read_to_string, File},
77
io::{Read, Write},
88
path::PathBuf,
9+
str::FromStr,
910
};
1011

1112
use clap::Parser;
@@ -14,7 +15,7 @@ use ed25519_dalek::{
1415
ed25519::signature::Signer,
1516
pkcs8::{DecodePrivateKey, DecodePublicKey},
1617
};
17-
use signed_doc::{DocumentRef, Metadata, UuidV7};
18+
use signed_doc::{DocumentRef, Kid, Metadata, UuidV7};
1819

1920
fn main() {
2021
if let Err(err) = Cli::parse().exec() {
@@ -132,9 +133,13 @@ impl Cli {
132133
store_cose_file(cose, &doc)?;
133134
},
134135
Self::Verify { pk, doc, schema } => {
135-
let pk = load_public_key_from_file(&pk)?;
136-
let schema = load_schema_from_file(&schema)?;
137-
let cose = load_cose_from_file(&doc)?;
136+
let pk = load_public_key_from_file(&pk)
137+
.map_err(|e| anyhow::anyhow!("Failed to load public key from file: {e}"))?;
138+
let schema = load_schema_from_file(&schema).map_err(|e| {
139+
anyhow::anyhow!("Failed to load document schema from file: {e}")
140+
})?;
141+
let cose = load_cose_from_file(&doc)
142+
.map_err(|e| anyhow::anyhow!("Failed to load COSE SIGN from file: {e}"))?;
138143
validate_cose(&cose, &pk, &schema)?;
139144
println!("Document is valid.");
140145
},
@@ -294,11 +299,15 @@ fn validate_cose(
294299
validate_json(&json_doc, schema)?;
295300

296301
for sign in &cose.signatures {
302+
let key_id = sign.protected.header.key_id.clone();
297303
anyhow::ensure!(
298-
!sign.protected.header.key_id.is_empty(),
304+
!key_id.is_empty(),
299305
"COSE missing signature protected header `kid` field "
300306
);
301307

308+
let kid_str = String::from_utf8_lossy(&key_id);
309+
let kid = Kid::from_str(&kid_str)?;
310+
println!("Signature Key ID: {kid}");
302311
let data_to_sign = cose.tbs_data(&[], sign);
303312
let signature_bytes = sign.signature.as_slice().try_into().map_err(|_| {
304313
anyhow::anyhow!(
@@ -307,6 +316,11 @@ fn validate_cose(
307316
sign.signature.len()
308317
)
309318
})?;
319+
println!(
320+
"Verifying Key Len({}): 0x{}",
321+
pk.as_bytes().len(),
322+
hex::encode(pk.as_bytes())
323+
);
310324
let signature = ed25519_dalek::Signature::from_bytes(signature_bytes);
311325
pk.verify_strict(&data_to_sign, &signature)?;
312326
}

rust/signed_doc/src/lib.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ use std::{
99
use coset::{CborSerializable, TaggedCborSerializable};
1010

1111
mod metadata;
12+
mod signature;
1213

1314
pub use metadata::{DocumentRef, Metadata, UuidV7};
15+
pub use signature::Kid;
1416

1517
/// Catalyst Signed Document Content Encoding Key.
1618
const CONTENT_ENCODING_KEY: &str = "Content-Encoding";
@@ -30,9 +32,14 @@ impl Display for CatalystSignedDocument {
3032
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
3133
writeln!(f, "{}", self.inner.metadata)?;
3234
writeln!(f, "JSON Payload {:#}\n", self.inner.payload)?;
33-
writeln!(f, "Signatures [")?;
35+
writeln!(f, "Signature Information [")?;
3436
for signature in &self.inner.signatures {
35-
writeln!(f, " 0x{:#}", hex::encode(signature.signature.as_slice()))?;
37+
writeln!(
38+
f,
39+
" {} 0x{:#}",
40+
String::from_utf8_lossy(&signature.protected.header.key_id),
41+
hex::encode(signature.signature.as_slice())
42+
)?;
3643
}
3744
writeln!(f, "]\n")?;
3845
writeln!(f, "Content Errors [")?;
@@ -52,7 +59,7 @@ struct InnerCatalystSignedDocument {
5259
payload: serde_json::Value,
5360
/// Signatures
5461
signatures: Vec<coset::CoseSignature>,
55-
/// Raw COSE Sign bytes
62+
/// Raw COSE Sign data
5663
cose_sign: coset::CoseSign,
5764
/// Content Errors found when parsing the Document
5865
content_errors: Vec<String>,

rust/signed_doc/src/metadata/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ impl Metadata {
8484
&self.content_errors
8585
}
8686
}
87+
8788
impl Display for Metadata {
8889
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
8990
writeln!(f, "Metadata {{")?;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//! COSE Signature Protected Header `kid` URI Authority.
2+
3+
use std::{
4+
fmt::{Display, Formatter},
5+
str::FromStr,
6+
};
7+
8+
/// URI Authority
9+
#[derive(Debug, Clone)]
10+
pub enum Authority {
11+
/// Cardano Blockchain
12+
Cardano,
13+
/// Midnight Blockchain
14+
Midnight,
15+
}
16+
17+
impl FromStr for Authority {
18+
type Err = anyhow::Error;
19+
20+
fn from_str(s: &str) -> Result<Self, Self::Err> {
21+
match s {
22+
"cardano" => Ok(Authority::Cardano),
23+
"midnight" => Ok(Authority::Midnight),
24+
_ => Err(anyhow::anyhow!("Unknown Authority: {s}")),
25+
}
26+
}
27+
}
28+
29+
impl Display for Authority {
30+
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
31+
let authority = match self {
32+
Self::Cardano => "cardano",
33+
Self::Midnight => "midnight",
34+
};
35+
write!(f, "{authority}")
36+
}
37+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//! COSE Signature Protected Header `kid` Role0 Key Version.
2+
3+
use std::fmt::{Display, Formatter};
4+
5+
/// Version of the Role0 Key.
6+
#[derive(Debug, Clone, PartialEq, Eq)]
7+
pub struct KeyVersion(u16);
8+
9+
impl From<u16> for KeyVersion {
10+
fn from(value: u16) -> Self {
11+
Self(value)
12+
}
13+
}
14+
15+
impl Display for KeyVersion {
16+
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
17+
write!(f, "{}", self.0)
18+
}
19+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
//! COSE Signature Protected Header `kid`.
2+
mod authority;
3+
mod key_version;
4+
mod role;
5+
mod role0_pk;
6+
7+
use std::{
8+
fmt::{Display, Formatter},
9+
str::FromStr,
10+
};
11+
12+
use authority::Authority;
13+
use key_version::KeyVersion;
14+
use role::Role;
15+
use role0_pk::Role0PublicKey;
16+
17+
/// Catalyst Signed Document Key ID
18+
///
19+
/// Key ID associated with a `COSE` Signature that is structured as a Universal Resource
20+
/// Identifier (`URI`).
21+
#[derive(Debug, Clone)]
22+
pub struct Kid {
23+
/// URI Authority
24+
authority: Authority,
25+
/// Role0 Public Key.
26+
role0_public_key: Role0PublicKey,
27+
/// User Role specified for the current document.
28+
role: Role,
29+
/// Role0 Public Key Version
30+
key_version: KeyVersion,
31+
}
32+
33+
impl Kid {
34+
/// URI scheme for Catalyst
35+
const URI_SCHEME_PREFIX: &str = "catalyst_kid://";
36+
}
37+
38+
impl FromStr for Kid {
39+
type Err = anyhow::Error;
40+
41+
fn from_str(s: &str) -> anyhow::Result<Self> {
42+
let Some(uri) = s.strip_prefix(Self::URI_SCHEME_PREFIX) else {
43+
anyhow::bail!("Key ID scheme must be '{}': {s}", Self::URI_SCHEME_PREFIX);
44+
};
45+
46+
let Some((authority_str, key_role_version)) = uri.split_once('/') else {
47+
anyhow::bail!("Key ID must have an authority: {uri}");
48+
};
49+
50+
let authority = Authority::from_str(authority_str)
51+
.map_err(|e| anyhow::anyhow!("Invalid Authority: {authority_str}. {e}"))?;
52+
53+
let Some((role0_key_str, role_version)) = key_role_version.split_once('/') else {
54+
anyhow::bail!("Expected Key ID have an Role0 Key set: {key_role_version}");
55+
};
56+
57+
let role0_public_key = Role0PublicKey::from_str(role0_key_str)
58+
.map_err(|e| anyhow::anyhow!("Invalid Role0 Public Key: {role0_key_str}. {e}"))?;
59+
60+
let Some((role_str, key_version_str)) = role_version.split_once('/') else {
61+
anyhow::bail!("Expected Key ID have a role set");
62+
};
63+
64+
let role = Role::from_str(role_str)
65+
.map_err(|e| anyhow::anyhow!("Invalid Role: {role_str}. {e}"))?;
66+
67+
let key_version: KeyVersion = u16::from_str(key_version_str)
68+
.map_err(|e| anyhow::anyhow!("Invalid Key Version: {key_version_str}. {e}"))?
69+
.into();
70+
71+
Ok(Kid {
72+
authority,
73+
role0_public_key,
74+
role,
75+
key_version,
76+
})
77+
}
78+
}
79+
80+
impl Display for Kid {
81+
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
82+
write!(
83+
f,
84+
"{}{}/{}/{}/{}",
85+
Self::URI_SCHEME_PREFIX,
86+
self.authority,
87+
self.role0_public_key,
88+
self.role,
89+
self.key_version,
90+
)
91+
}
92+
}
93+
94+
#[cfg(test)]
95+
mod tests {
96+
use std::str::FromStr;
97+
98+
use super::Kid;
99+
100+
const KID_STR: &str = "catalyst_kid://cardano/0x0063ce08eccfdd5c93dd5cc9ca959fe669fd762fa816d70438efa90c0a75288c/3/0";
101+
102+
#[test]
103+
fn test_kid_uri_from_str() {
104+
let kid_str = KID_STR;
105+
assert!(Kid::from_str(kid_str).is_ok());
106+
}
107+
108+
#[test]
109+
fn test_kid_uri_from_str_and_back() {
110+
let kid_str = KID_STR;
111+
let kid = Kid::from_str(kid_str).unwrap();
112+
assert_eq!(KID_STR, format!("{kid}"));
113+
}
114+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//! COSE Signature Protected Header `kid` URI Catalyst User Role.
2+
3+
use std::{
4+
fmt::{Display, Formatter},
5+
str::FromStr,
6+
};
7+
8+
/// Project Catalyst User Role associated with the signature.
9+
///
10+
/// <https://github.com/input-output-hk/catalyst-CIPs/blob/x509-catalyst-role-definitions/CIP-XXXX/README.md>
11+
#[repr(u16)]
12+
#[derive(Debug, Copy, Clone)]
13+
pub enum Role {
14+
/// Voter = 0
15+
Zero,
16+
/// Delegated Representative = 1
17+
One,
18+
/// Voter Delegation = 2
19+
Two,
20+
/// Proposer = 3
21+
Three,
22+
}
23+
24+
impl FromStr for Role {
25+
type Err = anyhow::Error;
26+
27+
fn from_str(s: &str) -> Result<Self, Self::Err> {
28+
match s {
29+
"0" => Ok(Role::Zero),
30+
"1" => Ok(Role::One),
31+
"2" => Ok(Role::Two),
32+
"3" => Ok(Role::Three),
33+
_ => Err(anyhow::anyhow!("Unknown Role: {}", s)),
34+
}
35+
}
36+
}
37+
38+
impl Display for Role {
39+
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
40+
write!(f, "{}", *self as u16)
41+
}
42+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//! COSE Signature Protected Header `kid` URI Role0 Public Key.
2+
3+
use std::{
4+
fmt::{Display, Formatter},
5+
str::FromStr,
6+
};
7+
8+
/// Role0 Public Key.
9+
#[derive(Debug, Clone)]
10+
pub struct Role0PublicKey([u8; 32]);
11+
12+
impl FromStr for Role0PublicKey {
13+
type Err = anyhow::Error;
14+
15+
fn from_str(s: &str) -> Result<Self, Self::Err> {
16+
let Some(role0_hex) = s.strip_prefix("0x") else {
17+
anyhow::bail!("Role0 Public Key hex string must start with '0x': {}", s);
18+
};
19+
let role0_key = hex::decode(role0_hex)
20+
.map_err(|e| anyhow::anyhow!("Role0 Public Key is not a valid hex string: {}", e))?;
21+
if role0_key.len() != 32 {
22+
anyhow::bail!(
23+
"Role0 Public Key must have 32 bytes: {role0_hex}, len: {}",
24+
role0_key.len()
25+
);
26+
}
27+
let role0 = role0_key.try_into().map_err(|e| {
28+
anyhow::anyhow!(
29+
"Unable to read Role0 Public Key, this should never happen. Eror: {e:?}"
30+
)
31+
})?;
32+
Ok(Role0PublicKey(role0))
33+
}
34+
}
35+
36+
impl Display for Role0PublicKey {
37+
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
38+
write!(f, "0x{}", hex::encode(self.0))
39+
}
40+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
//! Catalyst Signed Document COSE Signature information.
2+
mod kid;
3+
4+
pub use kid::Kid;

0 commit comments

Comments
 (0)