diff --git a/rust/rbac-registration/Cargo.toml b/rust/rbac-registration/Cargo.toml index d999c52fba..c818427c5f 100644 --- a/rust/rbac-registration/Cargo.toml +++ b/rust/rbac-registration/Cargo.toml @@ -32,6 +32,6 @@ uuid = "1.11.0" c509-certificate = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "v0.0.3" } pallas = { version = "0.30.1", git = "https://github.com/input-output-hk/catalyst-pallas.git", rev = "9b5183c8b90b90fe2cc319d986e933e9518957b3" } -cbork-utils = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250127-00" } +cbork-utils = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250212-00" } cardano-blockchain-types = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250214-00" } -catalyst-types = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250127-00" } +catalyst-types = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250212-00" } diff --git a/rust/rbac-registration/src/cardano/cip509/cip509.rs b/rust/rbac-registration/src/cardano/cip509/cip509.rs index 086ca39ea3..b04ec7925e 100644 --- a/rust/rbac-registration/src/cardano/cip509/cip509.rs +++ b/rust/rbac-registration/src/cardano/cip509/cip509.rs @@ -5,10 +5,11 @@ use std::{borrow::Cow, collections::HashMap}; use anyhow::{anyhow, Context}; -use cardano_blockchain_types::{MetadatumLabel, MultiEraBlock, TxnIndex}; +use cardano_blockchain_types::{MetadatumLabel, MultiEraBlock, TransactionHash, TxnIndex}; use catalyst_types::{ cbor_utils::{report_duplicated_key, report_missing_keys}, hashes::{Blake2b256Hash, BLAKE_2B256_SIZE}, + id_uri::IdUri, problem_report::ProblemReport, uuid::UuidV4, }; @@ -59,7 +60,7 @@ pub struct Cip509 { /// An optional hash of the previous transaction. /// /// The hash must always be present except for the first registration transaction. - prv_tx_id: Option, + prv_tx_id: Option, /// Metadata. /// /// This field encoded in chunks. See [`X509Chunks`] for more details. @@ -72,10 +73,14 @@ pub struct Cip509 { /// constructors. payment_history: PaymentHistory, /// A hash of the transaction from which this registration is extracted. - txn_hash: Blake2b256Hash, + txn_hash: TransactionHash, /// A point (slot) and a transaction index identifying the block and the transaction /// that this `Cip509` was extracted from. origin: PointTxnIdx, + /// A catalyst ID. + /// + /// This field is only present in role 0 registrations. + catalyst_id: Option, /// A report potentially containing all the issues occurred during `Cip509` decoding /// and validation. /// @@ -139,7 +144,7 @@ impl Cip509 { payment_history, report: &mut report, }; - let cip509 = + let mut cip509 = Cip509::decode(&mut decoder, &mut decode_context).context("Failed to decode Cip509")?; // Perform the validation. @@ -156,7 +161,7 @@ impl Cip509 { validate_stake_public_key(txn, cip509.certificate_uris(), &cip509.report); } if let Some(metadata) = &cip509.metadata { - validate_role_data(metadata, &cip509.report); + cip509.catalyst_id = validate_role_data(metadata, &cip509.report); } Ok(Some(cip509)) @@ -210,7 +215,7 @@ impl Cip509 { /// Returns a hash of the previous transaction. #[must_use] - pub fn previous_transaction(&self) -> Option { + pub fn previous_transaction(&self) -> Option { self.prv_tx_id } @@ -228,7 +233,7 @@ impl Cip509 { /// Returns a hash of the transaction where this data is originating from. #[must_use] - pub fn txn_hash(&self) -> Blake2b256Hash { + pub fn txn_hash(&self) -> TransactionHash { self.txn_hash } @@ -244,6 +249,12 @@ impl Cip509 { self.txn_inputs_hash.as_ref() } + /// Returns a Catalyst ID of this registration if role 0 is present. + #[must_use] + pub fn catalyst_id(&self) -> Option<&IdUri> { + self.catalyst_id.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. @@ -370,9 +381,10 @@ impl Decode<'_, DecodeContext<'_, '_>> for Cip509 { .missing_field("metadata (10, 11 or 12 chunks)", context); } - let txn_hash = MultiEraTx::Conway(Box::new(Cow::Borrowed(decode_context.txn))) - .hash() - .into(); + let txn_hash = Blake2b256Hash::from( + MultiEraTx::Conway(Box::new(Cow::Borrowed(decode_context.txn))).hash(), + ) + .into(); Ok(Self { purpose, txn_inputs_hash, @@ -382,6 +394,7 @@ impl Decode<'_, DecodeContext<'_, '_>> for Cip509 { payment_history: HashMap::new(), txn_hash, origin: decode_context.origin.clone(), + catalyst_id: None, report: decode_context.report.clone(), }) } @@ -501,7 +514,7 @@ fn decode_input_hash( /// Decodes previous transaction id. fn decode_previous_transaction_id( d: &mut Decoder, context: &str, report: &ProblemReport, -) -> Result, ()> { +) -> Result, ()> { let bytes = match decode_bytes(d, "Cip509 previous transaction id") { Ok(v) => v, Err(e) => { @@ -515,7 +528,7 @@ fn decode_previous_transaction_id( let len = bytes.len(); if let Ok(v) = Blake2b256Hash::try_from(bytes) { - Ok(Some(v)) + Ok(Some(v.into())) } else { report.invalid_value( "previous transaction hash", diff --git a/rust/rbac-registration/src/cardano/cip509/validation.rs b/rust/rbac-registration/src/cardano/cip509/validation.rs index aec646aba2..885e23f846 100644 --- a/rust/rbac-registration/src/cardano/cip509/validation.rs +++ b/rust/rbac-registration/src/cardano/cip509/validation.rs @@ -6,11 +6,14 @@ use std::borrow::Cow; +use c509_certificate::c509::C509; use cardano_blockchain_types::{TxnWitness, VKeyHash}; use catalyst_types::{ hashes::{Blake2b128Hash, Blake2b256Hash}, + id_uri::IdUri, problem_report::ProblemReport, }; +use ed25519_dalek::{VerifyingKey, PUBLIC_KEY_LENGTH}; use pallas::{ codec::{ minicbor::{Encode, Encoder}, @@ -18,6 +21,7 @@ use pallas::{ }, ledger::{addresses::Address, primitives::conway, traverse::MultiEraTx}, }; +use x509_cert::Certificate; use super::utils::cip19::compare_key_hash; use crate::cardano::cip509::{ @@ -158,9 +162,9 @@ fn extract_stake_addresses(uris: Option<&Cip0134UriSet>) -> Vec { .collect() } -/// Checks that only role 0 uses certificates with zero index. +/// Checks the role data. #[allow(clippy::similar_names)] -pub fn validate_role_data(metadata: &Cip509RbacMetadata, report: &ProblemReport) { +pub fn validate_role_data(metadata: &Cip509RbacMetadata, report: &ProblemReport) -> Option { let context = "Role data validation"; if metadata.role_data.contains_key(&RoleNumber::ROLE_0) { @@ -226,9 +230,10 @@ pub fn validate_role_data(metadata: &Cip509RbacMetadata, report: &ProblemReport) ); } + let mut catalyst_id = None; for (number, data) in &metadata.role_data { if number == &RoleNumber::ROLE_0 { - validate_role_0(data, metadata, context, report); + catalyst_id = validate_role_0(data, metadata, context, report); } else { if let Some(signing_key) = data.signing_key() { if signing_key.key_offset == 0 { @@ -252,12 +257,13 @@ pub fn validate_role_data(metadata: &Cip509RbacMetadata, report: &ProblemReport) } } } + catalyst_id } /// Checks that the role 0 data is correct. fn validate_role_0( role: &RoleData, metadata: &Cip509RbacMetadata, context: &str, report: &ProblemReport, -) { +) -> Option { if let Some(key) = role.encryption_key() { report.invalid_value( "Role 0 encryption key", @@ -269,7 +275,7 @@ fn validate_role_0( let Some(signing_key) = role.signing_key() else { report.missing_field("(Role 0) RoleData::signing_key", context); - return; + return None; }; if signing_key.key_offset != 0 { @@ -277,14 +283,18 @@ fn validate_role_0( &format!("The role 0 must reference a certificate with 0 index ({role:?})"), context, ); - return; + return None; } + let mut catalyst_id = None; + let network = "cardano"; + match signing_key.local_ref { LocalRefInt::X509Certs => { match metadata.x509_certs.first() { - Some(X509DerCert::X509Cert(_)) => { + Some(X509DerCert::X509Cert(cert)) => { // All good: role 0 references a valid X509 certificate. + catalyst_id = x509_cert_key(cert, context, report).map(|k| IdUri::new(network, None, k)); } Some(c) => report.other(&format!("Invalid X509 certificate value ({c:?}) for role 0 ({role:?})"), context), None => report.other("Role 0 reference X509 certificate at index 0, but there is no such certificate", context), @@ -292,8 +302,9 @@ fn validate_role_0( }, LocalRefInt::C509Certs => { match metadata.c509_certs.first() { - Some(C509Cert::C509Certificate(_)) => { + Some(C509Cert::C509Certificate(cert)) => { // All good: role 0 references a valid C509 certificate. + catalyst_id = c509_cert_key(cert, context, report).map(|k| IdUri::new(network, None, k)); } Some(c) => report.other(&format!("Invalid C509 certificate value ({c:?}) for role 0 ({role:?})"), context), None => report.other("Role 0 reference C509 certificate at index 0, but there is no such certificate", context), @@ -308,6 +319,77 @@ fn validate_role_0( ); }, } + catalyst_id +} + +/// Extracts `VerifyingKey` from the given `X509` certificate. +fn x509_cert_key( + cert: &Certificate, context: &str, report: &ProblemReport, +) -> Option { + let Some(extended_public_key) = cert + .tbs_certificate + .subject_public_key_info + .subject_public_key + .as_bytes() + else { + report.invalid_value( + "subject_public_key", + "is not octet aligned", + "Must not have unused bits", + context, + ); + return None; + }; + verifying_key(extended_public_key, context, report) +} + +/// Extracts `VerifyingKey` from the given `C509` certificate. +fn c509_cert_key(cert: &C509, context: &str, report: &ProblemReport) -> Option { + verifying_key(cert.tbs_cert().subject_public_key(), context, report) +} + +/// Creates `VerifyingKey` from the given extended public key. +fn verifying_key( + extended_public_key: &[u8], context: &str, report: &ProblemReport, +) -> Option { + /// An extender public key length in bytes. + const EXTENDED_PUBLIC_KEY_LENGTH: usize = 64; + + if extended_public_key.len() != EXTENDED_PUBLIC_KEY_LENGTH { + report.other( + &format!("Unexpected extended public key length in certificate: {}, expected {EXTENDED_PUBLIC_KEY_LENGTH}", + extended_public_key.len()), + context, + ); + return None; + } + + // This should never fail because of the check above. + let Some(public_key) = extended_public_key.get(0..PUBLIC_KEY_LENGTH) else { + report.other("Unable to get public key part", context); + return None; + }; + + let bytes: &[u8; PUBLIC_KEY_LENGTH] = match public_key.try_into() { + Ok(v) => v, + Err(e) => { + report.other( + &format!("Invalid public key length in X509 certificate: {e:?}"), + context, + ); + return None; + }, + }; + match VerifyingKey::from_bytes(bytes) { + Ok(k) => Some(k), + Err(e) => { + report.other( + &format!("Invalid public key in C509 certificate: {e:?}"), + context, + ); + None + }, + } } #[cfg(test)] diff --git a/rust/rbac-registration/src/registration/cardano/mod.rs b/rust/rbac-registration/src/registration/cardano/mod.rs index 8d562944a4..0ac95fb4a1 100644 --- a/rust/rbac-registration/src/registration/cardano/mod.rs +++ b/rust/rbac-registration/src/registration/cardano/mod.rs @@ -4,7 +4,8 @@ use std::{collections::HashMap, sync::Arc}; use anyhow::bail; use c509_certificate::c509::C509; -use catalyst_types::{hashes::Blake2b256Hash, uuid::UuidV4}; +use cardano_blockchain_types::TransactionHash; +use catalyst_types::uuid::UuidV4; use ed25519_dalek::VerifyingKey; use tracing::{error, warn}; use x509_cert::certificate::Certificate as X509Certificate; @@ -57,7 +58,7 @@ impl RegistrationChain { /// Get the current transaction ID hash. #[must_use] - pub fn current_tx_id_hash(&self) -> Blake2b256Hash { + pub fn current_tx_id_hash(&self) -> TransactionHash { self.inner.current_tx_id_hash } @@ -108,7 +109,7 @@ impl RegistrationChain { #[derive(Debug, Clone)] struct RegistrationChainInner { /// The current transaction ID hash (32 bytes) - current_tx_id_hash: Blake2b256Hash, + current_tx_id_hash: TransactionHash, /// List of purpose for this registration chain purpose: Vec, diff --git a/rust/rbac-registration/src/utils/test.rs b/rust/rbac-registration/src/utils/test.rs index 4d8b141da5..4a89c9ce94 100644 --- a/rust/rbac-registration/src/utils/test.rs +++ b/rust/rbac-registration/src/utils/test.rs @@ -2,8 +2,8 @@ // cspell: words stake_test1urs8t0ssa3w9wh90ld5tprp3gurxd487rth2qlqk6ernjqcef4ugr -use cardano_blockchain_types::{MultiEraBlock, Network, Point, Slot, TxnIndex}; -use catalyst_types::{hashes::Blake2b256Hash, uuid::UuidV4}; +use cardano_blockchain_types::{MultiEraBlock, Network, Point, Slot, TransactionHash, TxnIndex}; +use catalyst_types::uuid::UuidV4; use uuid::Uuid; use crate::cardano::cip509::{Cip509, RoleNumber}; @@ -20,9 +20,9 @@ pub struct BlockTestData { /// Transaction index. pub txn_index: TxnIndex, /// Transaction hash. - pub txn_hash: Blake2b256Hash, + pub txn_hash: TransactionHash, /// Previous hash. - pub prv_hash: Option, + pub prv_hash: Option, /// Purpose. pub purpose: UuidV4, /// Stake address.