Skip to content

Commit 6b1a6be

Browse files
feat(pki): Allow to verify a certificate against trusted roots
1 parent 591f4e2 commit 6b1a6be

File tree

13 files changed

+216
-70
lines changed

13 files changed

+216
-70
lines changed

.cspell/custom-words.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ orgname
184184
parsecdoc
185185
patchelf
186186
pdflatex
187+
peekable
187188
pemfile
188189
pems
189190
perflint

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libparsec/crates/platform_pki/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ libparsec_types = { workspace = true }
2323
# Use to decode subject & issuer fields of used x509 certificate.
2424
x509-cert = { workspace = true, features = ["std"] }
2525
log = { workspace = true, features = ["kv"] }
26+
chrono = { workspace = true }
2627

2728
[target.'cfg(target_os = "windows")'.dependencies]
2829
schannel = { workspace = true }

libparsec/crates/platform_pki/examples/init_windows_credstore.ps1

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ $CAParam = @{
5656
NotAfter = (Get-Date).AddHours(24)
5757
# We cannot add it directly to `CA` store as the command only allow for `My` store
5858
CertStoreLocation = 'Cert:\CurrentUser\My'
59+
# Use RSA-PSS-SHA256 over RSASSA-SHA256 for signature
60+
AlternateSignatureAlgorithm = $true
5961
}
6062
$test_ca = New-SelfSignedCertificate @CAParam
6163

@@ -79,6 +81,8 @@ $SharedUserCertificate = @{
7981
NotBefore = $test_ca.NotBefore
8082
NotAfter = $test_ca.NotAfter
8183
CertStoreLocation = 'Cert:\CurrentUser\My'
84+
# Use RSA-PSS-SHA256 over RSASSA-SHA256 for signature
85+
AlternateSignatureAlgorithm = $true
8286
}
8387

8488
echo "Create certificate 'cert:\CurrentUser\My\test_ca_alice'"

libparsec/crates/platform_pki/examples/list_trusted_roots.rs

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,20 @@
11
// Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS
22
#![allow(clippy::unwrap_used)]
33

4-
use libparsec_platform_pki::{list_trusted_root_certificate_der, x509};
5-
use x509_cert::der::{DecodeValue, Header, SliceReader, Tag};
4+
mod utils;
5+
6+
use libparsec_platform_pki::list_trusted_root_certificate_anchor;
67

78
fn main() -> anyhow::Result<()> {
89
env_logger::init();
9-
let roots = list_trusted_root_certificate_der()?;
10+
let roots = list_trusted_root_certificate_anchor()?;
1011

1112
println!("Found {} trusted roots", roots.len());
1213

1314
for (i, anchor) in roots.iter().enumerate() {
1415
println!(
15-
"Root Certificate #{i} subject: {:?}",
16-
x509_cert::name::Name::decode_value(
17-
&mut SliceReader::new(&anchor.subject).unwrap(),
18-
Header::new(Tag::Sequence, anchor.subject.len()).unwrap()
19-
)
20-
.map(x509::extract_dn_list_from_rnd_seq)
21-
.unwrap_or_default()
16+
"Root Certificate #{i} subject={}",
17+
utils::display_x509_raw_name(anchor.subject.as_ref())
2218
);
2319
println!(
2420
" raw subject: {}",

libparsec/crates/platform_pki/examples/utils/mod.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ use clap::{
77
builder::{NonEmptyStringValueParser, TypedValueParser},
88
error::{Error, ErrorKind},
99
};
10-
use libparsec_platform_pki::Certificate;
10+
use libparsec_platform_pki::{x509::DistinguishedNameValue, Certificate};
1111
use libparsec_types::X509CertificateHash;
12+
use x509_cert::der::{DecodeValue, Header, SliceReader, Tag};
1213

1314
#[derive(Debug, Clone)]
1415
pub struct CertificateSRIHashParser;
@@ -98,3 +99,44 @@ impl CertificateOrRef {
9899
Ok(cert)
99100
}
100101
}
102+
103+
/// Used to display x509 certificate subject and issuer field
104+
// Not all examples need to display x509 name
105+
#[allow(dead_code)]
106+
pub fn display_x509_raw_name(raw_name: &[u8]) -> impl std::fmt::Display {
107+
struct DisplayName(Vec<DistinguishedNameValue>);
108+
109+
impl std::fmt::Display for DisplayName {
110+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111+
write!(f, "{:?}", self.0)
112+
}
113+
}
114+
115+
let components = x509_cert::name::Name::decode_value(
116+
&mut SliceReader::new(raw_name).unwrap(),
117+
Header::new(Tag::Sequence, raw_name.len()).unwrap(),
118+
)
119+
.map(libparsec_platform_pki::x509::extract_dn_list_from_rnd_seq)
120+
.unwrap_or_default();
121+
122+
DisplayName(components)
123+
}
124+
125+
// Not all example need to display a x509 cert
126+
#[allow(dead_code)]
127+
pub fn display_cert<'der>(cert: &'der webpki::Cert<'der>) -> impl std::fmt::Display + use<'der> {
128+
struct DisplayCert<'der>(&'der webpki::Cert<'der>);
129+
130+
impl<'der> std::fmt::Display for DisplayCert<'der> {
131+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132+
write!(
133+
f,
134+
"subject={subject}, issuer={issuer}",
135+
subject = display_x509_raw_name(self.0.subject()),
136+
issuer = display_x509_raw_name(self.0.issuer())
137+
)
138+
}
139+
}
140+
141+
DisplayCert(cert)
142+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS
2+
#![allow(clippy::unwrap_used)]
3+
4+
mod utils;
5+
6+
use anyhow::Context;
7+
use clap::Parser;
8+
use libparsec_platform_pki::{list_trusted_root_certificate_anchor, verify_certificate};
9+
use libparsec_types::DateTime;
10+
11+
#[derive(Debug, Parser)]
12+
struct Args {
13+
#[command(flatten)]
14+
cert: utils::CertificateOrRef,
15+
}
16+
17+
pub fn main() -> anyhow::Result<()> {
18+
let args = Args::parse();
19+
println!("args={args:?}");
20+
21+
let trusted_roots =
22+
list_trusted_root_certificate_anchor().context("Cannot list trusted root certificates")?;
23+
let untrusted_certificate = args
24+
.cert
25+
.get_certificate()
26+
.context("Cannot get certificate")?;
27+
28+
let end_cert = untrusted_certificate
29+
.into_end_certificate()
30+
.context("Invalid certificate")?;
31+
println!("Untrusted certificate: {}", utils::display_cert(&end_cert));
32+
33+
let path = verify_certificate(
34+
&end_cert,
35+
&trusted_roots,
36+
&[],
37+
DateTime::now(),
38+
webpki::KeyUsage::client_auth(),
39+
)
40+
.context("Cannot trust certificate")?;
41+
42+
println!("trusted path:");
43+
println!(
44+
" root: subject={}",
45+
utils::display_x509_raw_name(&path.anchor().subject)
46+
);
47+
let mut intermediate = path.intermediate_certificates().enumerate().peekable();
48+
if intermediate.peek().is_some() {
49+
println!(" intermediate certificates:");
50+
for (i, cert) in intermediate {
51+
println!(" {:02}. {}", i + 1, utils::display_cert(cert));
52+
}
53+
}
54+
let trusted_cert = path.end_entity();
55+
println!(" leaf: {}", utils::display_cert(trusted_cert));
56+
57+
Ok(())
58+
}

libparsec/crates/platform_pki/src/errors.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,10 @@ error_set::error_set! {
4444
InvalidPemContent(rustls_pki_types::pem::Error)
4545
}
4646
CreateLocalPendingError := EncryptMessageError
47+
VerifyCertificateError := {
48+
#[display("Time out of valid range: {0}")]
49+
DateTimeOutOfRange(chrono::OutOfRangeError),
50+
#[display("The provided certificate cannot be trusted: {0}")]
51+
Untrusted(webpki::Error),
52+
}
4753
}

libparsec/crates/platform_pki/src/lib.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ mod platform {
3535
unimplemented!("platform not supported")
3636
}
3737

38-
pub fn list_trusted_root_certificate_der(
38+
pub fn list_trusted_root_certificate_anchor(
3939
) -> Result<Vec<rustls_pki_types::TrustAnchor<'static>>, ListTrustedRootCertificatesError> {
4040
unimplemented!("platform not supported")
4141
}
@@ -112,7 +112,7 @@ pub struct CertificateDer {
112112
}
113113

114114
pub use errors::ListTrustedRootCertificatesError;
115-
pub use platform::list_trusted_root_certificate_der;
115+
pub use platform::list_trusted_root_certificate_anchor;
116116

117117
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118118
pub enum SignatureAlgorithm {
@@ -176,3 +176,6 @@ pub use errors::CreateLocalPendingError;
176176
pub use shared::create_local_pending;
177177

178178
pub use platform::is_available;
179+
180+
pub use errors::VerifyCertificateError;
181+
pub use shared::verify_certificate;

libparsec/crates/platform_pki/src/shared.rs renamed to libparsec/crates/platform_pki/src/shared/mod.rs

Lines changed: 38 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,19 @@
11
// Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS
22

3+
mod signature_verification;
4+
35
use crate::{
46
encrypt_message,
5-
errors::{InvalidPemContent, VerifySignatureError},
7+
errors::{InvalidPemContent, VerifyCertificateError, VerifySignatureError},
8+
shared::signature_verification::{RsassaPssSha256SignatureVerifier, SUPPORTED_SIG_ALGS},
69
EncryptedMessage, SignatureAlgorithm,
710
};
811
use libparsec_types::{
912
DateTime, EnrollmentID, LocalPendingEnrollment, ParsecPkiEnrollmentAddr,
1013
PkiEnrollmentSubmitPayload, PrivateParts, SecretKey, X509CertificateReference,
1114
};
12-
use rsa::{
13-
pkcs1,
14-
pss::{Signature, VerifyingKey},
15-
signature::Verifier,
16-
RsaPublicKey,
17-
};
18-
use rustls_pki_types::{
19-
pem::PemObject, CertificateDer, InvalidSignature, SignatureVerificationAlgorithm,
20-
};
21-
use sha2::Sha256;
22-
use webpki::{EndEntityCert, Error as WebPkiError};
15+
use rustls_pki_types::{pem::PemObject, CertificateDer, TrustAnchor};
16+
use webpki::{EndEntityCert, Error as WebPkiError, KeyUsage};
2317

2418
pub struct Certificate<'a> {
2519
internal: CertificateDer<'a>,
@@ -43,6 +37,10 @@ impl<'a> Certificate<'a> {
4337
pub fn into_owned(&self) -> Certificate<'static> {
4438
Certificate::new(self.internal.clone().into_owned())
4539
}
40+
41+
pub fn into_end_certificate(&self) -> Result<EndEntityCert<'_>, WebPkiError> {
42+
EndEntityCert::try_from(&self.internal)
43+
}
4644
}
4745

4846
impl Certificate<'static> {
@@ -80,50 +78,6 @@ pub fn verify_message<'message>(
8078
})
8179
}
8280

83-
#[derive(Debug)]
84-
struct RsassaPssSha256SignatureVerifier;
85-
86-
impl SignatureVerificationAlgorithm for RsassaPssSha256SignatureVerifier {
87-
fn verify_signature(
88-
&self,
89-
public_key: &[u8],
90-
message: &[u8],
91-
signature: &[u8],
92-
) -> Result<(), InvalidSignature> {
93-
// Webpki already checked that the key part correspond to an RSA public key.
94-
//
95-
// We are not using `pkcs8::DecodePublicKey::from_public_key_der` as
96-
// `public_key` is already the unwrapped key from the `subjectPublicKeyInfo` structure
97-
// which it's the expected data from the above method.
98-
//
99-
// Instead, we use `pkcs1::RsaPublicKey::try_from(&[u8])` which only expect the key element
100-
// (without the algorithm identifier).
101-
let pubkey = pkcs1::RsaPublicKey::try_from(public_key)
102-
.map_err(|_| InvalidSignature)
103-
// But `rsa` does not provide a conversion between the `pkcs1` and its `RsaPublicKey`, so
104-
// we need to perform the manual conversion
105-
.and_then(|pubkey| {
106-
let n = rsa::BigUint::from_bytes_be(pubkey.modulus.as_bytes());
107-
let e = rsa::BigUint::from_bytes_be(pubkey.public_exponent.as_bytes());
108-
RsaPublicKey::new(n, e).map_err(|_| InvalidSignature)
109-
})?;
110-
let verifying_key = VerifyingKey::<Sha256>::new(pubkey);
111-
112-
let signature = Signature::try_from(signature).map_err(|_| InvalidSignature)?;
113-
verifying_key
114-
.verify(message, &signature)
115-
.map_err(|_| InvalidSignature)
116-
}
117-
118-
fn public_key_alg_id(&self) -> rustls_pki_types::AlgorithmIdentifier {
119-
rustls_pki_types::alg_id::RSA_ENCRYPTION
120-
}
121-
122-
fn signature_alg_id(&self) -> rustls_pki_types::AlgorithmIdentifier {
123-
rustls_pki_types::alg_id::RSA_PSS_SHA256
124-
}
125-
}
126-
12781
pub fn create_local_pending(
12882
cert_ref: &X509CertificateReference,
12983
addr: ParsecPkiEnrollmentAddr,
@@ -152,3 +106,31 @@ pub fn create_local_pending(
152106
};
153107
Ok(local_pending)
154108
}
109+
110+
pub fn verify_certificate<'der>(
111+
certificate: &'der EndEntityCert<'der>,
112+
trusted_roots: &'der [TrustAnchor<'_>],
113+
intermediate_certs: &'der [CertificateDer<'der>],
114+
now: DateTime,
115+
key_usage: KeyUsage,
116+
) -> Result<webpki::VerifiedPath<'der>, VerifyCertificateError> {
117+
let time = rustls_pki_types::UnixTime::since_unix_epoch(
118+
now.duration_since_unix_epoch()
119+
.map_err(VerifyCertificateError::DateTimeOutOfRange)?,
120+
);
121+
certificate
122+
.verify_for_usage(
123+
SUPPORTED_SIG_ALGS,
124+
trusted_roots,
125+
intermediate_certs,
126+
time,
127+
key_usage,
128+
// TODO: Build the revocation options from a CRLS
129+
// webpki::RevocationOptionsBuilder require a non empty list, for now we provide None
130+
// instead
131+
None,
132+
// We do not have additional constrain to reject a valid path.
133+
None,
134+
)
135+
.map_err(VerifyCertificateError::Untrusted)
136+
}

0 commit comments

Comments
 (0)