Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 48 additions & 11 deletions src/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use http::header::LOCATION;
use http::{Method, Request};
#[cfg(feature = "time")]
use http_body_util::Full;
use rustls_pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -214,7 +215,7 @@ impl Account {

Ok(AccountCredentials {
id: self.inner.id.clone(),
key_pkcs8: new_key_pkcs8.as_ref().to_vec(),
key_pkcs8: new_key_pkcs8,
directory,
urls,
})
Expand Down Expand Up @@ -310,7 +311,10 @@ impl AccountInner {
) -> Result<Self, Error> {
Ok(Self {
id: credentials.id,
key: Key::from_pkcs8_der(credentials.key_pkcs8.as_ref())?,
key: match credentials.key_pkcs8 {
PrivateKeyDer::Pkcs8(inner) => Key::from_pkcs8_der(inner)?,
_ => return Err("unsupported key format, expected PKCS#8".into()),
},
client: Arc::new(match (credentials.directory, credentials.urls) {
(Some(server_url), _) => Client::new(server_url, http).await?,
(None, Some(directory)) => Client {
Expand Down Expand Up @@ -383,21 +387,46 @@ impl AccountBuilder {
///
/// The returned [`AccountCredentials`] can be serialized and stored for later use.
/// Use [`AccountBuilder::from_credentials()`] to restore the account from the credentials.
#[cfg(feature = "hyper-rustls")]
pub async fn create(
self,
account: &NewAccount<'_>,
server_url: String,
external_account: Option<&ExternalAccountKey>,
) -> Result<(Account, AccountCredentials), Error> {
let (key, key_pkcs8) = Key::generate()?;
Self::create_inner(
account,
(key, key_pkcs8),
external_account,
Client::new(server_url, self.http).await?,
)
.await
}

/// Load an existing account for the given private key
///
/// The returned [`AccountCredentials`] can be serialized and stored for later use.
/// Use [`AccountBuilder::from_credentials()`] to restore the account from the credentials.
///
/// Yields an error if no account matching the given key exists on the server.
pub async fn from_key(
self,
key: (Key, PrivateKeyDer<'static>),
server_url: String,
) -> Result<(Account, AccountCredentials), Error> {
Self::create_inner(
&NewAccount {
contact: &[],
terms_of_service_agreed: true,
only_return_existing: true,
},
key,
None,
Client::new(server_url, self.http).await?,
)
.await
}

/// Restore an existing account from the given ID, private key, server URL and HTTP client
///
/// The key must be provided in DER-encoded PKCS#8. This is usually how ECDSA keys are
Expand All @@ -406,7 +435,7 @@ impl AccountBuilder {
pub async fn from_parts(
self,
id: String,
key_pkcs8_der: &[u8],
key_pkcs8_der: PrivatePkcs8KeyDer<'_>,
server_url: String,
) -> Result<Account, Error> {
Ok(Account {
Expand All @@ -420,10 +449,10 @@ impl AccountBuilder {

async fn create_inner(
account: &NewAccount<'_>,
(key, key_pkcs8): (Key, PrivateKeyDer<'static>),
external_account: Option<&ExternalAccountKey>,
client: Client,
) -> Result<(Account, AccountCredentials), Error> {
let (key, key_pkcs8) = Key::generate()?;
let payload = NewAccountPayload {
new_account: account,
external_account_binding: external_account
Expand Down Expand Up @@ -453,7 +482,7 @@ impl AccountBuilder {
let id = account_url.ok_or("failed to get account URL")?;
let credentials = AccountCredentials {
id: id.clone(),
key_pkcs8: key_pkcs8.as_ref().to_vec(),
key_pkcs8,
directory: Some(client.server_url.clone().unwrap()), // New clients always have `server_url`
// We support deserializing URLs for compatibility with versions pre 0.4,
// but we prefer to get fresh URLs from the `server_url` for newer credentials.
Expand All @@ -475,23 +504,31 @@ impl AccountBuilder {
}
}

pub(crate) struct Key {
/// Private account key used to sign requests
pub struct Key {
rng: crypto::SystemRandom,
signing_algorithm: SigningAlgorithm,
inner: crypto::EcdsaKeyPair,
pub(crate) thumb: String,
}

impl Key {
fn generate() -> Result<(Self, crypto::pkcs8::Document), Error> {
/// Generate a new ECDSA P-256 key pair
pub fn generate() -> Result<(Self, PrivateKeyDer<'static>), Error> {
let rng = crypto::SystemRandom::new();
let pkcs8 =
crypto::EcdsaKeyPair::generate_pkcs8(&crypto::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)?;
Self::new(pkcs8.as_ref(), rng).map(|key| (key, pkcs8))
Ok((
Self::new(pkcs8.as_ref(), rng)?,
PrivatePkcs8KeyDer::from(pkcs8.as_ref().to_vec()).into(),
))
}

fn from_pkcs8_der(pkcs8_der: &[u8]) -> Result<Self, Error> {
Self::new(pkcs8_der, crypto::SystemRandom::new())
/// 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, Error> {
Self::new(pkcs8_der.secret_pkcs8_der(), crypto::SystemRandom::new())
}

fn new(pkcs8_der: &[u8], rng: crypto::SystemRandom) -> Result<Self, Error> {
Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use hyper_util::rt::TokioExecutor;
use serde::Serialize;

mod account;
use account::Key;
pub use account::Key;
pub use account::{Account, AccountBuilder, ExternalAccountKey};
mod order;
pub use order::{
Expand Down Expand Up @@ -297,10 +297,10 @@ mod crypto {

pub(crate) use ring_like::digest::{Digest, SHA256, digest};
pub(crate) use ring_like::error::{KeyRejected, Unspecified};
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};
pub(crate) use ring_like::{hmac, pkcs8};

#[cfg(feature = "aws-lc-rs")]
pub(crate) fn p256_key_pair_from_pkcs8(
Expand Down
34 changes: 20 additions & 14 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::net::IpAddr;

use base64::prelude::{BASE64_URL_SAFE_NO_PAD, Engine};
use bytes::Bytes;
use rustls_pki_types::{CertificateDer, Der};
use rustls_pki_types::{CertificateDer, Der, PrivateKeyDer};
use serde::de::{self, DeserializeOwned};
use serde::ser::SerializeMap;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -94,7 +94,7 @@ pub struct AccountCredentials {
pub(crate) id: String,
/// Stored in DER, serialized as base64
#[serde(with = "pkcs8_serde")]
pub(crate) key_pkcs8: Vec<u8>,
pub(crate) key_pkcs8: PrivateKeyDer<'static>,
pub(crate) directory: Option<String>,
/// We never serialize `urls` by default, but we support deserializing them
/// in order to support serialized data from older versions of the library.
Expand All @@ -106,33 +106,39 @@ mod pkcs8_serde {
use std::fmt;

use base64::prelude::{BASE64_URL_SAFE_NO_PAD, Engine};
use rustls_pki_types::PrivateKeyDer;
use serde::{Deserializer, Serializer, de};

pub(crate) fn serialize<S>(key_pkcs8: &[u8], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let encoded = BASE64_URL_SAFE_NO_PAD.encode(key_pkcs8.as_ref());
pub(crate) fn serialize<S: Serializer>(
key_pkcs8: &PrivateKeyDer<'_>,
serializer: S,
) -> Result<S::Ok, S::Error> {
let encoded = BASE64_URL_SAFE_NO_PAD.encode(key_pkcs8.secret_der());
serializer.serialize_str(&encoded)
}

pub(crate) fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Vec<u8>, D::Error> {
) -> Result<PrivateKeyDer<'static>, D::Error> {
struct Visitor;

impl de::Visitor<'_> for Visitor {
type Value = Vec<u8>;
type Value = PrivateKeyDer<'static>;

fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a base64-encoded PKCS#8 private key")
}

fn visit_str<E>(self, v: &str) -> Result<Vec<u8>, E>
where
E: de::Error,
{
BASE64_URL_SAFE_NO_PAD.decode(v).map_err(de::Error::custom)
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
let bytes = match BASE64_URL_SAFE_NO_PAD.decode(v) {
Ok(bytes) => bytes,
Err(err) => return Err(de::Error::custom(err)),
};

match PrivateKeyDer::try_from(bytes) {
Ok(key) => Ok(key),
Err(err) => Err(de::Error::custom(err)),
}
}
}

Expand Down
75 changes: 72 additions & 3 deletions tests/pebble.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use hyper_util::client::legacy::connect::HttpConnector;
use hyper_util::rt::TokioExecutor;
use instant_acme::{
Account, AuthorizationStatus, ChallengeHandle, ChallengeType, Error, ExternalAccountKey,
Identifier, KeyAuthorization, NewAccount, NewOrder, Order, OrderStatus, RetryPolicy,
Identifier, Key, KeyAuthorization, NewAccount, NewOrder, Order, OrderStatus, RetryPolicy,
};
#[cfg(all(feature = "time", feature = "x509-parser"))]
use instant_acme::{CertificateIdentifier, RevocationRequest};
Expand All @@ -35,8 +35,8 @@ use rustls::crypto::CryptoProvider;
use rustls::pki_types::pem::PemObject;
use rustls::pki_types::{CertificateDer, ServerName};
use rustls::server::ParsedCertificate;
use rustls_pki_types::UnixTime;
use serde::{Serialize, Serializer};
use rustls_pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer, UnixTime};
use serde::{Deserialize, Serialize, Serializer};
use tempfile::NamedTempFile;
#[cfg(all(feature = "time", feature = "x509-parser"))]
use time::OffsetDateTime;
Expand Down Expand Up @@ -370,6 +370,75 @@ async fn update_key() -> Result<(), Box<dyn StdError>> {
Ok(())
}

/// Test account loading by private key
#[tokio::test]
#[ignore]
async fn account_from_key() -> Result<(), Box<dyn StdError>> {
try_tracing_init();

// Creat an env/initial account

let env = Environment::new(EnvironmentConfig::default()).await?;
let server_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,
},
server_url.clone(),
None,
)
.await?;

#[derive(Deserialize)]
struct JsonKey<'a> {
key_pkcs8: &'a str,
}

let json1 = serde_json::to_string(&credentials)?;
let json_key = serde_json::from_str::<JsonKey>(&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 (account2, credentials2) = Account::builder_with_http(Box::new(env.client.clone()))
.from_key((key, PrivateKeyDer::try_from(key_der.clone())?), server_url)
.await?;

assert_eq!(account1.id(), account2.id());
assert_eq!(
serde_json::to_string(&credentials)?,
serde_json::to_string(&credentials2)?,
);

drop(env);

let env = Environment::new(EnvironmentConfig::default()).await?;
let server_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()))
.from_key((key, PrivateKeyDer::try_from(key_der)?), server_url)
.await;

let Err(err) = result else {
panic!("expected OK result");
};

let Error::Api(problem) = err else {
panic!("unexpected error result {err:?}");
};

assert_eq!(
problem.r#type,
Some("urn:ietf:params:acme:error:accountDoesNotExist".to_string())
);

Ok(())
}

fn try_tracing_init() {
let _ = tracing_subscriber::registry()
.with(fmt::layer())
Expand Down