• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

input-output-hk / catalyst-libs / 17374551469

01 Sep 2025 10:11AM UTC coverage: 67.889% (+0.04%) from 67.845%
17374551469

Pull #507

github

web-flow
Merge 494484f83 into fdce7e9e2
Pull Request #507: fix(rust/rbac-registration): Validate witness for stake address and payment address URIs in certificate

45 of 78 new or added lines in 4 files covered. (57.69%)

1 existing line in 1 file now uncovered.

13142 of 19358 relevant lines covered (67.89%)

2804.96 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

53.69
/rust/rbac-registration/src/cardano/cip509/utils/cip134_uri_set.rs
1
//! A set of [`Cip0134Uri`].
2

3
use std::{
4
    collections::{HashMap, HashSet},
5
    sync::Arc,
6
};
7

8
use c509_certificate::{
9
    extensions::{alt_name::GeneralNamesOrText, extension::ExtensionValue},
10
    general_names::general_name::{GeneralNameTypeRegistry, GeneralNameValue},
11
    C509ExtensionType,
12
};
13
use cardano_blockchain_types::{pallas_addresses::Address, Cip0134Uri, StakeAddress};
14
use catalyst_types::problem_report::ProblemReport;
15
use der_parser::der::parse_der_sequence;
16
use tracing::debug;
17
use x509_cert::der::oid::db::rfc5912::ID_CE_SUBJECT_ALT_NAME;
18

19
use crate::cardano::cip509::{
20
    rbac::{C509Cert, Cip509RbacMetadata, X509DerCert},
21
    validation::URI,
22
};
23

24
/// A mapping from a certificate index to URIs contained within.
25
type UrisMap = HashMap<usize, Box<[Cip0134Uri]>>;
26

27
/// A set of [`Cip0134Uri`] contained in both x509 and c509 certificates stored in the
28
/// metadata part of [`Cip509`](crate::cardano::cip509::Cip509).
29
///
30
/// This structure uses [`Arc`] internally, so it is cheap to clone.
31
#[derive(Debug, Clone, Eq, PartialEq)]
32
#[allow(clippy::module_name_repetitions)]
33
pub struct Cip0134UriSet(Arc<Cip0134UriSetInner>);
34

35
/// Internal `Cip0134UriSet` data.
36
#[derive(Debug, Clone, Eq, PartialEq)]
37
struct Cip0134UriSetInner {
38
    /// URIs from x509 certificates.
39
    x_uris: UrisMap,
40
    /// URIs from c509 certificates.
41
    c_uris: UrisMap,
42
}
43

44
impl Cip0134UriSet {
45
    /// Creates a new `Cip0134UriSet` instance from the given certificates.
46
    #[must_use]
47
    pub fn new(
15✔
48
        x509_certs: &[X509DerCert],
15✔
49
        c509_certs: &[C509Cert],
15✔
50
        report: &ProblemReport,
15✔
51
    ) -> Self {
15✔
52
        let x_uris = extract_x509_uris(x509_certs, report);
15✔
53
        let c_uris = extract_c509_uris(c509_certs, report);
15✔
54
        Self(Arc::new(Cip0134UriSetInner { x_uris, c_uris }))
15✔
55
    }
15✔
56

57
    /// Returns a mapping from the x509 certificate index to URIs contained within.
58
    #[must_use]
59
    pub fn x_uris(&self) -> &UrisMap {
43✔
60
        &self.0.x_uris
43✔
61
    }
43✔
62

63
    /// Returns a mapping from the c509 certificate index to URIs contained within.
64
    #[must_use]
65
    pub fn c_uris(&self) -> &UrisMap {
41✔
66
        &self.0.c_uris
41✔
67
    }
41✔
68

69
    /// Returns `true` if both x509 and c509 certificate maps are empty.
70
    #[must_use]
71
    pub fn is_empty(&self) -> bool {
1✔
72
        self.x_uris().is_empty() && self.c_uris().is_empty()
1✔
73
    }
1✔
74

75
    /// Returns a list of addresses by the given role.
76
    #[must_use]
77
    pub fn role_addresses(
11✔
78
        &self,
11✔
79
        role: usize,
11✔
80
    ) -> HashSet<Address> {
11✔
81
        let mut result = HashSet::new();
11✔
82

83
        if let Some(uris) = self.x_uris().get(&role) {
11✔
84
            result.extend(uris.iter().map(|uri| uri.address().clone()));
11✔
85
        }
×
86
        if let Some(uris) = self.c_uris().get(&role) {
11✔
NEW
87
            result.extend(uris.iter().map(|uri| uri.address().clone()));
×
88
        }
11✔
89

90
        result
11✔
91
    }
11✔
92

93
    /// Returns a list of stake addresses by the given role.
94
    #[must_use]
95
    pub fn role_stake_addresses(
11✔
96
        &self,
11✔
97
        role: usize,
11✔
98
    ) -> HashSet<StakeAddress> {
11✔
99
        self.role_addresses(role)
11✔
100
            .iter()
11✔
101
            .filter_map(|address| {
11✔
102
                match address {
11✔
103
                    Address::Stake(a) => Some(a.clone().into()),
11✔
NEW
104
                    _ => None,
×
105
                }
106
            })
11✔
107
            .collect()
11✔
108
    }
11✔
109

110
    /// Returns a list of all stake addresses.
111
    #[must_use]
NEW
112
    pub fn stake_addresses(&self) -> HashSet<StakeAddress> {
×
NEW
113
        self.x_uris()
×
NEW
114
            .values()
×
NEW
115
            .chain(self.c_uris().values())
×
NEW
116
            .flat_map(|uris| uris.iter())
×
NEW
117
            .filter_map(|uri| {
×
NEW
118
                match uri.address() {
×
NEW
119
                    Address::Stake(a) => Some(a.clone().into()),
×
NEW
120
                    _ => None,
×
121
                }
NEW
122
            })
×
NEW
123
            .collect()
×
NEW
124
    }
×
125

126
    /// Return the updated URIs set.
127
    ///
128
    /// The resulting set includes all the data from both the original and a new one. In
129
    /// the following example for brevity we only consider one type of uris:
130
    /// ```text
131
    /// // Original data:
132
    /// 0: [uri_1]
133
    /// 1: [uri_2, uri_3]
134
    ///
135
    /// // New data:
136
    /// 0: undefined
137
    /// 1: deleted
138
    /// 2: [uri_4]
139
    ///
140
    /// // Resulting data:
141
    /// 0: [uri_1]
142
    /// 2: [uri_4]
143
    /// ```
144
    #[must_use]
145
    pub fn update(
1✔
146
        self,
1✔
147
        metadata: &Cip509RbacMetadata,
1✔
148
    ) -> Self {
1✔
149
        if self == metadata.certificate_uris {
1✔
150
            // Nothing to update.
151
            return self;
1✔
152
        }
×
153

154
        let Cip0134UriSetInner {
155
            mut x_uris,
×
156
            mut c_uris,
×
157
        } = Arc::unwrap_or_clone(self.0);
×
158

159
        for (index, cert) in metadata.x509_certs.iter().enumerate() {
×
160
            match cert {
×
161
                X509DerCert::Undefined => {
×
162
                    // The certificate wasn't changed - there is nothing to do.
×
163
                },
×
164
                X509DerCert::Deleted => {
×
165
                    x_uris.remove(&index);
×
166
                },
×
167
                X509DerCert::X509Cert(_) => {
168
                    if let Some(uris) = metadata.certificate_uris.x_uris().get(&index) {
×
169
                        x_uris.insert(index, uris.clone());
×
170
                    }
×
171
                },
172
            }
173
        }
174

175
        for (index, cert) in metadata.c509_certs.iter().enumerate() {
×
176
            match cert {
×
177
                C509Cert::Undefined => {
×
178
                    // The certificate wasn't changed - there is nothing to do.
×
179
                },
×
180
                C509Cert::Deleted => {
×
181
                    c_uris.remove(&index);
×
182
                },
×
183
                C509Cert::C509CertInMetadatumReference(_) => {
184
                    debug!("Ignoring unsupported metadatum reference");
×
185
                },
186
                C509Cert::C509Certificate(_) => {
187
                    if let Some(uris) = metadata.certificate_uris.c_uris().get(&index) {
×
188
                        c_uris.insert(index, uris.clone());
×
189
                    }
×
190
                },
191
            }
192
        }
193

194
        Self(Arc::new(Cip0134UriSetInner { x_uris, c_uris }))
×
195
    }
1✔
196
}
197

198
/// Iterates over X509 certificates and extracts CIP-0134 URIs.
199
fn extract_x509_uris(
15✔
200
    certificates: &[X509DerCert],
15✔
201
    report: &ProblemReport,
15✔
202
) -> UrisMap {
15✔
203
    let mut result = UrisMap::new();
15✔
204
    let context = "Extracting URIs from X509 certificates in Cip509 metadata";
15✔
205

206
    for (index, cert) in certificates.iter().enumerate() {
16✔
207
        let X509DerCert::X509Cert(cert) = cert else {
16✔
208
            continue;
1✔
209
        };
210
        // Find the "subject alternative name" extension.
211
        let Some(extension) = cert
15✔
212
            .tbs_certificate
15✔
213
            .extensions
15✔
214
            .iter()
15✔
215
            .flatten()
15✔
216
            .find(|e| e.extn_id == ID_CE_SUBJECT_ALT_NAME)
15✔
217
        else {
218
            continue;
×
219
        };
220
        let Ok((_, der)) = parse_der_sequence(extension.extn_value.as_bytes()) else {
15✔
221
            report.other(
×
222
                &format!(
×
223
                    "Failed to parse DER sequence for Subject Alternative Name ({extension:?})"
×
224
                ),
×
225
                context,
×
226
            );
227
            continue;
×
228
        };
229

230
        let mut uris = Vec::new();
15✔
231
        for data in der.ref_iter() {
71✔
232
            if data.header.raw_tag() != Some(&[URI]) {
71✔
233
                continue;
56✔
234
            }
15✔
235
            let Ok(bytes) = data.content.as_slice() else {
15✔
236
                report.other(&format!("Unable to process content for {data:?}"), context);
×
237
                continue;
×
238
            };
239
            match Cip0134Uri::try_from(bytes) {
15✔
240
                Ok(u) => uris.push(u),
15✔
241
                Err(e) => {
×
242
                    // X.509 doesn't restrict the "alternative name" extension to be utf8 only, so
243
                    // we cannot treat this as error.
244
                    debug!("Ignoring invalid CIP-0134 address: {e:?}");
×
245
                },
246
            }
247
        }
248

249
        if !uris.is_empty() {
15✔
250
            result.insert(index, uris.into_boxed_slice());
15✔
251
        }
15✔
252
    }
253

254
    result
15✔
255
}
15✔
256

257
/// Iterates over C509 certificates and extracts CIP-0134 URIs.
258
fn extract_c509_uris(
15✔
259
    certificates: &[C509Cert],
15✔
260
    report: &ProblemReport,
15✔
261
) -> UrisMap {
15✔
262
    let mut result = UrisMap::new();
15✔
263
    let context = "Extracting URIs from C509 certificates in Cip509 metadata";
15✔
264

265
    for (index, cert) in certificates.iter().enumerate() {
15✔
266
        let cert = match cert {
×
267
            C509Cert::C509Certificate(c) => c,
×
268
            C509Cert::C509CertInMetadatumReference(_) => {
269
                debug!("Ignoring unsupported metadatum reference");
×
270
                continue;
×
271
            },
272
            _ => continue,
×
273
        };
274

275
        for extension in cert.tbs_cert().extensions().extensions() {
×
276
            if extension.registered_oid().c509_oid().oid()
×
277
                != &C509ExtensionType::SubjectAlternativeName.oid()
×
278
            {
279
                continue;
×
280
            }
×
281
            let ExtensionValue::AlternativeName(alt_name) = extension.value() else {
×
282
                report.other(
×
283
                    &format!("Unexpected extension value type for {extension:?}"),
×
284
                    context,
×
285
                );
286
                continue;
×
287
            };
288
            let GeneralNamesOrText::GeneralNames(gen_names) = alt_name.general_name() else {
×
289
                report.other(
×
290
                    &format!("Unexpected general name type: {extension:?}"),
×
291
                    context,
×
292
                );
293
                continue;
×
294
            };
295

296
            let mut uris = Vec::new();
×
297
            for name in gen_names.general_names() {
×
298
                if *name.gn_type() != GeneralNameTypeRegistry::UniformResourceIdentifier {
×
299
                    continue;
×
300
                }
×
301
                let GeneralNameValue::Text(address) = name.gn_value() else {
×
302
                    report.other(
×
303
                        &format!("Unexpected general name value format: {name:?}"),
×
304
                        context,
×
305
                    );
306
                    continue;
×
307
                };
308
                match Cip0134Uri::parse(address) {
×
309
                    Ok(u) => uris.push(u),
×
310
                    Err(e) => {
×
311
                        debug!("Ignoring invalid CIP-0134 address: {e:?}");
×
312
                    },
313
                }
314
            }
315

316
            if !uris.is_empty() {
×
317
                result.insert(index, uris.into_boxed_slice());
×
318
            }
×
319
        }
320
    }
321

322
    result
15✔
323
}
15✔
324

325
#[cfg(test)]
326
mod tests {
327

328
    use cardano_blockchain_types::pallas_addresses::{Address, Network};
329

330
    use crate::{cardano::cip509::Cip509, utils::test};
331

332
    #[test]
333
    fn set_new() {
1✔
334
        let data = test::block_1();
1✔
335
        let cip509 = Cip509::new(&data.block, data.txn_index, &[])
1✔
336
            .unwrap()
1✔
337
            .unwrap();
1✔
338
        assert!(
1✔
339
            !cip509.report().is_problematic(),
1✔
340
            "Failed to decode Cip509: {:?}",
×
341
            cip509.report()
×
342
        );
343

344
        let set = cip509.certificate_uris().unwrap();
1✔
345
        assert!(!set.is_empty());
1✔
346
        assert!(set.c_uris().is_empty());
1✔
347

348
        let x_uris = set.x_uris();
1✔
349
        assert_eq!(x_uris.len(), 1);
1✔
350

351
        let uris = x_uris.get(&0).unwrap();
1✔
352
        assert_eq!(uris.len(), 1);
1✔
353

354
        let uri = uris.first().unwrap();
1✔
355
        assert_eq!(
1✔
356
            uri.uri(),
1✔
357
            format!("web+cardano://addr/{}", data.stake_addr.unwrap())
1✔
358
        );
359
        let Address::Stake(address) = uri.address() else {
1✔
360
            panic!("Unexpected address type");
×
361
        };
362
        assert_eq!(Network::Testnet, address.network());
1✔
363
        assert_eq!(
1✔
364
            "e075be10ec5c575caffb68b08c31470666d4fe1aeea07c16d6473903",
365
            address.payload().as_hash().to_string()
1✔
366
        );
367
    }
1✔
368
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc