Skip to content

Commit 32b5ab2

Browse files
feat(pki): Allow to verify signature using a certificate
- Use `rustls` crates to provide the parsing of a DER/PEM certificate - Add example to verify a generated signature (will work on allow platforms, but on unix, we are currently not integrated with a cert store)
1 parent b474143 commit 32b5ab2

File tree

12 files changed

+298
-61
lines changed

12 files changed

+298
-61
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,8 @@ rsa = { version = "0.8.2", default-features = false }
186186
rstest = { version = "0.24.0", default-features = false }
187187
rstest_reuse = { version = "0.7.0", default-features = false }
188188
ruzstd = { version = "0.7.3", default-features = true }
189+
rustls-webpki = { version = "0.103.6", default-features = false }
190+
rustls-pki-types = { version = "1.12.0", default-features = false }
189191
# Crate to interact with windows credential store, it's used notably in `native-tls` crate.
190192
schannel = { version = "0.1.28", default-features = false }
191193
sentry = { version = "0.34.0", default-features = false }

libparsec/crates/platform_pki/Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,18 @@ hash-sri-display = ["dep:data-encoding"]
1919
bytes = { workspace = true }
2020
data-encoding = { workspace = true, optional = true }
2121
error_set = { workspace = true }
22+
rustls-webpki = { workspace = true, features = ["std"] }
23+
rustls-pki-types = { workspace = true }
24+
rsa = { workspace = true }
25+
sha2 = { workspace = true }
2226

2327
[target.'cfg(target_os = "windows")'.dependencies]
2428
schannel = { workspace = true }
2529
windows-sys = { workspace = true, features = ["Win32_Security_Cryptography_UI"] }
26-
sha2 = { workspace = true }
2730

2831
[target.'cfg(target_os = "linux")'.dev-dependencies]
2932
cryptoki = { workspace = true }
3033
percent-encoding = { workspace = true }
31-
sha2 = { workspace = true }
3234

3335
[dev-dependencies]
3436
# We use clap to provide a CLI for examples.

libparsec/crates/platform_pki/examples/decrypt_message.rs

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

33
mod utils;
4-
use std::path::PathBuf;
54

65
use anyhow::Context;
76
use clap::Parser;
@@ -15,31 +14,18 @@ struct Args {
1514
#[arg(value_parser = utils::CertificateSRIHashParser)]
1615
certificate_hash: CertificateHash,
1716
#[command(flatten)]
18-
content: ContentOpts,
17+
content: utils::ContentOpts,
1918
/// The algorithm used to encrypt the content.
2019
#[arg(long, default_value_t = EncryptionAlgorithm::RsaesOaepSha256)]
2120
algorithm: EncryptionAlgorithm,
2221
}
2322

24-
#[derive(Debug, Clone, clap::Args)]
25-
#[group(required = true, multiple = false)]
26-
struct ContentOpts {
27-
#[arg(long, conflicts_with = "content_file")]
28-
content: Option<String>,
29-
#[arg(long)]
30-
content_file: Option<PathBuf>,
31-
}
32-
3323
fn main() -> anyhow::Result<()> {
3424
let args = Args::parse();
3525
println!("args={args:?}");
3626

3727
let cert_ref = CertificateReference::Hash(args.certificate_hash);
38-
let b64_data: Vec<u8> = match (args.content.content, args.content.content_file) {
39-
(Some(content), None) => content.into(),
40-
(None, Some(filepath)) => std::fs::read(filepath).context("Failed to read file")?,
41-
(Some(_), Some(_)) | (None, None) => unreachable!("Handled by clap"),
42-
};
28+
let b64_data = args.content.into_bytes()?;
4329
let data = data_encoding::BASE64
4430
.decode(&b64_data)
4531
.context("Failed to decode hex encoded data")?;

libparsec/crates/platform_pki/examples/encrypt_message.rs

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

33
mod utils;
4-
use std::path::PathBuf;
54

65
use anyhow::Context;
76
use clap::Parser;
@@ -13,28 +12,15 @@ struct Args {
1312
#[arg(value_parser = utils::CertificateSRIHashParser)]
1413
certificate_hash: CertificateHash,
1514
#[command(flatten)]
16-
content: ContentOpts,
17-
}
18-
19-
#[derive(Debug, Clone, clap::Args)]
20-
#[group(required = true, multiple = false)]
21-
struct ContentOpts {
22-
#[arg(long, conflicts_with = "content_file")]
23-
content: Option<String>,
24-
#[arg(long)]
25-
content_file: Option<PathBuf>,
15+
content: utils::ContentOpts,
2616
}
2717

2818
fn main() -> anyhow::Result<()> {
2919
let args = Args::parse();
3020
println!("args={args:?}");
3121

3222
let cert_ref = CertificateReference::Hash(args.certificate_hash);
33-
let data: Vec<u8> = match (args.content.content, args.content.content_file) {
34-
(Some(content), None) => content.into(),
35-
(None, Some(filepath)) => std::fs::read(filepath).context("Failed to read file")?,
36-
(Some(_), Some(_)) | (None, None) => unreachable!("Handled by clap"),
37-
};
23+
let data = args.content.into_bytes()?;
3824

3925
let res = encrypt_message(&data, &cert_ref).context("Failed to encrypt message")?;
4026

libparsec/crates/platform_pki/examples/sign_message.rs

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

33
mod utils;
4-
use std::path::PathBuf;
54

65
use anyhow::Context;
76
use clap::Parser;
@@ -13,29 +12,15 @@ struct Args {
1312
#[arg(value_parser = utils::CertificateSRIHashParser)]
1413
certificate_hash: CertificateHash,
1514
#[command(flatten)]
16-
content: ContentOpts,
17-
}
18-
19-
#[derive(Debug, Clone, clap::Args)]
20-
#[group(required = true, multiple = false)]
21-
struct ContentOpts {
22-
#[arg(long, conflicts_with = "content_file")]
23-
content: Option<String>,
24-
#[arg(long)]
25-
content_file: Option<PathBuf>,
15+
content: utils::ContentOpts,
2616
}
2717

2818
fn main() -> anyhow::Result<()> {
2919
let args = Args::parse();
3020
println!("args={args:?}");
3121

3222
let cert_ref = CertificateReference::Hash(args.certificate_hash);
33-
let data: Vec<u8> = match (args.content.content, args.content.content_file) {
34-
(Some(content), None) => content.into(),
35-
(None, Some(filepath)) => std::fs::read(filepath).context("Failed to read file")?,
36-
(Some(_), Some(_)) | (None, None) => unreachable!("Handled by clap"),
37-
};
38-
23+
let data: Vec<u8> = args.content.into_bytes()?;
3924
let res = sign_message(&data, &cert_ref).context("Failed to sign message")?;
4025

4126
println!(
@@ -47,7 +32,7 @@ fn main() -> anyhow::Result<()> {
4732
println!("Signed by cert with fingerprint: {}", res.cert_ref.hash);
4833
println!(
4934
"Signature: {}",
50-
data_encoding::BASE64.encode_display(&res.signed_message)
35+
data_encoding::BASE64.encode_display(&res.signature)
5136
);
5237

5338
Ok(())

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

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

3+
use std::path::PathBuf;
4+
5+
use anyhow::Context;
36
use clap::{
47
builder::{NonEmptyStringValueParser, TypedValueParser},
58
error::{Error, ErrorKind},
@@ -38,3 +41,26 @@ impl TypedValueParser for CertificateSRIHashParser {
3841
}
3942
}
4043
}
44+
45+
#[derive(Debug, Clone, clap::Args)]
46+
#[group(required = true, multiple = false)]
47+
pub struct ContentOpts {
48+
/// The content to use.
49+
#[arg(long)]
50+
pub content: Option<String>,
51+
/// Read content from a file
52+
#[arg(long)]
53+
pub content_file: Option<PathBuf>,
54+
}
55+
56+
impl ContentOpts {
57+
// Not all examples uses `ContentOpts` so `into_bytes` is not always used.
58+
#[allow(dead_code)]
59+
pub fn into_bytes(self) -> anyhow::Result<Vec<u8>> {
60+
match (self.content, self.content_file) {
61+
(Some(content), None) => Ok(content.into()),
62+
(None, Some(filepath)) => std::fs::read(filepath).context("Failed to read file"),
63+
(Some(_), Some(_)) | (None, None) => unreachable!("Handled by clap"),
64+
}
65+
}
66+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS
2+
3+
use std::path::PathBuf;
4+
5+
use anyhow::Context;
6+
use clap::Parser;
7+
use libparsec_platform_pki::{
8+
get_der_encoded_certificate, verify_message, Certificate, CertificateHash, SignatureAlgorithm,
9+
SignedMessage,
10+
};
11+
use sha2::Digest;
12+
13+
mod utils;
14+
15+
#[derive(Debug, Parser)]
16+
struct Args {
17+
#[command(flatten)]
18+
cert: CertificateOrRef,
19+
#[command(flatten)]
20+
content: utils::ContentOpts,
21+
signature_header: SignatureAlgorithm,
22+
/// Signature in base64
23+
signature: String,
24+
}
25+
26+
#[derive(Debug, Clone, clap::Args)]
27+
#[group(required = true, multiple = false)]
28+
struct CertificateOrRef {
29+
/// Hash of the certificate from the store to use to verify the signature.
30+
#[arg(value_parser = utils::CertificateSRIHashParser)]
31+
certificate_hash: Option<CertificateHash>,
32+
/// Path to a file containing the certificate in DER format.
33+
der_file: Option<PathBuf>,
34+
/// Path to a file containing the certificate in PEM format.
35+
pem_file: Option<PathBuf>,
36+
/// Certificate in PEM format but without headers.
37+
pem: Option<String>,
38+
}
39+
40+
fn main() -> anyhow::Result<()> {
41+
let args = Args::parse();
42+
println!("args={args:?}");
43+
44+
let data = args.content.into_bytes()?;
45+
46+
let signature = data_encoding::BASE64
47+
.decode(args.signature.as_bytes())
48+
.context("Invalid signature format")?;
49+
50+
let cert = if let Some(hash) = args.cert.certificate_hash {
51+
let res =
52+
get_der_encoded_certificate(&libparsec_platform_pki::CertificateReference::Hash(hash))?;
53+
println!(
54+
"Will verify signature using cert with id {{{}}}",
55+
data_encoding::BASE64.encode_display(&res.cert_ref.id)
56+
);
57+
Certificate::from_der_owned(res.der_content.into())
58+
} else if let Some(der_file) = args.cert.der_file {
59+
let raw = std::fs::read(der_file).context("Failed to read file")?;
60+
Certificate::from_der_owned(raw)
61+
} else if let Some(pem_file) = args.cert.pem_file {
62+
let raw = std::fs::read(pem_file).context("Failed to read file")?;
63+
Certificate::try_from_pem(&r#raw)?.into_owned()
64+
} else if let Some(pem) = args.cert.pem {
65+
let raw = data_encoding::BASE64
66+
.decode(pem.as_bytes())
67+
.context("Invalid pem base64")?;
68+
Certificate::from_der_owned(raw)
69+
} else {
70+
unreachable!("Should be handle by clap")
71+
};
72+
73+
#[cfg(feature = "hash-sri-display")]
74+
{
75+
let fingerprint =
76+
CertificateHash::SHA256(Box::new(sha2::Sha256::digest(cert.as_ref()).into()));
77+
println!("Certificate fingerprint: {fingerprint}");
78+
}
79+
80+
let signed_message = SignedMessage {
81+
algo: args.signature_header,
82+
signature,
83+
message: data,
84+
};
85+
86+
match verify_message(&signed_message, cert) {
87+
Ok(_) => {
88+
println!("The message as a correct signature")
89+
}
90+
Err(e) => println!("The message as an incorrect signature: {e}"),
91+
}
92+
93+
Ok(())
94+
}

libparsec/crates/platform_pki/src/errors.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,18 @@ error_set::error_set! {
2626
#[display("Cannot decrypt message: {0}")]
2727
CannotDecrypt(std::io::Error),
2828
};
29+
InvalidCertificateDer = {
30+
#[display("Invalid certificate: {0}")]
31+
InvalidCertificateDer(webpki::Error),
32+
};
33+
VerifySignatureError = InvalidCertificateDer || {
34+
#[display("Invalid signature for the given message and certificate")]
35+
InvalidSignature,
36+
#[display("Unexpected signature will verifying signature of a message: {0}")]
37+
UnexpectedError(webpki::Error)
38+
};
39+
InvalidPemContent = {
40+
#[display("Invalid PEM content: {0}")]
41+
InvalidPemContent(rustls_pki_types::pem::Error)
42+
};
2943
}

0 commit comments

Comments
 (0)