From 149bf11e91cc2a954ac6b70d7871bfa58703b57d Mon Sep 17 00:00:00 2001 From: bkioshn <35752733+bkioshn@users.noreply.github.com> Date: Mon, 2 Dec 2024 22:41:34 +0700 Subject: [PATCH] feat(rust/rbac-registration): RBAC registration (#85) * feat: move cip509 from cardano chain follower Signed-off-by: bkioshn * fix: remove key ref and use key local ref Signed-off-by: bkioshn * fix: structure of x509, c509, and public key Signed-off-by: bkioshn * fix: validation Signed-off-by: bkioshn * fix: update x509 and c509 certs Signed-off-by: bkioshn * fix: key local ref * fix: c509 cert Signed-off-by: bkioshn * fix: to string Signed-off-by: bkioshn * fix: rearrange utils Signed-off-by: bkioshn * fix: cargo toml Signed-off-by: bkioshn * fix: add new type + comment Signed-off-by: bkioshn * fix: linter Signed-off-by: bkioshn * fix: smart pointer Signed-off-by: bkioshn * chore: fix format Signed-off-by: bkioshn * fix: cleanup cargo.toml Signed-off-by: bkioshn * fix: add rbac-registration to earthly rust * fix: move cip509 validation Signed-off-by: bkioshn * fix: rust ci Signed-off-by: bkioshn * feat: add role signing key validation Signed-off-by: bkioshn * fix: add Bytes conversion for Pubkey Signed-off-by: bkioshn * fix: error return * fix: use Hash from pallas for tx hash Signed-off-by: bkioshn * fix: format Signed-off-by: bkioshn * fix: bytes to pubkey conversion * fix: add Bytes conversion for Pubkey Signed-off-by: bkioshn * fix: add rbac-reg to semantic pull request * fix: test data Signed-off-by: bkioshn * fix: signing and encryption key Signed-off-by: bkioshn * fix: format Signed-off-by: bkioshn * fix: witness test data to be compatible with new test data Signed-off-by: bkioshn * fix: validate stake public key witness and add test for role signing key Signed-off-by: bkioshn * chore: doc about context specific Signed-off-by: bkioshn * chore: fix format Signed-off-by: bkioshn * fix: remove test since cip509 decode are done in validation test * fix: test * fix: test compression data Signed-off-by: bkioshn * feat(rust/rbac-registration): add cardano registration chain (#90) * feat: add cardano registration chain Signed-off-by: bkioshn * feat: add wrapper to registration chain inner Signed-off-by: bkioshn * fix: format Signed-off-by: bkioshn * fix: comment Signed-off-by: bkioshn * fix: signing and encryption key Signed-off-by: bkioshn * chore: fix format Signed-off-by: bkioshn * fix: move struct to its own file and private the inner regis Signed-off-by: bkioshn --------- Signed-off-by: bkioshn * fix: remove txn index and change test data to chain root Signed-off-by: bkioshn * chore: fix comment Signed-off-by: bkioshn * fix: remove tx index from cip-509 validate function Signed-off-by: bkioshn * fix: test data Signed-off-by: bkioshn * chore: fix format * fix: remove txn index from validate Signed-off-by: bkioshn * fix: format * fix(rust/rbac-registration): Tracking payment key type (#93) * fix: payment key shelley address and add test for registration chain Signed-off-by: bkioshn * chore: comment * fix: format * fix: use ShelleyPaymentPart Signed-off-by: bkioshn * fix: track ShelleyAddress Signed-off-by: bkioshn * fix: remove tracking_payment_key and use tracking_payment_history Signed-off-by: bkioshn --------- Signed-off-by: bkioshn Co-authored-by: Steven Johnson --------- Signed-off-by: bkioshn Co-authored-by: Steven Johnson --- .github/workflows/semantic_pull_request.yml | 1 + rust/Cargo.toml | 1 + rust/Earthfile | 3 +- rust/rbac-registration/Cargo.toml | 36 + .../src/cardano/cip509/mod.rs | 218 ++++++ .../src/cardano/cip509/rbac/certs.rs | 137 ++++ .../src/cardano/cip509/rbac/mod.rs | 178 +++++ .../src/cardano/cip509/rbac/pub_key.rs | 87 +++ .../src/cardano/cip509/rbac/role_data.rs | 114 +++ .../src/cardano/cip509/rbac/tag.rs | 21 + .../src/cardano/cip509/utils/cip19.rs | 137 ++++ .../src/cardano/cip509/utils/mod.rs | 3 + .../src/cardano/cip509/validation.rs | 664 +++++++++++++++++ .../src/cardano/cip509/x509_chunks.rs | 122 ++++ rust/rbac-registration/src/cardano/mod.rs | 4 + .../src/cardano/transaction/mod.rs | 4 + .../src/cardano/transaction/raw_aux_data.rs | 273 +++++++ .../src/cardano/transaction/witness.rs | 123 ++++ rust/rbac-registration/src/lib.rs | 5 + .../src/registration/cardano/mod.rs | 685 ++++++++++++++++++ .../registration/cardano/payment_history.rs | 56 ++ .../src/registration/cardano/point_tx_idx.rs | 26 + .../src/registration/cardano/role_data.rs | 59 ++ .../rbac-registration/src/registration/mod.rs | 3 + .../src/test_data/cardano/conway_1.block | 1 + .../src/test_data/cardano/conway_2.block | 1 + .../src/test_data/cardano/conway_3.block | 1 + .../src/test_data/cardano/conway_4.block | 1 + .../src/utils/decode_helper.rs | 214 ++++++ rust/rbac-registration/src/utils/general.rs | 27 + rust/rbac-registration/src/utils/hashing.rs | 27 + rust/rbac-registration/src/utils/mod.rs | 5 + 32 files changed, 3236 insertions(+), 1 deletion(-) create mode 100644 rust/rbac-registration/Cargo.toml create mode 100644 rust/rbac-registration/src/cardano/cip509/mod.rs create mode 100644 rust/rbac-registration/src/cardano/cip509/rbac/certs.rs create mode 100644 rust/rbac-registration/src/cardano/cip509/rbac/mod.rs create mode 100644 rust/rbac-registration/src/cardano/cip509/rbac/pub_key.rs create mode 100644 rust/rbac-registration/src/cardano/cip509/rbac/role_data.rs create mode 100644 rust/rbac-registration/src/cardano/cip509/rbac/tag.rs create mode 100644 rust/rbac-registration/src/cardano/cip509/utils/cip19.rs create mode 100644 rust/rbac-registration/src/cardano/cip509/utils/mod.rs create mode 100644 rust/rbac-registration/src/cardano/cip509/validation.rs create mode 100644 rust/rbac-registration/src/cardano/cip509/x509_chunks.rs create mode 100644 rust/rbac-registration/src/cardano/mod.rs create mode 100644 rust/rbac-registration/src/cardano/transaction/mod.rs create mode 100644 rust/rbac-registration/src/cardano/transaction/raw_aux_data.rs create mode 100644 rust/rbac-registration/src/cardano/transaction/witness.rs create mode 100644 rust/rbac-registration/src/lib.rs create mode 100644 rust/rbac-registration/src/registration/cardano/mod.rs create mode 100644 rust/rbac-registration/src/registration/cardano/payment_history.rs create mode 100644 rust/rbac-registration/src/registration/cardano/point_tx_idx.rs create mode 100644 rust/rbac-registration/src/registration/cardano/role_data.rs create mode 100644 rust/rbac-registration/src/registration/mod.rs create mode 100644 rust/rbac-registration/src/test_data/cardano/conway_1.block create mode 100644 rust/rbac-registration/src/test_data/cardano/conway_2.block create mode 100644 rust/rbac-registration/src/test_data/cardano/conway_3.block create mode 100644 rust/rbac-registration/src/test_data/cardano/conway_4.block create mode 100644 rust/rbac-registration/src/utils/decode_helper.rs create mode 100644 rust/rbac-registration/src/utils/general.rs create mode 100644 rust/rbac-registration/src/utils/hashing.rs create mode 100644 rust/rbac-registration/src/utils/mod.rs diff --git a/.github/workflows/semantic_pull_request.yml b/.github/workflows/semantic_pull_request.yml index 022bb4036..a9e4a4930 100644 --- a/.github/workflows/semantic_pull_request.yml +++ b/.github/workflows/semantic_pull_request.yml @@ -24,6 +24,7 @@ jobs: rust/vote-tx-v2 rust/cbork rust/hermes-ipfs + rust/rbac-registration dart docs general diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 498c5eff0..5c90291cb 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -12,6 +12,7 @@ members = [ "immutable-ledger", "vote-tx-v1", "vote-tx-v2", + "rbac-registration", ] [workspace.package] diff --git a/rust/Earthfile b/rust/Earthfile index 0f282bb53..dc1f64f6d 100644 --- a/rust/Earthfile +++ b/rust/Earthfile @@ -13,6 +13,7 @@ COPY_SRC: catalyst-voting vote-tx-v1 vote-tx-v2 \ cbork cbork-abnf-parser cbork-cddl-parser \ hermes-ipfs \ + rbac-registration \ immutable-ledger . # builder : Set up our target toolchains, and copy our files. @@ -54,7 +55,7 @@ build: --args1="--libs=c509-certificate --libs=cardano-chain-follower --libs=hermes-ipfs" \ --args2="--libs=cbork-cddl-parser --libs=cbork-abnf-parser" \ --args3="--libs=catalyst-voting --libs=vote-tx-v1 --libs=vote-tx-v2" \ - --args4="--bins=cbork/cbork" \ + --args4="--bins=cbork/cbork --libs=rbac-registration" \ --args5="--cov_report=$HOME/build/coverage-report.info" \ --output="release/[^\./]+" \ --junit="cat-libs.junit-report.xml" \ diff --git a/rust/rbac-registration/Cargo.toml b/rust/rbac-registration/Cargo.toml new file mode 100644 index 000000000..22d03698c --- /dev/null +++ b/rust/rbac-registration/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "rbac-registration" +description = "Role Based Access Control Registration" +keywords = ["cardano", "catalyst", "rbac registration"] +version = "0.0.1" +authors = [ + "Arissara Chotivichit " +] +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[lib] +crate-type = ["cdylib", "rlib"] + +[lints] +workspace = true + +[dependencies] +hex = "0.4.3" +anyhow = "1.0.89" +strum_macros = "0.26.4" +regex = "1.11.0" +minicbor = { version = "0.25.1", features = ["alloc", "derive", "half"] } +brotli = "7.0.0" +zstd = "0.13.2" +x509-cert = "0.2.5" +der-parser = "9.0.0" +bech32 = "0.11.0" +dashmap = "6.1.0" +blake2b_simd = "1.0.2" +tracing = "0.1.40" + +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" } diff --git a/rust/rbac-registration/src/cardano/cip509/mod.rs b/rust/rbac-registration/src/cardano/cip509/mod.rs new file mode 100644 index 000000000..e88528f46 --- /dev/null +++ b/rust/rbac-registration/src/cardano/cip509/mod.rs @@ -0,0 +1,218 @@ +//! Cardano Improvement Proposal 509 (CIP-509) metadata module. +//! Doc Reference: +//! CDDL Reference: + +// cspell: words pkix + +pub mod rbac; +pub(crate) mod utils; +pub(crate) mod validation; +pub mod x509_chunks; + +use minicbor::{ + decode::{self}, + Decode, Decoder, +}; +use pallas::{crypto::hash::Hash, ledger::traverse::MultiEraTx}; +use strum_macros::FromRepr; +use validation::{ + validate_aux, validate_payment_key, validate_role_singing_key, validate_stake_public_key, + validate_txn_inputs_hash, +}; +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}, +}; + +/// CIP509 label. +pub const LABEL: u64 = 509; + +/// CIP509. +#[derive(Debug, PartialEq, Clone, Default)] +pub struct Cip509 { + /// `UUIDv4` Purpose . + pub purpose: UuidV4, // (bytes .size 16) + /// 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 ] + /// Validation signature. + pub validation_signature: Vec, // bytes size (1..64) +} + +/// `UUIDv4` representing in 16 bytes. +#[derive(Debug, PartialEq, Clone, Default)] +pub struct UuidV4([u8; 16]); + +impl From<[u8; 16]> for UuidV4 { + fn from(bytes: [u8; 16]) -> Self { + UuidV4(bytes) + } +} + +impl TryFrom> for UuidV4 { + type Error = &'static str; + + fn try_from(vec: Vec) -> Result { + if vec.len() == 16 { + let mut array = [0u8; 16]; + array.copy_from_slice(&vec); + Ok(UuidV4(array)) + } else { + Err("Input Vec must be exactly 16 bytes") + } + } +} + +/// Transaction input hash representing in 16 bytes. +#[derive(Debug, PartialEq, Clone, Default)] +pub struct TxInputHash([u8; 16]); + +impl From<[u8; 16]> for TxInputHash { + fn from(bytes: [u8; 16]) -> Self { + TxInputHash(bytes) + } +} + +impl TryFrom> for TxInputHash { + type Error = &'static str; + + fn try_from(vec: Vec) -> Result { + if vec.len() == 16 { + let mut array = [0u8; 16]; + array.copy_from_slice(&vec); + Ok(TxInputHash(array)) + } else { + Err("Input Vec must be exactly 16 bytes") + } + } +} + +/// Enum of CIP509 metadatum with its associated unsigned integer value. +#[allow(clippy::module_name_repetitions)] +#[derive(FromRepr, Debug, PartialEq)] +#[repr(u8)] +pub(crate) enum Cip509IntIdentifier { + /// Purpose. + Purpose = 0, + /// Transaction inputs hash. + TxInputsHash = 1, + /// Previous transaction ID. + PreviousTxId = 2, + /// Validation signature. + ValidationSignature = 99, +} + +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(); + for _ in 0..map_len { + // Use probe to peak + let key = d.probe().u8()?; + if let Some(key) = Cip509IntIdentifier::from_repr(key) { + // Consuming the int + let _: u8 = decode_helper(d, "CIP509", ctx)?; + match key { + Cip509IntIdentifier::Purpose => { + cip509_metadatum.purpose = + UuidV4::try_from(decode_bytes(d, "CIP509 purpose")?).map_err(|_| { + decode::Error::message("Invalid data size of Purpose") + })?; + }, + Cip509IntIdentifier::TxInputsHash => { + cip509_metadatum.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")? + .try_into() + .map_err(|_| { + decode::Error::message("Invalid data size of PreviousTxId") + })?; + cip509_metadatum.prv_tx_id = Some(Hash::from(prv_tx_hash)); + }, + Cip509IntIdentifier::ValidationSignature => { + let validation_signature = decode_bytes(d, "CIP509 validation signature")?; + if validation_signature.is_empty() || validation_signature.len() > 64 { + return Err(decode::Error::message( + "Invalid data size of ValidationSignature", + )); + } + cip509_metadatum.validation_signature = validation_signature; + }, + } + } else { + // Handle the x509 chunks 10 11 12 + let x509_chunks = X509Chunks::decode(d, ctx)?; + cip509_metadatum.x509_chunks = x509_chunks; + } + } + Ok(cip509_metadatum) + } +} + +impl Cip509 { + /// Basic validation for CIP509 + /// The validation include the following: + /// * Hashing the transaction inputs within the transaction should match the + /// txn-inputs-hash + /// * Auxiliary data hash within the transaction should match the hash of the + /// auxiliary data itself. + /// * Public key validation for role 0 where public key extracted from x509 and c509 + /// subject alternative name should match one of the witness in witness set within + /// the transaction. + /// * Payment key reference validation for role 0 where the reference should be either + /// 1. Negative index reference - reference to transaction output in transaction: + /// should match some of the key within witness set. + /// 2. Positive index reference - reference to the transaction input in + /// transaction: only check whether the index exist within the transaction + /// inputs. + /// * Role signing key validation for role 0 where the signing keys should only be the + /// certificates + /// + /// See: + /// * + /// * + /// + /// Note: This CIP509 is still under development and is subject to change. + /// + /// # Parameters + /// * `txn` - Transaction data was attached to and to be validated/decoded against. + /// * `validation_report` - Validation report to store the validation result. + pub fn validate(&self, txn: &MultiEraTx, validation_report: &mut Vec) -> bool { + let tx_input_validate = + validate_txn_inputs_hash(self, txn, validation_report).unwrap_or(false); + let aux_validate = validate_aux(txn, validation_report).unwrap_or(false); + let mut stake_key_validate = true; + let mut payment_key_validate = true; + let mut signing_key = true; + // Validate the role 0 + 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 { + stake_key_validate = + validate_stake_public_key(self, txn, validation_report).unwrap_or(false); + payment_key_validate = + validate_payment_key(txn, role, validation_report).unwrap_or(false); + signing_key = validate_role_singing_key(role, validation_report); + } + } + } + tx_input_validate + && aux_validate + && stake_key_validate + && payment_key_validate + && signing_key + } +} diff --git a/rust/rbac-registration/src/cardano/cip509/rbac/certs.rs b/rust/rbac-registration/src/cardano/cip509/rbac/certs.rs new file mode 100644 index 000000000..08a3aa9b8 --- /dev/null +++ b/rust/rbac-registration/src/cardano/cip509/rbac/certs.rs @@ -0,0 +1,137 @@ +//! Certificates for the RBAC metadata. + +use c509_certificate::c509::C509; +use minicbor::{decode, Decode, Decoder}; +use x509_cert::{der::Decode as x509Decode, Certificate}; + +use super::tag::KeyTag; +use crate::utils::decode_helper::{decode_array_len, decode_bytes, decode_helper, decode_tag}; + +// ------------------x509------------------------ + +/// Enum of possible X.509 DER certificate. +#[derive(Debug, PartialEq, Clone, Default)] +pub enum X509DerCert { + /// Undefined indicates skipped element. + #[default] + Undefined, + /// Deleted indicates the key is deleted. + Deleted, + /// X.509 certificate. + X509Cert(Vec), +} + +impl Decode<'_, ()> for X509DerCert { + fn decode(d: &mut Decoder, _ctx: &mut ()) -> Result { + match d.datatype()? { + minicbor::data::Type::Tag => { + let tag = decode_tag(d, "X509DerCert")?; + match tag { + t if t == KeyTag::Deleted.tag() => Ok(Self::Deleted), + _ => Err(decode::Error::message("Unknown tag for X509DerCert")), + } + }, + minicbor::data::Type::Undefined => Ok(Self::Undefined), + minicbor::data::Type::Bytes => { + let data = decode_bytes(d, "X509DerCert")?; + Certificate::from_der(&data) + .map_err(|_| decode::Error::message("Invalid x509 certificate"))?; + Ok(Self::X509Cert(data.clone())) + }, + _ => Err(decode::Error::message("Invalid datatype for X509DerCert")), + } + } +} + +// ------------------c509----------------------- + +/// Enum of possible X.509 DER certificate. +#[derive(Debug, PartialEq, Clone, Default)] +pub enum C509Cert { + /// Undefined indicates skipped element. + #[default] + Undefined, + /// Deleted indicates the key is deleted. + Deleted, + /// A c509 certificate in metadatum reference. + C509CertInMetadatumReference(C509CertInMetadatumReference), + /// A c509 certificate. + C509Certificate(Box), +} + +impl Decode<'_, ()> for C509Cert { + fn decode(d: &mut Decoder, ctx: &mut ()) -> Result { + match d.datatype()? { + minicbor::data::Type::Tag => { + let tag = decode_tag(d, "C509Cert")?; + match tag { + t if t == KeyTag::Deleted.tag() => Ok(Self::Deleted), + _ => Err(decode::Error::message("Unknown tag for C509Cert")), + } + }, + minicbor::data::Type::Array => { + let arr_len = decode_array_len(d, "C509Cert")?; + // C509CertInMetadatumReference must have 3 items + if arr_len == 3 { + Ok(Self::C509CertInMetadatumReference( + C509CertInMetadatumReference::decode(d, ctx)?, + )) + } else { + Err(decode::Error::message( + "Invalid length C509CertInMetadatumReference, expected 3", + )) + } + }, + minicbor::data::Type::Bytes => { + let c509 = decode_bytes(d, "C509Cert")?; + let mut c509_d = Decoder::new(&c509); + Ok(Self::C509Certificate(Box::new(C509::decode( + &mut c509_d, + ctx, + )?))) + }, + minicbor::data::Type::Undefined => Ok(Self::Undefined), + _ => Err(decode::Error::message("Invalid datatype for C509Cert")), + } + } +} + +/// C509 certificate in metadatum reference. +#[derive(Debug, PartialEq, Clone)] +pub struct C509CertInMetadatumReference { + /// Transaction output field. + txn_output_field: u8, + /// Transaction output index. + txn_output_index: u64, + /// Optional certificate reference. + cert_ref: Option>, +} + +impl Decode<'_, ()> for C509CertInMetadatumReference { + fn decode(d: &mut Decoder, ctx: &mut ()) -> Result { + let txn_output_field: u8 = + decode_helper(d, "txn output field in C509CertInMetadatumReference", ctx)?; + let txn_output_index: u64 = + decode_helper(d, "txn output index in C509CertInMetadatumReference", ctx)?; + let cert_ref = match d.datatype()? { + minicbor::data::Type::Array => { + let len = decode_array_len(d, "cert ref in C509CertInMetadatumReference")?; + let arr: Result, _> = (0..len).map(|_| d.u64()).collect(); + arr.map(Some) + }, + minicbor::data::Type::Null => Ok(None), + _ => { + Ok(Some(vec![decode_helper( + d, + "C509CertInMetadatumReference", + ctx, + )?])) + }, + }?; + Ok(Self { + txn_output_field, + txn_output_index, + cert_ref, + }) + } +} diff --git a/rust/rbac-registration/src/cardano/cip509/rbac/mod.rs b/rust/rbac-registration/src/cardano/cip509/rbac/mod.rs new file mode 100644 index 000000000..151110dff --- /dev/null +++ b/rust/rbac-registration/src/cardano/cip509/rbac/mod.rs @@ -0,0 +1,178 @@ +//! Role Based Access Control (RBAC) metadata for CIP509. +//! Doc Reference: +//! CDDL Reference: + +pub mod certs; +pub mod pub_key; +pub mod role_data; +pub(crate) mod tag; + +use std::collections::HashMap; + +use certs::{C509Cert, X509DerCert}; +use minicbor::{decode, Decode, Decoder}; +use pub_key::SimplePublicKeyType; +use role_data::RoleData; +use strum_macros::FromRepr; + +use crate::utils::decode_helper::{ + decode_any, decode_array_len, decode_bytes, decode_helper, decode_map_len, +}; + +/// Cip509 RBAC metadata. +#[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>, + /// Optional map of purpose key data. + /// Empty map if no purpose key data is present. + pub purpose_key_data: HashMap>, +} + +/// Certificate key hash use in revocation list. +#[derive(Debug, PartialEq, Clone, Default)] +pub struct CertKeyHash([u8; 16]); + +impl From<[u8; 16]> for CertKeyHash { + fn from(bytes: [u8; 16]) -> Self { + CertKeyHash(bytes) + } +} + +/// The first valid purpose key. +const FIRST_PURPOSE_KEY: u16 = 200; +/// The last valid purpose key. +const LAST_PURPOSE_KEY: u16 = 299; + +/// Enum of CIP509 RBAC metadata with its associated unsigned integer value. +#[derive(FromRepr, Debug, PartialEq)] +#[repr(u16)] +pub enum Cip509RbacMetadataInt { + /// x509 certificates. + X509Certs = 10, + /// c509 certificates. + C509Certs = 20, + /// Public keys. + PubKeys = 30, + /// Revocation list. + RevocationList = 40, + /// Role data set. + 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(); + + 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); + }, + Cip509RbacMetadataInt::C509Certs => { + let c509_certs = decode_array_rbac(d, "c509 certificate")?; + x509_rbac_metadata.set_c509_certs(c509_certs); + }, + Cip509RbacMetadataInt::PubKeys => { + let pub_keys = decode_array_rbac(d, "public keys")?; + x509_rbac_metadata.set_pub_keys(pub_keys); + }, + Cip509RbacMetadataInt::RevocationList => { + let revocation_list = decode_revocation_list(d)?; + x509_rbac_metadata.set_revocation_list(revocation_list); + }, + Cip509RbacMetadataInt::RoleSet => { + let role_set = decode_array_rbac(d, "role set")?; + x509_rbac_metadata.set_role_set(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")?); + } + } + Ok(x509_rbac_metadata) + } +} + +/// Decode an array of type T. +fn decode_array_rbac<'b, T>(d: &mut Decoder<'b>, from: &str) -> Result, decode::Error> +where T: Decode<'b, ()> { + let len = decode_array_len(d, &format!("{from} Cip509RbacMetadata"))?; + let mut vec = Vec::with_capacity(usize::try_from(len).map_err(decode::Error::message)?); + for _ in 0..len { + vec.push(T::decode(d, &mut ())?); + } + Ok(vec) +} + +/// Decode an array of revocation list. +fn decode_revocation_list(d: &mut Decoder) -> Result, decode::Error> { + let len = decode_array_len(d, "revocation list Cip509RbacMetadata")?; + let mut revocation_list = + Vec::with_capacity(usize::try_from(len).map_err(decode::Error::message)?); + for _ in 0..len { + let arr: [u8; 16] = decode_bytes(d, "revocation list Cip509RbacMetadata")? + .try_into() + .map_err(|_| decode::Error::message("Invalid revocation list size"))?; + revocation_list.push(CertKeyHash::from(arr)); + } + Ok(revocation_list) +} diff --git a/rust/rbac-registration/src/cardano/cip509/rbac/pub_key.rs b/rust/rbac-registration/src/cardano/cip509/rbac/pub_key.rs new file mode 100644 index 000000000..2f2f3047d --- /dev/null +++ b/rust/rbac-registration/src/cardano/cip509/rbac/pub_key.rs @@ -0,0 +1,87 @@ +//! Public key type for RBAC metadata + +use minicbor::{decode, Decode, Decoder}; +use pallas::codec::utils::Bytes; + +use super::tag::KeyTag; +use crate::utils::decode_helper::{decode_bytes, decode_tag}; + +/// Enum of possible public key type. +#[derive(Debug, PartialEq, Clone, Default)] +pub enum SimplePublicKeyType { + /// Undefined indicates skipped element. + #[default] + Undefined, + /// Deleted indicates the key is deleted. + Deleted, + /// Ed25519 public key. + Ed25519(Ed25519PublicKey), +} + +/// 32 bytes Ed25519 public key. +#[derive(Debug, PartialEq, Clone, Default, Eq, Hash)] +pub struct Ed25519PublicKey([u8; 32]); + +impl From<[u8; 32]> for Ed25519PublicKey { + fn from(bytes: [u8; 32]) -> Self { + Ed25519PublicKey(bytes) + } +} + +impl TryFrom for Ed25519PublicKey { + type Error = &'static str; + + fn try_from(bytes: Bytes) -> Result { + let byte_vec: Vec = bytes.into(); + + if byte_vec.len() != 32 { + return Err("Invalid length for Ed25519 public key: expected 32 bytes."); + } + + let byte_array: [u8; 32] = byte_vec + .try_into() + .map_err(|_| "Failed to convert Vec to [u8; 32]")?; + + Ok(Ed25519PublicKey::from(byte_array)) + } +} + +impl From for Bytes { + fn from(val: Ed25519PublicKey) -> Self { + let vec: Vec = val.0.to_vec(); + Bytes::from(vec) + } +} + +impl Decode<'_, ()> for SimplePublicKeyType { + fn decode(d: &mut Decoder, _ctx: &mut ()) -> Result { + match d.datatype()? { + minicbor::data::Type::Tag => { + let tag = decode_tag(d, "SimplePublicKeyType")?; + match tag { + t if t == KeyTag::Deleted.tag() => Ok(Self::Deleted), + t if t == KeyTag::Ed25519.tag() => { + let bytes = decode_bytes(d, "Ed25519 SimplePublicKeyType")?; + let mut ed25519 = [0u8; 32]; + if bytes.len() == 32 { + ed25519.copy_from_slice(&bytes); + Ok(Self::Ed25519(Ed25519PublicKey(ed25519))) + } else { + Err(decode::Error::message(format!( + "Invalid length for Ed25519 key, got {}", + bytes.len() + ))) + } + }, + _ => Err(decode::Error::message("Unknown tag for Self")), + } + }, + minicbor::data::Type::Undefined => Ok(Self::Undefined), + _ => { + Err(decode::Error::message( + "Invalid datatype for SimplePublicKeyType", + )) + }, + } + } +} diff --git a/rust/rbac-registration/src/cardano/cip509/rbac/role_data.rs b/rust/rbac-registration/src/cardano/cip509/rbac/role_data.rs new file mode 100644 index 000000000..486c2c8de --- /dev/null +++ b/rust/rbac-registration/src/cardano/cip509/rbac/role_data.rs @@ -0,0 +1,114 @@ +//! Role data for RBAC metadata. + +use std::collections::HashMap; + +use minicbor::{decode, Decode, Decoder}; +use strum_macros::FromRepr; + +use super::{decode_any, decode_map_len, Cip509RbacMetadataInt}; +use crate::utils::decode_helper::{decode_array_len, decode_helper}; + +/// Role data. +#[derive(Debug, PartialEq, Clone, Default)] +pub struct RoleData { + /// Role number. + pub role_number: u8, + /// Optional role signing key. + pub role_signing_key: Option, + /// Optional role encryption key. + pub role_encryption_key: Option, + /// Optional payment key. + pub payment_key: Option, + /// Optional role extended data keys. + /// Empty map if no role extended data keys. + pub role_extended_data_keys: HashMap>, +} + +/// The first valid role extended data key. +const FIRST_ROLE_EXT_KEY: u8 = 10; +/// The last valid role extended data key. +const LAST_ROLE_EXT_KEY: u8 = 99; + +/// Enum of role data with its associated unsigned integer value. +#[allow(clippy::module_name_repetitions)] +#[derive(FromRepr, Debug, PartialEq)] +#[repr(u8)] +pub enum RoleDataInt { + /// Role number. + RoleNumber = 0, + /// Role signing key. + RoleSigningKey = 1, + /// Role encryption key. + RoleEncryptionKey = 2, + /// Payment key. + PaymentKey = 3, +} + +impl Decode<'_, ()> for RoleData { + fn decode(d: &mut Decoder, ctx: &mut ()) -> Result { + let map_len = decode_map_len(d, "RoleData")?; + let mut role_data = RoleData::default(); + for _ in 0..map_len { + let key: u8 = decode_helper(d, "key in RoleData", ctx)?; + if let Some(key) = RoleDataInt::from_repr(key) { + match key { + RoleDataInt::RoleNumber => { + role_data.role_number = decode_helper(d, "RoleNumber in RoleData", ctx)?; + }, + RoleDataInt::RoleSigningKey => { + decode_array_len(d, "RoleSigningKey")?; + role_data.role_signing_key = Some(KeyLocalRef::decode(d, ctx)?); + }, + RoleDataInt::RoleEncryptionKey => { + decode_array_len(d, "RoleEncryptionKey")?; + role_data.role_encryption_key = Some(KeyLocalRef::decode(d, ctx)?); + }, + RoleDataInt::PaymentKey => { + role_data.payment_key = + Some(decode_helper(d, "PaymentKey in RoleData", ctx)?); + }, + } + } else { + if !(FIRST_ROLE_EXT_KEY..=LAST_ROLE_EXT_KEY).contains(&key) { + return Err(decode::Error::message(format!("Invalid role extended data key, should be with the range {FIRST_ROLE_EXT_KEY} - {LAST_ROLE_EXT_KEY}"))); + } + role_data + .role_extended_data_keys + .insert(key, decode_any(d, "Role extended data keys")?); + } + } + Ok(role_data) + } +} +/// Local key reference. +#[derive(Debug, PartialEq, Clone)] +pub struct KeyLocalRef { + /// Local reference. + pub local_ref: LocalRefInt, + /// Key offset. + pub key_offset: u64, +} + +/// Enum of local reference with its associated unsigned integer value. +#[derive(FromRepr, Debug, PartialEq, Clone, Eq, Hash)] +#[repr(u8)] +pub enum LocalRefInt { + /// x509 certificates. + X509Certs = Cip509RbacMetadataInt::X509Certs as u8, // 10 + /// c509 certificates. + C509Certs = Cip509RbacMetadataInt::C509Certs as u8, // 20 + /// Public keys. + PubKeys = Cip509RbacMetadataInt::PubKeys as u8, // 30 +} + +impl Decode<'_, ()> for KeyLocalRef { + fn decode(d: &mut Decoder, ctx: &mut ()) -> Result { + let local_ref = LocalRefInt::from_repr(decode_helper(d, "LocalRef in KeyLocalRef", ctx)?) + .ok_or(decode::Error::message("Invalid local reference"))?; + let key_offset: u64 = decode_helper(d, "KeyOffset in KeyLocalRef", ctx)?; + Ok(Self { + local_ref, + key_offset, + }) + } +} diff --git a/rust/rbac-registration/src/cardano/cip509/rbac/tag.rs b/rust/rbac-registration/src/cardano/cip509/rbac/tag.rs new file mode 100644 index 000000000..7fc84c548 --- /dev/null +++ b/rust/rbac-registration/src/cardano/cip509/rbac/tag.rs @@ -0,0 +1,21 @@ +//! Tags used in CIP-0509 besides from the original tags defined in minicbor. + +use minicbor::data::Tag; + +/// Enum of possible key tag which are use in public key and certificates. +pub(crate) enum KeyTag { + /// Deleted Key tag 31. + Deleted, + /// Ed25519 Key tag 32773. + Ed25519, +} + +impl KeyTag { + /// Get the tag value. + pub(crate) fn tag(self) -> Tag { + match self { + KeyTag::Deleted => Tag::new(0x31), + KeyTag::Ed25519 => Tag::new(0x8005), + } + } +} diff --git a/rust/rbac-registration/src/cardano/cip509/utils/cip19.rs b/rust/rbac-registration/src/cardano/cip509/utils/cip19.rs new file mode 100644 index 000000000..20f6fd6fd --- /dev/null +++ b/rust/rbac-registration/src/cardano/cip509/utils/cip19.rs @@ -0,0 +1,137 @@ +//! Utility functions for CIP-19 address. + +use anyhow::bail; +use regex::Regex; + +use crate::cardano::transaction::witness::TxWitness; + +/// Extracts the CIP-19 bytes from a URI. +/// Example input: `web+cardano://addr/` +/// +/// URI = scheme ":" ["//" authority] path ["?" query] ["#" fragment] +#[must_use] +pub fn extract_cip19_hash(uri: &str, prefix: Option<&str>) -> Option> { + // Regex pattern to match the expected URI format + let r = Regex::new("^.+://addr/(.+)$").ok()?; + + // Apply the regex pattern to capture the CIP-19 address string + let address = r + .captures(uri) + .and_then(|cap| cap.get(1).map(|m| m.as_str().to_string())); + + match address { + Some(addr) => { + if let Some(prefix) = prefix { + if !addr.starts_with(prefix) { + return None; + } + } + let addr = bech32::decode(&addr).ok()?.1; + // As in CIP19, the first byte is the header, so extract only the payload + extract_key_hash(&addr) + }, + None => None, + } +} + +/// Extract the first 28 bytes from the given key +/// Refer to for more information. +pub(crate) fn extract_key_hash(key: &[u8]) -> Option> { + key.get(1..29).map(<[u8]>::to_vec) +} + +/// Compare the given public key bytes with the transaction witness set. +pub(crate) fn compare_key_hash( + pk_addrs: &[Vec], witness: &TxWitness, txn_idx: u16, +) -> anyhow::Result<()> { + if pk_addrs.is_empty() { + bail!("No public key addresses provided"); + } + + pk_addrs.iter().try_for_each(|pk_addr| { + let pk_addr: [u8; 28] = pk_addr.as_slice().try_into().map_err(|_| { + anyhow::anyhow!( + "Invalid length for vkey, expected 28 bytes but got {}", + pk_addr.len() + ) + })?; + + // Key hash not found in the transaction witness set + if !witness.check_witness_in_tx(&pk_addr, txn_idx) { + bail!( + "Public key hash not found in transaction witness set given {:?}", + pk_addr + ); + } + + Ok(()) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + // Test data from https://cips.cardano.org/cip/CIP-19 + // cSpell:disable + const STAKE_ADDR: &str = "stake_test1uqehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gssrtvn"; + const PAYMENT_ADDR: &str = "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs68faae"; + // cSpell:enable + + #[test] + fn test_extract_cip19_hash_with_stake() { + // Additional tools to check for bech32 https://slowli.github.io/bech32-buffer/ + let uri = &format!("web+cardano://addr/{STAKE_ADDR}"); + // Given: + // e0337b62cfff6403a06a3acbc34f8c46003c69fe79a3628cefa9c47251 + // The first byte is the header, so extract only the payload + let bytes = hex::decode("337b62cfff6403a06a3acbc34f8c46003c69fe79a3628cefa9c47251") + .expect("Failed to decode bytes"); + assert_eq!( + extract_cip19_hash(uri, Some("stake")).expect("Failed to extract CIP-19 hash"), + bytes + ); + } + + #[test] + fn test_extract_cip19_hash_with_addr_with_prefix_set() { + let uri = &format!("web+cardano://addr/{PAYMENT_ADDR}"); + let result = extract_cip19_hash(uri, Some("stake")); + assert_eq!(result, None); + } + + #[test] + fn test_extract_cip19_hash_with_addr_without_prefix_set() { + let uri = &format!("web+cardano://addr/{PAYMENT_ADDR}"); + let result = extract_cip19_hash(uri, None); + assert!(result.is_some()); + } + + #[test] + fn test_extract_cip19_hash_invalid_uri() { + let uri = "invalid_uri"; + let result = extract_cip19_hash(uri, None); + assert_eq!(result, None); + } + + #[test] + fn test_extract_cip19_hash_non_bech32_address() { + let uri = "example://addr/not_bech32"; + let result = extract_cip19_hash(uri, None); + assert_eq!(result, None); + } + + #[test] + fn test_extract_cip19_hash_empty_uri() { + let uri = ""; + let result = extract_cip19_hash(uri, None); + assert_eq!(result, None); + } + + #[test] + fn test_extract_cip19_hash_no_address() { + let uri = "example://addr/"; + let result = extract_cip19_hash(uri, None); + assert_eq!(result, None); + } +} diff --git a/rust/rbac-registration/src/cardano/cip509/utils/mod.rs b/rust/rbac-registration/src/cardano/cip509/utils/mod.rs new file mode 100644 index 000000000..27d0e5205 --- /dev/null +++ b/rust/rbac-registration/src/cardano/cip509/utils/mod.rs @@ -0,0 +1,3 @@ +//! Utility functions for CIP-509 + +pub(crate) mod cip19; diff --git a/rust/rbac-registration/src/cardano/cip509/validation.rs b/rust/rbac-registration/src/cardano/cip509/validation.rs new file mode 100644 index 000000000..1be2dee1f --- /dev/null +++ b/rust/rbac-registration/src/cardano/cip509/validation.rs @@ -0,0 +1,664 @@ +//! Basic validation for CIP-0509 +//! The validation include the following: +//! * Hashing the transaction inputs within the transaction should match the +//! txn-inputs-hash in CIP-0509 data. +//! * Auxiliary data hash within the transaction should match the hash of the auxiliary +//! data itself. +//! * Public key validation for role 0 where public key extracted from x509 and c509 +//! subject alternative name should match one of the witness in witness set within the +//! transaction. +//! * Payment key reference validation for role 0 where the reference should be either +//! 1. Negative index reference - reference to transaction output in transaction: +//! should match some of the key within witness set. +//! 2. Positive index reference - reference to the transaction input in transaction: +//! only check whether the index exist within the transaction inputs. +//! * Role signing key validation for role 0 where the signing keys should only be the +//! certificates +//! +//! See: +//! * +//! * +//! +//! 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}, + utils::Bytes, + }, + ledger::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_cip19_hash, extract_key_hash}, + Cip509, TxInputHash, TxWitness, +}; + +/// Context-specific primitive type with tag number 6 (`raw_tag` 134) for +/// uniform resource identifier (URI) in the subject alternative name extension. +/// Following the ASN.1 +/// +/// the tag is derive from +/// | Class (2 bit) | P/C (1 bit) | Tag Number (5 bit) | +/// |`CONTEXT_SPECIFIC` | `PRIMITIVE` `| 6` | +/// |`10` | `0` `| 00110` | +/// Result in 0x86 or 134 in decimal. +pub(crate) const URI: u8 = 134; + +// ------------------------ Validate Txn Inputs Hash ------------------------ + +/// Transaction inputs hash validation. +/// CIP509 `txn_inputs_hash` must match the hash of the transaction inputs within the +/// body. +pub(crate) fn validate_txn_inputs_hash( + cip509: &Cip509, txn: &MultiEraTx, validation_report: &mut Vec, +) -> Option { + let function_name = "Validate Transaction Inputs Hash"; + let mut buffer = Vec::new(); + let mut e = Encoder::new(&mut buffer); + // CIP-0509 should only be in conway era + if let MultiEraTx::Conway(tx) = txn { + let inputs = tx.transaction_body.inputs.clone(); + if let Err(e) = e.array(inputs.len() as u64) { + validation_report.push(format!( + "{function_name}, Failed to encode array of transaction input: {e}" + )); + return None; + } + for input in &inputs { + match input.encode(&mut e, &mut ()) { + Ok(()) => {}, + Err(e) => { + validation_report.push(format!( + "{function_name}, Failed to encode transaction input {e}" + )); + return None; + }, + } + } + // Hash the transaction inputs + let inputs_hash = match blake2b_128(&buffer) { + Ok(hash) => hash, + Err(e) => { + validation_report.push(format!( + "{function_name}, Failed to hash transaction inputs {e}" + )); + return None; + }, + }; + Some(TxInputHash(inputs_hash) == cip509.txn_inputs_hash) + } else { + validation_report.push(format!("{function_name}, Unsupported transaction era for")); + None + } +} + +// ------------------------ Validate Stake Public Key ------------------------ + +/// Validate the stake public key in the certificate with witness set in transaction. +#[allow(clippy::too_many_lines)] +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 Some(h) = + extract_cip19_hash(&addr, Some("stake")) + { + pk_addrs.push(h); + } + }, + 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 Some(h) = extract_cip19_hash(s, Some("stake")) { + pk_addrs.push(h); + } + }, + _ => { + 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; + } + + // Create TxWitness + // Note that TxWitness designs to work with multiple transactions + let witnesses = match TxWitness::new(&[txn.clone()]) { + Ok(witnesses) => witnesses, + Err(e) => { + validation_report.push(format!("{function_name}, Failed to create TxWitness: {e}")); + return None; + }, + }; + + 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 + // the witness within this transaction. + compare_key_hash(&pk_addrs, &witnesses, 0) + .map_err(|e| { + validation_report.push(format!( + "{function_name}, Failed to compare public keys with witnesses: {e}" + )); + }) + .is_ok(), + ) +} + +// ------------------------ Validate Aux ------------------------ + +/// Validate the auxiliary data with the auxiliary data hash in the transaction body. +pub(crate) fn validate_aux(txn: &MultiEraTx, validation_report: &mut Vec) -> Option { + let function_name = "Validate Aux"; + + // CIP-0509 should only be in conway era + if let MultiEraTx::Conway(tx) = txn { + if let pallas::codec::utils::Nullable::Some(a) = &tx.auxiliary_data { + let original_aux = a.raw_cbor(); + let aux_data_hash = tx + .transaction_body + .auxiliary_data_hash + .as_ref() + .or_else(|| { + validation_report.push(format!( + "{function_name}, Auxiliary data hash not found in transaction" + )); + None + })?; + validate_aux_helper(original_aux, aux_data_hash, validation_report) + } else { + validation_report.push(format!( + "{function_name}, Auxiliary data not found in transaction" + )); + None + } + } else { + validation_report.push(format!("{function_name}, Unsupported transaction era")); + None + } +} + +/// Helper function for auxiliary data validation. +fn validate_aux_helper( + original_aux: &[u8], aux_data_hash: &Bytes, validation_report: &mut Vec, +) -> Option { + // Compare the hash + match blake2b_256(original_aux) { + Ok(original_hash) => { + return Some(aux_data_hash.as_ref() == original_hash); + }, + Err(e) => { + validation_report.push(format!("Cannot hash auxiliary data {e}")); + None + }, + } +} + +// ------------------------ Validate Payment Key ------------------------ + +/// Validate the payment key reference. +/// Negative ref is for transaction output. +/// Positive ref is for transaction input. +pub(crate) fn validate_payment_key( + txn: &MultiEraTx, role_data: &RoleData, validation_report: &mut Vec, +) -> Option { + let function_name = "Validate Payment Key"; + + if let Some(payment_key) = role_data.payment_key { + if payment_key == 0 { + validation_report.push(format!( + "{function_name}, Invalid payment reference key, 0 is not allowed" + )); + return None; + } + // CIP-0509 should only be in conway era + if let MultiEraTx::Conway(tx) = txn { + // Negative indicates reference to tx output + if payment_key < 0 { + let index = match decremented_index(payment_key.abs()) { + Ok(value) => value, + Err(e) => { + validation_report.push(format!( + "{function_name}, Failed to get index of payment key: {e}" + )); + return None; + }, + }; + let outputs = tx.transaction_body.outputs.clone(); + let witness = match TxWitness::new(&[txn.clone()]) { + Ok(witnesses) => witnesses, + Err(e) => { + validation_report + .push(format!("{function_name}, Failed to create TxWitness: {e}")); + return None; + }, + }; + + if let Some(output) = outputs.get(index) { + match output { + pallas::ledger::primitives::conway::PseudoTransactionOutput::Legacy(o) => { + return validate_payment_output_key_helper( + &o.address.to_vec(), + validation_report, + &witness, + ); + }, + pallas::ledger::primitives::conway::PseudoTransactionOutput::PostAlonzo( + o, + ) => { + return validate_payment_output_key_helper( + &o.address.to_vec(), + validation_report, + &witness, + ); + }, + }; + } + validation_report.push( + format!("{function_name}, Role payment key reference index is not found in transaction outputs") + ); + return None; + } + // Positive indicates reference to tx input + let inputs = &tx.transaction_body.inputs; + let index = match decremented_index(payment_key) { + Ok(value) => value, + Err(e) => { + validation_report.push(format!( + "{function_name}, Failed to get index of payment key: {e}" + )); + return None; + }, + }; + // Check whether the index exists in transaction inputs + if inputs.get(index).is_none() { + validation_report.push( + format!("{function_name}, Role payment key reference index is not found in transaction inputs") + ); + return None; + } + Some(true) + } else { + validation_report.push(format!( + "{function_name}, Unsupported transaction era for stake payment key validation" + )); + None + } + } else { + Some(false) + } +} + +/// Helper function for validating payment output key. +fn validate_payment_output_key_helper( + output_address: &[u8], validation_report: &mut Vec, witness: &TxWitness, +) -> Option { + // Extract the key hash from the output address + if let Some(key) = extract_key_hash(output_address) { + // Compare the key hash and return the result + // 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 + // the witness within this transaction. + return Some(compare_key_hash(&[key], witness, 0).is_ok()); + } + validation_report.push("Failed to extract payment key hash from address".to_string()); + None +} + +// ------------------------ Validate role signing key ------------------------ + +/// Validate role singing key for role 0. +/// Must reference certificate not the public key +pub(crate) fn validate_role_singing_key( + role_data: &RoleData, validation_report: &mut Vec, +) -> bool { + let function_name = "Validate Role Signing Key"; + + // If signing key exist, it should not contain public key + if let Some(local_ref) = &role_data.role_signing_key { + if local_ref.local_ref == LocalRefInt::PubKeys { + validation_report.push(format!( + "{function_name}, Role signing key should reference certificate, not public key", + )); + println!("ja"); + return false; + } + } + + true +} + +// ------------------------ Tests ------------------------ + +#[cfg(test)] +mod tests { + + use minicbor::{Decode, Decoder}; + + use super::*; + use crate::cardano::transaction::raw_aux_data::RawAuxData; + + fn cip_509_aux_data(tx: &MultiEraTx<'_>) -> Vec { + let raw_auxiliary_data = tx + .as_conway() + .unwrap() + .clone() + .auxiliary_data + .map(|aux| aux.raw_cbor()); + + let raw_cbor_data = match raw_auxiliary_data { + pallas::codec::utils::Nullable::Some(data) => Ok(data), + _ => Err("Auxiliary data not found"), + }; + + let auxiliary_data = RawAuxData::new(raw_cbor_data.expect("Failed to get raw cbor data")); + auxiliary_data + .get_metadata(509) + .expect("Failed to get metadata") + .to_vec() + } + + fn conway_1() -> Vec { + hex::decode(include_str!("../../test_data/cardano/conway_1.block")) + .expect("Failed to decode hex block.") + } + + fn conway_2() -> Vec { + hex::decode(include_str!("../../test_data/cardano/conway_2.block")) + .expect("Failed to decode hex block.") + } + + fn conway_3() -> Vec { + hex::decode(include_str!("../../test_data/cardano/conway_3.block")) + .expect("Failed to decode hex block.") + } + + #[test] + fn test_validate_txn_inputs_hash() { + let mut validation_report = Vec::new(); + let conway_block_data = conway_1(); + let multi_era_block = pallas::ledger::traverse::MultiEraBlock::decode(&conway_block_data) + .expect("Failed to decode MultiEraBlock"); + + let transactions = multi_era_block.txs(); + // Forth transaction of this test data contains the CIP509 auxiliary data + let tx = transactions + .get(3) + .expect("Failed to get transaction index"); + let aux_data = cip_509_aux_data(tx); + let mut decoder = Decoder::new(aux_data.as_slice()); + let cip509 = Cip509::decode(&mut decoder, &mut ()).expect("Failed to decode Cip509"); + assert!(validate_txn_inputs_hash(&cip509, tx, &mut validation_report).unwrap()); + } + + #[test] + fn test_validate_aux() { + let mut validation_report = Vec::new(); + let conway_block_data = conway_1(); + let multi_era_block = pallas::ledger::traverse::MultiEraBlock::decode(&conway_block_data) + .expect("Failed to decode MultiEraBlock"); + + let transactions = multi_era_block.txs(); + // Forth transaction of this test data contains the CIP509 auxiliary data + let tx = transactions + .get(3) + .expect("Failed to get transaction index"); + + validate_aux(tx, &mut validation_report); + assert!(validate_aux(tx, &mut validation_report).unwrap()); + } + + #[test] + fn test_validate_public_key_success() { + let mut validation_report = Vec::new(); + let conway_block_data = conway_1(); + let multi_era_block = pallas::ledger::traverse::MultiEraBlock::decode(&conway_block_data) + .expect("Failed to decode MultiEraBlock"); + + let transactions = multi_era_block.txs(); + // Forth transaction of this test data contains the CIP509 auxiliary data + let tx = transactions + .get(3) + .expect("Failed to get transaction index"); + + let aux_data = cip_509_aux_data(tx); + + let mut decoder = Decoder::new(aux_data.as_slice()); + let cip509 = Cip509::decode(&mut decoder, &mut ()).expect("Failed to decode Cip509"); + assert!(validate_stake_public_key(&cip509, tx, &mut validation_report).unwrap()); + } + + #[test] + fn test_validate_payment_key_success_negative_ref() { + let mut validation_report = Vec::new(); + let conway_block_data = conway_1(); + let multi_era_block = pallas::ledger::traverse::MultiEraBlock::decode(&conway_block_data) + .expect("Failed to decode MultiEraBlock"); + + let transactions = multi_era_block.txs(); + // Forth transaction of this test data contains the CIP509 auxiliary data + let tx = transactions + .get(3) + .expect("Failed to get transaction index"); + + let aux_data = cip_509_aux_data(tx); + + 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()); + } + } + } + } + + #[test] + fn test_role_0_signing_key() { + let mut validation_report = Vec::new(); + let conway_block_data = conway_1(); + let multi_era_block = pallas::ledger::traverse::MultiEraBlock::decode(&conway_block_data) + .expect("Failed to decode MultiEraBlock"); + + let transactions = multi_era_block.txs(); + // Forth transaction of this test data contains the CIP509 auxiliary data + let tx = transactions + .get(3) + .expect("Failed to get transaction index"); + + let aux_data = cip_509_aux_data(tx); + + 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,)); + } + } + } + } + + #[test] + fn test_validate_payment_key_success_positive_ref() { + let mut validation_report = Vec::new(); + let conway_block_data = conway_3(); + let multi_era_block = pallas::ledger::traverse::MultiEraBlock::decode(&conway_block_data) + .expect("Failed to decode MultiEraBlock"); + + let transactions = multi_era_block.txs(); + // First transaction of this test data contains the CIP509 auxiliary data + let tx = transactions + .first() + .expect("Failed to get transaction index"); + + let aux_data = cip_509_aux_data(tx); + + 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()); + } + } + } + } + + #[test] + fn test_validate_public_key_fail() { + let mut validation_report = Vec::new(); + let conway_block_data = conway_2(); + let multi_era_block = pallas::ledger::traverse::MultiEraBlock::decode(&conway_block_data) + .expect("Failed to decode MultiEraBlock"); + + let transactions = multi_era_block.txs(); + // First transaction of this test data contains the CIP509 auxiliary data + let tx = transactions + .first() + .expect("Failed to get transaction index"); + + let aux_data = cip_509_aux_data(tx); + + let mut decoder = Decoder::new(aux_data.as_slice()); + let cip509 = Cip509::decode(&mut decoder, &mut ()).expect("Failed to decode Cip509"); + assert!(!validate_stake_public_key(&cip509, tx, &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 new file mode 100644 index 000000000..457faef6a --- /dev/null +++ b/rust/rbac-registration/src/cardano/cip509/x509_chunks.rs @@ -0,0 +1,122 @@ +//! X509 chunks handler where compressed chunks are decompressed and decoded. + +use std::io::Read; + +use minicbor::{decode, Decode, Decoder}; +use strum_macros::FromRepr; + +use super::rbac::Cip509RbacMetadata; +use crate::utils::decode_helper::{decode_array_len, decode_bytes, decode_helper}; + +/// Enum of compression algorithms used to compress chunks. +#[derive(FromRepr, Debug, PartialEq, Clone, Default)] +#[repr(u8)] +pub enum CompressionAlgorithm { + /// Raw data, no compression. + #[default] + Raw = 10, + /// Brotli compression. + Brotli = 11, + /// Zstd compression. + 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) + } +} + +impl Decode<'_, ()> for X509Chunks { + fn decode(d: &mut Decoder, ctx: &mut ()) -> Result { + // Determine the algorithm + let algo: u8 = decode_helper(d, "algorithm in X509Chunks", ctx)?; + let algorithm = CompressionAlgorithm::from_repr(algo) + .ok_or(decode::Error::message("Invalid chunk data type"))?; + + // Decompress the data + let decompressed = decompress(d, &algorithm) + .map_err(|e| decode::Error::message(format!("Failed to decompress {e}")))?; + + // Decode the decompressed data. + let mut decoder = Decoder::new(&decompressed); + let chunk_data = Cip509RbacMetadata::decode(&mut decoder, &mut ()) + .map_err(|e| decode::Error::message(format!("Failed to decode {e}")))?; + + Ok(X509Chunks(chunk_data)) + } +} + +/// Decompress the data using the given algorithm. +fn decompress(d: &mut Decoder, algorithm: &CompressionAlgorithm) -> anyhow::Result> { + let chunk_len = decode_array_len(d, "decompression in X509Chunks")?; + // Vector containing the concatenated chunks + let mut concat_chunk = vec![]; + for _ in 0..chunk_len { + let chunk_data = decode_bytes(d, "decompression in X509Chunks")?; + concat_chunk.extend_from_slice(&chunk_data); + } + + let mut buffer = vec![]; + + match algorithm { + CompressionAlgorithm::Raw => { + buffer.extend_from_slice(concat_chunk.as_slice()); + }, + CompressionAlgorithm::Zstd => { + zstd::stream::copy_decode(concat_chunk.as_slice(), &mut buffer)?; + }, + CompressionAlgorithm::Brotli => { + let mut decoder = brotli::Decompressor::new(concat_chunk.as_slice(), 4096); + decoder + .read_to_end(&mut buffer) + .map_err(|_| anyhow::anyhow!("Failed to decompress using Brotli algorithm"))?; + }, + } + Ok(buffer) +} + +#[cfg(test)] +mod tests { + use super::*; + + // RAW data: 10 + const RAW: &str = "0a8c5840a30a815902ae308202aa3082025ca00302010202147735a70599e68b49554b1cb3a6cf5e34583b3c2f300506032b6570307c310b300906035504061302555331584013301106035504080c0a43616c69666f726e69613116301406035504070c0d53616e204672616e636973636f31123010060355040a0c094d79436f6d70616e79584031153013060355040b0c0c4d794465706172746d656e743115301306035504030c0c6d79646f6d61696e2e636f6d301e170d3234313132393034333134305a1758400d3235313132393034333134305a307c310b30090603550406130255533113301106035504080c0a43616c69666f726e69613116301406035504070c0d53616e5840204672616e636973636f31123010060355040a0c094d79436f6d70616e7931153013060355040b0c0c4d794465706172746d656e743115301306035504030c0c58406d79646f6d61696e2e636f6d302a300506032b65700321007e082c662a8d4d3271d797067f36caf25d6472b83901620a2eac193331a7f871a381ef3081ec308158409e0603551d11048196308193820c6d79646f6d61696e2e636f6d82107777772e6d79646f6d61696e2e636f6d820b6578616d706c652e636f6d820f7777772e65584078616d706c652e636f6d86537765622b63617264616e6f3a2f2f616464722f7374616b655f7465737431757165686b636b306c616a713867723238743975786e5840757667637172633630373078336b3972383034387a3879356773737274766e300b0603551d0f0404030205e0301d0603551d250416301406082b06010505070358400106082b06010505070302301d0603551d0e04160414251ddd56123655faa9348ff93c1e92ce3bc15a29300506032b6570034100b11c80d36fdcba650b950f06584087e448b3bcbeb2caa5249b24aff83d16ebbb71249e44bd0ecfab8b40fb772b6f977f98ac9122e13954439d0120980b347e3f9707181e81d9800558206e42f8e5582e89a76ebb13ef279df7841efce978f106bee196f0e3cfd347bb31a2e8186481a4000001820a0003010a6454657374"; + // Brotli data: 11 + const BROTLI: &str = "0b8958401bed02003c0e772c72637668c289a39b361dd1161a123da11e1118d08c7ab73ed1455e25aab3105e92334ba1fe128febedfb3e4912243755f42aca92094de82658404a4149a26917374c7cfd021376195439e4ea64844b46cfe1f87e9b6bf4d43c9dace1920ceeb2cc82bb60018b5b2de9571c5ea9c81dddd4077cc4571eb33181ce58409b38965811866581e4e903e6967333e85ab02e1b25665f272d24db06fb0183d3dd1cd937e2e260e3e0d045c976e057dde418766ea47dd551a68c20c015f508e25840118815ae0511d0e258b4440bd4b921222339016000ba853d2119394d8006a8a220d1c4755d3920f4403b302919d9c22f32106e1e10c3942a7d0f1c8ce42283205840528a20238dc8802d44cafe99612022d4abb8dc58894462642a21bb150449720b1f0e4e2a62bb9210b334d2f13ba4057d05f409d0fa3c666a3cb41cd012cc8e29584086ea45acd180f40932c052962cc156bad9f4a8a80d2f5d2e488c7ba8a496b1b1bf332482c7f8b9f981bfcb862878854a29842b460c8c782fb7905037399087685840693fc55005905188558891f50a0b0b0d8f0c04528e5d4a3c3c5c1cfc3360ef293f8938271225c0c6c727c4c59e3e4e09a2788847c7cfc79646ce62974cc11f1558405fbca2dc36eaf3594d18624bde0c7adac3324a828c2b833b3b3fbcd0c657c337f05bd53ece84f0adf329567b7234fe45897252656f11cf8ae6e56073452a93e85835133271a0fbc9f8d641adafb33a0267685f05fd95caf1ff3efa9d60febcfced727553ff21cd774cee682b161636860470b149c61f40"; + // Zstd data: 12 + const ZSTD: &str= "0c89584028b52ffd6000029d1100b41fa30a815902ae308202aa3082025ca00302010202147735a70599e68b49554b1cb3a6cf5e34583b3c2f300506032b6570307c310b5840300906035504061302555331133011080c0a43616c69666f726e696131163014070c0d53616e204672616e636973636f311230100a0c094d79436f6d70616e79584031153013060355040b0c0c4d794465706172746d656e74030c0c6d79646f6d61696e2e636f6d301e170d3234313132393034333134305a170d32352a0321007e5840082c662a8d4d3271d797067f36caf25d6472b83901620a2eac193331a7f871a381ef3081ec30819e0603551d110481963081938282107777772e820b6578616d5840706c65820f86537765622b63617264616e6f3a2f2f616464722f7374616b655f7465737431757165686b636b306c616a713867723238743975786e7576676371584072633630373078336b3972383034387a3879356773737274766e300b0f0404030205e0301d2504082b06010505070301020e04160414251ddd56123655faa93458408ff93c1e92ce3bc15a294100b11c80d36fdcba650b950f0687e448b3bcbeb2caa5249b24aff83d16ebbb71249e44bd0ecfab8b40fb772b6f977f98ac9122e139584054439d0120980b347e3f9707181e81d9800558206e42f8e589a76ebb13ef279df7841efce978f106bee196f0e3cfd347bb31a2e8186481a4000001820a000301583d0a64546573740013003d3e631feb0da1b068d5115f8161e2aed10b46d3acd0c00e1b9c80e50abeed00ca66cc432659ca8c6f3affd9b92ccedd01d66906"; + + #[test] + fn test_decode_x509_chunks_raw() { + let raw_bytes = hex::decode(RAW).unwrap(); + let mut decoder = Decoder::new(raw_bytes.as_slice()); + let x509_chunks = X509Chunks::decode(&mut decoder, &mut ()); + // Decode the decompressed data should success. + assert!(x509_chunks.is_ok()); + } + + #[test] + fn test_decode_x509_chunks_brotli() { + let brotli_bytes = hex::decode(BROTLI).unwrap(); + let mut decoder = Decoder::new(brotli_bytes.as_slice()); + let x509_chunks = X509Chunks::decode(&mut decoder, &mut ()); + // Decode the decompressed data should success. + assert!(x509_chunks.is_ok()); + } + + #[test] + fn test_decode_x509_chunks_zstd() { + let zstd_bytes = hex::decode(ZSTD).unwrap(); + let mut decoder = Decoder::new(zstd_bytes.as_slice()); + let x509_chunks = X509Chunks::decode(&mut decoder, &mut ()); + // Decode the decompressed data should success. + assert!(x509_chunks.is_ok()); + } +} diff --git a/rust/rbac-registration/src/cardano/mod.rs b/rust/rbac-registration/src/cardano/mod.rs new file mode 100644 index 000000000..f8c6b996d --- /dev/null +++ b/rust/rbac-registration/src/cardano/mod.rs @@ -0,0 +1,4 @@ +//! Cardano module + +pub mod cip509; +pub mod transaction; diff --git a/rust/rbac-registration/src/cardano/transaction/mod.rs b/rust/rbac-registration/src/cardano/transaction/mod.rs new file mode 100644 index 000000000..b5faf4ab4 --- /dev/null +++ b/rust/rbac-registration/src/cardano/transaction/mod.rs @@ -0,0 +1,4 @@ +//! Cardano transaction module + +pub(crate) mod raw_aux_data; +pub(crate) mod witness; diff --git a/rust/rbac-registration/src/cardano/transaction/raw_aux_data.rs b/rust/rbac-registration/src/cardano/transaction/raw_aux_data.rs new file mode 100644 index 000000000..6117c0397 --- /dev/null +++ b/rust/rbac-registration/src/cardano/transaction/raw_aux_data.rs @@ -0,0 +1,273 @@ +//! Raw Auxiliary Data Decoding + +use std::sync::Arc; + +use anyhow::bail; +use dashmap::DashMap; +use minicbor::{data::Type, Decoder}; +use tracing::{error, warn}; + +/// What type of smart contract is this list. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, strum_macros::Display, Hash)] +#[allow(dead_code)] +pub enum SmartContractType { + /// Native smart contracts + Native, + /// Plutus smart contracts (with version number 1-x) + Plutus(u64), +} + +// We CAN NOT use the Pallas library metadata decoding because it does not preserve raw +// metadata values which are critical for performing operations like signature checks on +// data. So we have a bespoke metadata decoder here. +#[derive(Debug)] +#[allow(dead_code)] +pub(crate) struct RawAuxData { + /// Metadata: key = label, value = raw metadata bytes + metadata: DashMap>>, + /// Scripts: 1 = Native, 2 = Plutus V1, 3 = Plutus V2, 4 = Plutus V3 + scripts: DashMap>>>, +} + +impl RawAuxData { + /// Create a new `RawDecodedMetadata`. + #[allow(dead_code)] + pub(crate) fn new(aux_data: &[u8]) -> Self { + let mut raw_decoded_data = Self { + metadata: DashMap::new(), + scripts: DashMap::new(), + }; + + let mut decoder = Decoder::new(aux_data); + + match decoder.datatype() { + Ok(minicbor::data::Type::Map) => { + if let Err(error) = Self::decode_shelley_map(&mut raw_decoded_data, &mut decoder) { + error!("Failed to Deserialize Shelley Metadata: {error}: {aux_data:02x?}"); + } + }, + Ok(minicbor::data::Type::Array) => { + if let Err(error) = + Self::decode_shelley_ma_array(&mut raw_decoded_data, &mut decoder) + { + error!("Failed to Deserialize Shelley-MA Metadata: {error}: {aux_data:02x?}"); + } + }, + Ok(minicbor::data::Type::Tag) => { + if let Err(error) = + Self::decode_alonzo_plus_map(&mut raw_decoded_data, &mut decoder) + { + error!("Failed to Deserialize Alonzo+ Metadata: {error}: {aux_data:02x?}"); + } + }, + Ok(unexpected) => { + error!("Unexpected datatype for Aux data: {unexpected}: {aux_data:02x?}"); + }, + Err(error) => { + error!("Error decoding metadata: {error}: {aux_data:02x?}"); + }, + } + + raw_decoded_data + } + + /// Decode the Shelley map of metadata. + fn decode_shelley_map( + raw_decoded_data: &mut Self, decoder: &mut minicbor::Decoder, + ) -> anyhow::Result<()> { + let entries = match decoder.map() { + Ok(Some(entries)) => entries, + Ok(None) => { + // Sadly... Indefinite Maps are allowed in Cardano CBOR Encoding. + u64::MAX + }, + Err(error) => { + bail!("Error decoding metadata: {error}"); + }, + }; + + // debug!("Decoding shelley metadata map with {} entries", entries); + + let raw_metadata = decoder.input(); + + for _ in 0..entries { + let key = match decoder.u64() { + Ok(key) => key, + Err(error) => { + bail!("Error decoding metadata key: {error}"); + }, + }; + let value_start = decoder.position(); + if let Err(error) = decoder.skip() { + bail!("Error decoding metadata value: {error}"); + } + let value_end = decoder.position(); + let Some(value_slice) = raw_metadata.get(value_start..value_end) else { + bail!("Invalid metadata value found. Unable to extract raw value slice."); + }; + let value = value_slice.to_vec(); + + // debug!("Decoded metadata key: {key}, value: {value:?}"); + + let _unused = raw_decoded_data.metadata.insert(key, Arc::new(value)); + + // Look for End Sentinel IF its an indefinite MAP (which we know because entries is + // u64::MAX). + if entries == u64::MAX { + match decoder.datatype() { + Ok(Type::Break) => { + // Skip over the break token. + let _unused = decoder.skip(); + break; + }, + Ok(_) => (), // Not break, so do next loop, should be the next key. + Err(error) => { + bail!("Error checking indefinite metadata map end sentinel: {error}"); + }, + } + } + } + + Ok(()) + } + + /// Decode a Shelley-MA Auxiliary Data Array + fn decode_shelley_ma_array( + raw_decoded_data: &mut Self, decoder: &mut minicbor::Decoder, + ) -> anyhow::Result<()> { + match decoder.array() { + Ok(Some(entries)) => { + if entries != 2 { + bail!( + "Invalid number of entries in Metadata Array. Expected 2, found {entries}." + ); + } + }, + Ok(None) => { + bail!("Indefinite Array found decoding Metadata. Invalid."); + }, + Err(error) => { + bail!("Error decoding metadata: {error}"); + }, + }; + + // First entry is the metadata map, so just decode that now. + Self::decode_shelley_map(raw_decoded_data, decoder)?; + // Second entry is an array of native scripts. + Self::decode_script_array(raw_decoded_data, decoder, SmartContractType::Native)?; + + Ok(()) + } + + /// Decode a Shelley-MA Auxiliary Data Array + fn decode_alonzo_plus_map( + raw_decoded_data: &mut Self, decoder: &mut minicbor::Decoder, + ) -> anyhow::Result<()> { + match decoder.tag() { + Ok(tag) => { + if tag.as_u64() != 259 { + bail!("Invalid tag for alonzo+ aux data. Expected 259, found {tag}."); + } + }, + Err(error) => { + bail!("Error decoding tag for alonzo+ aux data: {error}"); + }, + } + + let entries = match decoder.map() { + Ok(Some(entries)) => entries, + Ok(None) => bail!("Indefinite Map found decoding Alonzo+ Metadata. Invalid."), + Err(error) => bail!("Error decoding Alonzo+ Metadata: {error}"), + }; + + // iterate the map + for _ in 0..entries { + let aux_type_key = match decoder.u64() { + Ok(key) => key, + Err(error) => { + bail!("Error decoding Alonzo+ Metadata Aux Data Type Key: {error}"); + }, + }; + + let contract_type = match aux_type_key { + 0 => { + if raw_decoded_data.metadata.is_empty() { + Self::decode_shelley_map(raw_decoded_data, decoder)?; + continue; + } + bail!("Multiple Alonzo+ Metadata entries found. Invalid."); + }, + 1 => SmartContractType::Native, + _ => { + if aux_type_key > 4 { + warn!( + "Auxiliary Type Key > 4 detected, assuming its a plutus script > V3." + ); + } + SmartContractType::Plutus(aux_type_key - 1) + }, + }; + + if raw_decoded_data.scripts.contains_key(&contract_type) { + bail!("Multiple Alonzo+ Scripts of type {contract_type} found. Invalid."); + } + + Self::decode_script_array(raw_decoded_data, decoder, contract_type)?; + } + Ok(()) + } + + /// Decode an array of smart contract scripts + fn decode_script_array( + raw_decoded_data: &mut Self, decoder: &mut minicbor::Decoder, + contract_type: SmartContractType, + ) -> anyhow::Result<()> { + let mut scripts: Vec> = Vec::new(); + + let entries = match decoder.array() { + Ok(Some(entries)) => entries, + Ok(None) => { + bail!("Indefinite Script Array found decoding Metadata. Invalid."); + }, + Err(error) => { + bail!("Error decoding metadata: {error}"); + }, + }; + + let raw_metadata = decoder.input(); + + for _entry in 0..entries { + if contract_type == SmartContractType::Native { + // Native Scripts are actually CBOR arrays, so capture their data as bytes for + // later processing. + let value_start = decoder.position(); + if let Err(error) = decoder.skip() { + bail!("Error decoding native script value: {error}"); + } + let value_end = decoder.position(); + let Some(value_slice) = raw_metadata.get(value_start..value_end) else { + bail!("Invalid metadata value found. Unable to extract native script slice."); + }; + scripts.push(value_slice.to_vec()); + } else { + let script = match decoder.bytes() { + Ok(script) => script, + Err(error) => bail!("Error decoding script data from metadata: {error}"), + }; + scripts.push(script.to_vec()); + } + } + + let _unused = raw_decoded_data + .scripts + .insert(contract_type, Arc::new(scripts)); + + Ok(()) + } + + /// Get Raw metadata for a given metadata label, if it exists. + #[allow(dead_code)] + pub(crate) fn get_metadata(&self, label: u64) -> Option>> { + self.metadata.get(&label).map(|v| v.value().clone()) + } +} diff --git a/rust/rbac-registration/src/cardano/transaction/witness.rs b/rust/rbac-registration/src/cardano/transaction/witness.rs new file mode 100644 index 000000000..736f67013 --- /dev/null +++ b/rust/rbac-registration/src/cardano/transaction/witness.rs @@ -0,0 +1,123 @@ +//! Transaction Witness +use std::fmt::{Display, Formatter}; + +use anyhow::bail; +use dashmap::DashMap; +use pallas::{codec::utils::Bytes, ledger::traverse::MultiEraTx}; + +use crate::utils::hashing::blake2b_244; + +/// `WitnessMap` type of `DashMap` with +/// key as [u8; 28] = (`blake2b_244` hash of the public key) +/// value as `(Bytes, Vec) = (public key, tx index within the block)` +pub(crate) type WitnessMap = DashMap<[u8; 28], (Bytes, Vec)>; + +#[derive(Debug)] +/// `TxWitness` struct to store the witness data. +pub(crate) struct TxWitness(WitnessMap); + +impl TxWitness { + /// Create a new `TxWitness` from a list of `MultiEraTx`. + pub(crate) fn new(txs: &[MultiEraTx]) -> anyhow::Result { + let map: WitnessMap = DashMap::new(); + for (i, tx) in txs.iter().enumerate() { + match tx { + MultiEraTx::AlonzoCompatible(tx, _) => { + let witness_set = &tx.transaction_witness_set; + if let Some(vkey_witness_set) = witness_set.vkeywitness.clone() { + for vkey_witness in vkey_witness_set { + let vkey_hash = blake2b_244(&vkey_witness.vkey)?; + let tx_num = u16::try_from(i)?; + map.entry(vkey_hash) + .and_modify(|entry: &mut (_, Vec)| entry.1.push(tx_num)) + .or_insert((vkey_witness.vkey.clone(), vec![tx_num])); + } + }; + }, + MultiEraTx::Babbage(tx) => { + let witness_set = &tx.transaction_witness_set; + if let Some(vkey_witness_set) = witness_set.vkeywitness.clone() { + for vkey_witness in vkey_witness_set { + let vkey_hash = blake2b_244(&vkey_witness.vkey)?; + let tx_num = u16::try_from(i)?; + map.entry(vkey_hash) + .and_modify(|entry: &mut (_, Vec)| entry.1.push(tx_num)) + .or_insert((vkey_witness.vkey.clone(), vec![tx_num])); + } + } + }, + MultiEraTx::Conway(tx) => { + let witness_set = &tx.transaction_witness_set; + if let Some(vkey_witness_set) = &witness_set.vkeywitness.clone() { + for vkey_witness in vkey_witness_set { + let vkey_hash = blake2b_244(&vkey_witness.vkey)?; + let tx_num = u16::try_from(i)?; + map.entry(vkey_hash) + .and_modify(|entry: &mut (_, Vec)| entry.1.push(tx_num)) + .or_insert((vkey_witness.vkey.clone(), vec![tx_num])); + } + } + }, + _ => { + bail!("Unsupported transaction type"); + }, + }; + } + Ok(Self(map)) + } + + /// Check whether the public key hash is in the given transaction number. + pub(crate) fn check_witness_in_tx(&self, vkey_hash: &[u8; 28], tx_num: u16) -> bool { + self.0 + .get(vkey_hash) + .map_or(false, |entry| entry.1.contains(&tx_num)) + } + + /// Get the actual address from the given public key hash. + #[allow(dead_code)] + pub(crate) fn get_witness_pk_addr(&self, vkey_hash: &[u8; 28]) -> Option { + self.0.get(vkey_hash).map(|entry| entry.0.clone()) + } +} + +impl Display for TxWitness { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + for data in &self.0 { + let vkey_hash = hex::encode(data.key()); + let vkey: Vec = data.0.clone().into(); + let vkey_encoded = hex::encode(&vkey); + writeln!( + f, + "Key Hash: {}, PublicKey: {}, Tx: {:?}", + vkey_hash, vkey_encoded, data.1 + )?; + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + fn conway() -> Vec { + hex::decode(include_str!("../../test_data/cardano/conway_1.block")) + .expect("Failed to decode hex block.") + } + + #[test] + fn tx_witness() { + let conway = conway(); + let conway_block = pallas::ledger::traverse::MultiEraBlock::decode(&conway) + .expect("Failed to decode MultiEraBlock"); + let txs_conway = conway_block.txs(); + let tx_witness_conway = TxWitness::new(&txs_conway).expect("Failed to create TxWitness"); + let vkey1_hash: [u8; 28] = + hex::decode("c0359ebb7d0688d79064bd118c99c8b87b5853e3af59245bb97e84d2") + .expect("Failed to decode vkey1_hash") + .try_into() + .expect("Invalid length of vkey1_hash"); + assert!(tx_witness_conway.get_witness_pk_addr(&vkey1_hash).is_some()); + assert!(tx_witness_conway.check_witness_in_tx(&vkey1_hash, 0)); + } +} diff --git a/rust/rbac-registration/src/lib.rs b/rust/rbac-registration/src/lib.rs new file mode 100644 index 000000000..ab9cfd1e4 --- /dev/null +++ b/rust/rbac-registration/src/lib.rs @@ -0,0 +1,5 @@ +//! This crate provides functionalities for RBAC registration. + +pub mod cardano; +pub mod registration; +pub(crate) mod utils; diff --git a/rust/rbac-registration/src/registration/cardano/mod.rs b/rust/rbac-registration/src/registration/cardano/mod.rs new file mode 100644 index 000000000..3895d8971 --- /dev/null +++ b/rust/rbac-registration/src/registration/cardano/mod.rs @@ -0,0 +1,685 @@ +//! Chain of Cardano registration data + +pub mod payment_history; +pub mod point_tx_idx; +pub mod role_data; + +use std::{collections::HashMap, sync::Arc}; + +use anyhow::bail; +use c509_certificate::c509::C509; +use pallas::{ + crypto::hash::Hash, + ledger::{ + addresses::{Address, ShelleyAddress, ShelleyPaymentPart}, + traverse::MultiEraTx, + }, + network::miniprotocols::Point, +}; +use payment_history::PaymentHistory; +use point_tx_idx::PointTxIdx; +use role_data::RoleData; +use tracing::error; + +use crate::{ + cardano::cip509::{ + self, + rbac::{ + certs::{C509Cert, X509DerCert}, + pub_key::{Ed25519PublicKey, SimplePublicKeyType}, + CertKeyHash, + }, + Cip509, UuidV4, + }, + utils::general::decremented_index, +}; + +/// Registration chains. +pub struct RegistrationChain { + /// Inner part of the registration chain. + inner: Arc, +} + +impl RegistrationChain { + /// Create a new instance of registration chain. + /// The first new value should be the chain root. + /// + /// # Arguments + /// - `cip509` - The CIP509. + /// - `tracking_payment_keys` - The list of payment keys to track. + /// - `point` - The point (slot) of the transaction. + /// - `tx_idx` - The transaction index. + /// - `txn` - The transaction. + /// + /// # Errors + /// + /// Returns an error if data is invalid + pub fn new( + point: Point, tracking_payment_keys: &[ShelleyAddress], tx_idx: usize, txn: &MultiEraTx, + cip509: Cip509, + ) -> anyhow::Result { + let inner = RegistrationChainInner::new(cip509, tracking_payment_keys, point, tx_idx, txn)?; + + Ok(Self { + inner: Arc::new(inner), + }) + } + + /// Update the registration chain. + /// + /// # Arguments + /// - `point` - The point (slot) of the transaction. + /// - `tx_idx` - The transaction index. + /// - `txn` - The transaction. + /// - `cip509` - The CIP509. + /// + /// # Errors + /// + /// Returns an error if data is invalid + pub fn update( + &self, point: Point, tx_idx: usize, txn: &MultiEraTx, cip509: Cip509, + ) -> anyhow::Result { + let new_inner = self.inner.update(point, tx_idx, txn, cip509)?; + + Ok(Self { + inner: Arc::new(new_inner), + }) + } + + /// Get the current transaction ID hash. + #[must_use] + pub fn current_tx_id_hash(&self) -> Hash<32> { + self.inner.current_tx_id_hash + } + + /// Get a list of purpose for this registration chain. + #[must_use] + pub fn purpose(&self) -> &[UuidV4] { + &self.inner.purpose + } + + /// Get the map of index in array to point, transaction index, and x509 certificate. + #[must_use] + pub fn x509_certs(&self) -> &HashMap)> { + &self.inner.x509_certs + } + + /// Get the map of index in array to point, transaction index, and c509 certificate. + #[must_use] + pub fn c509_certs(&self) -> &HashMap { + &self.inner.c509_certs + } + + /// Get the map of index in array to point, transaction index, and public key. + #[must_use] + pub fn simple_keys(&self) -> &HashMap { + &self.inner.simple_keys + } + + /// Get a list of revocations. + #[must_use] + pub fn revocations(&self) -> &[(PointTxIdx, CertKeyHash)] { + &self.inner.revocations + } + + /// Get the map of role number to point, transaction index, and role data. + #[must_use] + pub fn role_data(&self) -> &HashMap { + &self.inner.role_data + } + + /// Get the map of tracked payment keys to its history. + #[must_use] + pub fn tracking_payment_history(&self) -> &HashMap> { + &self.inner.tracking_payment_history + } +} + +/// Inner structure of registration chain. +#[derive(Clone)] +struct RegistrationChainInner { + /// The current transaction ID hash (32 bytes) + current_tx_id_hash: Hash<32>, + /// List of purpose for this registration chain + purpose: Vec, + + // RBAC + /// Map of index in array to point, transaction index, and x509 certificate. + x509_certs: HashMap)>, + /// Map of index in array to point, transaction index, and c509 certificate. + c509_certs: HashMap, + /// Map of index in array to point, transaction index, and public key. + simple_keys: HashMap, + /// List of point, transaction index, and certificate key hash. + revocations: Vec<(PointTxIdx, CertKeyHash)>, + + // Role + /// Map of role number to point, transaction index, and role data. + role_data: HashMap, + /// Map of tracked payment key to its history. + tracking_payment_history: HashMap>, +} + +impl RegistrationChainInner { + /// Create a new instance of registration chain. + /// The first new value should be the chain root. + /// + /// # Arguments + /// - `cip509` - The CIP509. + /// - `tracking_payment_keys` - The list of payment keys to track. + /// - `point` - The point (slot) of the transaction. + /// - `tx_idx` - The transaction index. + /// - `txn` - The transaction. + /// + /// # Errors + /// + /// Returns an error if data is invalid + fn new( + cip509: Cip509, tracking_payment_keys: &[ShelleyAddress], point: Point, tx_idx: usize, + txn: &MultiEraTx, + ) -> anyhow::Result { + // Should be chain root, return immediately if not + if cip509.prv_tx_id.is_some() { + bail!("Invalid chain root, previous transaction ID should be None."); + } + + let mut validation_report = Vec::new(); + // Do the CIP509 validation, ensuring the basic validation pass. + if !cip509.validate(txn, &mut validation_report) { + // Log out the error if any + error!("CIP509 validation failed: {:?}", validation_report); + bail!("CIP509 validation failed, {:?}", validation_report); + } + + // Add purpose to the list + let purpose = vec![cip509.purpose]; + + let registration = cip509.x509_chunks.0; + let point_tx_idx = PointTxIdx::new(point, tx_idx); + + let x509_cert_map = chain_root_x509_certs(registration.x509_certs, &point_tx_idx); + let c509_cert_map = chain_root_c509_certs(registration.c509_certs, &point_tx_idx); + let public_key_map = chain_root_public_keys(registration.pub_keys, &point_tx_idx); + let revocations = revocations_list(registration.revocation_list, &point_tx_idx); + let role_data_map = chain_root_role_data(registration.role_set, txn, &point_tx_idx)?; + + let mut tracking_payment_history = HashMap::new(); + // Create a payment history for each tracking payment key + for tracking_key in tracking_payment_keys { + tracking_payment_history.insert(tracking_key.clone(), Vec::new()); + } + // Keep record of payment history, the payment key that we want to track + update_tracking_payment_history(&mut tracking_payment_history, txn, &point_tx_idx)?; + + Ok(Self { + purpose, + current_tx_id_hash: txn.hash(), + x509_certs: x509_cert_map, + c509_certs: c509_cert_map, + simple_keys: public_key_map, + revocations, + role_data: role_data_map, + tracking_payment_history, + }) + } + + /// Update the registration chain. + /// + /// # Arguments + /// - `point` - The point (slot) of the transaction. + /// - `tx_idx` - The transaction index. + /// - `txn` - The transaction. + /// - `cip509` - The CIP509. + /// + /// # Errors + /// + /// Returns an error if data is invalid + fn update( + &self, point: Point, tx_idx: usize, txn: &MultiEraTx, cip509: Cip509, + ) -> anyhow::Result { + let mut new_inner = self.clone(); + + let mut validation_report = Vec::new(); + // Do the CIP509 validation, ensuring the basic validation pass. + if !cip509.validate(txn, &mut validation_report) { + error!("CIP509 validation failed: {:?}", validation_report); + bail!("CIP509 validation failed, {:?}", validation_report); + } + + // Check and update the current transaction ID hash + if let Some(prv_tx_id) = cip509.prv_tx_id { + // Previous transaction ID in the CIP509 should equal to the current transaction ID + // or else it is not a part of the chain + if prv_tx_id == self.current_tx_id_hash { + new_inner.current_tx_id_hash = prv_tx_id; + } else { + bail!("Invalid previous transaction ID, not a part of this registration chain"); + } + } + + // Add purpose to the chain, if not already exist + let purpose = cip509.purpose; + if !self.purpose.contains(&purpose) { + new_inner.purpose.push(purpose); + } + + let registration = cip509.x509_chunks.0; + let point_tx_idx = PointTxIdx::new(point, tx_idx); + + update_x509_certs(&mut new_inner, registration.x509_certs, &point_tx_idx); + update_c509_certs(&mut new_inner, registration.c509_certs, &point_tx_idx)?; + update_public_keys(&mut new_inner, registration.pub_keys, &point_tx_idx); + + let revocations = revocations_list(registration.revocation_list, &point_tx_idx); + // Revocation list should be appended + new_inner.revocations.extend(revocations); + + update_role_data(&mut new_inner, registration.role_set, txn, &point_tx_idx)?; + + update_tracking_payment_history( + &mut new_inner.tracking_payment_history, + txn, + &point_tx_idx, + )?; + + Ok(new_inner) + } +} + +/// Process x509 certificate for chain root. +fn chain_root_x509_certs( + x509_certs: Option>, 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())); + } + } + } + map +} + +/// Update x509 certificates in the registration chain. +fn update_x509_certs( + new_inner: &mut RegistrationChainInner, x509_certs: Option>, + 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())); + }, + } + } + } +} + +/// Process c509 certificates for chain root. +fn chain_root_c509_certs( + c509_certs: Option>, 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())); + } + } + } + map +} + +/// Update c509 certificates in the registration chain. +fn update_c509_certs( + new_inner: &mut RegistrationChainInner, c509_certs: Option>, + 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())); + }, + } + } + } + Ok(()) +} + +/// Process public keys for chain root. +fn chain_root_public_keys( + pub_keys: Option>, 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.clone())); + } + } + } + map +} + +/// Update public keys in the registration chain. +fn update_public_keys( + new_inner: &mut RegistrationChainInner, pub_keys: Option>, + 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.clone())); + }, + } + } + } +} + +/// Process the revocation list. +fn revocations_list( + revocation_list: Option>, 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())); + } + } + revocations +} + +/// Process the role data for chain root. +fn chain_root_role_data( + role_set: Option>, 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(), + ), + ), + ); + } + } + Ok(role_data_map) +} + +/// Update the role data in the registration chain. +fn update_role_data( + inner: &mut RegistrationChainInner, role_set: Option>, + 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(), + ), + ), + ); + } + } + Ok(()) +} + +/// Helper function for retrieving the Shelley address from the transaction. +fn get_payment_addr_from_tx( + txn: &MultiEraTx, payment_key_ref: Option, +) -> anyhow::Result> { + // The index should exist since it pass the basic validation + if let Some(key_ref) = payment_key_ref { + if let MultiEraTx::Conway(tx) = txn { + // Transaction output + if key_ref < 0 { + let index = decremented_index(key_ref.abs())?; + if let Some(output) = tx.transaction_body.outputs.get(index) { + // Conway era -> Post alonzo tx output + match output { + pallas::ledger::primitives::conway::PseudoTransactionOutput::PostAlonzo( + o, + ) => { + let address = + Address::from_bytes(&o.address).map_err(|e| anyhow::anyhow!(e))?; + + if let Address::Shelley(addr) = address { + return Ok(Some(addr.payment().clone())); + } + bail!("Unsupported address type in payment key reference"); + }, + // Not support legacy form of transaction output + pallas::ledger::primitives::conway::PseudoTransactionOutput::Legacy(_) => { + bail!("Unsupported transaction output type in payment key reference"); + }, + } + } + // Index doesn't exist + bail!("Payment key not found in transaction output"); + } + // Transaction input, currently unsupported because of the reference to transaction hash + bail!("Unsupported payment key reference to transaction input"); + } + } + Ok(None) +} + +/// Update the payment history given the tracking payment keys. +fn update_tracking_payment_history( + tracking_payment_history: &mut HashMap>, txn: &MultiEraTx, + point_tx_idx: &PointTxIdx, +) -> anyhow::Result<()> { + if let MultiEraTx::Conway(tx) = txn { + // Conway era -> Post alonzo tx output + for (index, output) in tx.transaction_body.outputs.iter().enumerate() { + match output { + pallas::ledger::primitives::conway::PseudoTransactionOutput::PostAlonzo(o) => { + let address = + Address::from_bytes(&o.address).map_err(|e| anyhow::anyhow!(e))?; + let shelley_payment = if let Address::Shelley(addr) = address { + addr.clone() + } else { + bail!("Unsupported address type in update payment history"); + }; + // If the payment key from the output exist in the payment history, add the + // history + if let Some(vec) = tracking_payment_history.get_mut(&shelley_payment) { + let output_index: u16 = index.try_into().map_err(|_| { + anyhow::anyhow!("Cannot convert usize to u16 in update payment history") + })?; + + vec.push(PaymentHistory::new( + point_tx_idx.clone(), + txn.hash(), + output_index, + o.value.clone(), + )); + } + }, + pallas::ledger::primitives::conway::PseudoTransactionOutput::Legacy(_) => { + bail!("Unsupported transaction output type in update payment history"); + }, + } + } + } + Ok(()) +} + +#[cfg(test)] +mod test { + use minicbor::{Decode, Decoder}; + use pallas::{ledger::traverse::MultiEraTx, network::miniprotocols::Point}; + + use super::RegistrationChain; + use crate::cardano::{cip509::Cip509, transaction::raw_aux_data::RawAuxData}; + + fn cip_509_aux_data(tx: &MultiEraTx<'_>) -> Vec { + let raw_auxiliary_data = tx + .as_conway() + .unwrap() + .clone() + .auxiliary_data + .map(|aux| aux.raw_cbor()); + + let raw_cbor_data = match raw_auxiliary_data { + pallas::codec::utils::Nullable::Some(data) => Ok(data), + _ => Err("Auxiliary data not found"), + }; + + let auxiliary_data = RawAuxData::new(raw_cbor_data.expect("Failed to get raw cbor data")); + auxiliary_data + .get_metadata(509) + .expect("Failed to get metadata") + .to_vec() + } + + fn conway_1() -> Vec { + hex::decode(include_str!("../../test_data/cardano/conway_1.block")) + .expect("Failed to decode hex block.") + } + + fn conway_4() -> Vec { + hex::decode(include_str!("../../test_data/cardano/conway_4.block")) + .expect("Failed to decode hex block.") + } + + #[test] + fn test_new_and_update_registration() { + let conway_block_data_1 = conway_1(); + let point_1 = Point::new( + 77_429_134, + hex::decode("62483f96613b4c48acd28de482eb735522ac180df61766bdb476a7bf83e7bb98") + .unwrap(), + ); + let multi_era_block_1 = + pallas::ledger::traverse::MultiEraBlock::decode(&conway_block_data_1) + .expect("Failed to decode MultiEraBlock"); + + let transactions_1 = multi_era_block_1.txs(); + // Forth transaction of this test data contains the CIP509 auxiliary data + let tx_1 = transactions_1 + .get(3) + .expect("Failed to get transaction index"); + + let aux_data_1 = cip_509_aux_data(tx_1); + let mut decoder = Decoder::new(aux_data_1.as_slice()); + let cip509_1 = Cip509::decode(&mut decoder, &mut ()).expect("Failed to decode Cip509"); + let tracking_payment_keys = vec![]; + + let registration_chain = + RegistrationChain::new(point_1.clone(), &tracking_payment_keys, 3, tx_1, cip509_1); + // Able to add chain root to the registration chain + assert!(registration_chain.is_ok()); + + let conway_block_data_4 = conway_4(); + let point_4 = Point::new( + 77_436_369, + hex::decode("b174fc697126f05046b847d47e60d66cbedaf25240027f9c07f27150889aac24") + .unwrap(), + ); + + let multi_era_block_4 = + pallas::ledger::traverse::MultiEraBlock::decode(&conway_block_data_4) + .expect("Failed to decode MultiEraBlock"); + + let transactions_4 = multi_era_block_4.txs(); + // Second transaction of this test data contains the CIP509 auxiliary data + let tx = transactions_4 + .get(1) + .expect("Failed to get transaction index"); + + let aux_data_4 = cip_509_aux_data(tx); + let mut decoder = Decoder::new(aux_data_4.as_slice()); + let cip509 = Cip509::decode(&mut decoder, &mut ()).expect("Failed to decode Cip509"); + + // Update the registration chain + assert!(registration_chain + .unwrap() + .update(point_4.clone(), 1, tx, cip509) + .is_ok()); + } +} diff --git a/rust/rbac-registration/src/registration/cardano/payment_history.rs b/rust/rbac-registration/src/registration/cardano/payment_history.rs new file mode 100644 index 000000000..13e4aabc5 --- /dev/null +++ b/rust/rbac-registration/src/registration/cardano/payment_history.rs @@ -0,0 +1,56 @@ +//! Payment history of the public key in tracking payment keys. + +use pallas::{crypto::hash::Hash, ledger::primitives::conway::Value}; + +use super::point_tx_idx::PointTxIdx; + +/// Payment history of the public key in tracking payment keys. +#[derive(Clone)] +pub struct PaymentHistory { + /// The point and transaction index. + point_tx_idx: PointTxIdx, + /// Transaction hash that this payment come from. + tx_hash: Hash<32>, + /// The transaction output index that this payment come from. + output_index: u16, + /// The value of the payment. + value: Value, +} + +impl PaymentHistory { + /// Create an instance of payment history. + pub(crate) fn new( + point_tx_idx: PointTxIdx, tx_hash: Hash<32>, output_index: u16, value: Value, + ) -> Self { + PaymentHistory { + point_tx_idx, + tx_hash, + output_index, + value, + } + } + + /// Get the point and transaction index. + #[must_use] + pub fn point_tx_idx(&self) -> &PointTxIdx { + &self.point_tx_idx + } + + /// Get the transaction hash. + #[must_use] + pub fn tx_hash(&self) -> Hash<32> { + self.tx_hash + } + + /// Get the transaction output index. + #[must_use] + pub fn output_index(&self) -> u16 { + self.output_index + } + + /// Get the value of the payment. + #[must_use] + pub fn value(&self) -> &Value { + &self.value + } +} diff --git a/rust/rbac-registration/src/registration/cardano/point_tx_idx.rs b/rust/rbac-registration/src/registration/cardano/point_tx_idx.rs new file mode 100644 index 000000000..27247dd5c --- /dev/null +++ b/rust/rbac-registration/src/registration/cardano/point_tx_idx.rs @@ -0,0 +1,26 @@ +//! Point or absolute slot and transaction index. + +use pallas::network::miniprotocols::Point; + +/// Point (slot) and transaction index. +#[derive(Clone)] +pub struct PointTxIdx((Point, usize)); + +impl PointTxIdx { + /// Create an instance of point and transaction index. + pub(crate) fn new(point: Point, tx_idx: usize) -> Self { + PointTxIdx((point, tx_idx)) + } + + /// Get the point. + #[must_use] + pub fn point(&self) -> &Point { + &self.0 .0 + } + + /// Get the transaction index. + #[must_use] + pub fn tx_idx(&self) -> usize { + self.0 .1 + } +} diff --git a/rust/rbac-registration/src/registration/cardano/role_data.rs b/rust/rbac-registration/src/registration/cardano/role_data.rs new file mode 100644 index 000000000..b6b4d947d --- /dev/null +++ b/rust/rbac-registration/src/registration/cardano/role_data.rs @@ -0,0 +1,59 @@ +//! RBAC role data + +use std::collections::HashMap; + +use pallas::ledger::addresses::ShelleyPaymentPart; + +use crate::cardano::cip509::rbac::role_data::KeyLocalRef; + +/// Role data +#[derive(Clone)] +pub struct RoleData { + /// A signing keys to the data within registration. + signing_key_ref: Option, + /// An encryption keys to the data within registration. + encryption_ref: Option, + /// A payment key where reward will be distributed to. + payment_key: Option, + /// Map of role extended data (10-99) to its data + role_extended_data: HashMap>, +} + +impl RoleData { + /// Create an instance of role data. + pub(crate) fn new( + signing_key_ref: Option, encryption_ref: Option, + payment_key: Option, role_extended_data: HashMap>, + ) -> Self { + RoleData { + signing_key_ref, + encryption_ref, + payment_key, + role_extended_data, + } + } + + /// Get the reference of signing keys. + #[must_use] + pub fn signing_key_ref(&self) -> &Option { + &self.signing_key_ref + } + + /// Get the reference of encryption keys. + #[must_use] + pub fn encryption_ref(&self) -> &Option { + &self.encryption_ref + } + + /// Get the payment key. + #[must_use] + pub fn payment_key(&self) -> &Option { + &self.payment_key + } + + /// Get the role extended data. + #[must_use] + pub fn role_extended_data(&self) -> &HashMap> { + &self.role_extended_data + } +} diff --git a/rust/rbac-registration/src/registration/mod.rs b/rust/rbac-registration/src/registration/mod.rs new file mode 100644 index 000000000..a6a5c3c00 --- /dev/null +++ b/rust/rbac-registration/src/registration/mod.rs @@ -0,0 +1,3 @@ +//! Registration module + +pub mod cardano; diff --git a/rust/rbac-registration/src/test_data/cardano/conway_1.block b/rust/rbac-registration/src/test_data/cardano/conway_1.block new file mode 100644 index 000000000..536145021 --- /dev/null +++ b/rust/rbac-registration/src/test_data/cardano/conway_1.block @@ -0,0 +1 @@ +820785828a1a002cf1361a049d798e5820f8105b5f1ff5805a1ad73a4bed95297c11a530957fd124fe116d847b9a8a2d18582054ca56226c90d5544bf6f558026b814027b634e60de0bc215237944110569c6c582037d6147adbf2907948d66f9e4751774bbe9d6c4ad562ce04ef66f0fc1e7ac3018258408efeee2071a87217e4e5d45c8afcefbca69a9f7521be99cf007cadd5657436f05e801b96f75465cc631dbca05f58c0f44406f3bf80f055ac47285dfe4e72f5185850dc503d811e615d063298b2688dfcdeb4c82b82a6509672ad7eaeafa87ec8af4144530d7ad542daf0f007b3ba2133ea5abebb253ec95180f3805eefd1a1cfebafd497bf0215506912fd50497db4ba150e19173458200cb42de7dcf6a9dd27dac89dd10e98af017f352c5d4c9d995fea811ed22221d484582074deff4c062c74d426154a221193a5fb9472c401d42217b07d617c03cc2e9bb00819023f584049885039bea644a49bce5493fd4dffb0c835074820bf1912e3b372bfbba36904c7a47ae3ad59da8148876d83868488b94fa81df458fd3bf3d3706f4ec11a5d00820a005901c0509399cc5ca4713330d1370f986def5b3096f61c96f3de54015334c5abccad85c0b4c99a898399ccdf5979a0b810be83479c2b4430c665d350878f08c1bdc80b7c4cdd132322a7ccf28796aedaf1767c7ba8d9029430f8e3ecc88963f671a07163f7d2677e3c31db8af39c51732fd158d4784bd48381006d4a892d186266279241a9e786e2b4fe28d83a9890694670950c6d981e96a1d182d5e02241cf36f6278ff8e7cf6206735d54e8bd026f696d3b46d31268357318c4e2a91b084b7a49975bf06ec920ca357a17f85d6fca8ac7b388c599e542ec32f0361e2fda5c8d43e3e3b52e31761953c114ca9f952eff1195245df3aeef714de2bc5dab7e971b325b99ed37346741bf75ef68b201a9a9f85978d83ad602e9c8e31987395473eb87b2f5a34c30882a1812dc5081f04e731071cb8a58c5c7fc4ba168028154b07827faa64a6fd86e0594804c8e74213860a0d0a602306b8468a2f7ee112b380f143437227bdc37e8efee149b5bbe436f1b1d23a1afecd4c2d17a2484827f655b85228d587e54d26530645bb4de4aa4a706d0676974ac64fa4c2bc158adecd78efbed7f7522e0627056391093fcf37967f35f3479878ef3e731e21eac3d787f7a2af2c784ab00828258206f135b9e227de387653f19561fdc0a014eacdb9c30c5d8da5fb8adb302f24c6d01825820dee38df4b12e03388390d6452765086e4348da004f13f4b3dff24cc2fdd99055000182a300581d70bdc22da682cd9aceed5fd48914789fc98c94abc79fed8b40cb8c431401821a001e8480a1581cc13ddf298a5d25aff2933695987912b4f1748bdf0df8e4b5d85f2360a14e50524550524f445f4f5241434c4501028201d81856d8799fd8799f1a0bf5b443ff1b0000019386d2d7a8ff82581d60c0359ebb7d0688d79064bd118c99c8b87b5853e3af59245bb97e84d21b000000011722b776021a0006a164031a049d7c7d081a049d75760b582078c96a0e0f77a9be3baedbc5e24fe7c5f4de3f13dc79ada0676d7a0fd12fa6d90d818258206f135b9e227de387653f19561fdc0a014eacdb9c30c5d8da5fb8adb302f24c6d010e81581cc0359ebb7d0688d79064bd118c99c8b87b5853e3af59245bb97e84d20f001082581d60c0359ebb7d0688d79064bd118c99c8b87b5853e3af59245bb97e84d21b0000000116dd0d9a111a004c4b40a900d9010282825820b7141e4950cd9311eb083caffda6dad64ca0956e44e2855316d4c1553da230e800825820b7141e4950cd9311eb083caffda6dad64ca0956e44e2855316d4c1553da230e8010dd9010281825820688a09a2a039214f244e8f8eed49bf4f190104ebe32f6d9680277d70901c20220112d9010281825820c47f5b7661fd8295674076b8b76db11d08d4ddafb2529a6159e059ed6128af73010182a300581d701db5ea0b1cb006f32a3c3f6438f6c4b3df7b2018b965a63d29b54d0301821a001b9f18a1581caa7f3d7240a9d06e947fcf387957e7f01fd4484a432a4b879cdbf24aa14d446a65644f7261636c654e465401028201d81858a6d8799f5840fe28ac9c796a0157a6da71b9738beae6a085d1debdd682838584e03592e6581a50bc341e879b8b197a88a6d175a01e8d51c72c4acbf3fbaac3fb7158e3c2500fd8799fd8799f1a00030d401a000381f7ffd8799fd8799fd87a9f1b00000193858940a8ffd87a80ffd8799fd87a9f1b000001938596fc48ffd87a80ffff43555344ff581caa7f3d7240a9d06e947fcf387957e7f01fd4484a432a4b879cdbf24aff82583900f8e2356e21af3563556c167c31bcc00f9fc061a8a852a16a42ffcc7927ce10b87147e3e19a5dc622b6a535ab9ce01cb2d586a3a690a74c0e1a00f81c6a021a000426b4031a049d7c7d081a049d78f90ed9010281581cf8e2356e21af3563556c167c31bcc00f9fc061a8a852a16a42ffcc790b5820cc99e113594d6781a6f5a4a16205a0ae3072c1f4414121e08df388fdeb656fd9a700d9010282825820d59aa48a2208563e6fac203419186430c87b5c07ed72c5c1947f5c2b8cdd810600825820ffc0aaef9f13145bee27320c87410bd607c9eeb7f9d624ad4d18266227ac9596010183a300581d709dcb619aefe878561bc3067b7a0e923f1ce961c38b7bfc1a283a873e01821a001bafeea1581cec64872c1965bbbaa8868aef2bd9a343b821a9f2c7787ea096f62262a154000643b043495036382047656e657261746f727301028201d8185884d8799fa6446e616d6549496d616765204e46544b6465736372697074696f6e514173736574204465736372697074696f6e45696d6167654a697066733a2f2f2e2e2e496d656469615479706549696d6167652f706e6744726f6c654475736572435f706b581c8e3b54aca70c6778daca5f9ec4e99a7817052e64f23979080f97beca01ff82583900f7a232fecd012dc3c556587593c5a5cbed826a49ce06c19ece9f350cb74a4ce22b780d2c9139c32806dd402df8947de0303ed3f976136a231a000f4240825839008e3b54aca70c6778daca5f9ec4e99a7817052e64f23979080f97beca718188b968725132a2cf6bb0925bad1f4b30e000990e526b2e9443611a24ad8844021a0003bf5c0b58201cf1170ee1369bd082ae8d39b0d99db7caacc10a79b41cde1122bd0f0c7e5bf00dd90102818258206a3f0a51813098b342c8fa71b5766922ca2cef531cdcf925da83c990c82dcb75050ed9010281581c8e3b54aca70c6778daca5f9ec4e99a7817052e64f23979080f97beca12d90102818258208a4ad7d31a93bc9c25430056bc7910b4cfe706bcd099e59e2332e041ade7132c00a600818258205bb5b41eae680aca501bf92d2830fef5dbe9ef7d74d605c018899031a7f74463000181a200583900eb21979c03eed7207020b2a0b47565b5aafb1a2c3849b59a1fa8e6c5e075be10ec5c575caffb68b08c31470666d4fe1aeea07c16d6473903011b000000024f96ecf5021a0002fea1075820b1553a57d1cbb2ac233e98a4700d1dfd7972210cfd4e248ab4ade30afefd00d30e81581ce075be10ec5c575caffb68b08c31470666d4fe1aeea07c16d64739030f0084a30081825820ef603099b2579d5ca273dce1f1257c11f054664ba972fd61c008b58a1b2325c158405faba20a8a371470fac259901656b63bb81201e1ef6768809b581ff0bab12cc6f236e6a52923e0ce4772f8e6d4c88db03da5db3d8b8a1770b7b0eb5e4a7e9b0f0681590bd7590bd401000033232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323233027222232325335333006533357346074607e0022646605a60586ae84004dd69aba135744002607c00206e64a666ae68c0ecc1000044c8c8c848cc00400c008c0b8d5d09aba2002375a6ae84004c0fc0040e0dd50022999ab9a303a303f002132321233001003002357426ae88c0fc00cd5d0981f00101b8890008a99ab9c490103505435001637540086ea8004cc09c8888c8d4014888d40188c88c94cd54cd4ccd54c0d40e0c0a80a4c0ac01ccc0b08004d40048888888888880100e44cd5ce24812d546865207472616e73616374696f6e206973206e6f74207369676e6564206279206f7261636c65206f776e657200030132533553355335333553036039302b02a253335302135001202103e215333573466ebcc118cd5d0182319aba0375000a6ec4080cd5d01ba835500b03937620400022a66a666036446a0044466603e446a00444a666ae68c12c004400c4cc11848ccc00401c00c00800c00c0040c0cd54c0e80ec8d400488cc0e0008cd54c0f40f88d400488cc0ec008ccd4004dc0a4000e048cdc0800a400000266aa607407646a0024466070004666a002466aa607c07e46a0024466078004604600200244666038066004002466aa607c07e46a0024466078004604000200266602e05c666050400204e6a004404e0060784426a004444a66a0082a666ae68cdd78018238a999ab9a3375e00408e2a666ae68cdc4000a400006e2666ae68cdc4800a410125e80206a07006e06e44072064407e6044014074266ae712401024c310003103a1335738921156f757470757420646f6573206e6f74206d6174636800031153355333573466e2400d20001335738921165072696365206d75737420626520706f7369746976650003103a15335533535353301275a6a00407e06e44a666a00442a666ae68cdc419b8000100935500c03a0331333573466e24004d540300e80c40d00c80c84d4cc049d69a80101f911a801112999a99980980a980f19b8100900b0021533500113330120150030081333012015003008034133301201500300803003a13357389212345787069726174696f6e2074696d65206973206e6f742070726f7065726c792073657400031030030333025200102435302635533530270092100116036202402f302d350080393302f3016337000020060542a666a602e6aa66a60320022602602a442a66a0022004442602e032402e02842a66a6666666ae900048c94ccd5cd181d98200008991999aab9f001202d23233335573e002405e46666aae7cd5d1001119299a9999999aba400123253335734608660900022646666aae7c00480d48cccd55cf9aba20022533530203574200642605e00206a406c07a078608e0020646ea800880c880c880c880c80e4854cd4c070d5d08029098159981a8010008188181aba1005203003703635744004068607e0020546ea800880a880a880a880a80c48400405480548c94ccd5cd181b181d800899191919091998008028018011bad357426ae88008dd69aba10013574460760046ae84c0e80040ccdd50009111a801111a801912999a9998040038020010a99a8018800819814819911192999a80190a999a80190a999a80290980200b0980180a8a999a80210980200b0980180a80c0060a999a80210980180a8980100a0a999a80190980180a8980100a00b8a999a801100700b0068a999a80110a999a80210980180a8980100a0a999a80190980180a8980100a00b8058a999a80190980100a098008098a999a80110980100a0980080980b12999a80110a999a80210a999a802109998040038010008b0b0b0060a999a80190a999a801909998038030010008b0b0b00580691a80091111111003891999999980091199ab9a3370e00400204004644a666ae68cdc38010008098a999ab9a3371200400201401044666ae68cdc400100081001191199ab9a3371200400204004644666ae68cdc480100081181011199ab9a3371000400204604044a666ae68cdc480100088008801112999ab9a33712004002200420024464a666ae68c0c8c0dc0044c8c8cc0994ccd5cd181a181c801099813198038029aba130380023006357426ae88c0e00080c54ccd5cd181a181c800899813198038029aba130380013006357426ae88c0e00040c4dd51aba135744606e0046ea8d5d0981b0008179baa0012325333573460620020522a666ae68c0c000407c0b4c0d0dd50009119192999ab9a303300100815333573460640022601460086ae84c0d400854ccd5cd1818800803017181a8009baa00122233355302302702a335530260272350012233024002300b001333553023027223500222533533355302802b301d01c235001223300a002005006100313302e00400301c001335530260272350012233024002330382253350011300a003221350022253353300c002008112223300200a0041300600300400211009212223001004112220012230302253350011003221330060023004001212223003004233333335748002403040304030460206eb4008806007c94cd5ce2481286578706563746564206f6e6c7920612073696e676c6520636f6e74696e75696e67206f7574707574001615335738920117496e76616c696420646174756d20696e206f757470757400164988880088c8c94ccd5cd1813000899091118010021aba13028002153335734604a002264244460020086ae84c0a000854ccd5cd181200080201098140009baa00111003253353007001213335530150192235300535300935003019222200422353007350042222004225333573466ebc01000854ccd5cd19baf0030011330220060051005100500e3300d00735300f3500201b22222222222200a15335738921024c660016232533357346040604a0022646424660020060046ae84d5d118128011aba1302400101d3754002444006660024002eb4888cc09088cccd55cf80090071191980e9980a180398138009803181300098021aba2003357420040306eac0048c94ccd5cd180e181080089919191919190919998008038028018011aba1357440046ae84004d5d10011aba10013574460420046ae84c080004064dd5000919191999aa999ab9a3370e90030008990911118020029aba13020002153335734603c00226424444600400a6ae84c08000854ccd5cd180e8008990911118008029aba13020002153335734603800226424444600600a6ae84c0800080648034803480348ccd54c048054cc03c894cd40088400c400403494ccd5cd19baf002350010181300600100d3300923253335734603e60480022646424660020060046ae84d5d118120011aba1302300101c37540026a60166a00802e44444444444401860400026ea8d40040408488c00800c48cc004894cd400804c40040208cc02488ccd400c04c008004d400403488ccd5cd19baf002001004007223301c2233335573e002400c466028600a6ae84008c00cd5d10010081bac001100d23253335734602860320022646464646464646464646464646464646464646464642466666666666600202e02a02602201e01a01601200e00a0060046ae84d5d10011aba1001357440046ae84004d5d10011aba1001357440046ae84004d5d10011aba1001357440046ae84004d5d10011aba1001357440046ae84004d5d10011aba1001357440046ae84004d5d1180c8011aba1301800101137540022002200c464a666ae68c044c0580044dd69aba1301500100e37540024424660020060044446006600400260244422444a66a00220044426600a004666aa600e01600a00800260224422444a66a00226a00600c442666a00a0186008004666aa600e01400a00800244002601e442244a66a00200c44266014600800466aa600c00e00800224002220024400444244660020080062a66ae712411f496e76616c696420646174756d20746f20636865636b4f776e4f757470757400161533573892010350543100162222222222220054c1014000370e90001b8748008dc3a40086e9520005573caae748c8c00400488cc00cc00800800530012cd8799f581cc0359ebb7d0688d79064bd118c99c8b87b5853e3af59245bb97e84d21a000dbba01a01499700ff00010581840001d8799f1b00000193858940a8d8799f1a0bf5b443ffff821a001862581a18087eb7a200d9010281825820c58b6e67ad72d3e4de04c0c143b15e0e60573ea9b740cf0a8fbfafb0475d50fd5840e8a05b2cc57a1952d1c3179b2b5019c1511fd49590dd2967b1fc5b3469251592698aca04e5b7a190b94f1d4a2545dce999aaa0921b50705e022bf02eb4b8720a05a182000082d87980821a000810e71a0b99de0ea20081825820f78c0ef2db02673d535e54b3e534281bce897181a3725593eb73f9d13cc7344a58407ca5fef9ff8a97006259f3308ea0320b2f361aaf38992c2316caea9bd62a8de30c99b9be9316fa36aca04a58eeaedc5f39335d714c05e09f3e282071466e4c0405a182000082d87980821a000619741a0893bcdfa100828258208469288efa6f9cb49040b43dcff93f40969d72433f751afde50235012c0160205840bf6cb0ebd00118d87bd8ea9a0e70f9f7954a3450dbbb2ba76a1558b1f7664d8c3dff9da85419f1f6a50e1a041d86de98ebcb511b22d885e0e092ffa7dc78910182582076af5530fa318a370820270031d1838545a4ceed8696510627563d1114d4182b58401f42ca3f529ee9765330674a56c3d70a885a8c62912a18145f51afaafb2da78b6319c70aa432f5f8d976a4d9dc309b1f17bcf89af1d08e939aef43f7bbea4004a103a11901fda40050ca7a1457ef9f4c7f9c747f8c4a4cfa6c0150d127265b6c2024bf7e254571bce41fa70b8758401b3f020064404f2b8fc22b3919605fb86cb9d3c7573ce58ffaedb716517ba0020ab9162510786a81643723fe5297dac6f96dd080032c60107110fa2fe7ddb570584018140643422a31e1cde248145cc8c55f5c53022b8e46c14d10282a880206d080033cc1bd382b1d51525a62210b69716949294b6305852895928c82f7b73cc70c58402f7d155c03e2abf9fd94d6e23b41f5ce57f595c8faff16fe899abe2bfe78d839d09b90a87fcd96f55ffbcfc99674fc5a392896703ceb8ffacb6b193beb1bd9a858405c94f4e2de6d001ac5813a71a03a962c1408a0581c2800093e112d939167909cae2d6af8fd08809295d4a04b360a737114e2c8d4c1d74f514cccc1d939502c6858409e6ac7cd4724420283e483c583821ca4c214c2dc15c4bd9d6582fd03fda5dc4202c39da5e5e50283dd2503bc03bc645d027d3d039c5c5ca543dc021ffd8d347b58408b716dad363d2b3776d27fc3acb2a63d1f99d49dfffebca526c518ba311787c30c97732f77b04e2844c7c3c5aa8bfdcd526d3e822da4d57aba35b15c1445cf0a579cc523cdd99bb7a177061a2110288883c0d971cebfea00186358400cd4f3828590fb603b04ce534efbce76320f0828b3773a3d5b80f0f8da8a3b134b33ab6e621c9a4e749f6fd61fede6c1ee6210c2264c176cdd8271e39a116d0680 \ No newline at end of file diff --git a/rust/rbac-registration/src/test_data/cardano/conway_2.block b/rust/rbac-registration/src/test_data/cardano/conway_2.block new file mode 100644 index 000000000..51e86d94c --- /dev/null +++ b/rust/rbac-registration/src/test_data/cardano/conway_2.block @@ -0,0 +1 @@ +820785828a1a002ccb5a1a04998bb05820c2401f491a275eace00ee76cbc4265323377eeda7f18b26a0b6f3e7b237ecb3b5820087932a594533b96a2abd339715dd222835d36856ac2c0b8fcd171e3ec944b8e5820b5b14e8eb25a9366e0c1a43afcd8d0859249202f3a8dc03b13f416c70d4de7d7825840f40b889e2dff194f6a6b21943b190b3aa2549698b13af0856dd490a74be701c9a45e700335fd7ae03a3fb732ec4982c529909075ccb31c6d4741ed921c0e44c75850502e08f267c7ba9abd821d099ba1afec773280bc89867ebf3045c646ba497e18c8ca2024b22a78f5ff055a674f8cff840c43b2e7ef7976b0e7d791d17587b410912adb655b8b92fcd1b556a99239e207190466582026900aefbc5e59ffeeb1cd80acad7a15479095b2a3068e42f74aa211669afd60845820928c2533874afbcbd5f11379eefe8fd038039b6390d5a67f197eff71ffe8242d0519023358404373b24cde0054d3bee81cbf5cde4d40732edac587be46c79d8e95c1340bd68684b05eb6010e132ad700775e1ea7dcaaf75607248e9abdb48abedc0eec476f03820a005901c03de125d80cb791bfcfcc1259437499ad25864bb618805cdbb002722c14e80285afa2db5c72517002850ffec2ee0f276472cf1d18d6928ad9f0014ac51ebf3b0cef881a64db9de443f84ede8f05e031fecb6eb1d9f711b521c0333a78bb6dfb9af101eb457dba359a60ebb49591abe105bbff8364b3e84ae3f80dd14498531c0aad6e959f55ad61c1c0c3f5ac91d964d59e283b64589300e49210e4dab33b2333f975a2074e4e4ac051167f51cdb96c477acc61cc68faee34a74a12c84858d65de751d8b800812caa02d5714922e9102a6d1361568227a4a23dd4a14655e7f7032c230b620cb8c3104f05d1adb8c8bff209bc75de808100fc5e879998463d2646a6a1e5089249de795a18d3ac4663543c0366552503f5d41830f3448601c48e955c6e791e3a1c4ed7e65745565580294651bcc5aa92b57f6e2a2ff6b52e0434f0b2141ced79aeb9a85f42f3dd50714630e0f98bd10a0abb7832b8d6f07c6729731e97e0eb13f43ce54533762c9b53977a3644101d47f37865a3c2208644ea56413dadcabe8c880f2ed0e7bd8a6cdf00eaeeb18cab609ebefbab9c2c9b58f87357cc4206fd1dbe0806d1bcef3112389926a755c00abedc20706d32b81cb772431e81a60081825820196fd754d9c6ec7815715632c43579b183982eafdbae9ec5ef4e1c4868f170a2000181a200583900eb21979c03eed7207020b2a0b47565b5aafb1a2c3849b59a1fa8e6c5e075be10ec5c575caffb68b08c31470666d4fe1aeea07c16d6473903011b000000024f9cf03b021a000320a9075820c18d15d64b54b1f14056916ff9cf6d4d027c4ed97c67290704ec07a3e99eeaee0e81581ce075be10ec5c575caffb68b08c31470666d4fe1aeea07c16d64739030f0081a100828258208469288efa6f9cb49040b43dcff93f40969d72433f751afde50235012c016020584018430fd6585dda6e1dc40b7446bfd9356dae24e6d8697fa70d44640688d1b100bf6189494cefa13f31289fdb9f4aa205c6575680b32324b2eb54a0c778df870782582076af5530fa318a370820270031d1838545a4ceed8696510627563d1114d4182b58403a668351115e280dc49d463163649cc98919db909f656af493a2a66883b5d840ea516e49a619bf54207bd93a530822e2a7a55acb732cf0e0aef65e709c7e450da100a11901fda50050ca7a1457ef9f4c7f9c747f8c4a4cfa6c01501011f74e337c2f1747fa47c5c55844b80258204d3f576f26db29139981a69443c2325daa812cc353a31b5a4db794a5bcbb06c20b8958401bed02003c0e772c72637668c289a39b361dd1161a123da11e1118d08c7ab73ed1455e25aab3105e92334ba1fe128febedfb3e4912243755f42aca92094de82658404a4149a26917374c7cfd021376195439e4ea64844b46cfe1f87e9b6bf4d43c9dace1920ceeb2cc82bb60018b5b2de9571c5ea9c81dddd4077cc4571eb33181ce58409b38965811866581e4e903e6967333e85ab02e1b25665f272d24db06fb0183d3dd1cd937e2e260e3e0d045c976e057dde418766ea47dd551a68c20c015f508e25840118815ae0511d0e258b4440bd4b921222339016000ba853d2119394d8006a8a220d1c4755d3920f4403b302919d9c22f32106e1e10c3942a7d0f1c8ce42283205840528a20238dc8802d44cafe99612022d4abb8dc58894462642a21bb150449720b1f0e4e2a62bb9210b334d2f13ba4057d05f409d0fa3c666a3cb41cd012cc8e29584086ea45acd180f40932c052962cc156bad9f4a8a80d2f5d2e488c7ba8a496b1b1bf332482c7f8b9f981bfcb862878854a29842b460c8c782fb7905037399087685840693fc55005905188558891f50a0b0b0d8f0c04528e5d4a3c3c5c1cfc3360ef293f8938271225c0c6c727c4c59e3e4e09a2788847c7cfc79646ce62974cc11f1558405fbca2dc36eaf3594d18624bde0c7adac3324a828c2b833b3b3fbcd0c657c337f05bd53ece84f0adf329567b7234fe45897252656f11cf8ae6e56073452a93e85835133271a0fbc9f8d641adafb33a0267685f05fd95caf1ff3efa9d60febcfced727553ff21cd774cee682b161636860470b149c61f40186358400462942f9403413877b1ec95608d07a5d2f5c668a650d673044a6ecff9cdf3caf2d26482a1df522acb99bc89885df8b4418a8add23b01de5276bc4371cd8f60680 \ No newline at end of file diff --git a/rust/rbac-registration/src/test_data/cardano/conway_3.block b/rust/rbac-registration/src/test_data/cardano/conway_3.block new file mode 100644 index 000000000..c79dee4fe --- /dev/null +++ b/rust/rbac-registration/src/test_data/cardano/conway_3.block @@ -0,0 +1 @@ +820785828a1a002ccb3d1a049987cf582015b83a54c895c25a0b801321e37b8e739f8f48e6c8de774f8b902cf0d689ff855820bae7fccf57e9292efa30244aed16f64471ac32957c50e5261c33d31226df84ba5820d754ac94a56d8ca60309a56c42d38a6eb910250b771cfc039644464d7c2a384c825840f43e71e6a14fc82c42da848d83a15520c3ee88e747729038c31564e9bd34da76876a3f5e007d282dd200b7b5f053a8e4a3b7eba91fbf5f8e63629cecc966672e5850fac87b66d699befd76858c79efe609ff44e20f75af2cfb6afd8dca1a664b3adc11fe4d3ef812df82012f9538c1f0e57cd64793148b9c0c344ca1b0370446fb314dac42296fa8c47863dca2caf2932f0a19042c58209c55f69be15d6dc821b18b41bf56cf39e83ea0f3ee8db63ca2a014e0b10513d8845820ddf29f596c53637abfc18ff36f6ef1ecb7f9df738188b0156a0fe6983a51aea20119024a5840e602a673cb7146fc38abad1dec9330c0f8d57a147c3b1278112203d379e7d057e337ea37e8fc4bfdaf4680d83906e591660e0e5480efc5057b719c3f68aa6b05820a015901c0a1e0c65487c6bde1b1177a9ee9eca9e1bc5cc8db429a28a4193015eb6107b9a21de4f7fd1af59e0c99d02494911e6525aabec2f1b8b24e05d5c032ea6dc5bc078e2522aaf8c03b2c4b0c8211e1ef99c1a7a1a9b8670ba332f3e54f4d312a628f777cd93ef428e9a416ff433d449c90706d88c1b606c6d294e9c74c77402d47f42bbd0ed3d62a9b4bcd0d938afb6eea1c700f25eda01f07489301f9580469b57e2d67c132c019830f95b2bdcdcc022c30aab889b645baff27000336085d6f8f85e536802d4af42b49884bfc0f54fa031b4d121812eb3dbdc533bd8888fbf58af4dfca253a020248ec172a127960f6556208ed9663a5d2f5791584c81f7f7bffca0f056c0a7dd027b284911db62b56363bf34b6ae0f7bc7d2e7e809b96712212c656e53e347b5394e8a9ed4a6693d210b8f3c00a9b29b777a112dd3ab9703ab0de66cca4f2a078483eb6ff7b02287366260254404027783fac9f7403b21d2ad8d4966afc937f66563ca8ef591b64e67ac33862660ce796d2fb7b0ff03d5e64cdd96d828239537717b35a1257e89170e407ec14adb4bbec2fe6545ef9874a1a09785d04d05604f23843b30248033067699f2b99ec2fff6171ddea34f6fc3f8c3fe881a600818258202dc856d3b4a0c81ddde5099d44d50964f23ad28b63cb260ae13b35aeb9f6123d000181a200583900eb21979c03eed7207020b2a0b47565b5aafb1a2c3849b59a1fa8e6c5e075be10ec5c575caffb68b08c31470666d4fe1aeea07c16d6473903011b000000024fa326b9021a000316b107582066a0c1b30e0b0f98665c6ebe08d0bcb4cf0f90248734adca108d01c36e83b8b90e81581ce075be10ec5c575caffb68b08c31470666d4fe1aeea07c16d64739030f0081a100828258208469288efa6f9cb49040b43dcff93f40969d72433f751afde50235012c0160205840933228faf5c8c4985ca65109175978c117454cc3b1025214c4c178347d60462f30f637d8e1fa2f11667a59ed78aa80ba00c075417ea19570bffe9e2f86d7f90b82582076af5530fa318a370820270031d1838545a4ceed8696510627563d1114d4182b584098078a7af8ccf41b263a960426ab5821c0727c6973e862c6fc87b56ba14c5122ed6c92e251261ad37151eb87b5c69eb9da679f39f92fee90325390560b7cba04a100a11901fda50050ca7a1457ef9f4c7f9c747f8c4a4cfa6c01501b3dc524cf72f482bbbf60454f3a4a330258204d3f576f26db29139981a69443c2325daa812cc353a31b5a4db794a5bcbb06c20b8858401bed020064f03bdf8a48de44553c0c4e47f98bc0d14d7dc0477ce5311b13e8bc89638915615816489e3e606e393783ae05ebb25162f675b28a87bad0b682126d58406373d47d399d46a5d1142d473d1af3a3f10b5be75d4bde3c343115d1cb8fa980c9a2fba3053014f2019745efc060c9691dda413990e68623e09d005005140b7b5840b6404813a00c48a2e009b82d065d394025900fcc1708167e9181e879761053aaf43d30a98565e5212cab02ca832ab0725788acf06786814fa857e956ca704e205840e69bd2a2ac78731e6b44c5335f7a44b0fd55a87d79a1cdda7270ddafe22af411a077002dcd63a39481e60234abb4635a92bc88351af09f2003acb86409b6c6b658401b10edebb7e1659f5cbe6a38dc2712c1c2b8656c2ffb3b8324865726008ef72937a0ca804415908954200a58a15c9cc40646ca57ae841189b262a698ae18c5725840a962bf68ac3c19c7009f6357cc60d069cc47c0d8532743c439c7cfa232996c3a75fab412247ac850388df73bca2a76f8b3a1fcb4dfd54d332f543bd1d5abcf675840eb50d6677fcdf8f26ddcfb758710d1aae18c33e25ddc931f326555175cfb76e596af36110e6ead5277e07f5d615deab4aa8a6d633b6cf499a79dae8e46036e9e583d2e3edf2cd098d0ed91ccce965cdd5fcf27accb1d927f742ff939daf4eff5c0cfaca38fe77e7a7af16aa34370e52b4d04ada650a8251e854ee545fe0002186358405ba46fa3c1161a6069721779a976c74cf721b555a4a8b3350e6a531f89406e77b2646c2d16d4e52abbd658a3225f449becf5f87266e31ed1179eda1dfef4f50e80 \ No newline at end of file diff --git a/rust/rbac-registration/src/test_data/cardano/conway_4.block b/rust/rbac-registration/src/test_data/cardano/conway_4.block new file mode 100644 index 000000000..b16edd16d --- /dev/null +++ b/rust/rbac-registration/src/test_data/cardano/conway_4.block @@ -0,0 +1 @@ +820785828a1a002cf24d1a049d95d15820a82d38572884fc69c5b1105dd190651e4fc5c821878a1a166b6bca897983b83d58206f281ba212f118ac018aaeb805dda9f6fd54fe28cecafe7bec1347509510797f5820870dca606397b85d2e4ee1e46bdb9b784a092a1ec25cb8146cf4046456ff861c825840c085ebef69c078f6fcfd53a2eb2e209b6bd1a517390c8971f079fe55a4b30c82641385a618acf269bceb2d3c83a4ad6addc8901cc3bb359da7875c672ed5a6a55850758500c16a229759d672f0c5aea206fe924238915de34dfdf54413d438a27c4bdb453a080d1fe6ce0f3427452d20aeb1d1907d034899d19021e951a1badeef442de3f4f1bd4e9a4ce5924870f068a4021911c65820c8fc1a8e30b35038644284be081128c4a7ad8b3c39edbfeea871084b9b3566cb845820b7e792177f506de2a34d1c4099f7a1687ff7af54caa093d85c5755e709f757fd0d190241584095a171db3d0e3808bca5e84970ead6b0a736e1d5a2b5fc8c169c8fab7bc1a5d3d60b0b582fbbc8c23d43b3301c4b3eede3ac5438bddfebe1cc8e5a8d71a94802820a005901c05ae6633913fc08585a1ddf905f10b2cc1134d914f9f656b58e72cad542e53a90ce0cc9c6f0e819f7243431a5416cbc1a3fa67d8ab8c547a09f78c86c20073208dc3b328c9d70a24a912aa4e170b9d6fbe8e55c03b52dc296435c86a358bc3f20e89f4c92748dff2ed24a71b8377b4683851a52cfd023dccaa92239d9e989fe8d1d360529c15417937f60530a085ee82dd4ed5f1bc46d0bcf2ab68d4729063a7a5c5074cdb289655f23954f7eebfd418e3a133bb53cfa0bd5ff544fc9d0599640b46619d3bfc5d9782c98ee86fcaa1efe8503980fd9219db77c3003ed2cee13d85f510f9a3381d442f5314a91c52ccf01cc796b9fb7ea14d046a00c3f96e6ee2ec28896f72808952cda9aec2feb690ecb59c1a729816f602a58b06fbab79977a9e96ff7eed2dd262eb86cee8f35d4bad55be2919413ff5cd8fbab3acc25f608630fa7796debcdae0239aafb45e949053af2523bb112b5f6be5fc4ece0ac55f59783466565dad12d42b76fb3ad45112c25d0f4c0880b88ad0ff87f207538c5c7cb96690633a72de47fd36980846cfba4865e324b7ccf1ca80382a4eedc72caf1a0d828049fbd7b3906877b89980bd2f0ca5ffc229e667d56fd11c1dd6e3c0ba7ed82ab0082825820276df9bb9d245ea87e6def1a995ad919edacc907c6cbf028e0e552b1c53c10df0182582062e4dfcb06b8401ea1c405d6290c6aede934c7a6cd31ae000c2a5dd34961af5b000182a300581d70bdc22da682cd9aceed5fd48914789fc98c94abc79fed8b40cb8c431401821a001e8480a1581cc13ddf298a5d25aff2933695987912b4f1748bdf0df8e4b5d85f2360a14e50524550524f445f4f5241434c4501028201d81856d8799fd8799f1a0bf85ea8ff1b000001938742fe98ff82581d60c0359ebb7d0688d79064bd118c99c8b87b5853e3af59245bb97e84d21b000000011587887e021a0006a164031a049d9933081a049d922c0b5820e33c25b2f65204adbf1a8f29ba4c7157c35f9479346e2d6717e23d6786e8b6340d81825820276df9bb9d245ea87e6def1a995ad919edacc907c6cbf028e0e552b1c53c10df010e81581cc0359ebb7d0688d79064bd118c99c8b87b5853e3af59245bb97e84d20f001082581d60c0359ebb7d0688d79064bd118c99c8b87b5853e3af59245bb97e84d21b000000011541dea2111a004c4b40a600818258206695b9cac9230af5c8ee50747b1ca3c78a854d181c7e5c6c371de01b80274d31000181a200583900eb21979c03eed7207020b2a0b47565b5aafb1a2c3849b59a1fa8e6c5e075be10ec5c575caffb68b08c31470666d4fe1aeea07c16d6473903011b000000024f93e7f8021a000304fd07582091e3a1050ba7f37e9d94938ec94a9d187f24b363374c109e3fcc44fedb5de8b70e81581ce075be10ec5c575caffb68b08c31470666d4fe1aeea07c16d64739030f0082a30081825820ef603099b2579d5ca273dce1f1257c11f054664ba972fd61c008b58a1b2325c1584049e2d6c6a948a7d1398f200213a65cd15ac2dd491e903892aaca6ad487717f3cad6c66e201dd8fd74f69898f77aa3de953e83df61f2822c01a30eb992f58f0060681590bd7590bd401000033232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323233027222232325335333006533357346074607e0022646605a60586ae84004dd69aba135744002607c00206e64a666ae68c0ecc1000044c8c8c848cc00400c008c0b8d5d09aba2002375a6ae84004c0fc0040e0dd50022999ab9a303a303f002132321233001003002357426ae88c0fc00cd5d0981f00101b8890008a99ab9c490103505435001637540086ea8004cc09c8888c8d4014888d40188c88c94cd54cd4ccd54c0d40e0c0a80a4c0ac01ccc0b08004d40048888888888880100e44cd5ce24812d546865207472616e73616374696f6e206973206e6f74207369676e6564206279206f7261636c65206f776e657200030132533553355335333553036039302b02a253335302135001202103e215333573466ebcc118cd5d0182319aba0375000a6ec4080cd5d01ba835500b03937620400022a66a666036446a0044466603e446a00444a666ae68c12c004400c4cc11848ccc00401c00c00800c00c0040c0cd54c0e80ec8d400488cc0e0008cd54c0f40f88d400488cc0ec008ccd4004dc0a4000e048cdc0800a400000266aa607407646a0024466070004666a002466aa607c07e46a0024466078004604600200244666038066004002466aa607c07e46a0024466078004604000200266602e05c666050400204e6a004404e0060784426a004444a66a0082a666ae68cdd78018238a999ab9a3375e00408e2a666ae68cdc4000a400006e2666ae68cdc4800a410125e80206a07006e06e44072064407e6044014074266ae712401024c310003103a1335738921156f757470757420646f6573206e6f74206d6174636800031153355333573466e2400d20001335738921165072696365206d75737420626520706f7369746976650003103a15335533535353301275a6a00407e06e44a666a00442a666ae68cdc419b8000100935500c03a0331333573466e24004d540300e80c40d00c80c84d4cc049d69a80101f911a801112999a99980980a980f19b8100900b0021533500113330120150030081333012015003008034133301201500300803003a13357389212345787069726174696f6e2074696d65206973206e6f742070726f7065726c792073657400031030030333025200102435302635533530270092100116036202402f302d350080393302f3016337000020060542a666a602e6aa66a60320022602602a442a66a0022004442602e032402e02842a66a6666666ae900048c94ccd5cd181d98200008991999aab9f001202d23233335573e002405e46666aae7cd5d1001119299a9999999aba400123253335734608660900022646666aae7c00480d48cccd55cf9aba20022533530203574200642605e00206a406c07a078608e0020646ea800880c880c880c880c80e4854cd4c070d5d08029098159981a8010008188181aba1005203003703635744004068607e0020546ea800880a880a880a880a80c48400405480548c94ccd5cd181b181d800899191919091998008028018011bad357426ae88008dd69aba10013574460760046ae84c0e80040ccdd50009111a801111a801912999a9998040038020010a99a8018800819814819911192999a80190a999a80190a999a80290980200b0980180a8a999a80210980200b0980180a80c0060a999a80210980180a8980100a0a999a80190980180a8980100a00b8a999a801100700b0068a999a80110a999a80210980180a8980100a0a999a80190980180a8980100a00b8058a999a80190980100a098008098a999a80110980100a0980080980b12999a80110a999a80210a999a802109998040038010008b0b0b0060a999a80190a999a801909998038030010008b0b0b00580691a80091111111003891999999980091199ab9a3370e00400204004644a666ae68cdc38010008098a999ab9a3371200400201401044666ae68cdc400100081001191199ab9a3371200400204004644666ae68cdc480100081181011199ab9a3371000400204604044a666ae68cdc480100088008801112999ab9a33712004002200420024464a666ae68c0c8c0dc0044c8c8cc0994ccd5cd181a181c801099813198038029aba130380023006357426ae88c0e00080c54ccd5cd181a181c800899813198038029aba130380013006357426ae88c0e00040c4dd51aba135744606e0046ea8d5d0981b0008179baa0012325333573460620020522a666ae68c0c000407c0b4c0d0dd50009119192999ab9a303300100815333573460640022601460086ae84c0d400854ccd5cd1818800803017181a8009baa00122233355302302702a335530260272350012233024002300b001333553023027223500222533533355302802b301d01c235001223300a002005006100313302e00400301c001335530260272350012233024002330382253350011300a003221350022253353300c002008112223300200a0041300600300400211009212223001004112220012230302253350011003221330060023004001212223003004233333335748002403040304030460206eb4008806007c94cd5ce2481286578706563746564206f6e6c7920612073696e676c6520636f6e74696e75696e67206f7574707574001615335738920117496e76616c696420646174756d20696e206f757470757400164988880088c8c94ccd5cd1813000899091118010021aba13028002153335734604a002264244460020086ae84c0a000854ccd5cd181200080201098140009baa00111003253353007001213335530150192235300535300935003019222200422353007350042222004225333573466ebc01000854ccd5cd19baf0030011330220060051005100500e3300d00735300f3500201b22222222222200a15335738921024c660016232533357346040604a0022646424660020060046ae84d5d118128011aba1302400101d3754002444006660024002eb4888cc09088cccd55cf80090071191980e9980a180398138009803181300098021aba2003357420040306eac0048c94ccd5cd180e181080089919191919190919998008038028018011aba1357440046ae84004d5d10011aba10013574460420046ae84c080004064dd5000919191999aa999ab9a3370e90030008990911118020029aba13020002153335734603c00226424444600400a6ae84c08000854ccd5cd180e8008990911118008029aba13020002153335734603800226424444600600a6ae84c0800080648034803480348ccd54c048054cc03c894cd40088400c400403494ccd5cd19baf002350010181300600100d3300923253335734603e60480022646424660020060046ae84d5d118120011aba1302300101c37540026a60166a00802e44444444444401860400026ea8d40040408488c00800c48cc004894cd400804c40040208cc02488ccd400c04c008004d400403488ccd5cd19baf002001004007223301c2233335573e002400c466028600a6ae84008c00cd5d10010081bac001100d23253335734602860320022646464646464646464646464646464646464646464642466666666666600202e02a02602201e01a01601200e00a0060046ae84d5d10011aba1001357440046ae84004d5d10011aba1001357440046ae84004d5d10011aba1001357440046ae84004d5d10011aba1001357440046ae84004d5d10011aba1001357440046ae84004d5d1180c8011aba1301800101137540022002200c464a666ae68c044c0580044dd69aba1301500100e37540024424660020060044446006600400260244422444a66a00220044426600a004666aa600e01600a00800260224422444a66a00226a00600c442666a00a0186008004666aa600e01400a00800244002601e442244a66a00200c44266014600800466aa600c00e00800224002220024400444244660020080062a66ae712411f496e76616c696420646174756d20746f20636865636b4f776e4f757470757400161533573892010350543100162222222222220054c1014000370e90001b8748008dc3a40086e9520005573caae748c8c00400488cc00cc00800800530012cd8799f581cc0359ebb7d0688d79064bd118c99c8b87b5853e3af59245bb97e84d21a000dbba01a01499700ff00010581840001d8799f1b0000019385f96798d8799f1a0bf85ea8ffff821a001862581a18087eb7a100828258208469288efa6f9cb49040b43dcff93f40969d72433f751afde50235012c01602058404ac843657462bd1b0f2db7e1abfa2fa3ccd3105ad2278649e155ceb3cf4e44b3cc2532815570cee26a55910a04db4de50cd3c07784ecfacf4c74706f54cd470382582076af5530fa318a370820270031d1838545a4ceed8696510627563d1114d4182b5840c9fe85cdcef2b32d76958d564be1b7bc0124a99594cc9e69e1a4d0cbe8f96eed4ca94fe6f2471382d19d74f610a6a5e01e56d8f6dc2a5519a090ec2e00dc3e05a101a11901fda50050ca7a1457ef9f4c7f9c747f8c4a4cfa6c01505be7b97fa11335b727bbf095a3f0fb7c0258206695b9cac9230af5c8ee50747b1ca3c78a854d181c7e5c6c371de01b80274d310b8758401b3e020864c84e53c9f7a9c54b8d09c6047bc2049aed7438c5575ed4ee265182dd808a52ae8b12083cb540fa9ccc7e0ec3a7b52ad5f134682286f658ff0083f558400dc73df538d858d8d8b89e5247f2000f1f47c43b0c68204480001fc71a179f0685400804400c2484e3013b03195a0a79c94206484bc93b10cbcb474999a5e5a55840b17f9619347e158e3a2a44e3e33eb72d648ae645a0e63d83fde739ee0d7ff6baeaf5767357770a697c2a92095bfe292e427bbc2fee56211ec78389dfa56d13c758400363abd573e840dead26d80c600380b5bc64a3c285550096634883e3a7265b4f18ca836d1d23b991a02a45ac365c9665acb787484da6ee21a10a1212ee5e5e11584012912b55d77a3e82444744ca4581c84877c958f9583f7910e4251d15161126e91b1d11e72525271b11e5870e0f0a0f94f18e080908f7f4f6918af68de8fc1bdd5840ec35fc967e64936c930f279eb8d043d31112e6a71aaaa76fc3455e9d30b90ea671971e5c9ecd4be5b4d87ab3e11f7de3b8984ea353e9a2d8d65c6ad6f9102c46581876789c8667c739c00da3176c46a1b030c4281c4e62afcf3a186358401824797b7e12054dabb6fef573ceba9c3bbed87b522a4141c2357d05d63ffabee59add33aac71f4573e43520a1c000d0c4e7fa2adf7f8e92718320527d23e60580 \ No newline at end of file diff --git a/rust/rbac-registration/src/utils/decode_helper.rs b/rust/rbac-registration/src/utils/decode_helper.rs new file mode 100644 index 000000000..b6f12c7a2 --- /dev/null +++ b/rust/rbac-registration/src/utils/decode_helper.rs @@ -0,0 +1,214 @@ +//! CBOR decoding helper functions. + +use minicbor::{data::Tag, decode, Decoder}; + +/// Generic helper function for decoding different types. +pub(crate) fn decode_helper<'a, T, C>( + d: &mut Decoder<'a>, from: &str, context: &mut C, +) -> Result +where T: minicbor::Decode<'a, C> { + T::decode(d, context).map_err(|e| { + decode::Error::message(format!( + "Failed to decode {:?} in {from}: {e}", + std::any::type_name::() + )) + }) +} + +/// Helper function for decoding bytes. +pub(crate) fn decode_bytes(d: &mut Decoder, from: &str) -> Result, decode::Error> { + d.bytes().map(<[u8]>::to_vec).map_err(|e| { + decode::Error::message(format!( + "Failed to decode bytes in {from}: + {e}" + )) + }) +} + +/// Helper function for decoding array. +pub(crate) fn decode_array_len(d: &mut Decoder, from: &str) -> Result { + d.array() + .map_err(|e| { + decode::Error::message(format!( + "Failed to decode array in {from}: + {e}" + )) + })? + .ok_or(decode::Error::message(format!( + "Failed to decode array in {from}, unexpected indefinite length", + ))) +} + +/// Helper function for decoding map. +pub(crate) fn decode_map_len(d: &mut Decoder, from: &str) -> Result { + d.map() + .map_err(|e| decode::Error::message(format!("Failed to decode map in {from}: {e}")))? + .ok_or(decode::Error::message(format!( + "Failed to decode map in {from}, unexpected indefinite length", + ))) +} + +/// Helper function for decoding tag. +pub(crate) fn decode_tag(d: &mut Decoder, from: &str) -> Result { + d.tag() + .map_err(|e| decode::Error::message(format!("Failed to decode tag in {from}: {e}"))) +} + +/// Decode any in CDDL, only support basic datatype +pub(crate) fn decode_any(d: &mut Decoder, from: &str) -> Result, decode::Error> { + match d.datatype()? { + minicbor::data::Type::String => { + match decode_helper::(d, &format!("{from} Any"), &mut ()) { + Ok(i) => Ok(i.as_bytes().to_vec()), + Err(e) => Err(e), + } + }, + minicbor::data::Type::U8 => { + match decode_helper::(d, &format!("{from} Any"), &mut ()) { + Ok(i) => Ok(i.to_be_bytes().to_vec()), + Err(e) => Err(e), + } + }, + minicbor::data::Type::U16 => { + match decode_helper::(d, &format!("{from} Any"), &mut ()) { + Ok(i) => Ok(i.to_be_bytes().to_vec()), + Err(e) => Err(e), + } + }, + minicbor::data::Type::U32 => { + match decode_helper::(d, &format!("{from} Any"), &mut ()) { + Ok(i) => Ok(i.to_be_bytes().to_vec()), + Err(e) => Err(e), + } + }, + minicbor::data::Type::U64 => { + match decode_helper::(d, &format!("{from} Any"), &mut ()) { + Ok(i) => Ok(i.to_be_bytes().to_vec()), + Err(e) => Err(e), + } + }, + minicbor::data::Type::I8 => { + match decode_helper::(d, &format!("{from} Any"), &mut ()) { + Ok(i) => Ok(i.to_be_bytes().to_vec()), + Err(e) => Err(e), + } + }, + minicbor::data::Type::I16 => { + match decode_helper::(d, &format!("{from} Any"), &mut ()) { + Ok(i) => Ok(i.to_be_bytes().to_vec()), + Err(e) => Err(e), + } + }, + minicbor::data::Type::I32 => { + match decode_helper::(d, &format!("{from} Any"), &mut ()) { + Ok(i) => Ok(i.to_be_bytes().to_vec()), + Err(e) => Err(e), + } + }, + minicbor::data::Type::I64 => { + match decode_helper::(d, &format!("{from} Any"), &mut ()) { + Ok(i) => Ok(i.to_be_bytes().to_vec()), + Err(e) => Err(e), + } + }, + minicbor::data::Type::Bytes => Ok(decode_bytes(d, &format!("{from} Any"))?), + minicbor::data::Type::Array => { + Ok(decode_array_len(d, &format!("{from} Any"))? + .to_be_bytes() + .to_vec()) + }, + _ => { + Err(decode::Error::message(format!( + "{from} Any, Data type not supported" + ))) + }, + } +} + +#[cfg(test)] +mod tests { + + use minicbor::Encoder; + + use super::*; + + #[test] + fn test_decode_any_bytes() { + let mut buf = Vec::new(); + let mut e = Encoder::new(&mut buf); + e.bytes(&[1, 2, 3, 4]).expect("Error encoding bytes"); + + let mut d = Decoder::new(&buf); + let result = decode_any(&mut d, "test").expect("Error decoding bytes"); + assert_eq!(result, vec![1, 2, 3, 4]); + } + + #[test] + fn test_decode_any_string() { + let mut buf = Vec::new(); + let mut e = Encoder::new(&mut buf); + e.str("hello").expect("Error encoding string"); + + let mut d = Decoder::new(&buf); + let result = decode_any(&mut d, "test").expect("Error decoding string"); + assert_eq!(result, b"hello".to_vec()); + } + + #[test] + fn test_decode_any_array() { + // The array should contain a supported type + let mut buf = Vec::new(); + let mut e = Encoder::new(&mut buf); + e.array(2).expect("Error encoding array"); + e.u8(1).expect("Error encoding u8"); + e.u8(2).expect("Error encoding u8"); + let mut d = Decoder::new(&buf); + let result = decode_any(&mut d, "test").expect("Error decoding array"); + // The decode of array is just a length of the array + assert_eq!( + u64::from_be_bytes(result.try_into().expect("Error converting bytes to u64")), + 2 + ); + } + + #[test] + fn test_decode_any_u32() { + let mut buf = Vec::new(); + let mut e = Encoder::new(&mut buf); + let num: u32 = 123_456_789; + e.u32(num).expect("Error encoding u32"); + + let mut d = Decoder::new(&buf); + let result = decode_any(&mut d, "test").expect("Error decoding u32"); + assert_eq!( + u32::from_be_bytes(result.try_into().expect("Error converting bytes to u32")), + num + ); + } + + #[test] + fn test_decode_any_i32() { + let mut buf = Vec::new(); + let mut e = Encoder::new(&mut buf); + let num: i32 = -123_456_789; + e.i32(num).expect("Error encoding i32"); + let mut d = Decoder::new(&buf); + let result = decode_any(&mut d, "test").expect("Error decoding i32"); + assert_eq!( + i32::from_be_bytes(result.try_into().expect("Error converting bytes to i32")), + num + ); + } + + #[test] + fn test_decode_any_unsupported_type() { + let mut buf = Vec::new(); + let mut e = Encoder::new(&mut buf); + e.null().expect("Error encoding null"); // Encode a null type which is unsupported + + let mut d = Decoder::new(&buf); + let result = decode_any(&mut d, "test"); + // Should print out the error message with the location of the error + assert!(result.is_err()); + } +} diff --git a/rust/rbac-registration/src/utils/general.rs b/rust/rbac-registration/src/utils/general.rs new file mode 100644 index 000000000..3596b6680 --- /dev/null +++ b/rust/rbac-registration/src/utils/general.rs @@ -0,0 +1,27 @@ +//! General utility functions + +/// Getting the index by decrementing by 1. +/// e.g. 1 should refers to index 0 +pub(crate) fn decremented_index(int: i16) -> anyhow::Result { + match usize::try_from(int) { + Ok(value) => Ok(value - 1), + Err(e) => { + Err(anyhow::Error::msg(format!( + "Failed to convert to usize: {e}" + ))) + }, + } +} + +/// Decode the given UTF-8 content. +pub(crate) fn decode_utf8(content: &[u8]) -> anyhow::Result { + // Decode the UTF-8 string + std::str::from_utf8(content) + .map(std::string::ToString::to_string) + .map_err(|_| { + anyhow::anyhow!( + "Invalid UTF-8 string, expected valid UTF-8 string but got {:?}", + content + ) + }) +} diff --git a/rust/rbac-registration/src/utils/hashing.rs b/rust/rbac-registration/src/utils/hashing.rs new file mode 100644 index 000000000..2ccf2e636 --- /dev/null +++ b/rust/rbac-registration/src/utils/hashing.rs @@ -0,0 +1,27 @@ +//! Hashing utility function. + +use blake2b_simd::{self, Params}; + +/// Convert the given value to `blake2b_244` array. +pub(crate) fn blake2b_244(value: &[u8]) -> anyhow::Result<[u8; 28]> { + let h = Params::new().hash_length(28).hash(value); + let b = h.as_bytes(); + b.try_into() + .map_err(|_| anyhow::anyhow!("Invalid length of blake2b_244, expected 28 got {}", b.len())) +} + +/// Convert the given value to `blake2b_256` array. +pub(crate) fn blake2b_256(value: &[u8]) -> anyhow::Result<[u8; 32]> { + let h = Params::new().hash_length(32).hash(value); + let b = h.as_bytes(); + b.try_into() + .map_err(|_| anyhow::anyhow!("Invalid length of blake2b_256, expected 32 got {}", b.len())) +} + +/// Convert the given value to `blake2b_128` array. +pub(crate) fn blake2b_128(value: &[u8]) -> anyhow::Result<[u8; 16]> { + let h = Params::new().hash_length(16).hash(value); + let b = h.as_bytes(); + b.try_into() + .map_err(|_| anyhow::anyhow!("Invalid length of blake2b_128, expected 16 got {}", b.len())) +} diff --git a/rust/rbac-registration/src/utils/mod.rs b/rust/rbac-registration/src/utils/mod.rs new file mode 100644 index 000000000..9baae3794 --- /dev/null +++ b/rust/rbac-registration/src/utils/mod.rs @@ -0,0 +1,5 @@ +//! Utility functions for the RBAC registration module. + +pub(crate) mod decode_helper; +pub(crate) mod general; +pub(crate) mod hashing;