Skip to content

Commit a13fa1b

Browse files
Introduce Cip0134UriList type
1 parent 0aeadb9 commit a13fa1b

File tree

6 files changed

+270
-165
lines changed

6 files changed

+270
-165
lines changed

rust/rbac-registration/src/cardano/cip509/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use x509_chunks::X509Chunks;
2727
use super::transaction::witness::TxWitness;
2828
use crate::utils::{
2929
decode_helper::{decode_bytes, decode_helper, decode_map_len},
30-
general::{decode_utf8, decremented_index},
30+
general::decremented_index,
3131
hashing::{blake2b_128, blake2b_256},
3232
};
3333

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
//! Utilities for [CIP-134] (Cardano URIs - Address Representation).
2+
//!
3+
//! [CIP-134]: https://github.com/cardano-foundation/CIPs/tree/master/CIP-0134
4+
5+
pub use self::{uri::Cip0134Uri, uri_list::Cip0134UriList};
6+
7+
mod uri;
8+
mod uri_list;

rust/rbac-registration/src/cardano/cip509/utils/cip134.rs renamed to rust/rbac-registration/src/cardano/cip509/utils/cip134/uri.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! Utility functions for CIP-0134 address.
1+
//! An URI in the CIP-0134 format.
22
33
// Ignore URIs that are used in tests and doc-examples.
44
// cSpell:ignoreRegExp web\+cardano:.+
@@ -14,6 +14,7 @@ use pallas::ledger::addresses::Address;
1414
///
1515
/// [proposal]: https://github.com/cardano-foundation/CIPs/pull/888
1616
#[derive(Debug)]
17+
#[allow(clippy::module_name_repetitions)]
1718
pub struct Cip0134Uri {
1819
/// A URI string.
1920
uri: String,
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
//! A list of [`Cip0134Uri`].
2+
3+
use std::sync::Arc;
4+
5+
use anyhow::{anyhow, Result};
6+
use c509_certificate::{
7+
extensions::{alt_name::GeneralNamesOrText, extension::ExtensionValue},
8+
general_names::general_name::{GeneralNameTypeRegistry, GeneralNameValue},
9+
C509ExtensionType,
10+
};
11+
use der_parser::der::parse_der_sequence;
12+
use pallas::ledger::traverse::MultiEraTx;
13+
use tracing::debug;
14+
use x509_cert::der::{oid::db::rfc5912::ID_CE_SUBJECT_ALT_NAME, Decode};
15+
16+
use crate::{
17+
cardano::cip509::{
18+
rbac::{
19+
certs::{C509Cert, X509DerCert},
20+
Cip509RbacMetadata,
21+
},
22+
utils::Cip0134Uri,
23+
validation::URI,
24+
Cip509,
25+
},
26+
utils::general::decode_utf8,
27+
};
28+
29+
/// A list of [`Cip0134Uri`].
30+
///
31+
/// This structure uses [`Arc`] internally, so it is cheap to clone.
32+
#[derive(Debug, Clone)]
33+
#[allow(clippy::module_name_repetitions)]
34+
pub struct Cip0134UriList {
35+
/// An internal list of URIs.
36+
uris: Arc<[Cip0134Uri]>,
37+
}
38+
39+
impl Cip0134UriList {
40+
/// Creates a new `Cip0134UriList` instance from the given `Cip509`.
41+
///
42+
/// # Errors
43+
/// - Unsupported transaction era.
44+
pub fn new(cip509: &Cip509, tx: &MultiEraTx) -> Result<Self> {
45+
if !matches!(tx, MultiEraTx::Conway(_)) {
46+
return Err(anyhow!("Unsupported transaction era ({})", tx.era()));
47+
}
48+
49+
let metadata = &cip509.x509_chunks.0;
50+
let mut uris = process_x509_certificates(metadata);
51+
uris.extend(process_c509_certificates(metadata));
52+
53+
Ok(Self { uris: uris.into() })
54+
}
55+
56+
/// Returns an iterator over the contained Cip0134 URIs.
57+
pub fn iter(&self) -> impl Iterator<Item = &Cip0134Uri> {
58+
self.uris.iter()
59+
}
60+
61+
/// Returns a slice with all URIs in the list.
62+
#[must_use]
63+
pub fn as_slice(&self) -> &[Cip0134Uri] {
64+
&self.uris
65+
}
66+
}
67+
68+
/// Iterates over X509 certificates and extracts CIP-0134 URIs.
69+
fn process_x509_certificates(metadata: &Cip509RbacMetadata) -> Vec<Cip0134Uri> {
70+
let mut result = Vec::new();
71+
72+
for cert in metadata.x509_certs.iter().flatten() {
73+
let X509DerCert::X509Cert(cert) = cert else {
74+
continue;
75+
};
76+
let cert = match x509_cert::Certificate::from_der(cert) {
77+
Ok(cert) => cert,
78+
Err(e) => {
79+
debug!("Failed to decode x509 certificate DER: {e:?}");
80+
continue;
81+
},
82+
};
83+
// Find the "subject alternative name" extension.
84+
let Some(extension) = cert
85+
.tbs_certificate
86+
.extensions
87+
.iter()
88+
.flatten()
89+
.find(|e| e.extn_id == ID_CE_SUBJECT_ALT_NAME)
90+
else {
91+
continue;
92+
};
93+
let der = match parse_der_sequence(extension.extn_value.as_bytes()) {
94+
Ok((_, der)) => der,
95+
Err(e) => {
96+
debug!("Failed to parse DER sequence for Subject Alternative Name ({extension:?}): {e:?}");
97+
continue;
98+
},
99+
};
100+
for data in der.ref_iter() {
101+
if data.header.raw_tag() != Some(&[URI]) {
102+
continue;
103+
}
104+
let content = match data.content.as_slice() {
105+
Ok(c) => c,
106+
Err(e) => {
107+
debug!("Unable to process content for {data:?}: {e:?}");
108+
continue;
109+
},
110+
};
111+
let address = match decode_utf8(content) {
112+
Ok(a) => a,
113+
Err(e) => {
114+
debug!("Failed to decode content of {data:?}: {e:?}");
115+
continue;
116+
},
117+
};
118+
match Cip0134Uri::parse(&address) {
119+
Ok(a) => result.push(a),
120+
Err(e) => {
121+
debug!("Failed to parse CIP-0134 address ({address}): {e:?}");
122+
},
123+
}
124+
}
125+
}
126+
127+
result
128+
}
129+
130+
/// Iterates over C509 certificates and extracts CIP-0134 URIs.
131+
fn process_c509_certificates(metadata: &Cip509RbacMetadata) -> Vec<Cip0134Uri> {
132+
let mut result = Vec::new();
133+
134+
for cert in metadata.c509_certs.iter().flatten() {
135+
let cert = match cert {
136+
C509Cert::C509Certificate(c) => c,
137+
C509Cert::C509CertInMetadatumReference(_) => {
138+
debug!("Ignoring unsupported metadatum reference");
139+
continue;
140+
},
141+
_ => continue,
142+
};
143+
144+
for extension in cert.tbs_cert().extensions().extensions() {
145+
if extension.registered_oid().c509_oid().oid()
146+
!= &C509ExtensionType::SubjectAlternativeName.oid()
147+
{
148+
continue;
149+
}
150+
let ExtensionValue::AlternativeName(alt_name) = extension.value() else {
151+
debug!("Unexpected extension value type for {extension:?}");
152+
continue;
153+
};
154+
let GeneralNamesOrText::GeneralNames(gen_names) = alt_name.general_name() else {
155+
debug!("Unexpected general name type: {extension:?}");
156+
continue;
157+
};
158+
for name in gen_names.general_names() {
159+
if *name.gn_type() != GeneralNameTypeRegistry::UniformResourceIdentifier {
160+
continue;
161+
}
162+
let GeneralNameValue::Text(address) = name.gn_value() else {
163+
debug!("Unexpected general name value format: {name:?}");
164+
continue;
165+
};
166+
match Cip0134Uri::parse(address) {
167+
Ok(a) => result.push(a),
168+
Err(e) => {
169+
debug!("Failed to parse CIP-0134 address ({address}): {e:?}");
170+
},
171+
}
172+
}
173+
}
174+
}
175+
176+
result
177+
}
178+
179+
#[cfg(test)]
180+
mod tests {
181+
use minicbor::{Decode, Decoder};
182+
use pallas::{
183+
codec::utils::Nullable,
184+
ledger::{
185+
addresses::{Address, Network},
186+
traverse::MultiEraBlock,
187+
},
188+
};
189+
190+
use super::*;
191+
use crate::cardano::transaction::raw_aux_data::RawAuxData;
192+
193+
// This lint is disabled locally because unfortunately there is no
194+
// `allow-indexing-slicing-in-tests` option. Also it is impossible to use
195+
// `get(n).unwrap()` instead because Clippy will still complain (clippy::get-unwrap).
196+
#[allow(clippy::indexing_slicing)]
197+
#[test]
198+
fn list_new() {
199+
let block =
200+
hex::decode(include_str!("../../../../test_data/cardano/conway_1.block")).unwrap();
201+
let block = MultiEraBlock::decode(&block).unwrap();
202+
let tx = &block.txs()[3];
203+
let cip509 = cip509(tx);
204+
205+
let list = Cip0134UriList::new(&cip509, tx).unwrap();
206+
assert_eq!(list.as_slice().len(), 1);
207+
// cSpell:disable
208+
assert_eq!(
209+
list.as_slice()[0].uri(),
210+
"web+cardano://addr/stake_test1urs8t0ssa3w9wh90ld5tprp3gurxd487rth2qlqk6ernjqcef4ugr"
211+
);
212+
// cSpell:enable
213+
let Address::Stake(address) = list.as_slice()[0].address() else {
214+
panic!("Unexpected address type");
215+
};
216+
assert_eq!(Network::Testnet, address.network());
217+
assert_eq!(
218+
"e075be10ec5c575caffb68b08c31470666d4fe1aeea07c16d6473903",
219+
address.payload().as_hash().to_string()
220+
);
221+
}
222+
223+
fn cip509(tx: &MultiEraTx) -> Cip509 {
224+
let Nullable::Some(data) = tx.as_conway().unwrap().clone().auxiliary_data else {
225+
panic!("Auxiliary data is missing");
226+
};
227+
let data = RawAuxData::new(data.raw_cbor());
228+
let metadata = data.get_metadata(509).unwrap();
229+
230+
let mut decoder = Decoder::new(metadata.as_slice());
231+
Cip509::decode(&mut decoder, &mut ()).unwrap()
232+
}
233+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Utility functions for CIP-509
22
33
pub mod cip19;
4-
pub use cip134::Cip0134Uri;
4+
pub use cip134::{Cip0134Uri, Cip0134UriList};
55

66
mod cip134;

0 commit comments

Comments
 (0)