diff --git a/rust/rbac-registration/src/cardano/cip509/mod.rs b/rust/rbac-registration/src/cardano/cip509/mod.rs index 421cf9852..6bdeb1c92 100644 --- a/rust/rbac-registration/src/cardano/cip509/mod.rs +++ b/rust/rbac-registration/src/cardano/cip509/mod.rs @@ -25,28 +25,42 @@ use validation::{ 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}, - hashing::{blake2b_128, blake2b_256}, +use crate::{ + cardano::cip509::rbac::Cip509RbacMetadata, + utils::{ + decode_helper::{decode_bytes, decode_helper, decode_map_len}, + general::decremented_index, + hashing::{blake2b_128, blake2b_256}, + }, }; /// CIP509 label. pub const LABEL: u64 = 509; -/// CIP509. -#[derive(Debug, PartialEq, Clone, Default)] +/// A x509 metadata envelope. +/// +/// The envelope is required to prevent replayability attacks. See [this document] for +/// more details. +/// +/// [this document]: https://github.com/input-output-hk/catalyst-CIPs/blob/x509-envelope-metadata/CIP-XXXX/README.md +#[derive(Debug, PartialEq, Clone)] pub struct Cip509 { - /// `UUIDv4` Purpose . - pub purpose: Uuid, // (bytes .size 16) + /// A registration purpose (`UUIDv4`). + /// + /// The purpose is defined by the consuming dApp. + pub purpose: Uuid, /// Transaction inputs hash. - pub txn_inputs_hash: TxInputHash, // bytes .size 16 - /// Optional previous transaction ID. - pub prv_tx_id: Option>, // bytes .size 32 - /// x509 chunks. - pub x509_chunks: X509Chunks, // chunk_type => [ + x509_chunk ] + pub txn_inputs_hash: TxInputHash, + /// An optional `BLAKE2b` hash of the previous transaction. + /// + /// The hash must always be present except for the first registration transaction. + pub prv_tx_id: Option>, + /// Metadata. + /// + /// This field encoded in chunks. See [`X509Chunks`] for more details. + pub metadata: Cip509RbacMetadata, /// Validation signature. - pub validation_signature: Vec, // bytes size (1..64) + pub validation_signature: Vec, } /// Validation value for CIP509 metadatum. @@ -92,7 +106,13 @@ pub(crate) enum Cip509IntIdentifier { impl Decode<'_, ()> for Cip509 { fn decode(d: &mut Decoder, ctx: &mut ()) -> Result { let map_len = decode_map_len(d, "CIP509")?; - let mut cip509_metadatum = Cip509::default(); + + let mut purpose = Uuid::default(); + let mut txn_inputs_hash = TxInputHash::default(); + let mut prv_tx_id = None; + let mut metadata = None; + let mut validation_signature = Vec::new(); + for _ in 0..map_len { // Use probe to peak let key = d.probe().u8()?; @@ -101,43 +121,56 @@ impl Decode<'_, ()> for Cip509 { let _: u8 = decode_helper(d, "CIP509", ctx)?; match key { Cip509IntIdentifier::Purpose => { - cip509_metadatum.purpose = - Uuid::try_from(decode_bytes(d, "CIP509 purpose")?).map_err(|_| { - decode::Error::message("Invalid data size of Purpose") - })?; + purpose = Uuid::try_from(decode_bytes(d, "CIP509 purpose")?) + .map_err(|_| decode::Error::message("Invalid data size of Purpose"))?; }, Cip509IntIdentifier::TxInputsHash => { - cip509_metadatum.txn_inputs_hash = + txn_inputs_hash = TxInputHash::try_from(decode_bytes(d, "CIP509 txn inputs hash")?) .map_err(|_| { decode::Error::message("Invalid data size of TxInputsHash") })?; }, Cip509IntIdentifier::PreviousTxId => { - let prv_tx_hash: [u8; 32] = decode_bytes(d, "CIP509 previous tx ID")? + let hash: [u8; 32] = decode_bytes(d, "CIP509 previous tx ID")? .try_into() .map_err(|_| { - decode::Error::message("Invalid data size of PreviousTxId") - })?; - cip509_metadatum.prv_tx_id = Some(Hash::from(prv_tx_hash)); + decode::Error::message("Invalid data size of PreviousTxId") + })?; + prv_tx_id = Some(Hash::from(hash)); }, Cip509IntIdentifier::ValidationSignature => { - let validation_signature = decode_bytes(d, "CIP509 validation signature")?; - if validation_signature.is_empty() || validation_signature.len() > 64 { + let signature = decode_bytes(d, "CIP509 validation signature")?; + if signature.is_empty() || signature.len() > 64 { return Err(decode::Error::message( "Invalid data size of ValidationSignature", )); } - cip509_metadatum.validation_signature = validation_signature; + validation_signature = signature; }, } } else { // Handle the x509 chunks 10 11 12 let x509_chunks = X509Chunks::decode(d, ctx)?; - cip509_metadatum.x509_chunks = x509_chunks; + // Technically it is possible to store multiple copies (or different instances) of + // metadata, but it isn't allowed. See this link for more details: + // https://github.com/input-output-hk/catalyst-CIPs/blob/x509-envelope-metadata/CIP-XXXX/README.md#keys-10-11-or-12---x509-chunked-data + if metadata.is_some() { + return Err(decode::Error::message( + "Only one instance of the chunked metadata should be present", + )); + } + metadata = Some(x509_chunks.into()); } } - Ok(cip509_metadatum) + + Ok(Self { + purpose, + txn_inputs_hash, + prv_tx_id, + metadata: metadata.unwrap_or_default(), + validation_signature, + }) } } @@ -179,16 +212,14 @@ impl Cip509 { let mut is_valid_stake_public_key = true; let mut is_valid_payment_key = true; let mut is_valid_signing_key = true; - if let Some(role_set) = &self.x509_chunks.0.role_set { - // Validate only role 0 - for role in role_set { - if role.role_number == 0 { - is_valid_stake_public_key = - validate_stake_public_key(self, txn, validation_report).unwrap_or(false); - is_valid_payment_key = - validate_payment_key(txn, role, validation_report).unwrap_or(false); - is_valid_signing_key = validate_role_singing_key(role, validation_report); - } + // Validate only role 0 + for role in &self.metadata.role_set { + if role.role_number == 0 { + is_valid_stake_public_key = + validate_stake_public_key(self, txn, validation_report).unwrap_or(false); + is_valid_payment_key = + validate_payment_key(txn, role, validation_report).unwrap_or(false); + is_valid_signing_key = validate_role_singing_key(role, validation_report); } } Cip509Validation { diff --git a/rust/rbac-registration/src/cardano/cip509/rbac/mod.rs b/rust/rbac-registration/src/cardano/cip509/rbac/mod.rs index 34675d80c..d3d0c0517 100644 --- a/rust/rbac-registration/src/cardano/cip509/rbac/mod.rs +++ b/rust/rbac-registration/src/cardano/cip509/rbac/mod.rs @@ -16,24 +16,43 @@ use role_data::RoleData; use strum_macros::FromRepr; use super::types::cert_key_hash::CertKeyHash; -use crate::utils::decode_helper::{ - decode_any, decode_array_len, decode_bytes, decode_helper, decode_map_len, +use crate::{ + cardano::cip509::utils::Cip0134UriSet, + utils::decode_helper::{ + decode_any, decode_array_len, decode_bytes, decode_helper, decode_map_len, + }, }; /// Cip509 RBAC metadata. +/// +/// See [this document] for more details. +/// +/// [this document]: https://github.com/input-output-hk/catalyst-CIPs/tree/x509-role-registration-metadata/CIP-XXXX #[derive(Debug, PartialEq, Clone, Default)] pub struct Cip509RbacMetadata { - /// Optional list of x509 certificates. - pub x509_certs: Option>, - /// Optional list of c509 certificates. - /// The value can be either the c509 certificate or c509 metadatum reference. - pub c509_certs: Option>, - /// Optional list of Public keys. - pub pub_keys: Option>, - /// Optional list of revocation list. - pub revocation_list: Option>, - /// Optional list of role data. - pub role_set: Option>, + /// A potentially empty list of x509 certificates. + pub x509_certs: Vec, + /// A potentially empty list of c509 certificates. + pub c509_certs: Vec, + /// A set of URIs contained in both x509 and c509 certificates. + /// + /// URIs from different certificate types are stored separately and certificate + /// indexes are preserved too. + /// + /// This field isn't present in the encoded format and is populated by processing both + /// `x509_certs` and `c509_certs` fields. + pub certificate_uris: Cip0134UriSet, + /// A list of public keys that can be used instead of storing full certificates. + /// + /// Check [this section] to understand the how certificates and the public keys list + /// are related. + /// + /// [this section]: https://github.com/input-output-hk/catalyst-CIPs/tree/x509-role-registration-metadata/CIP-XXXX#storing-certificates-and-public-key + pub pub_keys: Vec, + /// A potentially empty list of revoked certificates. + pub revocation_list: Vec, + /// A potentially empty list of role data. + pub role_set: Vec, /// Optional map of purpose key data. /// Empty map if no purpose key data is present. pub purpose_key_data: HashMap>, @@ -60,86 +79,59 @@ pub enum Cip509RbacMetadataInt { RoleSet = 100, } -impl Cip509RbacMetadata { - /// Create a new instance of `Cip509RbacMetadata`. - pub(crate) fn new() -> Self { - Self { - x509_certs: None, - c509_certs: None, - pub_keys: None, - revocation_list: None, - role_set: None, - purpose_key_data: HashMap::new(), - } - } - - /// Set the x509 certificates. - fn set_x509_certs(&mut self, x509_certs: Vec) { - self.x509_certs = Some(x509_certs); - } - - /// Set the c509 certificates. - fn set_c509_certs(&mut self, c509_certs: Vec) { - self.c509_certs = Some(c509_certs); - } - - /// Set the public keys. - fn set_pub_keys(&mut self, pub_keys: Vec) { - self.pub_keys = Some(pub_keys); - } - - /// Set the revocation list. - fn set_revocation_list(&mut self, revocation_list: Vec) { - self.revocation_list = Some(revocation_list); - } - - /// Set the role data set. - fn set_role_set(&mut self, role_set: Vec) { - self.role_set = Some(role_set); - } -} - impl Decode<'_, ()> for Cip509RbacMetadata { fn decode(d: &mut Decoder, ctx: &mut ()) -> Result { let map_len = decode_map_len(d, "Cip509RbacMetadata")?; - let mut x509_rbac_metadata = Cip509RbacMetadata::new(); + let mut x509_certs = Vec::new(); + let mut c509_certs = Vec::new(); + let mut pub_keys = Vec::new(); + let mut revocation_list = Vec::new(); + let mut role_set = Vec::new(); + let mut purpose_key_data = HashMap::new(); for _ in 0..map_len { let key: u16 = decode_helper(d, "key in Cip509RbacMetadata", ctx)?; if let Some(key) = Cip509RbacMetadataInt::from_repr(key) { match key { Cip509RbacMetadataInt::X509Certs => { - let x509_certs = decode_array_rbac(d, "x509 certificate")?; - x509_rbac_metadata.set_x509_certs(x509_certs); + x509_certs = decode_array_rbac(d, "x509 certificate")?; }, Cip509RbacMetadataInt::C509Certs => { - let c509_certs = decode_array_rbac(d, "c509 certificate")?; - x509_rbac_metadata.set_c509_certs(c509_certs); + c509_certs = decode_array_rbac(d, "c509 certificate")?; }, Cip509RbacMetadataInt::PubKeys => { - let pub_keys = decode_array_rbac(d, "public keys")?; - x509_rbac_metadata.set_pub_keys(pub_keys); + pub_keys = decode_array_rbac(d, "public keys")?; }, Cip509RbacMetadataInt::RevocationList => { - let revocation_list = decode_revocation_list(d)?; - x509_rbac_metadata.set_revocation_list(revocation_list); + revocation_list = decode_revocation_list(d)?; }, Cip509RbacMetadataInt::RoleSet => { - let role_set = decode_array_rbac(d, "role set")?; - x509_rbac_metadata.set_role_set(role_set); + role_set = decode_array_rbac(d, "role set")?; }, } } else { if !(FIRST_PURPOSE_KEY..=LAST_PURPOSE_KEY).contains(&key) { return Err(decode::Error::message(format!("Invalid purpose key set, should be with the range {FIRST_PURPOSE_KEY} - {LAST_PURPOSE_KEY}"))); } - x509_rbac_metadata - .purpose_key_data - .insert(key, decode_any(d, "purpose key")?); + + purpose_key_data.insert(key, decode_any(d, "purpose key")?); } } - Ok(x509_rbac_metadata) + + let certificate_uris = Cip0134UriSet::new(&x509_certs, &c509_certs).map_err(|e| { + decode::Error::message(format!("Unable to parse URIs from certificates: {e:?}")) + })?; + + Ok(Self { + x509_certs, + c509_certs, + certificate_uris, + pub_keys, + revocation_list, + role_set, + purpose_key_data, + }) } } diff --git a/rust/rbac-registration/src/cardano/cip509/types/tx_input_hash.rs b/rust/rbac-registration/src/cardano/cip509/types/tx_input_hash.rs index bcc0c0bc4..ec6faa945 100644 --- a/rust/rbac-registration/src/cardano/cip509/types/tx_input_hash.rs +++ b/rust/rbac-registration/src/cardano/cip509/types/tx_input_hash.rs @@ -1,6 +1,10 @@ //! Transaction input hash type -/// Transaction input hash representing in 16 bytes. +/// A 16-byte hash of the transaction inputs field. +/// +/// This type is described [here]. +/// +/// [here]: https://github.com/input-output-hk/catalyst-CIPs/blob/x509-envelope-metadata/CIP-XXXX/README.md#key-1-txn-inputs-hash #[derive(Debug, PartialEq, Clone, Default)] pub struct TxInputHash([u8; 16]); 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..702b09faa --- /dev/null +++ b/rust/rbac-registration/src/cardano/cip509/utils/cip134/mod.rs @@ -0,0 +1,7 @@ +//! 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_set::Cip0134UriSet}; + +mod uri; +mod uri_set; 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 6aa42f57c..2af2fc18c 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:.+ @@ -13,7 +13,8 @@ use pallas::ledger::addresses::Address; /// See the [proposal] for more details. /// /// [proposal]: https://github.com/cardano-foundation/CIPs/pull/888 -#[derive(Debug)] +#[derive(Debug, Eq, PartialEq)] +#[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_set.rs b/rust/rbac-registration/src/cardano/cip509/utils/cip134/uri_set.rs new file mode 100644 index 000000000..e067730fe --- /dev/null +++ b/rust/rbac-registration/src/cardano/cip509/utils/cip134/uri_set.rs @@ -0,0 +1,234 @@ +//! A set of [`Cip0134Uri`]. + +use std::{collections::HashMap, sync::Arc}; + +use anyhow::{anyhow, Context, 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 tracing::debug; +use x509_cert::der::{oid::db::rfc5912::ID_CE_SUBJECT_ALT_NAME, Decode}; + +use crate::{ + cardano::cip509::{ + rbac::certs::{C509Cert, X509DerCert}, + utils::Cip0134Uri, + validation::URI, + }, + utils::general::decode_utf8, +}; + +/// A mapping from a certificate index to URIs contained within. +type UrisMap = HashMap>; + +/// A set of [`Cip0134Uri`] contained in both x509 and c509 certificates stored in the +/// metadata part of [`Cip509`](crate::cardano::cip509::Cip509). +/// +/// This structure uses [`Arc`] internally, so it is cheap to clone. +#[derive(Debug, Clone, Eq, PartialEq, Default)] +#[allow(clippy::module_name_repetitions)] +pub struct Cip0134UriSet(Arc); + +/// Internal `Cip0134UriSet` data. +#[derive(Debug, Eq, PartialEq, Default)] +struct Cip0134UriSetInner { + /// URIs from x509 certificates. + x_uris: UrisMap, + /// URIs from c509 certificates. + c_uris: UrisMap, +} + +impl Cip0134UriSet { + /// Creates a new `Cip0134UriSet` instance from the given certificates. + /// + /// # Errors + /// - Invalid certificate. + /// - Invalid URI. + pub fn new(x509_certs: &[X509DerCert], c509_certs: &[C509Cert]) -> Result { + let x_uris = process_x509_certificates(x509_certs) + .with_context(|| "Error processing X509 certificates")?; + let c_uris = process_c509_certificates(c509_certs) + .with_context(|| "Error processing C509 certificates")?; + Ok(Self(Arc::new(Cip0134UriSetInner { x_uris, c_uris }))) + } + + /// Returns a mapping from the x509 certificate index to URIs contained within. + #[must_use] + pub fn x_uris(&self) -> &UrisMap { + &self.0.x_uris + } + + /// Returns a mapping from the c509 certificate index to URIs contained within. + #[must_use] + pub fn c_uris(&self) -> &UrisMap { + &self.0.c_uris + } + + /// Returns `true` if both x509 and c509 certificate maps are empty. + #[must_use] + pub fn is_empty(&self) -> bool { + self.x_uris().is_empty() && self.c_uris().is_empty() + } +} + +/// Iterates over X509 certificates and extracts CIP-0134 URIs. +fn process_x509_certificates(certificates: &[X509DerCert]) -> Result { + let mut result = UrisMap::new(); + + for (index, cert) in certificates.iter().enumerate() { + let X509DerCert::X509Cert(cert) = cert else { + continue; + }; + let cert = x509_cert::Certificate::from_der(cert) + .with_context(|| "Failed to decode X509 certificate from DER")?; + // 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) = parse_der_sequence(extension.extn_value.as_bytes()).with_context(|| { + format!("Failed to parse DER sequence for Subject Alternative Name ({extension:?})") + })?; + + let mut uris = Vec::new(); + for data in der.ref_iter() { + if data.header.raw_tag() != Some(&[URI]) { + continue; + } + let content = data + .content + .as_slice() + .with_context(|| "Unable to process content for {data:?}")?; + let address = decode_utf8(content)?; + let uri = Cip0134Uri::parse(&address) + .with_context(|| format!("Failed to parse CIP-0134 address ({address})"))?; + uris.push(uri); + } + + if !uris.is_empty() { + result.insert(index, uris.into_boxed_slice()); + } + } + + Ok(result) +} + +/// Iterates over C509 certificates and extracts CIP-0134 URIs. +fn process_c509_certificates(certificates: &[C509Cert]) -> Result { + let mut result = UrisMap::new(); + + for (index, cert) in certificates.iter().enumerate() { + 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 { + return Err(anyhow!("Unexpected extension value type for {extension:?}")); + }; + let GeneralNamesOrText::GeneralNames(gen_names) = alt_name.general_name() else { + return Err(anyhow!("Unexpected general name type: {extension:?}")); + }; + + let mut uris = Vec::new(); + for name in gen_names.general_names() { + if *name.gn_type() != GeneralNameTypeRegistry::UniformResourceIdentifier { + continue; + } + let GeneralNameValue::Text(address) = name.gn_value() else { + return Err(anyhow!("Unexpected general name value format: {name:?}")); + }; + let uri = Cip0134Uri::parse(address) + .with_context(|| format!("Failed to parse CIP-0134 address ({address})"))?; + uris.push(uri); + } + + if !uris.is_empty() { + result.insert(index, uris.into_boxed_slice()); + } + } + } + + Ok(result) +} + +#[cfg(test)] +mod tests { + use minicbor::{Decode, Decoder}; + use pallas::{ + codec::utils::Nullable, + ledger::{ + addresses::{Address, Network}, + traverse::{MultiEraBlock, MultiEraTx}, + }, + }; + + use crate::cardano::{cip509::Cip509, transaction::raw_aux_data::RawAuxData}; + + // This lint is disabled locally because the `allow-indexing-slicing-in-tests` was added + // very recently and isn't present in the stable clippy yet. Also it is impossible to use + // `get(n).unwrap()` instead because Clippy will still complain (clippy::get-unwrap). + #[allow(clippy::indexing_slicing)] + #[test] + fn set_new() { + let block = + hex::decode(include_str!("../../../../test_data/cardano/conway_1.block")).unwrap(); + let block = MultiEraBlock::decode(&block).unwrap(); + let tx = &block.txs()[3]; + let cip509 = cip509(tx); + let set = cip509.metadata.certificate_uris; + assert!(!set.is_empty()); + assert!(set.c_uris().is_empty()); + + let x_uris = set.x_uris(); + assert_eq!(x_uris.len(), 1); + + let uris = x_uris.get(&0).unwrap(); + assert_eq!(uris.len(), 1); + + let uri = &uris[0]; + // cSpell:disable + assert_eq!( + uri.uri(), + "web+cardano://addr/stake_test1urs8t0ssa3w9wh90ld5tprp3gurxd487rth2qlqk6ernjqcef4ugr" + ); + // cSpell:enable + let Address::Stake(address) = uri.address() else { + panic!("Unexpected address type"); + }; + assert_eq!(Network::Testnet, address.network()); + assert_eq!( + "e075be10ec5c575caffb68b08c31470666d4fe1aeea07c16d6473903", + address.payload().as_hash().to_string() + ); + } + + fn cip509(tx: &MultiEraTx) -> Cip509 { + let Nullable::Some(data) = tx.as_conway().unwrap().clone().auxiliary_data else { + panic!("Auxiliary data is missing"); + }; + let data = RawAuxData::new(data.raw_cbor()); + let metadata = data.get_metadata(509).unwrap(); + + let mut decoder = Decoder::new(metadata.as_slice()); + Cip509::decode(&mut decoder, &mut ()).unwrap() + } +} diff --git a/rust/rbac-registration/src/cardano/cip509/utils/mod.rs b/rust/rbac-registration/src/cardano/cip509/utils/mod.rs index ab7c3954d..6cba2f3d6 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, Cip0134UriSet}; mod cip134; diff --git a/rust/rbac-registration/src/cardano/cip509/validation.rs b/rust/rbac-registration/src/cardano/cip509/validation.rs index 205957f54..9b5d3237c 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,18 +28,11 @@ 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; @@ -113,152 +104,8 @@ 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 { + if !matches!(txn, MultiEraTx::Conway(_)) { validation_report.push(format!("{function_name}, Unsupported transaction era")); return None; } @@ -273,6 +120,24 @@ pub(crate) fn validate_stake_public_key( }, }; + // TODO: This should be improved, probably along with the `compare_key_hash` function + // implementation. + let pk_addrs: Vec<_> = cip509 + .metadata + .certificate_uris + .x_uris() + .iter() + .chain(cip509.metadata.certificate_uris.c_uris()) + .flat_map(|(_index, uris)| uris.iter()) + .filter_map(|uri| { + if let Address::Stake(a) = uri.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 @@ -597,11 +462,9 @@ mod tests { let mut decoder = Decoder::new(aux_data.as_slice()); let cip509 = Cip509::decode(&mut decoder, &mut ()).expect("Failed to decode Cip509"); - if let Some(role_set) = &cip509.x509_chunks.0.role_set { - for role in role_set { - if role.role_number == 0 { - assert!(validate_payment_key(tx, role, &mut validation_report,).unwrap()); - } + for role in &cip509.metadata.role_set { + if role.role_number == 0 { + assert!(validate_payment_key(tx, role, &mut validation_report,).unwrap()); } } } @@ -623,11 +486,9 @@ mod tests { let mut decoder = Decoder::new(aux_data.as_slice()); let cip509 = Cip509::decode(&mut decoder, &mut ()).expect("Failed to decode Cip509"); - if let Some(role_set) = &cip509.x509_chunks.0.role_set { - for role in role_set { - if role.role_number == 0 { - assert!(validate_role_singing_key(role, &mut validation_report,)); - } + for role in &cip509.metadata.role_set { + if role.role_number == 0 { + assert!(validate_role_singing_key(role, &mut validation_report,)); } } } @@ -650,11 +511,9 @@ mod tests { let mut decoder = Decoder::new(aux_data.as_slice()); let cip509 = Cip509::decode(&mut decoder, &mut ()).expect("Failed to decode Cip509"); - if let Some(role_set) = &cip509.x509_chunks.0.role_set { - for role in role_set { - if role.role_number == 0 { - assert!(validate_payment_key(tx, role, &mut validation_report,).unwrap()); - } + for role in &cip509.metadata.role_set { + if role.role_number == 0 { + assert!(validate_payment_key(tx, role, &mut validation_report,).unwrap()); } } } diff --git a/rust/rbac-registration/src/cardano/cip509/x509_chunks.rs b/rust/rbac-registration/src/cardano/cip509/x509_chunks.rs index 457faef6a..f45739957 100644 --- a/rust/rbac-registration/src/cardano/cip509/x509_chunks.rs +++ b/rust/rbac-registration/src/cardano/cip509/x509_chunks.rs @@ -21,15 +21,20 @@ pub enum CompressionAlgorithm { Zstd = 12, } -/// x509 chunks. -#[derive(Debug, PartialEq, Clone, Default)] -pub struct X509Chunks(pub Cip509RbacMetadata); - -#[allow(dead_code)] -impl X509Chunks { - /// Create new instance of `X509Chunks`. - fn new(chunk_data: Cip509RbacMetadata) -> Self { - Self(chunk_data) +/// A helper for decoding [`Cip509RbacMetadata`]. +/// +/// Due to encoding restrictions the [`Cip509`](crate::cardano::cip509::Cip509) metadata +/// is encoded in chunks: +/// ```text +/// chunk_type => [ + x509_chunk ] +/// ``` +/// This helper is used to decode them into the actual structure. +#[derive(Debug, PartialEq, Clone)] +pub struct X509Chunks(Cip509RbacMetadata); + +impl From for Cip509RbacMetadata { + fn from(value: X509Chunks) -> Self { + value.0 } } diff --git a/rust/rbac-registration/src/lib.rs b/rust/rbac-registration/src/lib.rs index ab9cfd1e4..6e35f5165 100644 --- a/rust/rbac-registration/src/lib.rs +++ b/rust/rbac-registration/src/lib.rs @@ -2,4 +2,5 @@ pub mod cardano; pub mod registration; -pub(crate) mod utils; + +mod utils; diff --git a/rust/rbac-registration/src/registration/cardano/mod.rs b/rust/rbac-registration/src/registration/cardano/mod.rs index eeac8dd42..552d5672f 100644 --- a/rust/rbac-registration/src/registration/cardano/mod.rs +++ b/rust/rbac-registration/src/registration/cardano/mod.rs @@ -198,7 +198,7 @@ impl RegistrationChainInner { // Add purpose to the list let purpose = vec![cip509.purpose]; - let registration = cip509.x509_chunks.0; + let registration = cip509.metadata; let point_tx_idx = PointTxIdx::new(point, tx_idx); let x509_cert_map = chain_root_x509_certs(registration.x509_certs, &point_tx_idx); @@ -269,7 +269,7 @@ impl RegistrationChainInner { new_inner.purpose.push(purpose); } - let registration = cip509.x509_chunks.0; + let registration = cip509.metadata; let point_tx_idx = PointTxIdx::new(point, tx_idx); update_x509_certs(&mut new_inner, registration.x509_certs, &point_tx_idx); @@ -303,15 +303,13 @@ fn is_valid_cip509(validation_data: &Cip509Validation) -> bool { /// Process x509 certificate for chain root. fn chain_root_x509_certs( - x509_certs: Option>, point_tx_idx: &PointTxIdx, + x509_certs: Vec, point_tx_idx: &PointTxIdx, ) -> HashMap)> { let mut map = HashMap::new(); - if let Some(cert_list) = x509_certs { - for (idx, cert) in cert_list.iter().enumerate() { - // Chain root, expect only the certificate not undefined or delete - if let cip509::rbac::certs::X509DerCert::X509Cert(cert) = cert { - map.insert(idx, (point_tx_idx.clone(), cert.clone())); - } + for (idx, cert) in x509_certs.into_iter().enumerate() { + // Chain root, expect only the certificate not undefined or delete + if let X509DerCert::X509Cert(cert) = cert { + map.insert(idx, (point_tx_idx.clone(), cert)); } } map @@ -319,40 +317,35 @@ fn chain_root_x509_certs( /// Update x509 certificates in the registration chain. fn update_x509_certs( - new_inner: &mut RegistrationChainInner, x509_certs: Option>, - point_tx_idx: &PointTxIdx, + new_inner: &mut RegistrationChainInner, x509_certs: Vec, point_tx_idx: &PointTxIdx, ) { - if let Some(cert_list) = x509_certs { - for (idx, cert) in cert_list.iter().enumerate() { - match cert { - // Unchanged to that index, so continue - cip509::rbac::certs::X509DerCert::Undefined => continue, - // Delete the certificate - cip509::rbac::certs::X509DerCert::Deleted => { - new_inner.x509_certs.remove(&idx); - }, - // Add the new certificate - cip509::rbac::certs::X509DerCert::X509Cert(cert) => { - new_inner - .x509_certs - .insert(idx, (point_tx_idx.clone(), cert.clone())); - }, - } + for (idx, cert) in x509_certs.into_iter().enumerate() { + match cert { + // Unchanged to that index, so continue + X509DerCert::Undefined => continue, + // Delete the certificate + X509DerCert::Deleted => { + new_inner.x509_certs.remove(&idx); + }, + // Add the new certificate + X509DerCert::X509Cert(cert) => { + new_inner + .x509_certs + .insert(idx, (point_tx_idx.clone(), cert)); + }, } } } /// Process c509 certificates for chain root. fn chain_root_c509_certs( - c509_certs: Option>, point_tx_idx: &PointTxIdx, + c509_certs: Vec, point_tx_idx: &PointTxIdx, ) -> HashMap { let mut map = HashMap::new(); - if let Some(cert_list) = c509_certs { - for (idx, cert) in cert_list.iter().enumerate() { - if let cip509::rbac::certs::C509Cert::C509Certificate(cert) = cert { - // Chain root, expect only the certificate not undefined or delete - map.insert(idx, (point_tx_idx.clone(), *cert.clone())); - } + for (idx, cert) in c509_certs.into_iter().enumerate() { + if let C509Cert::C509Certificate(cert) = cert { + // Chain root, expect only the certificate not undefined or delete + map.insert(idx, (point_tx_idx.clone(), *cert)); } } map @@ -360,29 +353,26 @@ fn chain_root_c509_certs( /// Update c509 certificates in the registration chain. fn update_c509_certs( - new_inner: &mut RegistrationChainInner, c509_certs: Option>, - point_tx_idx: &PointTxIdx, + new_inner: &mut RegistrationChainInner, c509_certs: Vec, point_tx_idx: &PointTxIdx, ) -> anyhow::Result<()> { - if let Some(cert_list) = c509_certs { - for (idx, cert) in cert_list.iter().enumerate() { - match cert { - // Unchanged to that index, so continue - cip509::rbac::certs::C509Cert::Undefined => continue, - // Delete the certificate - cip509::rbac::certs::C509Cert::Deleted => { - new_inner.c509_certs.remove(&idx); - }, - // Certificate reference - cip509::rbac::certs::C509Cert::C509CertInMetadatumReference(_) => { - bail!("Unsupported c509 certificate in metadatum reference") - }, - // Add the new certificate - cip509::rbac::certs::C509Cert::C509Certificate(c509) => { - new_inner - .c509_certs - .insert(idx, (point_tx_idx.clone(), *c509.clone())); - }, - } + for (idx, cert) in c509_certs.into_iter().enumerate() { + match cert { + // Unchanged to that index, so continue + C509Cert::Undefined => continue, + // Delete the certificate + C509Cert::Deleted => { + new_inner.c509_certs.remove(&idx); + }, + // Certificate reference + C509Cert::C509CertInMetadatumReference(_) => { + bail!("Unsupported c509 certificate in metadatum reference") + }, + // Add the new certificate + C509Cert::C509Certificate(c509) => { + new_inner + .c509_certs + .insert(idx, (point_tx_idx.clone(), *c509)); + }, } } Ok(()) @@ -390,15 +380,13 @@ fn update_c509_certs( /// Process public keys for chain root. fn chain_root_public_keys( - pub_keys: Option>, point_tx_idx: &PointTxIdx, + pub_keys: Vec, point_tx_idx: &PointTxIdx, ) -> HashMap { let mut map = HashMap::new(); - if let Some(key_list) = pub_keys { - for (idx, key) in key_list.iter().enumerate() { - // Chain root, expect only the public key not undefined or delete - if let cip509::rbac::pub_key::SimplePublicKeyType::Ed25519(key) = key { - map.insert(idx, (point_tx_idx.clone(), *key)); - } + for (idx, key) in pub_keys.into_iter().enumerate() { + // Chain root, expect only the public key not undefined or delete + if let SimplePublicKeyType::Ed25519(key) = key { + map.insert(idx, (point_tx_idx.clone(), key)); } } map @@ -406,119 +394,110 @@ fn chain_root_public_keys( /// Update public keys in the registration chain. fn update_public_keys( - new_inner: &mut RegistrationChainInner, pub_keys: Option>, + new_inner: &mut RegistrationChainInner, pub_keys: Vec, point_tx_idx: &PointTxIdx, ) { - if let Some(key_list) = pub_keys { - for (idx, cert) in key_list.iter().enumerate() { - match cert { - // Unchanged to that index, so continue - cip509::rbac::pub_key::SimplePublicKeyType::Undefined => continue, - // Delete the public key - cip509::rbac::pub_key::SimplePublicKeyType::Deleted => { - new_inner.simple_keys.remove(&idx); - }, - // Add the new public key - cip509::rbac::pub_key::SimplePublicKeyType::Ed25519(key) => { - new_inner - .simple_keys - .insert(idx, (point_tx_idx.clone(), *key)); - }, - } + for (idx, cert) in pub_keys.into_iter().enumerate() { + match cert { + // Unchanged to that index, so continue + SimplePublicKeyType::Undefined => continue, + // Delete the public key + SimplePublicKeyType::Deleted => { + new_inner.simple_keys.remove(&idx); + }, + // Add the new public key + SimplePublicKeyType::Ed25519(key) => { + new_inner + .simple_keys + .insert(idx, (point_tx_idx.clone(), key)); + }, } } } /// Process the revocation list. fn revocations_list( - revocation_list: Option>, point_tx_idx: &PointTxIdx, + revocation_list: Vec, point_tx_idx: &PointTxIdx, ) -> Vec<(PointTxIdx, CertKeyHash)> { let mut revocations = Vec::new(); - if let Some(revocations_data) = revocation_list { - for item in revocations_data { - revocations.push((point_tx_idx.clone(), item.clone())); - } + for item in revocation_list { + revocations.push((point_tx_idx.clone(), item.clone())); } revocations } /// Process the role data for chain root. fn chain_root_role_data( - role_set: Option>, txn: &MultiEraTx, - point_tx_idx: &PointTxIdx, + role_set: Vec, txn: &MultiEraTx, point_tx_idx: &PointTxIdx, ) -> anyhow::Result> { let mut role_data_map = HashMap::new(); - if let Some(role_set_data) = role_set { - for role_data in role_set_data { - let signing_key = role_data.role_signing_key.clone(); - let encryption_key = role_data.role_encryption_key.clone(); - - // Get the payment key - let payment_key = get_payment_addr_from_tx(txn, role_data.payment_key)?; - - // Map of role number to point and role data - role_data_map.insert( - role_data.role_number, - ( - point_tx_idx.clone(), - RoleData::new( - signing_key, - encryption_key, - payment_key, - role_data.role_extended_data_keys.clone(), - ), + for role_data in role_set { + let signing_key = role_data.role_signing_key.clone(); + let encryption_key = role_data.role_encryption_key.clone(); + + // Get the payment key + let payment_key = get_payment_addr_from_tx(txn, role_data.payment_key)?; + + // Map of role number to point and role data + role_data_map.insert( + role_data.role_number, + ( + point_tx_idx.clone(), + RoleData::new( + signing_key, + encryption_key, + payment_key, + role_data.role_extended_data_keys.clone(), ), - ); - } + ), + ); } Ok(role_data_map) } /// Update the role data in the registration chain. fn update_role_data( - inner: &mut RegistrationChainInner, role_set: Option>, + inner: &mut RegistrationChainInner, role_set: Vec, txn: &MultiEraTx, point_tx_idx: &PointTxIdx, ) -> anyhow::Result<()> { - if let Some(role_set_data) = role_set { - for role_data in role_set_data { - // If there is new role singing key, use it, else use the old one - let signing_key = match role_data.role_signing_key { - Some(key) => Some(key), - None => { - match inner.role_data.get(&role_data.role_number) { - Some((_, role_data)) => role_data.signing_key_ref().clone(), - None => None, - } - }, - }; - - // If there is new role encryption key, use it, else use the old one - let encryption_key = match role_data.role_encryption_key { - Some(key) => Some(key), - None => { - match inner.role_data.get(&role_data.role_number) { - Some((_, role_data)) => role_data.encryption_ref().clone(), - None => None, - } - }, - }; - let payment_key = get_payment_addr_from_tx(txn, role_data.payment_key)?; - - // Map of role number to point and role data - // Note that new role data will overwrite the old one - inner.role_data.insert( - role_data.role_number, - ( - point_tx_idx.clone(), - RoleData::new( - signing_key, - encryption_key, - payment_key, - role_data.role_extended_data_keys.clone(), - ), + for role_data in role_set { + // If there is new role singing key, use it, else use the old one + let signing_key = match role_data.role_signing_key { + Some(key) => Some(key), + None => { + match inner.role_data.get(&role_data.role_number) { + Some((_, role_data)) => role_data.signing_key_ref().clone(), + None => None, + } + }, + }; + + // If there is new role encryption key, use it, else use the old one + let encryption_key = match role_data.role_encryption_key { + Some(key) => Some(key), + None => { + match inner.role_data.get(&role_data.role_number) { + Some((_, role_data)) => role_data.encryption_ref().clone(), + None => None, + } + }, + }; + let payment_key = get_payment_addr_from_tx(txn, role_data.payment_key)?; + + // Map of role number to point and role data + // Note that new role data will overwrite the old one + inner.role_data.insert( + role_data.role_number, + ( + point_tx_idx.clone(), + RoleData::new( + signing_key, + encryption_key, + payment_key, + role_data.role_extended_data_keys.clone(), ), - ); - } + ), + ); } Ok(()) }