Skip to content

Commit 0aeadb9

Browse files
fix(rust/rbac-registration): Fix extraction of Cardano Addresses from a cardano address URI string (#102)
* Fix extraction of Cardano Addresses from a cardano address URI string * Introduce Cip0134Uri type * Update Clippy and rustfmt configs
1 parent 424cc0a commit 0aeadb9

File tree

8 files changed

+205
-110
lines changed

8 files changed

+205
-110
lines changed

rust/Earthfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
VERSION 0.8
22

3-
IMPORT github.com/input-output-hk/catalyst-ci/earthly/rust:v3.2.23 AS rust-ci
3+
IMPORT github.com/input-output-hk/catalyst-ci/earthly/rust:v3.2.25 AS rust-ci
44

55
COPY_SRC:
66
FUNCTION

rust/clippy.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
allow-unwrap-in-tests = true
22
allow-expect-in-tests = true
3+
allow-panic-in-tests = true

rust/rbac-registration/Cargo.toml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,11 @@ workspace = true
2121
hex = "0.4.3"
2222
anyhow = "1.0.89"
2323
strum_macros = "0.26.4"
24-
regex = "1.11.0"
2524
minicbor = { version = "0.25.1", features = ["alloc", "derive", "half"] }
2625
brotli = "7.0.0"
2726
zstd = "0.13.2"
2827
x509-cert = "0.2.5"
2928
der-parser = "9.0.0"
30-
bech32 = "0.11.0"
3129
dashmap = "6.1.0"
3230
blake2b_simd = "1.0.2"
3331
tracing = "0.1.40"
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
//! Utility functions for CIP-0134 address.
2+
3+
// Ignore URIs that are used in tests and doc-examples.
4+
// cSpell:ignoreRegExp web\+cardano:.+
5+
6+
use std::fmt::{Display, Formatter};
7+
8+
use anyhow::{anyhow, Context, Result};
9+
use pallas::ledger::addresses::Address;
10+
11+
/// An URI in the CIP-0134 format.
12+
///
13+
/// See the [proposal] for more details.
14+
///
15+
/// [proposal]: https://github.com/cardano-foundation/CIPs/pull/888
16+
#[derive(Debug)]
17+
pub struct Cip0134Uri {
18+
/// A URI string.
19+
uri: String,
20+
/// An address parsed from the URI.
21+
address: Address,
22+
}
23+
24+
impl Cip0134Uri {
25+
/// Creates a new `Cip0134Uri` instance by parsing the given URI.
26+
///
27+
/// # Errors
28+
/// - Invalid URI.
29+
///
30+
/// # Examples
31+
///
32+
/// ```
33+
/// use rbac_registration::cardano::cip509::utils::Cip0134Uri;
34+
///
35+
/// let uri = "web+cardano://addr/stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw";
36+
/// let cip0134_uri = Cip0134Uri::parse(uri).unwrap();
37+
/// ```
38+
pub fn parse(uri: &str) -> Result<Self> {
39+
let bech32 = uri
40+
.strip_prefix("web+cardano://addr/")
41+
.ok_or_else(|| anyhow!("Missing schema part of URI"))?;
42+
let address = Address::from_bech32(bech32).context("Unable to parse bech32 part of URI")?;
43+
44+
Ok(Self {
45+
uri: uri.to_owned(),
46+
address,
47+
})
48+
}
49+
50+
/// Returns a URI string.
51+
///
52+
/// # Examples
53+
///
54+
/// ```
55+
/// use rbac_registration::cardano::cip509::utils::Cip0134Uri;
56+
///
57+
/// let uri = "web+cardano://addr/stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw";
58+
/// let cip0134_uri = Cip0134Uri::parse(uri).unwrap();
59+
/// assert_eq!(cip0134_uri.uri(), uri);
60+
#[must_use]
61+
pub fn uri(&self) -> &str {
62+
&self.uri
63+
}
64+
65+
/// Returns a URI string.
66+
///
67+
/// # Examples
68+
///
69+
/// ```
70+
/// use pallas::ledger::addresses::{Address, Network};
71+
/// use rbac_registration::cardano::cip509::utils::Cip0134Uri;
72+
///
73+
/// let uri = "web+cardano://addr/stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw";
74+
/// let cip0134_uri = Cip0134Uri::parse(uri).unwrap();
75+
/// let Address::Stake(address) = cip0134_uri.address() else {
76+
/// panic!("Unexpected address type");
77+
/// };
78+
/// assert_eq!(address.network(), Network::Mainnet);
79+
#[must_use]
80+
pub fn address(&self) -> &Address {
81+
&self.address
82+
}
83+
}
84+
85+
impl Display for Cip0134Uri {
86+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
87+
write!(f, "{}", self.uri())
88+
}
89+
}
90+
91+
#[cfg(test)]
92+
mod tests {
93+
use pallas::ledger::addresses::{Address, Network};
94+
95+
use super::*;
96+
97+
#[test]
98+
fn invalid_prefix() {
99+
// cSpell:disable
100+
let test_uris = [
101+
"addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgse35a3x",
102+
"//addr/addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgse35a3x",
103+
"web+cardano:/addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgse35a3x",
104+
"somthing+unexpected://addr/addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgse35a3x",
105+
];
106+
// cSpell:enable
107+
108+
for uri in test_uris {
109+
let err = format!("{:?}", Cip0134Uri::parse(uri).expect_err(uri));
110+
assert!(err.starts_with("Missing schema part of URI"));
111+
}
112+
}
113+
114+
#[test]
115+
fn invalid_bech32() {
116+
let uri = "web+cardano://addr/adr1qx2fxv2umyh";
117+
let err = format!("{:?}", Cip0134Uri::parse(uri).unwrap_err());
118+
assert!(err.starts_with("Unable to parse bech32 part of URI"));
119+
}
120+
121+
#[test]
122+
fn stake_address() {
123+
let test_data = [
124+
(
125+
"web+cardano://addr/stake_test1uqehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gssrtvn",
126+
Network::Testnet,
127+
"337b62cfff6403a06a3acbc34f8c46003c69fe79a3628cefa9c47251",
128+
),
129+
(
130+
"web+cardano://addr/stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw",
131+
Network::Mainnet,
132+
"337b62cfff6403a06a3acbc34f8c46003c69fe79a3628cefa9c47251",
133+
),
134+
(
135+
"web+cardano://addr/drep_vk17axh4sc9zwkpsft3tlgpjemfwc0u5mnld80r85zw7zdqcst6w54sdv4a4e",
136+
Network::Other(7),
137+
"4d7ac30513ac1825715fd0196769761fca6e7f69de33d04ef09a0c41",
138+
)
139+
];
140+
141+
for (uri, network, payload) in test_data {
142+
let cip0134_uri = Cip0134Uri::parse(uri).expect(uri);
143+
let Address::Stake(address) = cip0134_uri.address() else {
144+
panic!("Unexpected address type ({uri})");
145+
};
146+
assert_eq!(network, address.network());
147+
assert_eq!(payload, address.payload().as_hash().to_string());
148+
}
149+
}
150+
151+
#[test]
152+
fn shelley_address() {
153+
let test_data = [
154+
(
155+
"web+cardano://addr/addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgse35a3x",
156+
Network::Mainnet,
157+
),
158+
(
159+
"web+cardano://addr/addr_test1gz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer5pnz75xxcrdw5vky",
160+
Network::Testnet,
161+
),
162+
(
163+
"web+cardano://addr/cc_hot_vk10y48lq72hypxraew74lwjjn9e2dscuwphckglh2nrrpkgweqk5hschnzv5",
164+
Network::Other(9),
165+
)
166+
];
167+
168+
for (uri, network) in test_data {
169+
let cip0134_uri = Cip0134Uri::parse(uri).expect(uri);
170+
let Address::Shelley(address) = cip0134_uri.address() else {
171+
panic!("Unexpected address type ({uri})");
172+
};
173+
assert_eq!(network, address.network());
174+
}
175+
}
176+
177+
// The Display should return the original URI.
178+
#[test]
179+
fn display() {
180+
let uri = "web+cardano://addr/stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw";
181+
let cip0134_uri = Cip0134Uri::parse(uri).expect(uri);
182+
assert_eq!(uri, cip0134_uri.to_string());
183+
}
184+
}
Lines changed: 0 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,9 @@
11
//! Utility functions for CIP-19 address.
22
33
use anyhow::bail;
4-
use regex::Regex;
54

65
use crate::cardano::transaction::witness::TxWitness;
76

8-
/// Extracts the CIP-19 bytes from a URI.
9-
/// Example input: `web+cardano://addr/<cip-19 address string>`
10-
/// <https://github.com/cardano-foundation/CIPs/tree/6bae5165dde5d803778efa5e93bd408f3317ca03/CPS-0016>
11-
/// URI = scheme ":" ["//" authority] path ["?" query] ["#" fragment]
12-
#[must_use]
13-
pub fn extract_cip19_hash(uri: &str, prefix: Option<&str>) -> Option<Vec<u8>> {
14-
// Regex pattern to match the expected URI format
15-
let r = Regex::new("^.+://addr/(.+)$").ok()?;
16-
17-
// Apply the regex pattern to capture the CIP-19 address string
18-
let address = r
19-
.captures(uri)
20-
.and_then(|cap| cap.get(1).map(|m| m.as_str().to_string()));
21-
22-
match address {
23-
Some(addr) => {
24-
if let Some(prefix) = prefix {
25-
if !addr.starts_with(prefix) {
26-
return None;
27-
}
28-
}
29-
let addr = bech32::decode(&addr).ok()?.1;
30-
// As in CIP19, the first byte is the header, so extract only the payload
31-
extract_key_hash(&addr)
32-
},
33-
None => None,
34-
}
35-
}
36-
377
/// Extract the first 28 bytes from the given key
388
/// Refer to <https://cips.cardano.org/cip/CIP-19> for more information.
399
pub(crate) fn extract_key_hash(key: &[u8]) -> Option<Vec<u8>> {
@@ -67,71 +37,3 @@ pub(crate) fn compare_key_hash(
6737
Ok(())
6838
})
6939
}
70-
71-
#[cfg(test)]
72-
mod tests {
73-
use super::*;
74-
75-
// Test data from https://cips.cardano.org/cip/CIP-19
76-
// cSpell:disable
77-
const STAKE_ADDR: &str = "stake_test1uqehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gssrtvn";
78-
const PAYMENT_ADDR: &str = "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs68faae";
79-
// cSpell:enable
80-
81-
#[test]
82-
fn test_extract_cip19_hash_with_stake() {
83-
// Additional tools to check for bech32 https://slowli.github.io/bech32-buffer/
84-
let uri = &format!("web+cardano://addr/{STAKE_ADDR}");
85-
// Given:
86-
// e0337b62cfff6403a06a3acbc34f8c46003c69fe79a3628cefa9c47251
87-
// The first byte is the header, so extract only the payload
88-
let bytes = hex::decode("337b62cfff6403a06a3acbc34f8c46003c69fe79a3628cefa9c47251")
89-
.expect("Failed to decode bytes");
90-
assert_eq!(
91-
extract_cip19_hash(uri, Some("stake")).expect("Failed to extract CIP-19 hash"),
92-
bytes
93-
);
94-
}
95-
96-
#[test]
97-
fn test_extract_cip19_hash_with_addr_with_prefix_set() {
98-
let uri = &format!("web+cardano://addr/{PAYMENT_ADDR}");
99-
let result = extract_cip19_hash(uri, Some("stake"));
100-
assert_eq!(result, None);
101-
}
102-
103-
#[test]
104-
fn test_extract_cip19_hash_with_addr_without_prefix_set() {
105-
let uri = &format!("web+cardano://addr/{PAYMENT_ADDR}");
106-
let result = extract_cip19_hash(uri, None);
107-
assert!(result.is_some());
108-
}
109-
110-
#[test]
111-
fn test_extract_cip19_hash_invalid_uri() {
112-
let uri = "invalid_uri";
113-
let result = extract_cip19_hash(uri, None);
114-
assert_eq!(result, None);
115-
}
116-
117-
#[test]
118-
fn test_extract_cip19_hash_non_bech32_address() {
119-
let uri = "example://addr/not_bech32";
120-
let result = extract_cip19_hash(uri, None);
121-
assert_eq!(result, None);
122-
}
123-
124-
#[test]
125-
fn test_extract_cip19_hash_empty_uri() {
126-
let uri = "";
127-
let result = extract_cip19_hash(uri, None);
128-
assert_eq!(result, None);
129-
}
130-
131-
#[test]
132-
fn test_extract_cip19_hash_no_address() {
133-
let uri = "example://addr/";
134-
let result = extract_cip19_hash(uri, None);
135-
assert_eq!(result, None);
136-
}
137-
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
//! Utility functions for CIP-509
22
33
pub mod cip19;
4+
pub use cip134::Cip0134Uri;
5+
6+
mod cip134;

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

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use pallas::{
2828
minicbor::{Encode, Encoder},
2929
utils::Bytes,
3030
},
31-
ledger::traverse::MultiEraTx,
31+
ledger::{addresses::Address, traverse::MultiEraTx},
3232
};
3333
use x509_cert::der::{oid::db::rfc5912::ID_CE_SUBJECT_ALT_NAME, Decode};
3434

@@ -38,7 +38,10 @@ use super::{
3838
certs::{C509Cert, X509DerCert},
3939
role_data::{LocalRefInt, RoleData},
4040
},
41-
utils::cip19::{compare_key_hash, extract_cip19_hash, extract_key_hash},
41+
utils::{
42+
cip19::{compare_key_hash, extract_key_hash},
43+
Cip0134Uri,
44+
},
4245
Cip509, TxInputHash, TxWitness,
4346
};
4447
use crate::utils::general::zero_out_last_n_bytes;
@@ -166,10 +169,12 @@ pub(crate) fn validate_stake_public_key(
166169

167170
// Extract the CIP19 hash and push into
168171
// array
169-
if let Some(h) =
170-
extract_cip19_hash(&addr, Some("stake"))
171-
{
172-
pk_addrs.push(h);
172+
if let Ok(uri) = Cip0134Uri::parse(&addr) {
173+
if let Address::Stake(a) = uri.address() {
174+
pk_addrs.push(
175+
a.payload().as_hash().to_vec(),
176+
);
177+
}
173178
}
174179
},
175180
Err(e) => {
@@ -218,9 +223,11 @@ pub(crate) fn validate_stake_public_key(
218223
if name.gn_type() == &c509_certificate::general_names::general_name::GeneralNameTypeRegistry::UniformResourceIdentifier {
219224
match name.gn_value() {
220225
GeneralNameValue::Text(s) => {
221-
if let Some(h) = extract_cip19_hash(s, Some("stake")) {
222-
pk_addrs.push(h);
226+
if let Ok(uri) = Cip0134Uri::parse(s) {
227+
if let Address::Stake(a) = uri.address() {
228+
pk_addrs.push(a.payload().as_hash().to_vec());
223229
}
230+
}
224231
},
225232
_ => {
226233
validation_report.push(

rust/rustfmt.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ max_width = 100
3636

3737
# Comments:
3838
normalize_comments = true
39-
normalize_doc_attributes = true
39+
normalize_doc_attributes = false
4040
wrap_comments = true
4141
comment_width = 90 # small excess is okay but prefer 80
4242
format_code_in_doc_comments = true

0 commit comments

Comments
 (0)