diff --git a/src/account.rs b/src/account.rs index 6ed16ab..2ecce37 100644 --- a/src/account.rs +++ b/src/account.rs @@ -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}; @@ -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, }) @@ -310,7 +311,10 @@ impl AccountInner { ) -> Result { 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 { @@ -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 @@ -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 { Ok(Account { @@ -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 @@ -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. @@ -475,7 +504,8 @@ 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, @@ -483,15 +513,22 @@ pub(crate) struct Key { } 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::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::new(pkcs8_der.secret_pkcs8_der(), crypto::SystemRandom::new()) } fn new(pkcs8_der: &[u8], rng: crypto::SystemRandom) -> Result { diff --git a/src/lib.rs b/src/lib.rs index e22f806..ff5f12c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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::{ @@ -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( diff --git a/src/types.rs b/src/types.rs index 4d4acaa..b8a5c73 100644 --- a/src/types.rs +++ b/src/types.rs @@ -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}; @@ -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, + pub(crate) key_pkcs8: PrivateKeyDer<'static>, pub(crate) directory: Option, /// We never serialize `urls` by default, but we support deserializing them /// in order to support serialized data from older versions of the library. @@ -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(key_pkcs8: &[u8], serializer: S) -> Result - where - S: Serializer, - { - let encoded = BASE64_URL_SAFE_NO_PAD.encode(key_pkcs8.as_ref()); + pub(crate) fn serialize( + key_pkcs8: &PrivateKeyDer<'_>, + serializer: S, + ) -> Result { + 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, D::Error> { + ) -> Result, D::Error> { struct Visitor; impl de::Visitor<'_> for Visitor { - type Value = Vec; + 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(self, v: &str) -> Result, E> - where - E: de::Error, - { - BASE64_URL_SAFE_NO_PAD.decode(v).map_err(de::Error::custom) + fn visit_str(self, v: &str) -> Result { + 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)), + } } } diff --git a/tests/pebble.rs b/tests/pebble.rs index ec0dff7..3216b8f 100644 --- a/tests/pebble.rs +++ b/tests/pebble.rs @@ -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}; @@ -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; @@ -370,6 +370,75 @@ async fn update_key() -> Result<(), Box> { Ok(()) } +/// Test account loading by private key +#[tokio::test] +#[ignore] +async fn account_from_key() -> Result<(), Box> { + 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::(&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())