diff --git a/Cargo.lock b/Cargo.lock index 193715c..d707b92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -640,7 +640,7 @@ dependencies = [ [[package]] name = "instant-acme" -version = "0.8.5" +version = "0.9.0" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 49b3a08..4e5b206 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "instant-acme" -version = "0.8.5" +version = "0.9.0" edition = "2021" rust-version = "1.70" license = "Apache-2.0" @@ -55,7 +55,11 @@ tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } [[example]] name = "provision" -required-features = ["hyper-rustls", "rcgen"] +required-features = ["hyper-rustls", "rcgen", "aws-lc-rs"] + +[[example]] +name = "certgen" +required-features = ["rcgen", "aws-lc-rs"] # Pebble integration test. # Ignored by default because it requires pebble & pebble-challtestsrv. @@ -63,7 +67,7 @@ required-features = ["hyper-rustls", "rcgen"] # PEBBLE=path/to/pebble CHALLTESTSRV=path/to/pebble-challtestsrv cargo test -- --ignored [[test]] name = "pebble" -required-features = ["hyper-rustls"] +required-features = ["hyper-rustls", "rcgen"] [package.metadata.docs.rs] # all non-default features except fips (cannot build on docs.rs environment) @@ -76,6 +80,7 @@ allowed_external_types = [ "http::*", "http_body::*", "hyper::*", + "rustls::crypto::CryptoProvider", "rustls_pki_types::*", "serde_core::*", "serde_json::*", diff --git a/examples/provision.rs b/examples/provision.rs index 9f92a43..a90022b 100644 --- a/examples/provision.rs +++ b/examples/provision.rs @@ -1,11 +1,13 @@ +#![cfg(any(feature = "ring", feature = "aws-lc-rs"))] use std::io; use clap::Parser; +use rustls::crypto::CryptoProvider as RustlsCryptoProvider; use tracing::info; use instant_acme::{ - Account, AuthorizationStatus, ChallengeType, Identifier, LetsEncrypt, NewAccount, NewOrder, - OrderStatus, RetryPolicy, + Account, AuthorizationStatus, ChallengeType, CryptoProvider, Identifier, LetsEncrypt, + NewAccount, NewOrder, OrderStatus, RetryPolicy, }; #[tokio::main] @@ -17,7 +19,13 @@ async fn main() -> anyhow::Result<()> { // Alternatively, restore an account from serialized credentials by // using `Account::from_credentials()`. - let (account, credentials) = Account::builder()? + #[cfg(feature = "aws-lc-rs")] + let provider = CryptoProvider::aws_lc_rs(); + + #[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))] + let provider = CryptoProvider::ring(); + + let (account, credentials) = Account::builder(provider, rustls_crypto_provider())? .create( &NewAccount { contact: &[], @@ -71,7 +79,7 @@ async fn main() -> anyhow::Result<()> { println!( "_acme-challenge.{} IN TXT {}", challenge.identifier(), - challenge.key_authorization().dns_value() + challenge.key_authorization()?.dns_value() ); io::stdin().read_line(&mut String::new())?; @@ -100,3 +108,13 @@ struct Options { #[clap(long)] names: Vec, } + +#[cfg(feature = "aws-lc-rs")] +fn rustls_crypto_provider() -> RustlsCryptoProvider { + rustls::crypto::aws_lc_rs::default_provider() +} + +#[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))] +fn rustls_crypto_provider() -> RustlsCryptoProvider { + rustls::crypto::ring::default_provider() +} diff --git a/src/account.rs b/src/account.rs index a02d37b..8ea8b80 100644 --- a/src/account.rs +++ b/src/account.rs @@ -1,6 +1,7 @@ #[cfg(feature = "hyper-rustls")] use std::path::Path; use std::sync::Arc; + #[cfg(feature = "time")] use std::time::{Duration, SystemTime}; @@ -11,17 +12,16 @@ use http::header::USER_AGENT; #[cfg(feature = "time")] use http::{Method, Request}; #[cfg(feature = "hyper-rustls")] -use rustls::RootCertStore; -#[cfg(feature = "hyper-rustls")] -use rustls_pki_types::CertificateDer; +use rustls::{RootCertStore, crypto::CryptoProvider as RustlsCryptoProvider}; #[cfg(feature = "hyper-rustls")] -use rustls_pki_types::pem::PemObject; +use rustls_pki_types::{CertificateDer, pem::PemObject}; use rustls_pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; #[cfg(feature = "hyper-rustls")] use crate::DefaultClient; +use crate::crypto::{CryptoProvider, SigningKey}; use crate::order::Order; use crate::types::{ AccountCredentials, AuthorizationStatus, Empty, Header, JoseJson, Jwk, KeyOrKeyId, NewAccount, @@ -32,7 +32,7 @@ use crate::types::{ use crate::types::{CertificateIdentifier, RenewalInfo}; #[cfg(feature = "time")] use crate::{BodyWrapper, CRATE_USER_AGENT, retry_after}; -use crate::{BytesResponse, Client, Error, HttpClient, crypto, nonce_from_response}; +use crate::{BytesResponse, Client, Error, HmacSha256, HttpClient, nonce_from_response}; /// An ACME account as described in RFC 8555 (section 7.1.2) /// @@ -50,11 +50,15 @@ pub struct Account { } impl Account { - /// Create an account builder with the default HTTP client + /// Create an account builder with the given [`CryptoProvider`] and default HTTP client #[cfg(feature = "hyper-rustls")] - pub fn builder() -> Result { + pub fn builder( + provider: &'static CryptoProvider, + rustls_crypto_provider: RustlsCryptoProvider, + ) -> Result { Ok(AccountBuilder { - http: Box::new(DefaultClient::try_new()?), + http: Box::new(DefaultClient::try_new(rustls_crypto_provider)?), + provider, }) } @@ -63,7 +67,11 @@ impl Account { /// This is useful if your ACME server uses a testing PKI and not a certificate /// chain issued by a publicly trusted CA. #[cfg(feature = "hyper-rustls")] - pub fn builder_with_root(pem_path: impl AsRef) -> Result { + pub fn builder_with_root( + pem_path: impl AsRef, + provider: &'static CryptoProvider, + rustls_crypto_provider: RustlsCryptoProvider, + ) -> Result { let root_der = match CertificateDer::from_pem_file(pem_path) { Ok(root_der) => root_der, Err(err) => return Err(Error::Other(err.into())), @@ -72,15 +80,19 @@ impl Account { let mut roots = RootCertStore::empty(); match roots.add(root_der) { Ok(()) => Ok(AccountBuilder { - http: Box::new(DefaultClient::with_roots(roots)?), + http: Box::new(DefaultClient::with_roots(roots, rustls_crypto_provider)?), + provider, }), Err(err) => Err(Error::Other(err.into())), } } - /// Create an account builder with the given HTTP client - pub fn builder_with_http(http: Box) -> AccountBuilder { - AccountBuilder { http } + /// Create an account builder with the given [`CryptoProvider`] and HTTP client + pub fn builder_with_http( + http: Box, + provider: &'static CryptoProvider, + ) -> AccountBuilder { + AccountBuilder { http, provider } } /// Create a new order based on the given [`NewOrder`] @@ -210,15 +222,31 @@ impl Account { Ok((Problem::check::(rsp).await?, delay)) } - /// Update the account's authentication key + /// Update the account's authentication key, reusing the existing [`CryptoProvider`] /// /// This is useful if you want to change the ACME account key of an existing account, e.g. /// to mitigate the risk of a key compromise. This method creates a new client key and changes /// the key associated with the existing account. `self` will be updated with the new key, /// and a fresh set of [`AccountCredentials`] will be returned to update stored credentials. /// + /// To change the key type (e.g. from P-256 to Ed25519), use + /// [`update_key_with_provider()`][Self::update_key_with_provider] instead. + /// /// See for more information. pub async fn update_key(&mut self) -> Result { + self.update_key_with_provider(self.inner.key.provider).await + } + + /// Update the account's authentication key using the given [`CryptoProvider`] + /// + /// Like [`update_key()`][Self::update_key], but allows switching to a different key type + /// by supplying a different [`CryptoProvider`] (e.g. migrating from P-256 to Ed25519). + /// + /// See for more information. + pub async fn update_key_with_provider( + &mut self, + provider: &'static CryptoProvider, + ) -> Result { let Some(new_key_url) = self.inner.client.directory.key_change.as_deref() else { return Err("Account key rollover not supported by ACME CA".into()); }; @@ -227,15 +255,15 @@ impl Account { struct NewKey<'a> { account: &'a str, #[serde(rename = "oldKey")] - old_key: Jwk, + old_key: Jwk<'a>, } - let (new_key, new_key_pkcs8) = Key::generate_pkcs8()?; + let (new_key, new_key_pkcs8) = Key::generate(provider)?; let mut header = new_key.header(Some("nonce"), new_key_url); header.nonce = None; let payload = NewKey { account: &self.inner.id, - old_key: Jwk::new(&self.inner.key.inner), + old_key: self.inner.key.inner.as_jwk(), }; let body = JoseJson::new(Some(&payload), header, &new_key)?; @@ -338,8 +366,8 @@ impl Account { } /// Get the [RFC 7638](https://www.rfc-editor.org/rfc/rfc7638) account key thumbprint - pub fn key_thumbprint(&self) -> &str { - &self.inner.key.thumb + pub fn key_thumbprint(&self) -> Result { + Ok(BASE64_URL_SAFE_NO_PAD.encode(self.inner.key.thumb_sha256()?)) } } @@ -353,10 +381,11 @@ impl AccountInner { async fn from_credentials( credentials: AccountCredentials, http: Box, + provider: &'static CryptoProvider, ) -> Result { Ok(Self { id: credentials.id, - key: Key::from_pkcs8_der(credentials.key_pkcs8)?, + key: Key::from_pkcs8_der(credentials.key_pkcs8, provider)?, client: Arc::new(match (credentials.directory, credentials.urls) { (Some(directory_url), _) => Client::new(directory_url, http).await?, (None, Some(directory)) => Client { @@ -390,12 +419,12 @@ impl AccountInner { } impl Signer for AccountInner { - type Signature = ::Signature; + type Signature = Vec; fn header<'n, 'u: 'n, 's: 'u>(&'s self, nonce: Option<&'n str>, url: &'u str) -> Header<'n> { debug_assert!(nonce.is_some()); Header { - alg: self.key.signing_algorithm, + alg: self.key.inner.jws_algorithm(), key: KeyOrKeyId::KeyId(&self.id), nonce, url, @@ -403,7 +432,7 @@ impl Signer for AccountInner { } fn sign(&self, payload: &[u8]) -> Result { - self.key.sign(payload) + self.key.inner.sign(payload) } } @@ -412,6 +441,7 @@ impl Signer for AccountInner { /// Create one via [`Account::builder()`] or [`Account::builder_with_http()`]. pub struct AccountBuilder { http: Box, + provider: &'static CryptoProvider, } impl AccountBuilder { @@ -421,7 +451,9 @@ impl AccountBuilder { #[allow(clippy::wrong_self_convention)] pub async fn from_credentials(self, credentials: AccountCredentials) -> Result { Ok(Account { - inner: Arc::new(AccountInner::from_credentials(credentials, self.http).await?), + inner: Arc::new( + AccountInner::from_credentials(credentials, self.http, self.provider).await?, + ), }) } @@ -435,7 +467,7 @@ impl AccountBuilder { directory_url: String, external_account: Option<&ExternalAccountKey>, ) -> Result<(Account, AccountCredentials), Error> { - let (key, key_pkcs8) = Key::generate_pkcs8()?; + let (key, key_pkcs8) = Key::generate(self.provider)?; Self::create_inner( account, (key, key_pkcs8), @@ -512,7 +544,7 @@ impl AccountBuilder { Ok(Account { inner: Arc::new(AccountInner { id, - key: Key::from_pkcs8_der(key_pkcs8_der)?, + key: Key::from_pkcs8_der(key_pkcs8_der, self.provider)?, client: Arc::new(Client::new(directory_url, self.http).await?), }), }) @@ -529,7 +561,7 @@ impl AccountBuilder { external_account_binding: external_account .map(|eak| { JoseJson::new( - Some(&Jwk::new(&key.inner)), + Some(&key.inner.as_jwk()), eak.header(None, &client.directory.new_account), eak, ) @@ -577,68 +609,64 @@ impl AccountBuilder { /// Private account key used to sign requests pub struct Key { - rng: crypto::SystemRandom, - signing_algorithm: SigningAlgorithm, - inner: crypto::EcdsaKeyPair, - pub(crate) thumb: String, + pub(crate) inner: Box, + pub(crate) provider: &'static CryptoProvider, } impl Key { - /// Generate a new ECDSA P-256 key pair - #[deprecated(since = "0.8.3", note = "use `generate_pkcs8()` instead")] - pub fn generate() -> Result<(Self, PrivateKeyDer<'static>), Error> { - let (key, pkcs8) = Self::generate_pkcs8()?; - Ok((key, PrivateKeyDer::Pkcs8(pkcs8))) - } - - /// Generate a new ECDSA P-256 key pair - pub fn generate_pkcs8() -> Result<(Self, PrivatePkcs8KeyDer<'static>), Error> { - let rng = crypto::SystemRandom::new(); - let pkcs8 = - crypto::EcdsaKeyPair::generate_pkcs8(&crypto::ECDSA_P256_SHA256_FIXED_SIGNING, &rng) - .map_err(|_| Error::Crypto)?; + /// Generate a new key pair using the given [`CryptoProvider`] + /// + /// The key type depends on the provider. + pub fn generate( + provider: &'static CryptoProvider, + ) -> Result<(Self, PrivatePkcs8KeyDer<'static>), Error> { + let (key, pkcs8) = provider.signing_key.generate_key()?; Ok(( - Self::new(pkcs8.as_ref(), rng)?, - PrivatePkcs8KeyDer::from(pkcs8.as_ref().to_vec()), + Self { + inner: key, + provider, + }, + pkcs8, )) } - /// Create a new key from the given PKCS#8 DER-encoded private key - /// - /// Currently, only ECDSA P-256 keys are supported. - pub fn from_pkcs8_der(pkcs8_der: PrivatePkcs8KeyDer<'_>) -> Result { - Self::new(pkcs8_der.secret_pkcs8_der(), crypto::SystemRandom::new()) + pub(crate) fn thumb_sha256(&self) -> Result<[u8; 32], Error> { + self.inner + .as_jwk() + .thumb_sha256(self.provider.sha256) + .map_err(Error::Json) } - fn new(pkcs8_der: &[u8], rng: crypto::SystemRandom) -> Result { - let inner = crypto::p256_key_pair_from_pkcs8(pkcs8_der, &rng)?; - let thumb = BASE64_URL_SAFE_NO_PAD.encode(Jwk::thumb_sha256(&inner)?); + /// Create a key from the given PKCS#8 DER-encoded private key using the given + /// [`CryptoProvider`] + pub fn from_pkcs8_der( + pkcs8_der: PrivatePkcs8KeyDer<'_>, + provider: &'static CryptoProvider, + ) -> Result { + let owned = PrivatePkcs8KeyDer::from(pkcs8_der.secret_pkcs8_der().to_vec()); + let key = provider.signing_key.load_key(owned)?; Ok(Self { - rng, - signing_algorithm: SigningAlgorithm::Es256, - inner, - thumb, + inner: key, + provider, }) } } impl Signer for Key { - type Signature = crypto::Signature; + type Signature = Vec; fn header<'n, 'u: 'n, 's: 'u>(&'s self, nonce: Option<&'n str>, url: &'u str) -> Header<'n> { debug_assert!(nonce.is_some()); Header { - alg: self.signing_algorithm, - key: KeyOrKeyId::from_key(&self.inner), + alg: self.inner.jws_algorithm(), + key: KeyOrKeyId::Key(self.inner.as_jwk()), nonce, url, } } fn sign(&self, payload: &[u8]) -> Result { - self.inner - .sign(&self.rng, payload) - .map_err(|_| Error::Crypto) + self.inner.sign(payload) } } @@ -647,24 +675,26 @@ impl Signer for Key { /// See RFC 8555 section 7.3.4 for more information. pub struct ExternalAccountKey { id: String, - key: crypto::hmac::Key, + key_value: Vec, + hmac_sha256: &'static dyn HmacSha256, } impl ExternalAccountKey { - /// Create a new external account key + /// Create a new external account key using the given [`CryptoProvider`] /// /// Note that the `key_value` argument represents the raw key value, so if the caller holds /// an encoded key value (for example, using base64), decode it before passing it in. - pub fn new(id: String, key_value: &[u8]) -> Self { + pub fn new(id: String, key_value: &[u8], provider: &'static CryptoProvider) -> Self { Self { id, - key: crypto::hmac::Key::new(crypto::hmac::HMAC_SHA256, key_value), + key_value: key_value.to_vec(), + hmac_sha256: provider.hmac_sha256, } } } impl Signer for ExternalAccountKey { - type Signature = crypto::hmac::Tag; + type Signature = [u8; 32]; fn header<'n, 'u: 'n, 's: 'u>(&'s self, nonce: Option<&'n str>, url: &'u str) -> Header<'n> { debug_assert_eq!(nonce, None); @@ -677,6 +707,6 @@ impl Signer for ExternalAccountKey { } fn sign(&self, payload: &[u8]) -> Result { - Ok(crypto::hmac::sign(&self.key, payload)) + Ok(self.hmac_sha256.sign(&self.key_value, payload)) } } diff --git a/src/crypto.rs b/src/crypto.rs new file mode 100644 index 0000000..498f956 --- /dev/null +++ b/src/crypto.rs @@ -0,0 +1,107 @@ +use std::fmt; + +use rustls_pki_types::PrivatePkcs8KeyDer; + +use crate::Error; +use crate::types::{Jwk, SigningAlgorithm}; + +#[cfg(feature = "aws-lc-rs")] +mod aws_lc_rs; + +#[cfg(feature = "ring")] +mod ring; + +/// Cryptographic provider for ACME operations. +/// +/// Use [`CryptoProvider::aws_lc_rs()`] or [`CryptoProvider::ring()`] for the built-in backend, +/// or populate the fields manually for a custom backend. +pub struct CryptoProvider { + /// Load and generate signing keys. + pub signing_key: &'static dyn SigningKeyProvider, + /// SHA-256 hash for ACME protocol operations. + /// + /// Used for JWK thumbprints ([RFC 7638]) and challenge digests ([RFC 8555 section 8.1]). + /// This is independent of the signing algorithm used for account keys. + /// + /// [RFC 7638]: https://www.rfc-editor.org/rfc/rfc7638 + /// [RFC 8555 section 8.1]: https://www.rfc-editor.org/rfc/rfc8555#section-8.1 + pub sha256: &'static dyn Sha256, + /// HMAC-SHA-256 for External Account Binding. + /// + /// See [RFC 8555 section 7.3.4]. + /// + /// [RFC 8555 section 7.3.4]: https://www.rfc-editor.org/rfc/rfc8555#section-7.3.4 + pub hmac_sha256: &'static dyn HmacSha256, +} + +impl CryptoProvider { + /// A `CryptoProvider` using ECDSA P-256 account keys backed by aws-lc-rs. + #[cfg(feature = "aws-lc-rs")] + pub fn aws_lc_rs() -> &'static Self { + aws_lc_rs::PROVIDER + } + + /// A `CryptoProvider` using ECDSA P-256 account keys backed by ring. + #[cfg(feature = "ring")] + pub fn ring() -> &'static Self { + ring::PROVIDER + } +} + +impl fmt::Debug for CryptoProvider { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("CryptoProvider").finish_non_exhaustive() + } +} + +/// Load existing account keys and generate new ones. +/// +/// Implementations are backend-specific (ring, aws-lc-rs, openssl, etc.) +/// and key-type-specific (P-256, Ed25519, RSA, etc.). +pub trait SigningKeyProvider: Send + Sync { + /// Load a signing key from PKCS#8 DER encoding. + fn load_key(&self, pkcs8: PrivatePkcs8KeyDer<'static>) -> Result, Error>; + + /// Generate a new key pair, returning the key and its PKCS#8 DER encoding. + fn generate_key(&self) -> Result<(Box, PrivatePkcs8KeyDer<'static>), Error>; +} + +/// A signing key for ACME account operations. +/// +/// Bundles signing, JWS algorithm identification, and JWK serialization. +/// Implement this trait to support any key type (P-256, P-384, Ed25519, RSA, etc.) +/// without changes to instant-acme. +pub trait SigningKey: Send + Sync { + /// Sign the given data using this key's algorithm. + /// + /// The implementation handles hashing internally where required + /// (e.g., SHA-256 for ES256, SHA-512 for Ed25519). + fn sign(&self, data: &[u8]) -> Result, Error>; + + /// The JWS `alg` header value (e.g., `ES256`). + fn jws_algorithm(&self) -> SigningAlgorithm; + + /// Serialize the public key as a JWK JSON object for the `jwk` JWS header. + fn as_jwk(&self) -> Jwk<'_>; +} + +/// SHA-256 hash function for ACME protocol operations. +/// +/// Used for JWK thumbprints ([RFC 7638]) and challenge digests ([RFC 8555]). +/// +/// [RFC 7638]: https://www.rfc-editor.org/rfc/rfc7638 +/// [RFC 8555]: https://www.rfc-editor.org/rfc/rfc8555 +pub trait Sha256: Send + Sync { + /// Compute the SHA-256 digest of the given data. + fn hash(&self, data: &[u8]) -> [u8; 32]; +} + +/// HMAC-SHA-256 for ACME External Account Binding. +/// +/// See [RFC 8555 section 7.3.4]. +/// +/// [RFC 8555 section 7.3.4]: https://www.rfc-editor.org/rfc/rfc8555#section-7.3.4 +pub trait HmacSha256: Send + Sync { + /// Compute HMAC-SHA-256 of `data` using the given `key`. + fn sign(&self, key: &[u8], data: &[u8]) -> [u8; 32]; +} diff --git a/src/crypto/aws_lc_rs.rs b/src/crypto/aws_lc_rs.rs new file mode 100644 index 0000000..e0a91c4 --- /dev/null +++ b/src/crypto/aws_lc_rs.rs @@ -0,0 +1,92 @@ +use rustls_pki_types::PrivatePkcs8KeyDer; + +use aws_lc_rs::rand::SystemRandom; +use aws_lc_rs::signature::{ECDSA_P256_SHA256_FIXED_SIGNING, EcdsaKeyPair, KeyPair}; +use aws_lc_rs::{digest, hmac}; + +use super::{HmacSha256, Sha256, SigningKey, SigningKeyProvider}; +use crate::Error; +use crate::types::{Jwk, JwkThumbFields, SigningAlgorithm}; + +pub(crate) static PROVIDER: &super::CryptoProvider = &super::CryptoProvider { + signing_key: &P256SigningKeyProvider, + sha256: &BuiltinSha256, + hmac_sha256: &BuiltinHmacSha256, +}; + +struct P256SigningKeyProvider; + +impl SigningKeyProvider for P256SigningKeyProvider { + fn load_key(&self, pkcs8: PrivatePkcs8KeyDer<'static>) -> Result, Error> { + let rng = SystemRandom::new(); + let key_pair = + EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.secret_pkcs8_der()) + .map_err(|_| Error::KeyRejected)?; + Ok(Box::new(P256Key { key_pair, rng })) + } + + fn generate_key(&self) -> Result<(Box, PrivatePkcs8KeyDer<'static>), Error> { + let rng = SystemRandom::new(); + let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng) + .map_err(|_| Error::Crypto)?; + let pkcs8_der = PrivatePkcs8KeyDer::from(pkcs8.as_ref().to_vec()); + let key_pair = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref()) + .map_err(|_| Error::KeyRejected)?; + Ok((Box::new(P256Key { key_pair, rng }), pkcs8_der)) + } +} + +struct P256Key { + key_pair: EcdsaKeyPair, + rng: SystemRandom, +} + +impl SigningKey for P256Key { + fn sign(&self, data: &[u8]) -> Result, Error> { + self.key_pair + .sign(&self.rng, data) + .map(|sig| sig.as_ref().to_vec()) + .map_err(|_| Error::Crypto) + } + + fn jws_algorithm(&self) -> SigningAlgorithm { + SigningAlgorithm::Es256 + } + + fn as_jwk(&self) -> Jwk<'_> { + let (x, y) = self.key_pair.public_key().as_ref()[1..].split_at(32); + Jwk { + alg: SigningAlgorithm::Es256, + key: JwkThumbFields::Ec { + crv: "P-256", + kty: "EC", + x, + y, + }, + r#use: "sig", + } + } +} + +struct BuiltinSha256; + +impl Sha256 for BuiltinSha256 { + fn hash(&self, data: &[u8]) -> [u8; 32] { + digest::digest(&digest::SHA256, data) + .as_ref() + .try_into() + .expect("SHA-256 output is always 32 bytes") + } +} + +struct BuiltinHmacSha256; + +impl HmacSha256 for BuiltinHmacSha256 { + fn sign(&self, key: &[u8], data: &[u8]) -> [u8; 32] { + let hmac_key = hmac::Key::new(hmac::HMAC_SHA256, key); + hmac::sign(&hmac_key, data) + .as_ref() + .try_into() + .expect("HMAC-SHA-256 output is always 32 bytes") + } +} diff --git a/src/crypto/ring.rs b/src/crypto/ring.rs new file mode 100644 index 0000000..b5efb97 --- /dev/null +++ b/src/crypto/ring.rs @@ -0,0 +1,96 @@ +use rustls_pki_types::PrivatePkcs8KeyDer; + +use ring::rand::SystemRandom; +use ring::signature::{ECDSA_P256_SHA256_FIXED_SIGNING, EcdsaKeyPair, KeyPair}; +use ring::{digest, hmac}; + +use super::{HmacSha256, Sha256, SigningKey, SigningKeyProvider}; +use crate::Error; +use crate::types::{Jwk, JwkThumbFields, SigningAlgorithm}; + +pub(crate) static PROVIDER: &super::CryptoProvider = &super::CryptoProvider { + signing_key: &P256SigningKeyProvider, + sha256: &BuiltinSha256, + hmac_sha256: &BuiltinHmacSha256, +}; + +struct P256SigningKeyProvider; + +impl SigningKeyProvider for P256SigningKeyProvider { + fn load_key(&self, pkcs8: PrivatePkcs8KeyDer<'static>) -> Result, Error> { + let rng = SystemRandom::new(); + let key_pair = EcdsaKeyPair::from_pkcs8( + &ECDSA_P256_SHA256_FIXED_SIGNING, + pkcs8.secret_pkcs8_der(), + &rng, + ) + .map_err(|_| Error::KeyRejected)?; + Ok(Box::new(P256Key { key_pair, rng })) + } + + fn generate_key(&self) -> Result<(Box, PrivatePkcs8KeyDer<'static>), Error> { + let rng = SystemRandom::new(); + let pkcs8 = EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng) + .map_err(|_| Error::Crypto)?; + let pkcs8_der = PrivatePkcs8KeyDer::from(pkcs8.as_ref().to_vec()); + let key_pair = + EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8.as_ref(), &rng) + .map_err(|_| Error::KeyRejected)?; + Ok((Box::new(P256Key { key_pair, rng }), pkcs8_der)) + } +} + +struct P256Key { + key_pair: EcdsaKeyPair, + rng: SystemRandom, +} + +impl SigningKey for P256Key { + fn sign(&self, data: &[u8]) -> Result, Error> { + self.key_pair + .sign(&self.rng, data) + .map(|sig| sig.as_ref().to_vec()) + .map_err(|_| Error::Crypto) + } + + fn jws_algorithm(&self) -> SigningAlgorithm { + SigningAlgorithm::Es256 + } + + fn as_jwk(&self) -> Jwk<'_> { + let (x, y) = self.key_pair.public_key().as_ref()[1..].split_at(32); + Jwk { + alg: SigningAlgorithm::Es256, + key: JwkThumbFields::Ec { + crv: "P-256", + kty: "EC", + x, + y, + }, + r#use: "sig", + } + } +} + +struct BuiltinSha256; + +impl Sha256 for BuiltinSha256 { + fn hash(&self, data: &[u8]) -> [u8; 32] { + digest::digest(&digest::SHA256, data) + .as_ref() + .try_into() + .expect("SHA-256 output is always 32 bytes") + } +} + +struct BuiltinHmacSha256; + +impl HmacSha256 for BuiltinHmacSha256 { + fn sign(&self, key: &[u8], data: &[u8]) -> [u8; 32] { + let hmac_key = hmac::Key::new(hmac::HMAC_SHA256, key); + hmac::sign(&hmac_key, data) + .as_ref() + .try_into() + .expect("HMAC-SHA-256 output is always 32 bytes") + } +} diff --git a/src/lib.rs b/src/lib.rs index fee784d..a711c77 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,8 @@ use std::fmt; use std::future::Future; use std::pin::Pin; use std::str::FromStr; +#[cfg(feature = "hyper-rustls")] +use std::sync::Arc; use std::task::{Context, Poll}; use std::time::{Duration, SystemTime}; @@ -22,20 +24,24 @@ use httpdate::HttpDate; #[cfg(feature = "hyper-rustls")] use hyper::body::Incoming; #[cfg(feature = "hyper-rustls")] -use hyper_rustls::HttpsConnectorBuilder; -#[cfg(feature = "hyper-rustls")] -use hyper_rustls::builderstates::WantsSchemes; +use hyper_rustls::{HttpsConnectorBuilder, builderstates::WantsSchemes}; #[cfg(feature = "hyper-rustls")] -use hyper_util::client::legacy::Client as HyperClient; -#[cfg(feature = "hyper-rustls")] -use hyper_util::client::legacy::connect::{Connect, HttpConnector}; +use hyper_util::client::legacy::{ + Client as HyperClient, + connect::{Connect, HttpConnector}, +}; #[cfg(feature = "hyper-rustls")] use hyper_util::rt::TokioExecutor; +#[cfg(feature = "hyper-rustls")] +use rustls::crypto::CryptoProvider as RustlsCryptoProvider; use serde::Serialize; mod account; pub use account::Key; pub use account::{Account, AccountBuilder, ExternalAccountKey}; +mod crypto; +pub use crypto::{CryptoProvider, HmacSha256, Sha256, SigningKey, SigningKeyProvider}; +pub use types::{Jwk, JwkThumbFields, SigningAlgorithm}; mod order; pub use order::{ AuthorizationHandle, Authorizations, ChallengeHandle, Identifiers, KeyAuthorization, Order, @@ -200,18 +206,23 @@ struct DefaultClient(HyperClient, Bo #[cfg(feature = "hyper-rustls")] impl DefaultClient { - fn try_new() -> Result { + fn try_new(rustls_crypto_provider: RustlsCryptoProvider) -> Result { Ok(Self::new( HttpsConnectorBuilder::new() - .try_with_platform_verifier() + .with_provider_and_platform_verifier(rustls_crypto_provider) .map_err(|e| Error::Other(Box::new(e)))?, )) } - fn with_roots(roots: rustls::RootCertStore) -> Result { + fn with_roots( + roots: rustls::RootCertStore, + rustls_crypto_provider: RustlsCryptoProvider, + ) -> Result { Ok(Self::new( HttpsConnectorBuilder::new().with_tls_config( - rustls::ClientConfig::builder() + rustls::ClientConfig::builder_with_provider(Arc::new(rustls_crypto_provider)) + .with_safe_default_protocol_versions() + .map_err(|e| Error::Other(Box::new(e)))? .with_root_certificates(roots) .with_no_client_auth(), ), @@ -369,51 +380,24 @@ pub trait BytesBody: Send { async fn into_bytes(&mut self) -> Result>; } -mod crypto { - #[cfg(feature = "aws-lc-rs")] - pub(crate) use aws_lc_rs as ring_like; - #[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))] - pub(crate) use ring as ring_like; - - pub(crate) use ring_like::digest::{Digest, SHA256, digest}; - pub(crate) use ring_like::hmac; - pub(crate) use ring_like::rand::SystemRandom; - pub(crate) use ring_like::signature::{ECDSA_P256_SHA256_FIXED_SIGNING, EcdsaKeyPair}; - pub(crate) use ring_like::signature::{KeyPair, Signature}; - - use super::Error; - - #[cfg(feature = "aws-lc-rs")] - pub(crate) fn p256_key_pair_from_pkcs8( - pkcs8: &[u8], - _: &SystemRandom, - ) -> Result { - EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8) - .map_err(|_| Error::KeyRejected) - } - - #[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))] - pub(crate) fn p256_key_pair_from_pkcs8( - pkcs8: &[u8], - rng: &SystemRandom, - ) -> Result { - EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, pkcs8, rng) - .map_err(|_| Error::KeyRejected) - } -} - const CRATE_USER_AGENT: &str = concat!("instant-acme/", env!("CARGO_PKG_VERSION")); const JOSE_JSON: &str = "application/jose+json"; const REPLAY_NONCE: &str = "Replay-Nonce"; -#[cfg(all(test, feature = "hyper-rustls"))] +#[cfg(all( + test, + feature = "hyper-rustls", + any(feature = "aws-lc-rs", feature = "ring") +))] mod tests { + use rustls::crypto::CryptoProvider as RustlsCryptoProvider; + use super::*; #[tokio::test] async fn deserialize_old_credentials() -> Result<(), Error> { const CREDENTIALS: &str = r#"{"id":"id","key_pkcs8":"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgJVWC_QzOTCS5vtsJp2IG-UDc8cdDfeoKtxSZxaznM-mhRANCAAQenCPoGgPFTdPJ7VLLKt56RxPlYT1wNXnHc54PEyBg3LxKaH0-sJkX0mL8LyPEdsfL_Oz4TxHkWLJGrXVtNhfH","urls":{"newNonce":"new-nonce","newAccount":"new-acct","newOrder":"new-order", "revokeCert": "revoke-cert"}}"#; - Account::builder()? + Account::builder(crypto_provider(), rustls_crypto_provider())? .from_credentials(serde_json::from_str::(CREDENTIALS)?) .await?; Ok(()) @@ -422,9 +406,29 @@ mod tests { #[tokio::test] async fn deserialize_new_credentials() -> Result<(), Error> { const CREDENTIALS: &str = r#"{"id":"id","key_pkcs8":"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgJVWC_QzOTCS5vtsJp2IG-UDc8cdDfeoKtxSZxaznM-mhRANCAAQenCPoGgPFTdPJ7VLLKt56RxPlYT1wNXnHc54PEyBg3LxKaH0-sJkX0mL8LyPEdsfL_Oz4TxHkWLJGrXVtNhfH","directory":"https://acme-staging-v02.api.letsencrypt.org/directory"}"#; - Account::builder()? + Account::builder(crypto_provider(), rustls_crypto_provider())? .from_credentials(serde_json::from_str::(CREDENTIALS)?) .await?; Ok(()) } + + #[cfg(feature = "aws-lc-rs")] + fn rustls_crypto_provider() -> RustlsCryptoProvider { + rustls::crypto::aws_lc_rs::default_provider() + } + + #[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))] + fn rustls_crypto_provider() -> RustlsCryptoProvider { + rustls::crypto::ring::default_provider() + } + + #[cfg(feature = "aws-lc-rs")] + fn crypto_provider() -> &'static CryptoProvider { + CryptoProvider::aws_lc_rs() + } + + #[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))] + fn crypto_provider() -> &'static CryptoProvider { + CryptoProvider::ring() + } } diff --git a/src/order.rs b/src/order.rs index 970e2bf..2aa8190 100644 --- a/src/order.rs +++ b/src/order.rs @@ -5,7 +5,7 @@ use std::time::{Duration, Instant, SystemTime}; use std::{fmt, slice}; use base64::prelude::{BASE64_URL_SAFE_NO_PAD, Engine}; -#[cfg(feature = "rcgen")] +#[cfg(all(feature = "rcgen", any(feature = "aws-lc-rs", feature = "ring")))] use rcgen::{CertificateParams, DistinguishedName, KeyPair}; use serde::Serialize; use tokio::time::sleep; @@ -15,7 +15,7 @@ use crate::types::{ Authorization, AuthorizationState, AuthorizationStatus, AuthorizedIdentifier, Challenge, ChallengeType, DeviceAttestation, Empty, FinalizeRequest, OrderState, OrderStatus, Problem, }; -use crate::{ChallengeStatus, Error, Key, crypto, nonce_from_response, retry_after}; +use crate::{ChallengeStatus, Error, Key, Sha256, nonce_from_response, retry_after}; /// An ACME order as described in RFC 8555 (section 7.1.3) /// @@ -61,7 +61,7 @@ impl Order { /// /// After this succeeds, call [`Order::certificate()`] to retrieve the certificate chain once /// the order is in the appropriate state. - #[cfg(feature = "rcgen")] + #[cfg(all(feature = "rcgen", any(feature = "aws-lc-rs", feature = "ring")))] pub async fn finalize(&mut self) -> Result { let mut names = Vec::with_capacity(self.state.authorizations.len()); let mut identifiers = self.identifiers(); @@ -503,7 +503,7 @@ impl ChallengeHandle<'_> { /// Combines a challenge's token with the thumbprint of the account's public key to compute /// the challenge's `KeyAuthorization`. The `KeyAuthorization` must be used to provision the /// expected challenge response based on the challenge type in use. - pub fn key_authorization(&self) -> KeyAuthorization { + pub fn key_authorization(&self) -> Result { KeyAuthorization::new(self.challenge, &self.account.key) } @@ -526,18 +526,28 @@ impl Deref for ChallengeHandle<'_> { /// Refer to the methods below to see which encoding to use for your challenge type. /// /// -pub struct KeyAuthorization(String); +pub struct KeyAuthorization { + inner: String, + sha256: &'static dyn Sha256, +} impl KeyAuthorization { - fn new(challenge: &Challenge, key: &Key) -> Self { - Self(format!("{}.{}", challenge.token, &key.thumb)) + fn new(challenge: &Challenge, key: &Key) -> Result { + Ok(Self { + inner: format!( + "{}.{}", + challenge.token, + BASE64_URL_SAFE_NO_PAD.encode(key.thumb_sha256()?) + ), + sha256: key.provider.sha256, + }) } /// Get the key authorization value /// /// This can be used for HTTP-01 challenge responses. pub fn as_str(&self) -> &str { - &self.0 + &self.inner } /// Get the SHA-256 digest of the key authorization @@ -545,15 +555,16 @@ impl KeyAuthorization { /// This can be used for TLS-ALPN-01 challenge responses. /// /// - pub fn digest(&self) -> impl AsRef<[u8]> { - crypto::digest(&crypto::SHA256, self.0.as_bytes()) + pub fn digest(&self) -> [u8; 32] { + self.sha256.hash(self.inner.as_bytes()) } /// Get the base64-encoded SHA256 digest of the key authorization /// /// This can be used for DNS-01 challenge responses. pub fn dns_value(&self) -> String { - BASE64_URL_SAFE_NO_PAD.encode(self.digest()) + let digest = self.digest(); + BASE64_URL_SAFE_NO_PAD.encode(digest) } } diff --git a/src/types.rs b/src/types.rs index eeb331d..45c4294 100644 --- a/src/types.rs +++ b/src/types.rs @@ -18,8 +18,7 @@ use x509_parser::extensions::ParsedExtension; #[cfg(feature = "x509-parser")] use x509_parser::parse_x509_certificate; -use crate::BytesResponse; -use crate::crypto::{self, KeyPair}; +use crate::{BytesResponse, Sha256}; /// Error type for instant-acme #[derive(Debug, Error)] @@ -66,7 +65,7 @@ pub enum Error { } impl Error { - #[cfg(feature = "rcgen")] + #[cfg(all(feature = "rcgen", any(feature = "aws-lc-rs", feature = "ring")))] pub(crate) fn from_rcgen(err: rcgen::Error) -> Self { Self::Other(Box::new(err)) } @@ -265,62 +264,86 @@ pub(crate) struct Header<'a> { #[derive(Debug, Serialize)] pub(crate) enum KeyOrKeyId<'a> { #[serde(rename = "jwk")] - Key(Jwk), + Key(Jwk<'a>), #[serde(rename = "kid")] KeyId(&'a str), } -impl KeyOrKeyId<'_> { - pub(crate) fn from_key(key: &crypto::EcdsaKeyPair) -> KeyOrKeyId<'static> { - KeyOrKeyId::Key(Jwk::new(key)) - } -} - +/// A JSON Web Key (JWK) as used in JWS headers +/// +/// See [RFC 7517](https://www.rfc-editor.org/rfc/rfc7517) for more information. #[derive(Debug, Serialize)] -pub(crate) struct Jwk { - alg: SigningAlgorithm, - crv: &'static str, - kty: &'static str, - r#use: &'static str, - x: String, - y: String, -} - -impl Jwk { - pub(crate) fn new(key: &crypto::EcdsaKeyPair) -> Self { - let (x, y) = key.public_key().as_ref()[1..].split_at(32); - Self { - alg: SigningAlgorithm::Es256, - crv: "P-256", - kty: "EC", - r#use: "sig", - x: BASE64_URL_SAFE_NO_PAD.encode(x), - y: BASE64_URL_SAFE_NO_PAD.encode(y), - } - } +pub struct Jwk<'a> { + /// The algorithm intended for use with this key + pub alg: SigningAlgorithm, + /// Key-type-specific parameters + #[serde(flatten)] + pub key: JwkThumbFields<'a>, + /// The intended use (`"sig"` for signing) + pub r#use: &'static str, +} - pub(crate) fn thumb_sha256( - key: &crypto::EcdsaKeyPair, - ) -> Result { - let jwk = Self::new(key); - Ok(crypto::digest( - &crypto::SHA256, - &serde_json::to_vec(&JwkThumb { - crv: jwk.crv, - kty: jwk.kty, - x: &jwk.x, - y: &jwk.y, - })?, - )) +impl Jwk<'_> { + /// Compute the [RFC 7638](https://www.rfc-editor.org/rfc/rfc7638) JWK thumbprint. + /// + /// Serializes only the required key-type-specific members in lexicographic order, + /// then hashes with SHA-256. + pub(crate) fn thumb_sha256(&self, sha256: &dyn Sha256) -> Result<[u8; 32], serde_json::Error> { + Ok(sha256.hash(&serde_json::to_vec(&self.key)?)) } } +/// Key-type-specific JWK parameters +/// +/// Each variant's fields are declared in lexicographic order for correct +/// [RFC 7638](https://www.rfc-editor.org/rfc/rfc7638) thumbprint computation. #[derive(Debug, Serialize)] -struct JwkThumb<'a> { - crv: &'a str, - kty: &'a str, - x: &'a str, - y: &'a str, +#[serde(untagged)] +#[non_exhaustive] +pub enum JwkThumbFields<'a> { + /// Elliptic Curve key (P-256, P-384, etc.) + Ec { + /// The curve name (e.g., `"P-256"`) + crv: &'static str, + /// Key type, must be `"EC"` + kty: &'static str, + /// The x coordinate (serialized as base64url) + #[serde(serialize_with = "base64url::serialize")] + x: &'a [u8], + /// The y coordinate (serialized as base64url) + #[serde(serialize_with = "base64url::serialize")] + y: &'a [u8], + }, + /// Octet Key Pair (Ed25519, Ed448, X25519, etc.) + Okp { + /// The curve name (e.g., `"Ed25519"`) + crv: &'static str, + /// Key type, must be `"OKP"` + kty: &'static str, + /// The public key (serialized as base64url) + #[serde(serialize_with = "base64url::serialize")] + x: &'a [u8], + }, + /// RSA key + Rsa { + /// The public exponent (serialized as base64url) + #[serde(serialize_with = "base64url::serialize")] + e: &'a [u8], + /// Key type, must be `"RSA"` + kty: &'static str, + /// The modulus (serialized as base64url) + #[serde(serialize_with = "base64url::serialize")] + n: &'a [u8], + }, +} + +mod base64url { + use base64::prelude::{BASE64_URL_SAFE_NO_PAD, Engine}; + use serde::Serializer; + + pub(crate) fn serialize(data: &&[u8], serializer: S) -> Result { + serializer.serialize_str(&BASE64_URL_SAFE_NO_PAD.encode(*data)) + } } /// An ACME challenge as described in RFC 8555 (section 7.1.5) @@ -599,7 +622,7 @@ impl JoseJson { Ok(Self { protected, payload, - signature: BASE64_URL_SAFE_NO_PAD.encode(signature.as_ref()), + signature: BASE64_URL_SAFE_NO_PAD.encode(signature), }) } } @@ -949,13 +972,38 @@ fn deserialize_static_certificate_identifier<'de, D: serde::Deserializer<'de>>( Ok(Some(cert_id.into_owned())) } +/// Algorithm identifier for JWS headers +/// +/// See the [IANA JOSE registry](https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms) +/// for the full list of registered algorithms. #[derive(Clone, Copy, Debug, Serialize)] #[serde(rename_all = "UPPERCASE")] -pub(crate) enum SigningAlgorithm { +#[non_exhaustive] +pub enum SigningAlgorithm { + /// EdDSA using the Ed25519 parameter set in Section 5.1 of [RFC 8032](https://www.rfc-editor.org/rfc/rfc8032) + /// + /// [RFC 9864, Section 2.2](https://www.rfc-editor.org/rfc/rfc9864#section-2.2) + #[serde(rename = "Ed25519")] + Ed25519, /// ECDSA using P-256 and SHA-256 + /// + /// [RFC 7518, Section 3.4](https://www.rfc-editor.org/rfc/rfc7518#section-3.4) Es256, - /// HMAC with SHA-256, + /// ECDSA using P-384 and SHA-384 + /// + /// [RFC 7518, Section 3.4](https://www.rfc-editor.org/rfc/rfc7518#section-3.4) + Es384, + /// HMAC using SHA-256 + /// + /// [RFC 7518, Section 3.2](https://www.rfc-editor.org/rfc/rfc7518#section-3.2) Hs256, + /// RSASSA-PKCS1-v1_5 using SHA-256 + /// + /// [RFC 7518, Section 3.3](https://www.rfc-editor.org/rfc/rfc7518#section-3.3) + Rs256, + /// Other algorithm not represented in the enum + #[serde(untagged)] + Other(&'static str), } /// Attestation payload used for device-attest-01 @@ -971,7 +1019,7 @@ pub(crate) struct Empty {} #[cfg(test)] mod tests { - #[cfg(feature = "x509-parser")] + #[cfg(all(feature = "x509-parser", any(feature = "aws-lc-rs", feature = "ring")))] use rcgen::{ BasicConstraints, CertificateParams, DistinguishedName, IsCa, Issuer, KeyIdMethod, KeyPair, SerialNumber, @@ -1189,7 +1237,7 @@ mod tests { assert_eq!(serialized, r#""aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE""#); } - #[cfg(feature = "x509-parser")] + #[cfg(all(feature = "x509-parser", any(feature = "aws-lc-rs", feature = "ring")))] #[test] fn encoded_certificate_identifier_from_cert() { // Generate a CA key_pair and self-signed cert with a specific subject key identifier. diff --git a/tests/pebble.rs b/tests/pebble.rs index 9976d1f..5b35b99 100644 --- a/tests/pebble.rs +++ b/tests/pebble.rs @@ -1,3 +1,4 @@ +#![cfg(any(feature = "aws-lc-rs", feature = "ring"))] //! Note: tests in the file are ignored by default because they requires `pebble` and //! `pebble-challtestsrv` binaries. //! @@ -24,15 +25,15 @@ use hyper_util::client::legacy::Client as HyperClient; use hyper_util::client::legacy::connect::HttpConnector; use hyper_util::rt::TokioExecutor; use instant_acme::{ - Account, AuthorizationStatus, BodyWrapper, ChallengeHandle, ChallengeType, Error, - ExternalAccountKey, Identifier, Key, KeyAuthorization, NewAccount, NewOrder, Order, + Account, AuthorizationStatus, BodyWrapper, ChallengeHandle, ChallengeType, CryptoProvider, + Error, ExternalAccountKey, Identifier, Key, KeyAuthorization, NewAccount, NewOrder, Order, OrderStatus, RetryPolicy, }; #[cfg(all(feature = "time", feature = "x509-parser"))] use instant_acme::{CertificateIdentifier, RevocationRequest}; use rustls::RootCertStore; use rustls::client::{verify_server_cert_signed_by_trust_anchor, verify_server_name}; -use rustls::crypto::CryptoProvider; +use rustls::crypto::CryptoProvider as RustlsCryptoProvider; use rustls::pki_types::pem::PemObject; use rustls::pki_types::{CertificateDer, ServerName}; use rustls::server::ParsedCertificate; @@ -162,6 +163,7 @@ async fn eab_required() -> Result<(), Box> { config.eab_key = Some(ExternalAccountKey::new( eab_id.to_string(), raw_eab_hmac_key.as_ref(), + test_provider(), )); Environment::new(config).await.map(|_| ()) } @@ -362,7 +364,7 @@ async fn update_key() -> Result<(), Box> { ); // Change the Pebble environment to use the new ACME account key. - env.account = Account::builder_with_http(Box::new(env.client.clone())) + env.account = Account::builder_with_http(Box::new(env.client.clone()), test_provider()) .from_credentials(new_credentials) .await?; @@ -385,17 +387,19 @@ async fn account_from_key() -> Result<(), Box> { let env = Environment::new(EnvironmentConfig::default()).await?; let directory_url = format!("https://{}/dir", &env.config.pebble.listen_address); - let (account1, credentials) = Account::builder_with_http(Box::new(env.client.clone())) - .create( - &NewAccount { - contact: &[], - terms_of_service_agreed: true, - only_return_existing: false, - }, - directory_url.clone(), - None, - ) - .await?; + let provider = test_provider(); + let (account1, credentials) = + Account::builder_with_http(Box::new(env.client.clone()), provider) + .create( + &NewAccount { + contact: &[], + terms_of_service_agreed: true, + only_return_existing: false, + }, + directory_url.clone(), + None, + ) + .await?; #[derive(Deserialize)] struct JsonKey<'a> { @@ -405,14 +409,15 @@ async fn account_from_key() -> Result<(), Box> { let json1 = serde_json::to_string(&credentials)?; let json_key = serde_json::from_str::>(&json1)?; let key_der = BASE64_URL_SAFE_NO_PAD.decode(json_key.key_pkcs8)?; - let key = Key::from_pkcs8_der(PrivatePkcs8KeyDer::from(key_der.clone()))?; + let key = Key::from_pkcs8_der(PrivatePkcs8KeyDer::from(key_der.clone()), provider)?; - let (account2, credentials2) = Account::builder_with_http(Box::new(env.client.clone())) - .from_key( - (key, PrivateKeyDer::try_from(key_der.clone())?), - directory_url, - ) - .await?; + let (account2, credentials2) = + Account::builder_with_http(Box::new(env.client.clone()), provider) + .from_key( + (key, PrivateKeyDer::try_from(key_der.clone())?), + directory_url, + ) + .await?; assert_eq!(account1.id(), account2.id()); assert_eq!( @@ -425,8 +430,8 @@ async fn account_from_key() -> Result<(), Box> { let env = Environment::new(EnvironmentConfig::default()).await?; let directory_url = format!("https://{}/dir", &env.config.pebble.listen_address); - let key = Key::from_pkcs8_der(PrivatePkcs8KeyDer::from(key_der.clone()))?; - let result = Account::builder_with_http(Box::new(env.client.clone())) + let key = Key::from_pkcs8_der(PrivatePkcs8KeyDer::from(key_der.clone()), provider)?; + let result = Account::builder_with_http(Box::new(env.client.clone()), provider) .from_key((key, PrivateKeyDer::try_from(key_der)?), directory_url) .await; @@ -456,18 +461,21 @@ async fn account_create_from_key() -> Result<(), Box> { let directory_url = format!("https://{}/dir", &env.config.pebble.listen_address); // Generate a new key - let (key, key_pkcs8) = Key::generate_pkcs8()?; + let provider = test_provider(); + let (key, key_pkcs8) = Key::generate(provider)?; // Create a new account with the generated key - let (account1, credentials1) = Account::builder_with_http(Box::new(env.client.clone())) - .create_from_key( - ( - key, - PrivateKeyDer::try_from(key_pkcs8.secret_pkcs8_der().to_vec())?, - ), - directory_url.clone(), - ) - .await?; + let (account1, credentials1) = + Account::builder_with_http(Box::new(env.client.clone()), provider) + .create_from_key( + ( + key, + PrivateKeyDer::try_from(key_pkcs8.secret_pkcs8_der().to_vec()) + .expect("PKCS#8 key should be valid"), + ), + directory_url.clone(), + ) + .await?; // Extract the key to verify it matches what we provided #[derive(Deserialize)] @@ -483,10 +491,11 @@ async fn account_create_from_key() -> Result<(), Box> { assert_eq!(key_der, key_pkcs8.secret_pkcs8_der()); // Now try to load the account using from_key to verify it was created - let key2 = Key::from_pkcs8_der(PrivatePkcs8KeyDer::from(key_der.clone()))?; - let (account2, credentials2) = Account::builder_with_http(Box::new(env.client.clone())) - .from_key((key2, PrivateKeyDer::try_from(key_der)?), directory_url) - .await?; + let key2 = Key::from_pkcs8_der(PrivatePkcs8KeyDer::from(key_der.clone()), provider)?; + let (account2, credentials2) = + Account::builder_with_http(Box::new(env.client.clone()), provider) + .from_key((key2, PrivateKeyDer::try_from(key_der)?), directory_url) + .await?; // Both should be the same account assert_eq!(account1.id(), account2.id()); @@ -602,7 +611,7 @@ impl Environment { // Create a new `Account` with the ACME server. debug!("creating test account"); - let (account, _) = Account::builder_with_http(Box::new(client.clone())) + let (account, _) = Account::builder_with_http(Box::new(client.clone()), test_provider()) .create( &NewAccount { contact: &[], @@ -648,7 +657,7 @@ impl Environment { .challenge(A::TYPE) .ok_or_else(|| format!("no {:?} challenge found", A::TYPE))?; - let key_authz = challenge.key_authorization(); + let key_authz = challenge.key_authorization()?; self.request_challenge::(&challenge, &key_authz).await?; debug!(challenge_url = challenge.url, "marking challenge ready"); @@ -669,7 +678,7 @@ impl Environment { let ee_cert = ParsedCertificate::try_from(ee_cert_der).unwrap(); // Use the default crypto provider to verify the certificate chain to the Pebble CA root. - let crypto_provider = CryptoProvider::get_default().unwrap(); + let crypto_provider = RustlsCryptoProvider::get_default().unwrap(); verify_server_cert_signed_by_trust_anchor( &ee_cert, &self.issuer_roots().await?, @@ -1021,3 +1030,10 @@ impl Drop for Subprocess { static NEXT_PORT: AtomicU16 = AtomicU16::new(5555); const RETRY_POLICY: RetryPolicy = RetryPolicy::new().backoff(1.0); + +fn test_provider() -> &'static CryptoProvider { + #[cfg(feature = "aws-lc-rs")] + return CryptoProvider::aws_lc_rs(); + #[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))] + return CryptoProvider::ring(); +}