Skip to content

Commit 8f61d83

Browse files
bkioshnMr-Leshiystevenj
authored
fix(rust/rbac-registration): Validate validation_signature with the latest signing public key of Role 0 (#312)
* bump to the latest mithril-client version 0.12.0 * wip * fix * remove commented out code * fix * fix comments * fix(cardano-blockchain-types): add func to get raw aux data from txn index Signed-off-by: bkioshn <[email protected]> * fix(catalyst-types): add zero out last n bytes func Signed-off-by: bkioshn <[email protected]> * fix(rbac-registration): add chain root check, should have role 0 Signed-off-by: bkioshn <[email protected]> * fix(rbac-registration): validation sig try into ed25519 sig Signed-off-by: bkioshn <[email protected]> * fix(rbac-registration): add aux data to rbac test data Signed-off-by: bkioshn <[email protected]> * fix(rbac-registration): update the registration chain Signed-off-by: bkioshn <[email protected]> * fix(rbac-registration): store raw aux in cip509 Signed-off-by: bkioshn <[email protected]> * revert rebase change Signed-off-by: bkioshn <[email protected]> * fix(cardano-blockchain-types): revert change Signed-off-by: bkioshn <[email protected]> * fix(rbac-registration): add problem report Signed-off-by: bkioshn <[email protected]> --------- Signed-off-by: bkioshn <[email protected]> Co-authored-by: Mr-Leshiy <[email protected]> Co-authored-by: Steven Johnson <[email protected]>
1 parent 737a649 commit 8f61d83

File tree

5 files changed

+175
-29
lines changed

5 files changed

+175
-29
lines changed

rust/catalyst-types/src/conversion.rs

+10
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,13 @@ pub fn vkey_from_bytes(bytes: &[u8]) -> Result<ed25519_dalek::VerifyingKey, VKey
6868
ed25519_dalek::VerifyingKey::from_bytes(&ed25519)
6969
.map_err(|source| VKeyFromBytesError::ParseError { source })
7070
}
71+
72+
/// Zero out the last n bytes
73+
#[must_use]
74+
pub fn zero_out_last_n_bytes(data: &[u8], n: usize) -> Vec<u8> {
75+
let mut vec = data.to_vec();
76+
if let Some(slice) = vec.get_mut(data.len().saturating_sub(n)..) {
77+
slice.fill(0);
78+
}
79+
vec
80+
}

rust/rbac-registration/src/cardano/cip509/cip509.rs

+26
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ pub struct Cip509 {
8787
///
8888
/// This field is only present in role 0 registrations.
8989
catalyst_id: Option<CatalystId>,
90+
/// Raw aux data associated with the transaction that CIP509 is attached to,
91+
raw_aux_data: Vec<u8>,
9092
/// A report potentially containing all the issues occurred during `Cip509` decoding
9193
/// and validation.
9294
///
@@ -136,6 +138,7 @@ impl Cip509 {
136138
Nullable::Some(v) => v.raw_cbor(),
137139
_ => return Ok(None),
138140
};
141+
139142
let Some(metadata) = block.txn_metadata(index, MetadatumLabel::CIP509_RBAC) else {
140143
return Ok(None);
141144
};
@@ -153,7 +156,17 @@ impl Cip509 {
153156
let mut cip509 =
154157
Cip509::decode(&mut decoder, &mut decode_context).context("Failed to decode Cip509")?;
155158

159+
cip509.raw_aux_data = raw_aux_data.to_vec();
160+
156161
// Perform the validation.
162+
163+
// Chain root (no previous transaction ID) must contain Role 0
164+
if cip509.previous_transaction().is_none() && cip509.role_data(RoleId::Role0).is_none() {
165+
cip509
166+
.report
167+
.missing_field("Chain root role data", "Missing Role 0");
168+
}
169+
157170
if let Some(txn_inputs_hash) = &cip509.txn_inputs_hash {
158171
validate_txn_inputs_hash(txn_inputs_hash, txn, &cip509.report);
159172
};
@@ -271,6 +284,18 @@ impl Cip509 {
271284
.unwrap_or_default()
272285
}
273286

287+
/// Return validation signature.
288+
#[must_use]
289+
pub fn validation_signature(&self) -> Option<&ValidationSignature> {
290+
self.validation_signature.as_ref()
291+
}
292+
293+
/// Raw aux data associated with the transaction that CIP509 is attached to,
294+
#[must_use]
295+
pub fn raw_aux_data(&self) -> &[u8] {
296+
self.raw_aux_data.as_ref()
297+
}
298+
274299
/// Returns `Cip509` fields consuming the structure if it was successfully decoded and
275300
/// validated otherwise return the problem report that contains all the encountered
276301
/// issues.
@@ -411,6 +436,7 @@ impl Decode<'_, DecodeContext<'_, '_>> for Cip509 {
411436
txn_hash,
412437
origin: decode_context.origin.clone(),
413438
catalyst_id: None,
439+
raw_aux_data: Vec::new(),
414440
report: decode_context.report.clone(),
415441
})
416442
}

rust/rbac-registration/src/cardano/cip509/types/validation_signature.rs

+13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! A validation signature wrapper.
22
33
use anyhow::{anyhow, Error};
4+
use ed25519_dalek::Signature;
45

56
/// A validation signature.
67
///
@@ -20,6 +21,18 @@ impl TryFrom<Vec<u8>> for ValidationSignature {
2021
}
2122
}
2223

24+
impl TryInto<Signature> for ValidationSignature {
25+
type Error = Error;
26+
27+
fn try_into(self) -> Result<Signature, Self::Error> {
28+
let sig_bytes: [u8; 64] = self
29+
.0
30+
.try_into()
31+
.map_err(|_| anyhow!("Invalid Ed25519 signature length, expect 64"))?;
32+
Ok(Signature::from_bytes(&sig_bytes))
33+
}
34+
}
35+
2336
#[cfg(test)]
2437
mod tests {
2538
use super::*;

rust/rbac-registration/src/registration/cardano/mod.rs

+125-28
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@ use std::{
77
sync::Arc,
88
};
99

10-
use anyhow::bail;
10+
use anyhow::{bail, Context};
1111
use c509_certificate::c509::C509;
1212
use cardano_blockchain_types::{StakeAddress, TransactionId};
1313
use catalyst_types::{
1414
catalyst_id::{key_rotation::KeyRotation, role_index::RoleId, CatalystId},
15+
conversion::zero_out_last_n_bytes,
16+
problem_report::ProblemReport,
1517
uuid::UuidV4,
1618
};
17-
use ed25519_dalek::VerifyingKey;
19+
use ed25519_dalek::{Signature, VerifyingKey};
1820
use tracing::error;
1921
use update_rbac::{
2022
revocations_list, update_c509_certs, update_public_keys, update_role_data, update_x509_certs,
@@ -23,7 +25,7 @@ use x509_cert::certificate::Certificate as X509Certificate;
2325

2426
use crate::cardano::cip509::{
2527
CertKeyHash, CertOrPk, Cip0134UriSet, Cip509, PaymentHistory, PointData, RoleData,
26-
RoleDataRecord,
28+
RoleDataRecord, ValidationSignature,
2729
};
2830

2931
/// Registration chains.
@@ -43,7 +45,7 @@ impl RegistrationChain {
4345
/// # Errors
4446
///
4547
/// Returns an error if data is invalid
46-
pub fn new(cip509: Cip509) -> anyhow::Result<Self> {
48+
pub fn new(cip509: &Cip509) -> anyhow::Result<Self> {
4749
let inner = RegistrationChainInner::new(cip509)?;
4850

4951
Ok(Self {
@@ -59,9 +61,17 @@ impl RegistrationChain {
5961
/// # Errors
6062
///
6163
/// Returns an error if data is invalid
62-
pub fn update(&self, cip509: Cip509) -> anyhow::Result<Self> {
63-
let new_inner = self.inner.update(cip509)?;
64-
64+
pub fn update(&self, cip509: &Cip509) -> anyhow::Result<Self> {
65+
let latest_signing_pk = self.get_latest_signing_pk_for_role(&RoleId::Role0);
66+
let new_inner = if let Some((signing_pk, _)) = latest_signing_pk {
67+
self.inner.update(cip509, signing_pk)?
68+
} else {
69+
cip509.report().missing_field(
70+
"latest signing key for role 0",
71+
"cannot perform signature validation during Registration Chain update",
72+
);
73+
bail!("No latest signing key found for role 0, cannot perform signature validation")
74+
};
6575
Ok(Self {
6676
inner: Arc::new(new_inner),
6777
})
@@ -264,18 +274,25 @@ impl RegistrationChainInner {
264274
/// # Errors
265275
///
266276
/// Returns an error if data is invalid
267-
fn new(cip509: Cip509) -> anyhow::Result<Self> {
277+
fn new(cip509: &Cip509) -> anyhow::Result<Self> {
278+
let context = "Registration Chain new";
268279
// Should be chain root, return immediately if not
269280
if cip509.previous_transaction().is_some() {
281+
cip509
282+
.report()
283+
.invalid_value("previous transaction ID", "None", "Some", context);
270284
bail!("Invalid chain root, previous transaction ID should be None.");
271285
}
272286
let Some(catalyst_id) = cip509.catalyst_id().cloned() else {
287+
cip509.report().missing_field("catalyst id", context);
273288
bail!("Invalid chain root, catalyst id should be present.");
274289
};
275290

276291
let point_tx_idx = cip509.origin().clone();
277292
let current_tx_id_hash = cip509.txn_hash();
278-
let (purpose, registration, payment_history) = match cip509.consume() {
293+
let validation_signature = cip509.validation_signature().cloned();
294+
let raw_aux_data = cip509.raw_aux_data().to_vec();
295+
let (purpose, registration, payment_history) = match cip509.clone().consume() {
279296
Ok(v) => v,
280297
Err(e) => {
281298
let error = format!("Invalid Cip509: {e:?}");
@@ -284,6 +301,42 @@ impl RegistrationChainInner {
284301
},
285302
};
286303

304+
// Role data
305+
let mut role_data_history = HashMap::new();
306+
let mut role_data_record = HashMap::new();
307+
308+
update_role_data(
309+
&registration,
310+
&mut role_data_history,
311+
&mut role_data_record,
312+
&point_tx_idx,
313+
);
314+
315+
// There should be role 0 since we already check that the chain root (no previous tx id)
316+
// must contain role 0
317+
let Some(role0_data) = role_data_record.get(&RoleId::Role0) else {
318+
cip509.report().missing_field("Role 0", context);
319+
bail!("Role 0 not found");
320+
};
321+
let Some(signing_pk) = role0_data
322+
.signing_keys()
323+
.last()
324+
.and_then(|key| key.data().extract_pk())
325+
else {
326+
cip509
327+
.report()
328+
.missing_field("Signing pk for role 0 not found", context);
329+
bail!("No valid signing key found for role 0");
330+
};
331+
332+
check_validation_signature(
333+
validation_signature,
334+
&raw_aux_data,
335+
signing_pk,
336+
cip509.report(),
337+
context,
338+
)?;
339+
287340
let purpose = vec![purpose];
288341
let certificate_uris = registration.certificate_uris.clone();
289342
let mut x509_certs = HashMap::new();
@@ -306,17 +359,6 @@ impl RegistrationChainInner {
306359
);
307360
let revocations = revocations_list(registration.revocation_list.clone(), &point_tx_idx);
308361

309-
// Role data
310-
let mut role_data_history = HashMap::new();
311-
let mut role_data_record = HashMap::new();
312-
313-
update_role_data(
314-
&registration,
315-
&mut role_data_history,
316-
&mut role_data_record,
317-
&point_tx_idx,
318-
);
319-
320362
Ok(Self {
321363
catalyst_id,
322364
current_tx_id_hash,
@@ -340,23 +382,43 @@ impl RegistrationChainInner {
340382
/// # Errors
341383
///
342384
/// Returns an error if data is invalid
343-
fn update(&self, cip509: Cip509) -> anyhow::Result<Self> {
385+
fn update(&self, cip509: &Cip509, signing_pk: VerifyingKey) -> anyhow::Result<Self> {
386+
let context = "Registration Chain update";
344387
let mut new_inner = self.clone();
345388

346389
let Some(prv_tx_id) = cip509.previous_transaction() else {
347-
bail!("Empty previous transaction ID");
390+
cip509
391+
.report()
392+
.missing_field("previous transaction ID", context);
393+
bail!("Missing previous transaction ID");
348394
};
395+
349396
// Previous transaction ID in the CIP509 should equal to the current transaction ID
350-
// or else it is not a part of the chain
351397
if prv_tx_id == self.current_tx_id_hash {
352-
// Update the current transaction ID hash
398+
// Perform signature validation
399+
// This should be done before updating the signing key
400+
check_validation_signature(
401+
cip509.validation_signature().cloned(),
402+
cip509.raw_aux_data(),
403+
signing_pk,
404+
cip509.report(),
405+
context,
406+
)?;
407+
408+
// If successful, update the chain current transaction ID hash
353409
new_inner.current_tx_id_hash = cip509.txn_hash();
354410
} else {
411+
cip509.report().invalid_value(
412+
"previous transaction ID",
413+
&format!("{prv_tx_id:?}"),
414+
&format!("{:?}", self.current_tx_id_hash),
415+
context,
416+
);
355417
bail!("Invalid previous transaction ID, not a part of this registration chain");
356418
}
357419

358420
let point_tx_idx = cip509.origin().clone();
359-
let (purpose, registration, payment_history) = match cip509.consume() {
421+
let (purpose, registration, payment_history) = match cip509.clone().consume() {
360422
Ok(v) => v,
361423
Err(e) => {
362424
let error = format!("Invalid Cip509: {e:?}");
@@ -403,6 +465,41 @@ impl RegistrationChainInner {
403465
}
404466
}
405467

468+
/// Perform a check on the validation signature.
469+
/// The auxiliary data should be sign with the latest signing public key.
470+
fn check_validation_signature(
471+
validation_signature: Option<ValidationSignature>, raw_aux_data: &[u8],
472+
signing_pk: VerifyingKey, report: &ProblemReport, context: &str,
473+
) -> anyhow::Result<()> {
474+
let context = &format!("Check Validation Signature in {context}");
475+
// Note that the validation signature can be in the range of 1 - 64 bytes
476+
// But since we allow only Ed25519, it should be 64 bytes
477+
let unsigned_aux = zero_out_last_n_bytes(raw_aux_data, Signature::BYTE_SIZE);
478+
479+
let validation_sig = validation_signature.with_context(|| {
480+
report.missing_field("validation signature", context);
481+
"Missing validation signature"
482+
})?;
483+
484+
let sig: Signature = validation_sig.clone().try_into().with_context(|| {
485+
report.conversion_error(
486+
"validation signature",
487+
&format!("{validation_sig:?}"),
488+
"Ed25519 signature",
489+
context,
490+
);
491+
"Failed to convert validation signature to Ed25519 Signature"
492+
})?;
493+
494+
// Verify the signature using the latest signing public key
495+
signing_pk
496+
.verify_strict(&unsigned_aux, &sig)
497+
.with_context(|| {
498+
report.other("Signature validation failed", context);
499+
"Signature verification failed"
500+
})
501+
}
502+
406503
#[cfg(test)]
407504
mod test {
408505
use catalyst_types::catalyst_id::role_index::RoleId;
@@ -419,7 +516,7 @@ mod test {
419516
data.assert_valid(&registration);
420517

421518
// Create a chain with the first registration.
422-
let chain = RegistrationChain::new(registration).unwrap();
519+
let chain = RegistrationChain::new(&registration).unwrap();
423520
assert_eq!(chain.purpose(), &[data.purpose]);
424521
assert_eq!(1, chain.x509_certs().len());
425522
let origin = &chain.x509_certs().get(&0).unwrap().first().unwrap();
@@ -445,7 +542,7 @@ mod test {
445542
.unwrap();
446543
assert!(registration.report().is_problematic());
447544

448-
let error = chain.update(registration).unwrap_err();
545+
let error = chain.update(&registration).unwrap_err();
449546
let error = format!("{error:?}");
450547
assert!(
451548
error.contains("Invalid previous transaction ID"),
@@ -459,7 +556,7 @@ mod test {
459556
.unwrap()
460557
.unwrap();
461558
data.assert_valid(&registration);
462-
let update = chain.update(registration).unwrap();
559+
let update = chain.update(&registration).unwrap();
463560
// Current tx hash should be equal to the hash from block 4.
464561
assert_eq!(update.current_tx_id_hash(), data.txn_hash);
465562
assert!(update.role_data_record().contains_key(&data.role));

rust/rbac-registration/src/utils/test.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ pub fn block_4() -> BlockTestData {
169169
BlockTestData {
170170
block: block(data),
171171
slot: 82_004_569.into(),
172-
role: RoleId::Role0,
172+
role: 4.into(),
173173
txn_index: 1.into(),
174174
txn_hash: "eef40a97a4ed1e40c3febd05a84b3ffaa191141b60806c2bba85d9c6879fb378"
175175
.parse()

0 commit comments

Comments
 (0)