Skip to content
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ allowed_external_types = [
"http::*",
"http_body::*",
"hyper::*",
"rustls::crypto::CryptoProvider",
"rustls_pki_types::*",
"serde_core::*",
"serde_json::*",
Expand Down
14 changes: 13 additions & 1 deletion examples/provision.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#![cfg(any(feature = "ring", feature = "aws-lc-rs"))]
use std::io;

use clap::Parser;
use rustls::crypto::CryptoProvider;
use tracing::info;

use instant_acme::{
Expand All @@ -17,7 +19,7 @@ async fn main() -> anyhow::Result<()> {
// Alternatively, restore an account from serialized credentials by
// using `Account::from_credentials()`.

let (account, credentials) = Account::builder()?
let (account, credentials) = Account::builder(rustls_crypto_provider())?
.create(
&NewAccount {
contact: &[],
Expand Down Expand Up @@ -100,3 +102,13 @@ struct Options {
#[clap(long)]
names: Vec<String>,
}

#[cfg(feature = "aws-lc-rs")]
fn rustls_crypto_provider() -> CryptoProvider {
rustls::crypto::aws_lc_rs::default_provider()
}

#[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))]
fn rustls_crypto_provider() -> CryptoProvider {
rustls::crypto::ring::default_provider()
}
30 changes: 14 additions & 16 deletions src/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ use http::{Method, Request};
#[cfg(feature = "hyper-rustls")]
use rustls::RootCertStore;
#[cfg(feature = "hyper-rustls")]
use rustls::crypto::CryptoProvider;
#[cfg(feature = "hyper-rustls")]
use rustls_pki_types::CertificateDer;
#[cfg(feature = "hyper-rustls")]
use rustls_pki_types::pem::PemObject;
Expand Down Expand Up @@ -52,9 +54,9 @@ pub struct Account {
impl Account {
/// Create an account builder with the default HTTP client
#[cfg(feature = "hyper-rustls")]
pub fn builder() -> Result<AccountBuilder, Error> {
pub fn builder(rustls_crypto_provider: CryptoProvider) -> Result<AccountBuilder, Error> {
Ok(AccountBuilder {
http: Box::new(DefaultClient::try_new()?),
http: Box::new(DefaultClient::try_new(rustls_crypto_provider)?),
})
}

Expand All @@ -63,7 +65,10 @@ 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<Path>) -> Result<AccountBuilder, Error> {
pub fn builder_with_root(
pem_path: impl AsRef<Path>,
rustls_crypto_provider: CryptoProvider,
) -> Result<AccountBuilder, Error> {
let root_der = match CertificateDer::from_pem_file(pem_path) {
Ok(root_der) => root_der,
Err(err) => return Err(Error::Other(err.into())),
Expand All @@ -72,7 +77,7 @@ 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)?),
}),
Err(err) => Err(Error::Other(err.into())),
}
Expand Down Expand Up @@ -227,10 +232,10 @@ 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()?;
let mut header = new_key.header(Some("nonce"), new_key_url);
header.nonce = None;
let payload = NewKey {
Expand Down Expand Up @@ -435,7 +440,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::create_inner(
account,
(key, key_pkcs8),
Expand Down Expand Up @@ -585,14 +590,7 @@ pub struct Key {

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> {
pub fn generate() -> Result<(Self, PrivatePkcs8KeyDer<'static>), Error> {
let rng = crypto::SystemRandom::new();
let pkcs8 =
crypto::EcdsaKeyPair::generate_pkcs8(&crypto::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
Expand All @@ -612,7 +610,7 @@ impl Key {

fn new(pkcs8_der: &[u8], rng: crypto::SystemRandom) -> Result<Self, Error> {
let inner = crypto::p256_key_pair_from_pkcs8(pkcs8_der, &rng)?;
let thumb = BASE64_URL_SAFE_NO_PAD.encode(Jwk::thumb_sha256(&inner)?);
let thumb = BASE64_URL_SAFE_NO_PAD.encode(Jwk::new(&inner).thumb_sha256()?);
Ok(Self {
rng,
signing_algorithm: SigningAlgorithm::Es256,
Expand Down
39 changes: 32 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand All @@ -31,6 +33,8 @@ use hyper_util::client::legacy::Client as HyperClient;
use hyper_util::client::legacy::connect::{Connect, HttpConnector};
#[cfg(feature = "hyper-rustls")]
use hyper_util::rt::TokioExecutor;
#[cfg(feature = "hyper-rustls")]
use rustls::crypto::CryptoProvider;
use serde::Serialize;

mod account;
Expand Down Expand Up @@ -200,18 +204,23 @@ struct DefaultClient(HyperClient<hyper_rustls::HttpsConnector<HttpConnector>, Bo

#[cfg(feature = "hyper-rustls")]
impl DefaultClient {
fn try_new() -> Result<Self, Error> {
fn try_new(rustls_crypto_provider: CryptoProvider) -> Result<Self, Error> {
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<Self, Error> {
fn with_roots(
roots: rustls::RootCertStore,
rustls_crypto_provider: CryptoProvider,
) -> Result<Self, Error> {
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(),
),
Expand Down Expand Up @@ -406,14 +415,20 @@ 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;

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(rustls_crypto_provider())?
.from_credentials(serde_json::from_str::<AccountCredentials>(CREDENTIALS)?)
.await?;
Ok(())
Expand All @@ -422,9 +437,19 @@ 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(rustls_crypto_provider())?
.from_credentials(serde_json::from_str::<AccountCredentials>(CREDENTIALS)?)
.await?;
Ok(())
}

#[cfg(feature = "aws-lc-rs")]
fn rustls_crypto_provider() -> CryptoProvider {
rustls::crypto::aws_lc_rs::default_provider()
}

#[cfg(all(feature = "ring", not(feature = "aws-lc-rs")))]
fn rustls_crypto_provider() -> CryptoProvider {
rustls::crypto::ring::default_provider()
}
}
84 changes: 55 additions & 29 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,62 +265,88 @@ 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> {
pub(crate) fn from_key(key: &crypto::EcdsaKeyPair) -> KeyOrKeyId<'_> {
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 {
pub(crate) struct Jwk<'a> {
/// The algorithm intended for use with this key
alg: SigningAlgorithm,
crv: &'static str,
kty: &'static str,
/// Key-type-specific parameters
#[serde(flatten)]
key: JwkThumbFields<'a>,
/// The intended use (`"sig"` for signing)
r#use: &'static str,
x: String,
y: String,
}

impl Jwk {
pub(crate) fn new(key: &crypto::EcdsaKeyPair) -> Self {
impl Jwk<'_> {
pub(crate) fn new(key: &crypto::EcdsaKeyPair) -> Jwk<'_> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to change from Self because of the lifetime?

let (x, y) = key.public_key().as_ref()[1..].split_at(32);
Self {
Jwk {
alg: SigningAlgorithm::Es256,
crv: "P-256",
kty: "EC",
key: JwkThumbFields::Ec {
crv: "P-256",
kty: "EC",
x,
y,
},
r#use: "sig",
x: BASE64_URL_SAFE_NO_PAD.encode(x),
y: BASE64_URL_SAFE_NO_PAD.encode(y),
}
}

pub(crate) fn thumb_sha256(
key: &crypto::EcdsaKeyPair,
) -> Result<crypto::Digest, serde_json::Error> {
let jwk = Self::new(key);
/// 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) -> Result<crypto::Digest, serde_json::Error> {
Ok(crypto::digest(
&crypto::SHA256,
&serde_json::to_vec(&JwkThumb {
crv: jwk.crv,
kty: jwk.kty,
x: &jwk.x,
y: &jwk.y,
})?,
&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(crate) 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],
},
}

mod base64url {
use base64::prelude::{BASE64_URL_SAFE_NO_PAD, Engine};
use serde::Serializer;

pub(crate) fn serialize<S: Serializer>(data: &&[u8], serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&BASE64_URL_SAFE_NO_PAD.encode(*data))
}
}

/// An ACME challenge as described in RFC 8555 (section 7.1.5)
Expand Down
2 changes: 1 addition & 1 deletion tests/pebble.rs
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ async fn account_create_from_key() -> Result<(), Box<dyn StdError>> {
let directory_url = format!("https://{}/dir", &env.config.pebble.listen_address);

// Generate a new key
let (key, key_pkcs8) = Key::generate_pkcs8()?;
let (key, key_pkcs8) = Key::generate()?;

// Create a new account with the generated key
let (account1, credentials1) = Account::builder_with_http(Box::new(env.client.clone()))
Expand Down