Skip to content

Commit 5df2086

Browse files
no30bitstevenjMr-Leshiy
authored
feat(rust/rbac-registration): refactor cip509 extract key fns (#310)
* feat(rust/rbac-registration): refactor cip509 exctrat key fns * fix: comments * avoid extended public keys, refactor error type, add unit test * remove outdoated doc comment * Update rust/rbac-registration/src/cardano/cip509/utils/extract_key.rs Co-authored-by: Alex Pozhylenkov <[email protected]> * fix docs & split invalid length err * fmt & debug --------- Co-authored-by: Steven Johnson <[email protected]> Co-authored-by: Alex Pozhylenkov <[email protected]>
1 parent 8f61d83 commit 5df2086

File tree

2 files changed

+142
-114
lines changed

2 files changed

+142
-114
lines changed
Lines changed: 135 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,86 @@
11
//! Functions for extracting public key from X509 and C509 certificates with additional
22
//! verification.
33
4-
use anyhow::{anyhow, Context};
4+
use std::borrow::Cow;
5+
56
use c509_certificate::c509::C509;
6-
use ed25519_dalek::{VerifyingKey, PUBLIC_KEY_LENGTH};
7+
use catalyst_types::problem_report::ProblemReport;
8+
use ed25519_dalek::{SignatureError, VerifyingKey, PUBLIC_KEY_LENGTH};
79
use oid_registry::{Oid, OID_SIG_ED25519};
810
use thiserror::Error;
9-
use x509_cert::Certificate as X509Certificate;
11+
use x509_cert::{spki, Certificate as X509Certificate};
12+
13+
/// Common error type.
14+
#[derive(Debug, Error)]
15+
pub enum Error {
16+
/// Unsupported signature algorithms.
17+
#[error("Unsupported signature algorithm (oid: {oid})")]
18+
UnsupportedSignatureAlgo {
19+
/// An OID of unsupported signature algorithm.
20+
oid: Oid<'static>,
21+
},
22+
/// Public key has invalid length.
23+
#[error(
24+
"Invalid public key length (found {bytes} bytes, but expected {PUBLIC_KEY_LENGTH} bytes)"
25+
)]
26+
InvalidPublicKeyLength {
27+
/// Number of bytes found.
28+
bytes: usize,
29+
},
30+
/// Public key is stored in a bit string, where number of unused bits is *not* equal
31+
/// to zero.
32+
#[error("Invalid public key is not octet aligned (found {bits} bits)")]
33+
PublicKeyIsNotOctetAligned {
34+
/// Number of bits found.
35+
bits: usize,
36+
},
37+
/// Public key doesn't pass [`ed25519_dalek`] constraint check.
38+
#[error("Invalid public key ({source})")]
39+
PublicKeyIsNotEd25519 {
40+
/// Underlying [`ed25519_dalek`] error.
41+
#[from]
42+
source: SignatureError,
43+
},
44+
}
1045

11-
/// Error type for unsupported signature algorithms.
12-
#[derive(Error, Debug)]
13-
#[error("Unsupported signature algorithm: {oid}")]
14-
pub struct SignatureAlgoError {
15-
/// An OID of unsupported signature algorithm.
16-
oid: String,
46+
impl Error {
47+
/// Shortcut function to report `Self` into a [`ProblemReport`].
48+
pub fn report_problem(&self, context: &str, report: &ProblemReport) {
49+
match self {
50+
Error::UnsupportedSignatureAlgo { oid } => {
51+
report.invalid_value(
52+
"subject_public_key_algorithm",
53+
&oid.to_id_string(),
54+
"Currently the only supported signature algorithm is ED25519",
55+
context,
56+
);
57+
},
58+
Error::InvalidPublicKeyLength { bytes } => {
59+
report.invalid_value(
60+
"subject_public_key",
61+
&format!("{bytes} bytes"),
62+
&format!("Must be {PUBLIC_KEY_LENGTH} bytes long"),
63+
context,
64+
);
65+
},
66+
Error::PublicKeyIsNotOctetAligned { bits } => {
67+
report.invalid_value(
68+
"subject_public_key",
69+
&format!("{bits} bits"),
70+
"Bit string must be octet aligned having no unused bits",
71+
context,
72+
);
73+
},
74+
Error::PublicKeyIsNotEd25519 { source } => {
75+
report.invalid_value(
76+
"subject_public_key",
77+
&source.to_string(),
78+
"Must be an Ed25519 public key",
79+
context,
80+
);
81+
},
82+
}
83+
}
1784
}
1885

1986
/// Returns `VerifyingKey` from the given X509 certificate.
@@ -22,24 +89,21 @@ pub struct SignatureAlgoError {
2289
///
2390
/// Returns an error if the signature algorithm is not supported and
2491
/// the public key cannot be extracted.
25-
pub fn x509_key(cert: &X509Certificate) -> anyhow::Result<VerifyingKey> {
26-
let oid: Oid = cert
27-
.tbs_certificate
28-
.subject_public_key_info
29-
.algorithm
30-
.oid
31-
.to_string()
32-
.parse()
33-
// `Context` cannot be used here because `OidParseError` doesn't implement `std::Error`.
34-
.map_err(|e| anyhow!("Invalid signature algorithm OID: {e:?}"))?;
35-
check_signature_algorithm(&oid)?;
36-
let extended_public_key = cert
92+
///
93+
/// Returns an error if public key has unexpected value.
94+
pub fn x509_key(cert: &X509Certificate) -> Result<VerifyingKey, Error> {
95+
let oid = &cert.tbs_certificate.subject_public_key_info.algorithm.oid;
96+
check_signature_algorithm(&spki_oid_as_asn1_rs_oid(oid))?;
97+
let public_key = &cert
3798
.tbs_certificate
3899
.subject_public_key_info
39-
.subject_public_key
100+
.subject_public_key;
101+
let public_key_bytes = public_key
40102
.as_bytes()
41-
.context("Invalid subject_public_key value (has unused bits)")?;
42-
verifying_key(extended_public_key).context("Unable to get verifying key from X509 certificate")
103+
.ok_or(Error::PublicKeyIsNotOctetAligned {
104+
bits: public_key.bit_len(),
105+
})?;
106+
verifying_key(public_key_bytes)
43107
}
44108

45109
/// Returns `VerifyingKey` from the given C509 certificate.
@@ -48,47 +112,65 @@ pub fn x509_key(cert: &X509Certificate) -> anyhow::Result<VerifyingKey> {
48112
///
49113
/// Returns an error if the signature algorithm is not supported and
50114
/// the public key cannot be extracted.
51-
pub fn c509_key(cert: &C509) -> anyhow::Result<VerifyingKey> {
115+
///
116+
/// Returns an error if public key has unexpected value.
117+
pub fn c509_key(cert: &C509) -> Result<VerifyingKey, Error> {
52118
let oid = cert
53119
.tbs_cert()
54120
.subject_public_key_algorithm()
55121
.algo_identifier()
56122
.oid();
57123
check_signature_algorithm(oid)?;
58-
verifying_key(cert.tbs_cert().subject_public_key())
59-
.context("Unable to get verifying key from C509 certificate")
124+
let public_key = cert.tbs_cert().subject_public_key();
125+
verifying_key(public_key)
60126
}
61127

62-
/// Checks that the signature algorithm is supported.
63-
fn check_signature_algorithm(oid: &Oid) -> Result<(), SignatureAlgoError> {
128+
/// Checks that the signature algorithm with the given [`spki::ObjectIdentifier`] is
129+
/// supported.
130+
fn check_signature_algorithm(oid: &Oid) -> Result<(), Error> {
64131
// Currently the only supported signature algorithm is ED25519.
65-
if *oid != OID_SIG_ED25519 {
66-
return Err(SignatureAlgoError {
67-
oid: oid.to_id_string(),
68-
});
132+
if *oid == OID_SIG_ED25519 {
133+
Ok(())
134+
} else {
135+
Err(Error::UnsupportedSignatureAlgo {
136+
oid: oid.to_owned(),
137+
})
69138
}
70-
Ok(())
71139
}
72140

73-
// TODO: The very similar logic exists in the `rbac-registration` crate. It should be
74-
// moved somewhere and reused. See https://github.com/input-output-hk/catalyst-voices/issues/1952
75-
/// Creates `VerifyingKey` from the given extended public key.
76-
fn verifying_key(extended_public_key: &[u8]) -> anyhow::Result<VerifyingKey> {
77-
/// An extender public key length in bytes.
78-
const EXTENDED_PUBLIC_KEY_LENGTH: usize = 64;
141+
/// Converts [`spki::ObjectIdentifier`] ref to an [`Oid`].
142+
fn spki_oid_as_asn1_rs_oid(oid: &'_ spki::ObjectIdentifier) -> Oid<'_> {
143+
// Note that this conversion always succeeds as both crates omit header.
144+
Oid::new(Cow::Borrowed(oid.as_bytes()))
145+
}
146+
147+
/// Creates [`VerifyingKey`] from the first 32 bytes in a slice.
148+
/// Since only prefix bytes are used, both extended and common public keys are supported
149+
/// here.
150+
fn verifying_key(public_key: &[u8]) -> Result<VerifyingKey, Error> {
151+
public_key
152+
// TODO: replace with checked `[u8; 32]` conversion once we only support common ed25119.
153+
.first_chunk()
154+
// Public key is too short.
155+
.ok_or(Error::InvalidPublicKeyLength {
156+
bytes: public_key.len(),
157+
})
158+
.and_then(|bytes| VerifyingKey::from_bytes(bytes).map_err(Error::from))
159+
}
160+
161+
#[cfg(test)]
162+
mod tests {
163+
use oid_registry::{asn1_rs, OID_SIG_ED25519};
164+
use x509_cert::spki;
165+
166+
#[test]
167+
fn spki_oid_as_asn1_rs_oid() {
168+
let spki_oid = spki::ObjectIdentifier::new_unwrap("1.3.101.112");
169+
let asn1_rs_oid = asn1_rs::oid!(1.3.101 .112);
170+
assert_eq!(spki_oid.to_string(), asn1_rs_oid.to_id_string());
79171

80-
if extended_public_key.len() != EXTENDED_PUBLIC_KEY_LENGTH {
81-
return Err(anyhow!(
82-
"Unexpected extended public key length in certificate: {}, expected {EXTENDED_PUBLIC_KEY_LENGTH}",
83-
extended_public_key.len()
84-
));
172+
let converted_spki_oid = super::spki_oid_as_asn1_rs_oid(&spki_oid);
173+
assert_eq!(converted_spki_oid.to_string(), asn1_rs_oid.to_id_string());
174+
assert_eq!(converted_spki_oid, OID_SIG_ED25519);
85175
}
86-
// This should never fail because of the check above.
87-
let public_key = extended_public_key
88-
.get(0..PUBLIC_KEY_LENGTH)
89-
.context("Unable to get public key part")?;
90-
let bytes: &[u8; PUBLIC_KEY_LENGTH] = public_key
91-
.try_into()
92-
.context("Invalid public key length in X509 certificate")?;
93-
VerifyingKey::from_bytes(bytes).context("Invalid public key in X509 certificate")
94176
}

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

Lines changed: 7 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use catalyst_types::{
1313
hashes::{Blake2b128Hash, Blake2b256Hash},
1414
problem_report::ProblemReport,
1515
};
16-
use ed25519_dalek::{Signature, VerifyingKey, PUBLIC_KEY_LENGTH};
16+
use ed25519_dalek::{Signature, VerifyingKey};
1717
use pallas::{
1818
codec::{
1919
minicbor::{Encode, Encoder},
@@ -464,70 +464,16 @@ fn validate_role_0(
464464

465465
/// Extracts `VerifyingKey` from the given `X509` certificate.
466466
fn x509_cert_key(cert: &X509, context: &str, report: &ProblemReport) -> Option<VerifyingKey> {
467-
let Some(extended_public_key) = cert
468-
.tbs_certificate
469-
.subject_public_key_info
470-
.subject_public_key
471-
.as_bytes()
472-
else {
473-
report.invalid_value(
474-
"subject_public_key",
475-
"is not octet aligned",
476-
"Must not have unused bits",
477-
context,
478-
);
479-
return None;
480-
};
481-
verifying_key(extended_public_key, context, report)
467+
x509_key(cert)
468+
.inspect_err(|err| err.report_problem(context, report))
469+
.ok()
482470
}
483471

484472
/// Extracts `VerifyingKey` from the given `C509` certificate.
485473
fn c509_cert_key(cert: &C509, context: &str, report: &ProblemReport) -> Option<VerifyingKey> {
486-
verifying_key(cert.tbs_cert().subject_public_key(), context, report)
487-
}
488-
489-
/// Creates `VerifyingKey` from the given extended public key.
490-
fn verifying_key(
491-
extended_public_key: &[u8], context: &str, report: &ProblemReport,
492-
) -> Option<VerifyingKey> {
493-
/// An extender public key length in bytes.
494-
const EXTENDED_PUBLIC_KEY_LENGTH: usize = 64;
495-
496-
if extended_public_key.len() != EXTENDED_PUBLIC_KEY_LENGTH {
497-
report.other(
498-
&format!("Unexpected extended public key length in certificate: {}, expected {EXTENDED_PUBLIC_KEY_LENGTH}",
499-
extended_public_key.len()),
500-
context,
501-
);
502-
return None;
503-
}
504-
505-
// This should never fail because of the check above.
506-
let Some(public_key) = extended_public_key.get(0..PUBLIC_KEY_LENGTH) else {
507-
report.other("Unable to get public key part", context);
508-
return None;
509-
};
510-
511-
let bytes: &[u8; PUBLIC_KEY_LENGTH] = match public_key.try_into() {
512-
Ok(v) => v,
513-
Err(e) => {
514-
report.other(
515-
&format!("Invalid public key length in X509 certificate: {e:?}"),
516-
context,
517-
);
518-
return None;
519-
},
520-
};
521-
match VerifyingKey::from_bytes(bytes) {
522-
Ok(k) => Some(k),
523-
Err(e) => {
524-
report.other(
525-
&format!("Invalid public key in C509 certificate: {e:?}"),
526-
context,
527-
);
528-
None
529-
},
530-
}
474+
c509_key(cert)
475+
.inspect_err(|err| err.report_problem(context, report))
476+
.ok()
531477
}
532478

533479
#[cfg(test)]

0 commit comments

Comments
 (0)