From 5207b4fc8b19cc051e3b4c0d51c3efe80c21a9b3 Mon Sep 17 00:00:00 2001 From: Andrew Oswald Date: Thu, 28 Nov 2024 12:48:57 -0500 Subject: [PATCH] Adds optional pkcs11 feature. --- Cargo.toml | 5 +++ src/config/ssl.rs | 96 +++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 5 +++ 3 files changed, 106 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index e1ab8ef2..ccac2848 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ cookies = ["httpdate"] http2 = ["curl/http2"] json = ["serde", "serde_json"] nightly = [] +pkcs11 = ["pk11-uri-parser"] psl = ["httpdate", "parking_lot", "publicsuffix"] spnego = ["curl-sys/spnego"] static-curl = ["curl/static-curl"] @@ -64,6 +65,10 @@ optional = true version = ">=0.9.0, <0.12.0" optional = true +[dependencies.pk11-uri-parser] +version = "0.1.5" +optional = true + [dependencies.publicsuffix] version = "2.0.6" features = ["std"] diff --git a/src/config/ssl.rs b/src/config/ssl.rs index bbcadfa8..aba9806a 100644 --- a/src/config/ssl.rs +++ b/src/config/ssl.rs @@ -31,6 +31,9 @@ pub struct ClientCertificate { /// Password to decrypt the certificate file. password: Option, + + /// The crypto engine. + engine: Option<&'static str>, } impl ClientCertificate { @@ -53,6 +56,7 @@ impl ClientCertificate { data: PathOrBlob::Blob(bytes.into()), private_key: private_key.into(), password: None, + engine: None, } } @@ -75,6 +79,7 @@ impl ClientCertificate { data: PathOrBlob::Blob(bytes.into()), private_key: private_key.into(), password: None, + engine: None, } } @@ -98,6 +103,7 @@ impl ClientCertificate { data: PathOrBlob::Blob(bytes.into()), private_key: None, password: password.into(), + engine: None, } } @@ -113,6 +119,7 @@ impl ClientCertificate { data: PathOrBlob::Path(path.into()), private_key: private_key.into(), password: None, + engine: None, } } @@ -128,6 +135,7 @@ impl ClientCertificate { data: PathOrBlob::Path(path.into()), private_key: private_key.into(), password: None, + engine: None, } } @@ -143,6 +151,51 @@ impl ClientCertificate { data: PathOrBlob::Path(path.into()), private_key: None, password: password.into(), + engine: None, + } + } + + /// Get an HSM-bound certificate by way of PKCS#11 URI. + /// + /// The `pkcs11_uri` is expected to be a [PKCS#11 URI][uri-scheme] whose + /// `type` is implicitly understood to be `cert`. + /// + /// It is expected that your OpenSSL configuration contains an appropriate `pkcs11` + /// stanza which properly resolves the PKCS#11 vendor module/shared object. A + /// reasonable way to test this is to *manually* invoke a curl command specifying + /// `--cert` and `--key` parameters using the pkcs11 uri values intended to be + /// used in your code. + /// + /// #### Example (these are YubiKey object alias values for slot `9E`) + /// ``` terminal + /// curl https://www.rust-lang.org \ + /// --key "pkcs11:object=Private%20key%20for%20Card%20Authentication?pin-value=123456" \ + /// --cert "pkcs11:object=X.509%20Certificate%20for%20Card%20Authentication" + /// ``` + /// The above key and cert isahc code (minus invocation) would be: + /// #### Example + /// ``` + /// use isahc::config::{ClientCertificate, PrivateKey,}; + /// let private_key: Option = PrivateKey::pkcs11( + /// "pkcs11:object=Private%20key%20for%20Card%20Authentication?pin-value=123456", + /// ) + /// .ok(); + /// let client_cert: ClientCertificate = ClientCertificate::pkcs11( + /// "pkcs11:object=X.509%20Certificate%20for%20Card%20Authentication", + /// private_key, + /// ); + /// ``` + /// + /// [uri-scheme]: https://datatracker.ietf.org/doc/html/rfc7512 + #[cfg(feature="pkcs11")] + pub fn pkcs11(pkcs11_uri: &str, private_key: Option) -> Self { + + Self { + format: "ENG", + data: PathOrBlob::Blob(pkcs11_uri.into()), + private_key, + password: None, + engine: Some("pkcs11"), } } @@ -169,6 +222,10 @@ impl SetOpt for ClientCertificate { PathOrBlob::Blob(bytes) => easy.ssl_cert_blob(bytes.as_slice()), }?; + if let Some(engine) = self.engine.as_ref() { + easy.ssl_engine(engine)?; + } + if let Some(key) = self.private_key.as_ref() { key.set_opt(easy)?; } @@ -192,6 +249,9 @@ pub struct PrivateKey { /// Password to decrypt the key file. password: Option, + + /// The crypto engine. + engine: Option<&'static str>, } impl PrivateKey { @@ -212,6 +272,7 @@ impl PrivateKey { format: "PEM", data: PathOrBlob::Blob(bytes.into()), password: password.into(), + engine: None, } } @@ -232,6 +293,7 @@ impl PrivateKey { format: "DER", data: PathOrBlob::Blob(bytes.into()), password: password.into(), + engine: None, } } @@ -246,6 +308,7 @@ impl PrivateKey { format: "PEM", data: PathOrBlob::Path(path.into()), password: password.into(), + engine: None, } } @@ -260,8 +323,37 @@ impl PrivateKey { format: "DER", data: PathOrBlob::Path(path.into()), password: password.into(), + engine: None, } } + + /// Use an HSM-bound private key by way of PKCS#11 URI. + /// + /// The `pkcs11_uri` is expected to be a PKCS#11 URI whose `type` is implicitly + /// understood to be `private`. If the `pin-value` attribute has been included, + /// it will effectively be assigned as the password. + /// + /// It is expected that your OpenSSL configuration contains an appropriate `pkcs11` + /// stanza which properly resolves the PKCS#11 vendor module/shared object. + /// + /// Please be aware we are limited to curl's API so not all PKCS#11 features are supported. + /// + /// #### Example + /// ``` + /// let pkcs11_uri = "pkcs11:object=Private%20key%20for%20Card%20Authentication?pin-value=123456"; + /// let private_key = isahc::config::PrivateKey::pkcs11(pkcs11_uri).expect("PrivateKey should be valid."); + /// ``` + #[cfg(feature = "pkcs11")] + pub fn pkcs11(pkcs11_uri: &str) -> Result { + let mapping = pk11_uri_parser::parse(pkcs11_uri)?; + + Ok(Self { + format: "ENG", + data: PathOrBlob::Blob(pkcs11_uri.into()), + password: mapping.pin_value().map(String::from), + engine: Some("pkcs11") + }) + } } impl SetOpt for PrivateKey { @@ -273,6 +365,10 @@ impl SetOpt for PrivateKey { PathOrBlob::Blob(bytes) => easy.ssl_key_blob(bytes.as_slice()), }?; + if let Some(engine) = self.engine.as_ref() { + easy.ssl_engine(engine)?; + } + if let Some(password) = self.password.as_ref() { easy.key_password(password)?; } diff --git a/src/lib.rs b/src/lib.rs index 8795881a..ae689ceb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -171,6 +171,11 @@ //! Additional serialization and deserialization of JSON bodies via //! [serde](https://serde.rs). Disabled by default. //! +//! ## `pkcs11` +//! +//! Enable certificate and private key configuration for PKCS#11 token +//! usage. Disabled by default. +//! //! ## `psl` //! //! Enable use of the Public Suffix List to filter out potentially malicious