diff --git a/rust/rbac-registration/Cargo.toml b/rust/rbac-registration/Cargo.toml index c238f89a4f..33b0464035 100644 --- a/rust/rbac-registration/Cargo.toml +++ b/rust/rbac-registration/Cargo.toml @@ -34,5 +34,5 @@ thiserror = "2.0.11" c509-certificate = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "c509-certificate-v0.0.3" } cbork-utils = { version = "0.0.2", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "cbork-utils-v0.0.2" } -cardano-blockchain-types = { version = "0.0.8", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "cardano-blockchain-types/v0.0.8" } +cardano-blockchain-types = { version = "0.0.9", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "cardano-blockchain-types/v0.0.9" } catalyst-types = { version = "0.0.10", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "catalyst-types/v0.0.10" } diff --git a/rust/rbac-registration/src/cardano/cip509/cip509.rs b/rust/rbac-registration/src/cardano/cip509/cip509.rs index 053b090c09..783123884b 100644 --- a/rust/rbac-registration/src/cardano/cip509/cip509.rs +++ b/rust/rbac-registration/src/cardano/cip509/cip509.rs @@ -9,7 +9,7 @@ use std::{ use anyhow::{Context, anyhow}; use cardano_blockchain_types::{ - MetadatumLabel, MultiEraBlock, TxnIndex, + MetadatumLabel, MultiEraBlock, StakeAddress, TxnIndex, hashes::{BLAKE_2B256_SIZE, Blake2b256Hash, TransactionId}, pallas_addresses::{Address, ShelleyAddress}, pallas_primitives::{Nullable, conway}, @@ -22,6 +22,7 @@ use catalyst_types::{ uuid::UuidV4, }; use cbork_utils::decode_helper::{decode_bytes, decode_helper, decode_map_len}; +use ed25519_dalek::VerifyingKey; use minicbor::{ Decode, Decoder, decode::{self}, @@ -31,8 +32,9 @@ use tracing::warn; use uuid::Uuid; use crate::cardano::cip509::{ - Payment, PointTxnIdx, RoleData, + C509Cert, LocalRefInt, Payment, PointTxnIdx, RoleData, SimplePublicKeyType, X509DerCert, decode_context::DecodeContext, + extract_key, rbac::Cip509RbacMetadata, types::{PaymentHistory, TxInputHash, ValidationSignature}, utils::Cip0134UriSet, @@ -80,7 +82,8 @@ pub struct Cip509 { origin: PointTxnIdx, /// A catalyst ID. /// - /// This field is only present in role 0 registrations. + /// This field is only present in role 0 registrations and only for the first + /// registration, which defines a `CatalystId` for the chain. catalyst_id: Option, /// Raw aux data associated with the transaction that CIP509 is attached to, raw_aux_data: Vec, @@ -180,6 +183,12 @@ impl Cip509 { validate_self_sign_cert(metadata, &report); } + // We want to keep `catalyst_id` field only for the first registration, + // which starts a new chain + if cip509.prv_tx_id.is_some() { + cip509.catalyst_id = None; + } + Ok(Some(cip509)) } @@ -227,6 +236,48 @@ impl Cip509 { self.metadata.as_ref().and_then(|m| m.role_data.get(&role)) } + /// Returns signing public key for a role. + /// Would return only signing public keys for the present certificates, + /// if certificate marked as deleted or undefined it would be skipped. + #[must_use] + pub fn signing_public_key_for_role( + &self, + role: RoleId, + ) -> Option { + self.metadata.as_ref().and_then(|m| { + let key_ref = m.role_data.get(&role).and_then(|d| d.signing_key())?; + match key_ref.local_ref { + LocalRefInt::X509Certs => { + m.x509_certs.get(key_ref.key_offset).and_then(|c| { + if let X509DerCert::X509Cert(c) = c { + extract_key::x509_key(c).ok() + } else { + None + } + }) + }, + LocalRefInt::C509Certs => { + m.c509_certs.get(key_ref.key_offset).and_then(|c| { + if let C509Cert::C509Certificate(c) = c { + extract_key::c509_key(c).ok() + } else { + None + } + }) + }, + LocalRefInt::PubKeys => { + m.pub_keys.get(key_ref.key_offset).and_then(|c| { + if let SimplePublicKeyType::Ed25519(c) = c { + Some(*c) + } else { + None + } + }) + }, + } + }) + } + /// Returns a purpose of this registration. #[must_use] pub fn purpose(&self) -> Option { @@ -269,24 +320,13 @@ impl Cip509 { self.txn_inputs_hash.as_ref() } - /// Returns a Catalyst ID of this registration if role 0 is present. + /// Returns a Catalyst ID of this registration if role 0 is present and if its a first + /// registration, which defines a `CatalystId` for the chain. #[must_use] pub fn catalyst_id(&self) -> Option<&CatalystId> { self.catalyst_id.as_ref() } - /// Returns a list of addresses extracted from certificate URIs of a specific role. - #[must_use] - pub fn certificate_addresses( - &self, - role: usize, - ) -> HashSet
{ - self.metadata - .as_ref() - .map(|m| m.certificate_uris.role_addresses(role)) - .unwrap_or_default() - } - /// Return validation signature. #[must_use] pub fn validation_signature(&self) -> Option<&ValidationSignature> { @@ -305,26 +345,18 @@ impl Cip509 { self.metadata.as_ref() } - /// Returns `Cip509` fields consuming the structure if it was successfully decoded and - /// validated otherwise return the problem report that contains all the encountered - /// issues. - /// - /// # Errors - /// - /// - `Err(ProblemReport)` - pub fn consume(self) -> Result<(UuidV4, Cip509RbacMetadata, PaymentHistory), ProblemReport> { - match ( - self.purpose, - self.txn_inputs_hash, - self.metadata, - self.validation_signature, - ) { - (Some(purpose), Some(_), Some(metadata), Some(_)) if !self.report.is_problematic() => { - Ok((purpose, metadata, self.payment_history)) - }, + /// Returns a set of stake addresses. + #[must_use] + pub fn stake_addresses(&self) -> HashSet { + self.certificate_uris() + .map(Cip0134UriSet::stake_addresses) + .unwrap_or_default() + } - _ => Err(self.report), - } + /// Returns a payment history map. + #[must_use] + pub fn payment_history(&self) -> &PaymentHistory { + &self.payment_history } } diff --git a/rust/rbac-registration/src/cardano/cip509/rbac/metadata.rs b/rust/rbac-registration/src/cardano/cip509/rbac/metadata.rs index cd49c11974..3ce70c6a10 100644 --- a/rust/rbac-registration/src/cardano/cip509/rbac/metadata.rs +++ b/rust/rbac-registration/src/cardano/cip509/rbac/metadata.rs @@ -28,9 +28,9 @@ use crate::cardano::cip509::{ #[allow(clippy::module_name_repetitions)] pub struct Cip509RbacMetadata { /// A potentially empty list of x509 certificates. - pub x509_certs: Vec, + pub(crate) x509_certs: Vec, /// A potentially empty list of c509 certificates. - pub c509_certs: Vec, + pub(crate) c509_certs: Vec, /// A set of URIs contained in both x509 and c509 certificates. /// /// URIs from different certificate types are stored separately and certificate @@ -38,21 +38,21 @@ pub struct Cip509RbacMetadata { /// /// 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, + pub(crate) certificate_uris: Cip0134UriSet, /// A list of public keys that can be used instead of storing full certificates. /// /// Check [this section] to understand 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, + pub(crate) pub_keys: Vec, /// A potentially empty list of revoked certificates. - pub revocation_list: Vec, + pub(crate) revocation_list: Vec, /// A potentially empty role data. - pub role_data: HashMap, + pub(crate) role_data: HashMap, /// Optional map of purpose key data. /// Empty map if no purpose key data is present. - pub purpose_key_data: HashMap>, + pub(crate) purpose_key_data: HashMap>, } /// The first valid purpose key. diff --git a/rust/rbac-registration/src/cardano/cip509/types/cert_or_pk.rs b/rust/rbac-registration/src/cardano/cip509/types/cert_or_pk.rs index d697755ecb..b75dfe805c 100644 --- a/rust/rbac-registration/src/cardano/cip509/types/cert_or_pk.rs +++ b/rust/rbac-registration/src/cardano/cip509/types/cert_or_pk.rs @@ -21,7 +21,7 @@ pub enum CertOrPk { impl CertOrPk { /// Extract public key from the given certificate or public key. - pub(crate) fn extract_pk(&self) -> Option { + pub(crate) fn extract_public_key(&self) -> Option { match self { CertOrPk::X509(Some(x509)) => extract_key::x509_key(x509).ok(), CertOrPk::C509(Some(c509)) => extract_key::c509_key(c509).ok(), 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 index cafe9da0f7..a5359d6c9d 100644 --- a/rust/rbac-registration/src/cardano/cip509/utils/cip134_uri_set.rs +++ b/rust/rbac-registration/src/cardano/cip509/utils/cip134_uri_set.rs @@ -39,67 +39,78 @@ struct Cip0134UriSetInner { x_uris: UrisMap, /// URIs from c509 certificates. c_uris: UrisMap, + /// `StakeAddress` which are taken by another chains. + taken_stake_addresses: HashSet, } impl Cip0134UriSet { /// Creates a new `Cip0134UriSet` instance from the given certificates. #[must_use] - pub fn new( + pub(crate) fn new( x509_certs: &[X509DerCert], c509_certs: &[C509Cert], report: &ProblemReport, ) -> Self { let x_uris = extract_x509_uris(x509_certs, report); let c_uris = extract_c509_uris(c509_certs, report); - Self(Arc::new(Cip0134UriSetInner { x_uris, c_uris })) + let taken_stake_addresses = HashSet::new(); + Self(Arc::new(Cip0134UriSetInner { + x_uris, + c_uris, + taken_stake_addresses, + })) } /// Returns a mapping from the x509 certificate index to URIs contained within. #[must_use] - pub fn x_uris(&self) -> &UrisMap { + pub(crate) 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 { + pub(crate) fn c_uris(&self) -> &UrisMap { &self.0.c_uris } + /// Returns an iterator over of `Cip0134Uri`. + pub fn values(&self) -> impl Iterator { + self.x_uris() + .values() + .chain(self.c_uris().values()) + .flat_map(|uris| uris.iter()) + } + /// 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() } - /// Returns a list of addresses by the given role. - #[must_use] - pub fn role_addresses( + /// Returns a list of URIs by the given role. + pub(crate) fn role_uris( &self, role: usize, - ) -> HashSet
{ - let mut result = HashSet::new(); - - if let Some(uris) = self.x_uris().get(&role) { - result.extend(uris.iter().map(|uri| uri.address().clone())); - } - if let Some(uris) = self.c_uris().get(&role) { - result.extend(uris.iter().map(|uri| uri.address().clone())); - } - - result + ) -> impl Iterator { + let x_iter = self + .x_uris() + .get(&role) + .map_or_else(|| [].iter(), |uris| uris.iter()); + let c_iter = self + .c_uris() + .get(&role) + .map_or_else(|| [].iter(), |uris| uris.iter()); + x_iter.chain(c_iter) } - /// Returns a list of stake addresses by the given role. - #[must_use] - pub fn role_stake_addresses( + /// Returns a set of stake addresses by the given role. + pub(crate) fn role_stake_addresses( &self, role: usize, ) -> HashSet { - self.role_addresses(role) - .iter() - .filter_map(|address| { - match address { + self.role_uris(role) + .filter_map(|uri| { + match uri.address() { Address::Stake(a) => Some(a.clone().into()), _ => None, } @@ -107,19 +118,17 @@ impl Cip0134UriSet { .collect() } - /// Returns a list of all stake addresses. + /// Returns a set of all active (without taken) stake addresses. #[must_use] pub fn stake_addresses(&self) -> HashSet { - self.x_uris() - .values() - .chain(self.c_uris().values()) - .flat_map(|uris| uris.iter()) + self.values() .filter_map(|uri| { match uri.address() { Address::Stake(a) => Some(a.clone().into()), _ => None, } }) + .filter(|v| !self.0.taken_stake_addresses.contains(v)) .collect() } @@ -142,7 +151,7 @@ impl Cip0134UriSet { /// 2: [uri_4] /// ``` #[must_use] - pub fn update( + pub(crate) fn update( self, metadata: &Cip509RbacMetadata, ) -> Self { @@ -154,6 +163,7 @@ impl Cip0134UriSet { let Cip0134UriSetInner { mut x_uris, mut c_uris, + mut taken_stake_addresses, } = Arc::unwrap_or_clone(self.0); for (index, cert) in metadata.x509_certs.iter().enumerate() { @@ -191,7 +201,50 @@ impl Cip0134UriSet { } } - Self(Arc::new(Cip0134UriSetInner { x_uris, c_uris })) + metadata + .certificate_uris + .stake_addresses() + .iter() + .for_each(|v| { + taken_stake_addresses.remove(v); + }); + + Self(Arc::new(Cip0134UriSetInner { + x_uris, + c_uris, + taken_stake_addresses, + })) + } + + /// Return the updated URIs set where the provided URIs were taken by other + /// registration chains. + /// + /// Updates the current URI set by marking URIs as taken. + #[must_use] + pub(crate) fn update_taken_uris( + self, + reg: &Cip509RbacMetadata, + ) -> Self { + let current_stake_addresses = self.stake_addresses(); + let latest_taken_stake_addresses = reg + .certificate_uris + .stake_addresses() + .into_iter() + .filter(|v| current_stake_addresses.contains(v)); + + let Cip0134UriSetInner { + x_uris, + c_uris, + mut taken_stake_addresses, + } = Arc::unwrap_or_clone(self.0); + + taken_stake_addresses.extend(latest_taken_stake_addresses); + + Self(Arc::new(Cip0134UriSetInner { + x_uris, + c_uris, + taken_stake_addresses, + })) } } @@ -344,7 +397,7 @@ mod tests { let set = cip509.certificate_uris().unwrap(); assert!(!set.is_empty()); assert!(set.c_uris().is_empty()); - assert_eq!(set.role_addresses(0).len(), 1); + assert_eq!(set.role_uris(0).count(), 1); assert_eq!(set.role_stake_addresses(0).len(), 1); assert_eq!(set.stake_addresses().len(), 1); diff --git a/rust/rbac-registration/src/cardano/cip509/validation.rs b/rust/rbac-registration/src/cardano/cip509/validation.rs index 507cfaa767..711b7a651e 100644 --- a/rust/rbac-registration/src/cardano/cip509/validation.rs +++ b/rust/rbac-registration/src/cardano/cip509/validation.rs @@ -157,10 +157,7 @@ fn extract_stake_addresses(uris: Option<&Cip0134UriSet>) -> Vec<(VKeyHash, Strin return Vec::new(); }; - uris.x_uris() - .iter() - .chain(uris.c_uris()) - .flat_map(|(_index, uris)| uris.iter()) + uris.values() .filter_map(|uri| { if let Address::Stake(a) = uri.address() { let bech32 = uri.address().to_string(); @@ -185,10 +182,7 @@ fn extract_payment_addresses(uris: Option<&Cip0134UriSet>) -> Vec<(VKeyHash, Str return Vec::new(); }; - uris.x_uris() - .iter() - .chain(uris.c_uris()) - .flat_map(|(_index, uris)| uris.iter()) + uris.values() .filter_map(|uri| { if let Address::Shelley(a) = uri.address() { match a.payment() { @@ -593,8 +587,7 @@ mod tests { assert_eq!(origin.txn_index(), data.txn_index); assert_eq!(origin.point().slot_or_default(), data.slot); - // The consume function must return the problem report contained within the registration. - let report = registration.consume().unwrap_err(); + let report = registration.report(); assert!(report.is_problematic()); let report = format!("{report:?}"); assert!(report.contains("is not present in the transaction witness set, and can not be verified as owned and spendable")); @@ -616,7 +609,7 @@ mod tests { assert_eq!(origin.txn_index(), data.txn_index); assert_eq!(origin.point().slot_or_default(), data.slot); - let report = registration.consume().unwrap_err(); + let report = registration.report(); assert!(report.is_problematic()); let report = format!("{report:?}"); assert!( @@ -640,8 +633,7 @@ mod tests { assert_eq!(origin.txn_index(), data.txn_index); assert_eq!(origin.point().slot_or_default(), data.slot); - // The consume function must return the problem report contained within the registration. - let report = registration.consume().unwrap_err(); + let report = registration.report(); assert!(report.is_problematic()); let report = format!("{report:?}"); assert!(report.contains("Unknown role found: 4")); diff --git a/rust/rbac-registration/src/cardano/mod.rs b/rust/rbac-registration/src/cardano/mod.rs index 8af2182b83..11784792f4 100644 --- a/rust/rbac-registration/src/cardano/mod.rs +++ b/rust/rbac-registration/src/cardano/mod.rs @@ -1,3 +1,4 @@ //! Cardano module pub mod cip509; +pub mod state; diff --git a/rust/rbac-registration/src/cardano/state.rs b/rust/rbac-registration/src/cardano/state.rs new file mode 100644 index 0000000000..ac427421a6 --- /dev/null +++ b/rust/rbac-registration/src/cardano/state.rs @@ -0,0 +1,48 @@ +//! Cardano RBAC state traits, which are used during different stateful validation +//! procedures. + +use std::future::Future; + +use cardano_blockchain_types::StakeAddress; +use catalyst_types::catalyst_id::CatalystId; +use ed25519_dalek::VerifyingKey; + +use crate::registration::cardano::RegistrationChain; + +/// RBAC chains state trait +pub trait RbacChainsState { + /// Returns RBAC chain for the given Catalyst ID. + fn chain( + &self, + id: &CatalystId, + ) -> impl Future>> + Send; + + /// Returns `true` if a RBAC chain with the given Catalyst ID already exists. + fn is_chain_known( + &self, + id: &CatalystId, + ) -> impl Future> + Send; + + /// Returns `true` if a provided address already used by any RBAC chain. + fn is_stake_address_used( + &self, + addr: &StakeAddress, + ) -> impl Future> + Send; + + /// Returns a corresponding to the RBAC chain's Catalyst ID corresponding by the given + /// signing public key. + fn chain_catalyst_id_from_signing_public_key( + &self, + key: &VerifyingKey, + ) -> impl Future>> + Send; + + /// Update the chain by "taking" the given `StakeAddress` for the corresponding + /// RBAC chain's by the given `CatalystId`. + fn take_stake_address_from_chains( + &mut self, + addresses: I, + ) -> impl Future> + Send + where + I: IntoIterator + Send, + ::IntoIter: Send; +} diff --git a/rust/rbac-registration/src/registration/cardano/mod.rs b/rust/rbac-registration/src/registration/cardano/mod.rs index bfc95caa25..774724d6d8 100644 --- a/rust/rbac-registration/src/registration/cardano/mod.rs +++ b/rust/rbac-registration/src/registration/cardano/mod.rs @@ -22,9 +22,12 @@ use update_rbac::{ }; use x509_cert::certificate::Certificate as X509Certificate; -use crate::cardano::cip509::{ - CertKeyHash, CertOrPk, Cip0134UriSet, Cip509, PaymentHistory, PointData, RoleData, - RoleDataRecord, ValidationSignature, +use crate::cardano::{ + cip509::{ + CertKeyHash, CertOrPk, Cip0134UriSet, Cip509, PaymentHistory, PointData, PointTxnIdx, + RoleData, RoleDataRecord, ValidationSignature, + }, + state::RbacChainsState, }; /// Registration chains. @@ -37,17 +40,58 @@ pub struct RegistrationChain { } impl RegistrationChain { - /// Create a new instance of registration chain. - /// The first new value should be the chain root. + /// Attempts to initialize a new RBAC registration chain + /// from a given CIP-509 registration, ensuring uniqueness of Catalyst ID, stake + /// addresses, and associated public keys. /// - /// # Arguments - /// - `cip509` - The CIP509. + /// Returns `None` if the `cip509` is invalid by any reason, properly updating + /// `cip509.report()`. /// /// # Errors + /// - Propagates any I/O or provider-level errors encountered while checking key + /// ownership (e.g., database lookup failures). + pub async fn new( + cip509: &Cip509, + state: &mut State, + ) -> anyhow::Result> + where + State: RbacChainsState, + { + let Some(new_chain) = Self::new_stateless(cip509) else { + return Ok(None); + }; + + // Verify that a Catalyst ID of this chain is unique. + { + let cat_id = new_chain.catalyst_id(); + if state.is_chain_known(cat_id).await? { + cip509.report().functional_validation( + &format!("{} is already used", cat_id.as_short_id()), + "It isn't allowed to use same Catalyst ID (certificate subject public key) in multiple registration chains", + ); + } + + check_signing_public_key(cat_id, cip509, state).await?; + } + + if cip509.report().is_problematic() { + return Ok(None); + } + + state + .take_stake_address_from_chains(cip509.stake_addresses()) + .await?; + + Ok(Some(new_chain)) + } + + /// Create a new instance of registration chain. + /// The first new value should be the chain root. /// - /// Returns an error if data is invalid + /// Returns `None` if the `cip509` is invalid by any reason, properly updating + /// `cip509.report()`. #[must_use] - pub fn new(cip509: Cip509) -> Option { + pub fn new_stateless(cip509: &Cip509) -> Option { let inner = RegistrationChainInner::new(cip509)?; Some(Self { @@ -55,20 +99,59 @@ impl RegistrationChain { }) } - /// Update the registration chain. + /// Attempts to update an existing RBAC registration chain + /// with a new CIP-509 registration, validating address and key usage consistency. /// - /// # Arguments - /// - `cip509` - The CIP509. + /// Returns `None` if the `cip509` is invalid by any reason, properly updating + /// `cip509.report()`. /// /// # Errors + /// - Propagates any I/O or provider-level errors encountered while checking key + /// ownership (e.g., database lookup failures). + pub async fn update( + &self, + cip509: &Cip509, + state: &State, + ) -> anyhow::Result> + where + State: RbacChainsState, + { + let Some(new_chain) = self.update_stateless(cip509) else { + return Ok(None); + }; + + // Check that addresses from the new registration aren't used in other chains. + let previous_addresses = self.stake_addresses(); + let reg_addresses = cip509.stake_addresses(); + let new_addresses: Vec<_> = reg_addresses.difference(&previous_addresses).collect(); + for address in &new_addresses { + if state.is_stake_address_used(address).await? { + cip509.report().functional_validation( + &format!("{address} stake address is already used"), + "It isn't allowed to use same stake address in multiple registration chains, if its not a new chain", + ); + } + } + + check_signing_public_key(self.catalyst_id(), cip509, state).await?; + + if cip509.report().is_problematic() { + Ok(None) + } else { + Ok(Some(new_chain)) + } + } + + /// Update the registration chain. /// - /// Returns an error if data is invalid + /// Returns `None` if the `cip509` is invalid by any reason, properly updating + /// `cip509.report()`. #[must_use] - pub fn update( + pub fn update_stateless( &self, - cip509: Cip509, + cip509: &Cip509, ) -> Option { - let latest_signing_pk = self.get_latest_signing_pk_for_role(&RoleId::Role0); + let latest_signing_pk = self.get_latest_signing_public_key_for_role(RoleId::Role0); let new_inner = if let Some((signing_pk, _)) = latest_signing_pk { self.inner.update(cip509, signing_pk)? } else { @@ -164,17 +247,11 @@ impl RegistrationChain { /// Get the latest signing public key for a role. /// Returns the public key and the rotation,`None` if not found. #[must_use] - pub fn get_latest_signing_pk_for_role( + pub fn get_latest_signing_public_key_for_role( &self, - role: &RoleId, + role: RoleId, ) -> Option<(VerifyingKey, KeyRotation)> { - self.inner.role_data_record.get(role).and_then(|rdr| { - rdr.signing_keys().last().and_then(|key| { - let rotation = KeyRotation::from_latest_rotation(rdr.signing_keys()); - - key.data().extract_pk().map(|pk| (pk, rotation)) - }) - }) + self.inner.get_latest_signing_public_key_for_role(role) } /// Get the latest encryption public key for a role. @@ -182,15 +259,9 @@ impl RegistrationChain { #[must_use] pub fn get_latest_encryption_pk_for_role( &self, - role: &RoleId, + role: RoleId, ) -> Option<(VerifyingKey, KeyRotation)> { - self.inner.role_data_record.get(role).and_then(|rdr| { - rdr.encryption_keys().last().and_then(|key| { - let rotation = KeyRotation::from_latest_rotation(rdr.encryption_keys()); - - key.data().extract_pk().map(|pk| (pk, rotation)) - }) - }) + self.inner.get_latest_encryption_public_key_for_role(role) } /// Get signing public key for a role with given rotation. @@ -203,7 +274,7 @@ impl RegistrationChain { ) -> Option { self.inner.role_data_record.get(role).and_then(|rdr| { rdr.signing_key_from_rotation(rotation) - .and_then(CertOrPk::extract_pk) + .and_then(CertOrPk::extract_public_key) }) } @@ -217,7 +288,7 @@ impl RegistrationChain { ) -> Option { self.inner.role_data_record.get(role).and_then(|rdr| { rdr.encryption_key_from_rotation(rotation) - .and_then(CertOrPk::extract_pk) + .and_then(CertOrPk::extract_public_key) }) } @@ -249,11 +320,23 @@ impl RegistrationChain { .and_then(|rdr| rdr.encryption_key_from_rotation(rotation)) } - /// Returns all stake addresses associated to this registration. + /// Returns most recent URIs contained from both x509 and c509 certificates. + #[must_use] + pub fn certificate_uris(&self) -> &Cip0134UriSet { + &self.inner.certificate_uris + } + + /// Returns all stake addresses associated to this chain. #[must_use] pub fn stake_addresses(&self) -> HashSet { self.inner.certificate_uris.stake_addresses() } + + /// Returns the latest know applied registration's `PointTxnIdx`. + #[must_use] + pub fn latest_applied(&self) -> PointTxnIdx { + self.inner.latest_applied() + } } /// Inner structure of registration chain. @@ -263,6 +346,9 @@ struct RegistrationChainInner { catalyst_id: CatalystId, /// The current transaction ID hash (32 bytes) current_tx_id_hash: PointData, + /// The latest `PointTxnIdx` of the taken URIs by another registration chains. + latest_taken_uris_point: Option, + /// List of purpose for this registration chain purpose: Vec, @@ -298,12 +384,8 @@ impl RegistrationChainInner { /// /// # Arguments /// - `cip509` - The CIP509. - /// - /// # Errors - /// - /// Returns an error if data is invalid #[must_use] - fn new(cip509: Cip509) -> Option { + fn new(cip509: &Cip509) -> Option { let context = "Registration Chain new"; // Should be chain root, return immediately if not if cip509.previous_transaction().is_some() { @@ -316,23 +398,21 @@ impl RegistrationChainInner { return None; }; - let point_tx_idx = cip509.origin().clone(); - let current_tx_id_hash = PointData::new(point_tx_idx.clone(), cip509.txn_hash()); - let validation_signature = cip509.validation_signature().cloned(); - let raw_aux_data = cip509.raw_aux_data().to_vec(); + let Some(registration) = cip509.metadata() else { + cip509.report().missing_field("metadata", context); + return None; + }; // Role data let mut role_data_history = HashMap::new(); let mut role_data_record = HashMap::new(); - - if let Some(registration) = cip509.metadata() { - update_role_data( - registration, - &mut role_data_history, - &mut role_data_record, - &point_tx_idx, - ); - } + let point_tx_idx = cip509.origin().clone(); + update_role_data( + registration, + &mut role_data_history, + &mut role_data_record, + &point_tx_idx, + ); // There should be role 0 since we already check that the chain root (no previous tx id) // must contain role 0 @@ -343,7 +423,7 @@ impl RegistrationChainInner { let Some(signing_pk) = role0_data .signing_keys() .last() - .and_then(|key| key.data().extract_pk()) + .and_then(|key| key.data().extract_public_key()) else { cip509 .report() @@ -352,17 +432,26 @@ impl RegistrationChainInner { }; check_validation_signature( - validation_signature, - &raw_aux_data, + cip509.validation_signature(), + cip509.raw_aux_data(), signing_pk, cip509.report(), context, ); - let Ok((purpose, registration, payment_history)) = cip509.consume() else { + if cip509.txn_inputs_hash().is_none() { + cip509.report().missing_field("txn inputs hash", context); + } + + let Some(purpose) = cip509.purpose() else { + cip509.report().missing_field("purpose", context); return None; }; + if cip509.report().is_problematic() { + return None; + } + let purpose = vec![purpose]; let certificate_uris = registration.certificate_uris.clone(); let mut x509_certs = HashMap::new(); @@ -384,6 +473,8 @@ impl RegistrationChainInner { &point_tx_idx, ); let revocations = revocations_list(registration.revocation_list.clone(), &point_tx_idx); + let current_tx_id_hash = PointData::new(point_tx_idx, cip509.txn_hash()); + let payment_history = cip509.payment_history().clone(); Some(Self { catalyst_id, @@ -392,6 +483,7 @@ impl RegistrationChainInner { x509_certs, c509_certs, certificate_uris, + latest_taken_uris_point: None, simple_keys, revocations, role_data_history, @@ -404,20 +496,42 @@ impl RegistrationChainInner { /// /// # Arguments /// - `cip509` - The CIP509. - /// - /// # Errors - /// - /// Returns an error if data is invalid #[must_use] fn update( &self, - cip509: Cip509, + cip509: &Cip509, signing_pk: VerifyingKey, ) -> Option { let context = "Registration Chain update"; + if self.latest_applied().point() >= cip509.origin().point() { + cip509.report().functional_validation( + &format!( + "The provided registration is earlier {} than the latest applied one {}", + cip509.origin().point(), + self.current_tx_id_hash.point() + ), + "Registrations must be applied in the correct ascending order.", + ); + return None; + } + let mut new_inner = self.clone(); let Some(prv_tx_id) = cip509.previous_transaction() else { + if let Some(cat_id) = cip509.catalyst_id() { + if cat_id == &self.catalyst_id { + cip509.report().functional_validation( + &format!( + "Trying to apply the first registration to the associated {} again", + cat_id.as_short_id() + ), + "It isn't allowed to submit first registration twice", + ); + return None; + } + + return Some(new_inner.update_cause_another_chain(cip509)); + } cip509 .report() .missing_field("previous transaction ID", context); @@ -429,7 +543,7 @@ impl RegistrationChainInner { // Perform signature validation // This should be done before updating the signing key check_validation_signature( - cip509.validation_signature().cloned(), + cip509.validation_signature(), cip509.raw_aux_data(), signing_pk, cip509.report(), @@ -449,18 +563,34 @@ impl RegistrationChainInner { return None; } - let point_tx_idx = cip509.origin().clone(); - let Ok((purpose, registration, payment_history)) = cip509.consume() else { + if cip509.txn_inputs_hash().is_none() { + cip509.report().missing_field("txn inputs hash", context); + } + + let Some(purpose) = cip509.purpose() else { + cip509.report().missing_field("purpose", context); return None; }; + let Some(registration) = cip509.metadata().cloned() else { + cip509.report().missing_field("metadata", context); + return None; + }; + + if cip509.report().is_problematic() { + return None; + } // Add purpose to the chain, if not already exist if !self.purpose.contains(&purpose) { new_inner.purpose.push(purpose); } + let point_tx_idx = cip509.origin().clone(); + new_inner.certificate_uris = new_inner.certificate_uris.update(®istration); - new_inner.payment_history.extend(payment_history); + new_inner + .payment_history + .extend(cip509.payment_history().clone()); update_x509_certs( &mut new_inner.x509_certs, registration.x509_certs.clone(), @@ -490,12 +620,78 @@ impl RegistrationChainInner { Some(new_inner) } + + /// Update the registration chain with the `cip509` associated to another chain. + /// This is the case when registration for different chain affecting the current one, + /// by invalidating some data for the current registration chain (stealing stake + /// addresses etc.). + /// + /// The provided `cip509` should be fully validated by another chain before trying to + /// submit to the current one. + #[must_use] + fn update_cause_another_chain( + mut self, + cip509: &Cip509, + ) -> Self { + if let Some(reg) = cip509.metadata() { + self.certificate_uris = self.certificate_uris.update_taken_uris(reg); + } + self.latest_taken_uris_point = Some(cip509.origin().clone()); + self + } + + /// Get the latest signing public key for a role. + /// Returns the public key and the rotation,`None` if not found. + #[must_use] + pub fn get_latest_signing_public_key_for_role( + &self, + role: RoleId, + ) -> Option<(VerifyingKey, KeyRotation)> { + self.role_data_record.get(&role).and_then(|rdr| { + rdr.signing_keys().last().and_then(|key| { + let rotation = KeyRotation::from_latest_rotation(rdr.signing_keys()); + + key.data().extract_public_key().map(|pk| (pk, rotation)) + }) + }) + } + + /// Get the latest encryption public key for a role. + /// Returns the public key and the rotation, `None` if not found. + #[must_use] + pub fn get_latest_encryption_public_key_for_role( + &self, + role: RoleId, + ) -> Option<(VerifyingKey, KeyRotation)> { + self.role_data_record.get(&role).and_then(|rdr| { + rdr.encryption_keys().last().and_then(|key| { + let rotation = KeyRotation::from_latest_rotation(rdr.encryption_keys()); + + key.data().extract_public_key().map(|pk| (pk, rotation)) + }) + }) + } + + /// Returns the latest know applied registration's `PointTxnIdx`. + #[must_use] + fn latest_applied(&self) -> PointTxnIdx { + if let Some(latest_taken_uris_point) = &self.latest_taken_uris_point + && latest_taken_uris_point.point() > self.current_tx_id_hash.point() + { + return latest_taken_uris_point.clone(); + } + + PointTxnIdx::new( + self.current_tx_id_hash.point().clone(), + self.current_tx_id_hash.txn_index(), + ) + } } /// Perform a check on the validation signature. /// The auxiliary data should be sign with the latest signing public key. fn check_validation_signature( - validation_signature: Option, + validation_signature: Option<&ValidationSignature>, raw_aux_data: &[u8], signing_pk: VerifyingKey, report: &ProblemReport, @@ -533,6 +729,33 @@ fn check_validation_signature( } } +/// Checks that a new registration doesn't contain a signing key that was used by any +/// other chain. +async fn check_signing_public_key( + cat_id: &CatalystId, + cip509: &Cip509, + state: &State, +) -> anyhow::Result<()> +where + State: RbacChainsState, +{ + for role in cip509.all_roles() { + if let Some(key) = cip509.signing_public_key_for_role(role) + && let Some(previous) = state + .chain_catalyst_id_from_signing_public_key(&key) + .await? + && &previous != cat_id + { + cip509.report().functional_validation( + &format!("An update to {cat_id} registration chain uses the same public key ({key:?}) as {previous} chain"), + "It isn't allowed to use role 0 signing (certificate subject public) key in different chains", + ); + } + } + + Ok(()) +} + #[cfg(test)] mod test { use catalyst_types::catalyst_id::role_index::RoleId; @@ -548,8 +771,9 @@ mod test { .unwrap(); data.assert_valid(®istration); + // Performs only stateless validations // Create a chain with the first registration. - let chain = RegistrationChain::new(registration).unwrap(); + let chain = RegistrationChain::new_stateless(®istration).unwrap(); assert_eq!(chain.purpose(), &[data.purpose]); assert_eq!(1, chain.x509_certs().len()); let origin = &chain.x509_certs().get(&0).unwrap().first().unwrap(); @@ -580,13 +804,8 @@ mod test { assert!(registration.report().is_problematic()); let report = registration.report().to_owned(); - assert!(chain.update(registration).is_none()); - let report = format!("{report:?}"); - assert!( - report.contains("kind: InvalidValue { field: \"previous transaction ID\""), - "{}", - report - ); + assert!(chain.update_stateless(®istration).is_none()); + assert!(report.is_problematic(), "{report:?}"); // Add the second registration. let data = test::block_6(); @@ -594,7 +813,7 @@ mod test { .unwrap() .unwrap(); data.assert_valid(®istration); - let update = chain.update(registration).unwrap(); + let update = chain.update_stateless(®istration).unwrap(); // Current tx hash should be equal to the hash from block 4. assert_eq!(update.current_tx_id_hash(), data.txn_hash); assert!(update.role_data_record().contains_key(&data.role)); @@ -618,7 +837,7 @@ mod test { assert_eq!(role_0_data.extended_data().len(), 2); let (_k, r) = update - .get_latest_signing_pk_for_role(&RoleId::Role0) + .get_latest_signing_public_key_for_role(RoleId::Role0) .unwrap(); assert_eq!(r, KeyRotation::from(1)); assert!( diff --git a/rust/rbac-registration/src/utils/test.rs b/rust/rbac-registration/src/utils/test.rs index 9abae4616b..aa1b7da352 100644 --- a/rust/rbac-registration/src/utils/test.rs +++ b/rust/rbac-registration/src/utils/test.rs @@ -48,8 +48,7 @@ impl BlockTestData { assert!(cip509.role_data(self.role).is_some()); assert_eq!(cip509.txn_hash(), self.txn_hash); assert_eq!(cip509.previous_transaction(), self.prv_hash); - let (purpose, ..) = cip509.clone().consume().unwrap(); - assert_eq!(purpose, self.purpose); + assert_eq!(cip509.purpose().unwrap(), self.purpose); } }