diff --git a/rust/rbac-registration/src/cardano/cip509/mod.rs b/rust/rbac-registration/src/cardano/cip509/mod.rs index 421cf9852..a1f73826a 100644 --- a/rust/rbac-registration/src/cardano/cip509/mod.rs +++ b/rust/rbac-registration/src/cardano/cip509/mod.rs @@ -27,7 +27,7 @@ use x509_chunks::X509Chunks; use super::transaction::witness::TxWitness; use crate::utils::{ decode_helper::{decode_bytes, decode_helper, decode_map_len}, - general::{decode_utf8, decremented_index}, + general::decremented_index, hashing::{blake2b_128, blake2b_256}, }; diff --git a/rust/rbac-registration/src/cardano/cip509/utils/cip134/mod.rs b/rust/rbac-registration/src/cardano/cip509/utils/cip134/mod.rs new file mode 100644 index 000000000..f89061e7a --- /dev/null +++ b/rust/rbac-registration/src/cardano/cip509/utils/cip134/mod.rs @@ -0,0 +1,8 @@ +//! Utilities for [CIP-134] (Cardano URIs - Address Representation). +//! +//! [CIP-134]: https://github.com/cardano-foundation/CIPs/tree/master/CIP-0134 + +pub use self::{uri::Cip0134Uri, uri_list::Cip0134UriList}; + +mod uri; +mod uri_list; diff --git a/rust/rbac-registration/src/cardano/cip509/utils/cip134.rs b/rust/rbac-registration/src/cardano/cip509/utils/cip134/uri.rs similarity index 98% rename from rust/rbac-registration/src/cardano/cip509/utils/cip134.rs rename to rust/rbac-registration/src/cardano/cip509/utils/cip134/uri.rs index d4be9cd88..cf8a3926b 100644 --- a/rust/rbac-registration/src/cardano/cip509/utils/cip134.rs +++ b/rust/rbac-registration/src/cardano/cip509/utils/cip134/uri.rs @@ -1,4 +1,4 @@ -//! Utility functions for CIP-0134 address. +//! An URI in the CIP-0134 format. // Ignore URIs that are used in tests and doc-examples. // cSpell:ignoreRegExp web\+cardano:.+ @@ -14,6 +14,7 @@ use pallas::ledger::addresses::Address; /// /// [proposal]: https://github.com/cardano-foundation/CIPs/pull/888 #[derive(Debug)] +#[allow(clippy::module_name_repetitions)] pub struct Cip0134Uri { /// A URI string. uri: String, diff --git a/rust/rbac-registration/src/cardano/cip509/utils/cip134/uri_list.rs b/rust/rbac-registration/src/cardano/cip509/utils/cip134/uri_list.rs new file mode 100644 index 000000000..31ab70d8d --- /dev/null +++ b/rust/rbac-registration/src/cardano/cip509/utils/cip134/uri_list.rs @@ -0,0 +1,177 @@ +//! A list of [`Cip0134Uri`]. + +use std::sync::Arc; + +use anyhow::{anyhow, Result}; +use c509_certificate::{ + extensions::{alt_name::GeneralNamesOrText, extension::ExtensionValue}, + general_names::general_name::{GeneralNameTypeRegistry, GeneralNameValue}, + C509ExtensionType, +}; +use der_parser::der::parse_der_sequence; +use pallas::ledger::traverse::MultiEraTx; +use tracing::debug; +use x509_cert::der::{oid::db::rfc5912::ID_CE_SUBJECT_ALT_NAME, Decode}; + +use crate::{ + cardano::cip509::{ + rbac::{ + certs::{C509Cert, X509DerCert}, + Cip509RbacMetadata, + }, + utils::Cip0134Uri, + validation::URI, + Cip509, + }, + utils::general::decode_utf8, +}; + +/// A list of [`Cip0134Uri`]. +/// +/// This structure uses [`Arc`] internally, so it is cheap to clone. +#[derive(Debug, Clone)] +#[allow(clippy::module_name_repetitions)] +pub struct Cip0134UriList { + /// An internal list of URIs. + uris: Arc<[Cip0134Uri]>, +} + +impl Cip0134UriList { + /// Creates a new `Cip0134UriList` instance from the given `Cip509`. + /// + /// # Errors + /// - Unsupported transaction era. + pub fn new(cip509: &Cip509, tx: &MultiEraTx) -> Result { + if !matches!(tx, MultiEraTx::Conway(_)) { + return Err(anyhow!("Unsupported transaction era ({})", tx.era())); + } + + let metadata = &cip509.x509_chunks.0; + let mut uris = process_x509_certificates(metadata); + uris.extend(process_c509_certificates(metadata)); + + Ok(Self { uris: uris.into() }) + } + + /// Returns an iterator over the contained Cip0134 URIs. + pub fn iter(&self) -> impl Iterator { + self.uris.iter() + } + + /// Returns a slice with all URIs in the list. + #[must_use] + pub fn as_slice(&self) -> &[Cip0134Uri] { + &self.uris + } +} + +/// Iterates over X509 certificates and extracts CIP-0134 URIs. +fn process_x509_certificates(metadata: &Cip509RbacMetadata) -> Vec { + let mut result = Vec::new(); + + for cert in metadata.x509_certs.iter().flatten() { + let X509DerCert::X509Cert(cert) = cert else { + continue; + }; + let cert = match x509_cert::Certificate::from_der(cert) { + Ok(cert) => cert, + Err(e) => { + debug!("Failed to decode x509 certificate DER: {e:?}"); + continue; + }, + }; + // Find the "subject alternative name" extension. + let Some(extension) = cert + .tbs_certificate + .extensions + .iter() + .flatten() + .find(|e| e.extn_id == ID_CE_SUBJECT_ALT_NAME) + else { + continue; + }; + let der = match parse_der_sequence(extension.extn_value.as_bytes()) { + Ok((_, der)) => der, + Err(e) => { + debug!("Failed to parse DER sequence for Subject Alternative Name ({extension:?}): {e:?}"); + continue; + }, + }; + for data in der.ref_iter() { + if data.header.raw_tag() != Some(&[URI]) { + continue; + } + let content = match data.content.as_slice() { + Ok(c) => c, + Err(e) => { + debug!("Unable to process content for {data:?}: {e:?}"); + continue; + }, + }; + let address = match decode_utf8(content) { + Ok(a) => a, + Err(e) => { + debug!("Failed to decode content of {data:?}: {e:?}"); + continue; + }, + }; + match Cip0134Uri::parse(&address) { + Ok(a) => result.push(a), + Err(e) => { + debug!("Failed to parse CIP-0134 address ({address}): {e:?}"); + }, + } + } + } + + result +} + +/// Iterates over C509 certificates and extracts CIP-0134 URIs. +fn process_c509_certificates(metadata: &Cip509RbacMetadata) -> Vec { + let mut result = Vec::new(); + + for cert in metadata.c509_certs.iter().flatten() { + let cert = match cert { + C509Cert::C509Certificate(c) => c, + C509Cert::C509CertInMetadatumReference(_) => { + debug!("Ignoring unsupported metadatum reference"); + continue; + }, + _ => continue, + }; + + for extension in cert.tbs_cert().extensions().extensions() { + if extension.registered_oid().c509_oid().oid() + != &C509ExtensionType::SubjectAlternativeName.oid() + { + continue; + } + let ExtensionValue::AlternativeName(alt_name) = extension.value() else { + debug!("Unexpected extension value type for {extension:?}"); + continue; + }; + let GeneralNamesOrText::GeneralNames(gen_names) = alt_name.general_name() else { + debug!("Unexpected general name type: {extension:?}"); + continue; + }; + for name in gen_names.general_names() { + if *name.gn_type() != GeneralNameTypeRegistry::UniformResourceIdentifier { + continue; + } + let GeneralNameValue::Text(address) = name.gn_value() else { + debug!("Unexpected general name value format: {name:?}"); + continue; + }; + match Cip0134Uri::parse(address) { + Ok(a) => result.push(a), + Err(e) => { + debug!("Failed to parse CIP-0134 address ({address}): {e:?}"); + }, + } + } + } + } + + result +} diff --git a/rust/rbac-registration/src/cardano/cip509/utils/mod.rs b/rust/rbac-registration/src/cardano/cip509/utils/mod.rs index ab7c3954d..69762faa5 100644 --- a/rust/rbac-registration/src/cardano/cip509/utils/mod.rs +++ b/rust/rbac-registration/src/cardano/cip509/utils/mod.rs @@ -1,6 +1,6 @@ //! Utility functions for CIP-509 pub mod cip19; -pub use cip134::Cip0134Uri; +pub use cip134::{Cip0134Uri, Cip0134UriList}; mod cip134; diff --git a/rust/rbac-registration/src/cardano/cip509/validation.rs b/rust/rbac-registration/src/cardano/cip509/validation.rs index 205957f54..4b7db276f 100644 --- a/rust/rbac-registration/src/cardano/cip509/validation.rs +++ b/rust/rbac-registration/src/cardano/cip509/validation.rs @@ -21,8 +21,6 @@ //! //! Note: This CIP509 is still under development and is subject to change. -use c509_certificate::{general_names::general_name::GeneralNameValue, C509ExtensionType}; -use der_parser::der::parse_der_sequence; use pallas::{ codec::{ minicbor::{Encode, Encoder}, @@ -30,21 +28,14 @@ use pallas::{ }, ledger::{addresses::Address, traverse::MultiEraTx}, }; -use x509_cert::der::{oid::db::rfc5912::ID_CE_SUBJECT_ALT_NAME, Decode}; use super::{ - blake2b_128, blake2b_256, decode_utf8, decremented_index, - rbac::{ - certs::{C509Cert, X509DerCert}, - role_data::{LocalRefInt, RoleData}, - }, - utils::{ - cip19::{compare_key_hash, extract_key_hash}, - Cip0134Uri, - }, + blake2b_128, blake2b_256, decremented_index, + rbac::role_data::{LocalRefInt, RoleData}, + utils::cip19::{compare_key_hash, extract_key_hash}, Cip509, TxInputHash, TxWitness, }; -use crate::utils::general::zero_out_last_n_bytes; +use crate::{cardano::cip509::utils::Cip0134UriList, utils::general::zero_out_last_n_bytes}; /// Context-specific primitive type with tag number 6 (`raw_tag` 134) for /// uniform resource identifier (URI) in the subject alternative name extension. @@ -113,155 +104,17 @@ pub(crate) fn validate_stake_public_key( cip509: &Cip509, txn: &MultiEraTx, validation_report: &mut Vec, ) -> Option { let function_name = "Validate Stake Public Key"; - let mut pk_addrs = Vec::new(); - // CIP-0509 should only be in conway era - if let MultiEraTx::Conway(_) = txn { - // X509 certificate - if let Some(x509_certs) = &cip509.x509_chunks.0.x509_certs { - for x509_cert in x509_certs { - match x509_cert { - X509DerCert::X509Cert(cert) => { - // Attempt to decode the DER certificate - let der_cert = match x509_cert::Certificate::from_der(cert) { - Ok(cert) => cert, - Err(e) => { - validation_report.push(format!( - "{function_name}, Failed to decode x509 certificate DER: {e}" - )); - return None; - }, - }; - - // Find the Subject Alternative Name extension - let san_ext = - der_cert - .tbs_certificate - .extensions - .as_ref() - .and_then(|exts| { - exts.iter() - .find(|ext| ext.extn_id == ID_CE_SUBJECT_ALT_NAME) - }); - - // Subject Alternative Name extension if it exists - if let Some(san_ext) = san_ext { - match parse_der_sequence(san_ext.extn_value.as_bytes()) { - Ok((_, parsed_seq)) => { - for data in parsed_seq.ref_iter() { - // Check for context-specific primitive type with tag - // number - // 6 (raw_tag 134) - if data.header.raw_tag() == Some(&[URI]) { - match data.content.as_slice() { - Ok(content) => { - // Decode the UTF-8 string - let addr: String = match decode_utf8(content) { - Ok(addr) => addr, - Err(e) => { - validation_report.push(format!( - "{function_name}, Failed to decode UTF-8 string for context-specific primitive type with raw tag 134: {e}", - ), - ); - return None; - }, - }; - - // Extract the CIP19 hash and push into - // array - if let Ok(uri) = Cip0134Uri::parse(&addr) { - if let Address::Stake(a) = uri.address() { - pk_addrs.push( - a.payload().as_hash().to_vec(), - ); - } - } - }, - Err(e) => { - validation_report.push( - format!("{function_name}, Failed to process content for context-specific primitive type with raw tag 134: {e}")); - return None; - }, - } - } - } - }, - Err(e) => { - validation_report.push( - format!( - "{function_name}, Failed to parse DER sequence for Subject Alternative Name extension: {e}" - ) - ); - return None; - }, - } - } - }, - _ => continue, - } - } - } - // C509 Certificate - if let Some(c509_certs) = &cip509.x509_chunks.0.c509_certs { - for c509_cert in c509_certs { - match c509_cert { - C509Cert::C509CertInMetadatumReference(_) => { - validation_report.push(format!( - "{function_name}, C509 metadatum reference is currently not supported" - )); - }, - C509Cert::C509Certificate(c509) => { - for exts in c509.tbs_cert().extensions().extensions() { - if *exts.registered_oid().c509_oid().oid() - == C509ExtensionType::SubjectAlternativeName.oid() - { - match exts.value() { - c509_certificate::extensions::extension::ExtensionValue::AlternativeName(alt_name) => { - match alt_name.general_name() { - c509_certificate::extensions::alt_name::GeneralNamesOrText::GeneralNames(gn) => { - for name in gn.general_names() { - if name.gn_type() == &c509_certificate::general_names::general_name::GeneralNameTypeRegistry::UniformResourceIdentifier { - match name.gn_value() { - GeneralNameValue::Text(s) => { - if let Ok(uri) = Cip0134Uri::parse(s) { - if let Address::Stake(a) = uri.address() { - pk_addrs.push(a.payload().as_hash().to_vec()); - } - } - }, - _ => { - validation_report.push( - format!("{function_name}, Failed to get the value of subject alternative name"), - ); - } - } - } - } - }, - c509_certificate::extensions::alt_name::GeneralNamesOrText::Text(_) => { - validation_report.push( - format!("{function_name}, Failed to find C509 general names in subject alternative name"), - ); - } - } - }, - _ => { - validation_report.push( - format!("{function_name}, Failed to get C509 subject alternative name") - ); - } - } - } - } - }, - _ => continue, - } - } - } - } else { - validation_report.push(format!("{function_name}, Unsupported transaction era")); - return None; - } + // TODO: FIXME: Filter addressed (Address::Stake) + let addresses = match Cip0134UriList::new(cip509, txn) { + Ok(a) => a, + Err(e) => { + validation_report.push(format!( + "{function_name}, Failed to extract CIP-0134 URIs: {e:?}" + )); + return None; + }, + }; // Create TxWitness // Note that TxWitness designs to work with multiple transactions @@ -273,6 +126,18 @@ pub(crate) fn validate_stake_public_key( }, }; + // TODO: FIXME: Update compare_key_hash to accept Cip0134UriList? + let pk_addrs: Vec<_> = addresses + .iter() + .filter_map(|a| { + if let Address::Stake(a) = a.address() { + Some(a.payload().as_hash().to_vec()) + } else { + None + } + }) + .collect(); + Some( // Set transaction index to 0 because the list of transaction is manually constructed // for TxWitness -> &[txn.clone()], so we can assume that the witness contains only