use std::sync::Arc;
use ed25519_dalek::{Signature, VerifyingKey};
use rcgen::{CertificateParams, KeyPair};
use x509_parser::certification_request::X509CertificationRequest;
use x509_parser::oid_registry::OID_SIG_ED25519;
use x509_parser::prelude::FromDer;
use x509_parser::x509::X509Version;
use crate::ca::rcgen_bridge::{IdentitySigner, distinguished_name};
use crate::error::{TrustError, TrustResult};
use crate::keys::Ed25519Identity;
const ED25519_KEY_LEN: usize = 32;
const ED25519_SIGNATURE_LEN: usize = 64;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerifiedRequest {
subject_public_key: [u8; ED25519_KEY_LEN],
common_name: String,
}
impl VerifiedRequest {
pub fn subject_public_key(&self) -> &[u8; ED25519_KEY_LEN] {
&self.subject_public_key
}
pub fn common_name(&self) -> &str {
&self.common_name
}
}
pub fn create_certificate_request(
identity: &Arc<Ed25519Identity>,
subject: &str,
) -> TrustResult<Vec<u8>> {
if subject.trim().is_empty() {
return Err(TrustError::CertificateGeneration {
reason: "certificate-signing request subject must not be empty".to_string(),
});
}
let mut params = CertificateParams::new(Vec::<String>::new()).map_err(|e| {
TrustError::CertificateGeneration {
reason: format!("failed to build certificate-signing request parameters: {e}"),
}
})?;
params.distinguished_name = distinguished_name(subject);
let signer = IdentitySigner::new(Arc::clone(identity));
let key_pair =
KeyPair::from_remote(Box::new(signer)).map_err(|e| TrustError::CertificateGeneration {
reason: format!("failed to construct request key from identity: {e}"),
})?;
let request =
params
.serialize_request(&key_pair)
.map_err(|e| TrustError::CertificateGeneration {
reason: format!("failed to sign certificate-signing request: {e}"),
})?;
Ok(request.der().to_vec())
}
pub fn verify_certificate_request(request_der: &[u8]) -> TrustResult<VerifiedRequest> {
let (remaining, request) = X509CertificationRequest::from_der(request_der).map_err(|e| {
TrustError::CertificateParsing {
reason: format!("failed to parse certificate-signing request DER: {e:?}"),
}
})?;
if !remaining.is_empty() {
return Err(TrustError::CertificateParsing {
reason: format!(
"certificate-signing request has {} trailing byte(s) after the outer SEQUENCE",
remaining.len()
),
});
}
let info = &request.certification_request_info;
if info.version != X509Version::V1 {
return Err(TrustError::CertificateParsing {
reason: format!(
"certificate-signing request version must be v1 per RFC 2986, got {}",
info.version.0
),
});
}
if request.signature_algorithm.algorithm != OID_SIG_ED25519 {
return Err(TrustError::CertificateParsing {
reason: "certificate-signing request signature algorithm is not Ed25519".to_string(),
});
}
if info.subject_pki.algorithm.algorithm != OID_SIG_ED25519 {
return Err(TrustError::CertificateParsing {
reason: "certificate-signing request subject key algorithm is not Ed25519".to_string(),
});
}
if info.subject_pki.algorithm.parameters.is_some() {
return Err(TrustError::CertificateParsing {
reason: "Ed25519 subject key algorithm must carry no parameters (RFC 8410)".to_string(),
});
}
let subject_public_key = ed25519_bit_string(
info.subject_pki.subject_public_key.unused_bits,
&info.subject_pki.subject_public_key.data,
ED25519_KEY_LEN,
"subject public key",
)?;
let subject_public_key: [u8; ED25519_KEY_LEN] =
subject_public_key
.try_into()
.map_err(|_err| TrustError::CertificateParsing {
reason: "subject public key length check did not yield 32 bytes".to_string(),
})?;
let signature_bytes = ed25519_bit_string(
request.signature_value.unused_bits,
&request.signature_value.data,
ED25519_SIGNATURE_LEN,
"signature",
)?;
let signature_bytes: [u8; ED25519_SIGNATURE_LEN] =
signature_bytes
.try_into()
.map_err(|_err| TrustError::CertificateParsing {
reason: "signature length check did not yield 64 bytes".to_string(),
})?;
let common_name = single_common_name(info)?;
if let Some(mut extensions) = request.requested_extensions() {
if extensions.next().is_some() {
return Err(TrustError::CertificateParsing {
reason: "certificate-signing request carries requested extensions, which are \
never honoured — the authority chooses every certificate field other \
than the subject key"
.to_string(),
});
}
}
let verifying_key = VerifyingKey::from_bytes(&subject_public_key).map_err(|_err| {
TrustError::CertificateVerification {
reason: "certificate-signing request subject key is not a valid Ed25519 point"
.to_string(),
}
})?;
let signature = Signature::from_bytes(&signature_bytes);
verifying_key
.verify_strict(info.raw, &signature)
.map_err(|_err| TrustError::CertificateVerification {
reason: "certificate-signing request signature did not verify under the subject \
key it presents — proof of possession failed"
.to_string(),
})?;
Ok(VerifiedRequest {
subject_public_key,
common_name,
})
}
fn ed25519_bit_string<'a>(
unused_bits: u8,
data: &'a [u8],
expected_len: usize,
field: &str,
) -> TrustResult<&'a [u8]> {
if unused_bits != 0 {
return Err(TrustError::CertificateParsing {
reason: format!(
"certificate-signing request {field} BIT STRING must have no unused bits, got \
{unused_bits}"
),
});
}
if data.len() != expected_len {
return Err(TrustError::CertificateParsing {
reason: format!(
"certificate-signing request {field} must be {expected_len} bytes for Ed25519, \
got {}",
data.len()
),
});
}
Ok(data)
}
fn single_common_name(
info: &x509_parser::certification_request::X509CertificationRequestInfo<'_>,
) -> TrustResult<String> {
let mut names = info.subject.iter_common_name();
let first = names.next().ok_or_else(|| TrustError::CertificateParsing {
reason: "certificate-signing request subject carries no common name".to_string(),
})?;
if names.next().is_some() {
return Err(TrustError::CertificateParsing {
reason: "certificate-signing request subject carries more than one common name"
.to_string(),
});
}
let common_name = first
.as_str()
.map_err(|e| TrustError::CertificateParsing {
reason: format!("certificate-signing request common name is not printable text: {e}"),
})?
.to_string();
if common_name.trim().is_empty() {
return Err(TrustError::CertificateParsing {
reason: "certificate-signing request common name must not be empty".to_string(),
});
}
Ok(common_name)
}
#[cfg(test)]
#[path = "request_tests.rs"]
mod tests;