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
21 changes: 11 additions & 10 deletions examples/provision.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,17 @@ async fn main() -> anyhow::Result<()> {
// Alternatively, restore an account from serialized credentials by
// using `Account::from_credentials()`.

let (account, credentials) = Account::create(
&NewAccount {
contact: &[],
terms_of_service_agreed: true,
only_return_existing: false,
},
LetsEncrypt::Staging.url().to_owned(),
None,
)
.await?;
let (account, credentials) = Account::builder()?
.create(
&NewAccount {
contact: &[],
terms_of_service_agreed: true,
only_return_existing: false,
},
LetsEncrypt::Staging.url().to_owned(),
None,
)
.await?;
info!(
"account credentials:\n\n{}",
serde_json::to_string_pretty(&credentials)?
Expand Down
257 changes: 125 additions & 132 deletions src/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ use crate::{BytesResponse, Client, Error, HttpClient, crypto, nonce_from_respons

/// An ACME account as described in RFC 8555 (section 7.1.2)
///
/// Create an [`Account`] with [`Account::create()`] or restore it from serialized data
/// by passing deserialized [`AccountCredentials`] to [`Account::from_credentials()`].
/// Create an [`Account`] via [`Account::builder()`] or [`Account::builder_with_http()`],
/// then use [`AccountBuilder::create()`] to create a new account or restore one from
/// serialized data by passing deserialized [`AccountCredentials`] to
/// [`AccountBuilder::from_credentials()`].
///
/// The [`Account`] type is cheap to clone.
///
Expand All @@ -35,140 +37,17 @@ pub struct Account {
}

impl Account {
/// Restore an existing account from the given credentials
///
/// The [`AccountCredentials`] type is opaque, but supports deserialization.
/// Create an account builder with the default HTTP client
#[cfg(feature = "hyper-rustls")]
pub async fn from_credentials(credentials: AccountCredentials) -> Result<Self, Error> {
Ok(Self {
inner: Arc::new(
AccountInner::from_credentials(credentials, Box::new(DefaultClient::try_new()?))
.await?,
),
})
}

/// Restore an existing account from the given credentials and HTTP client
///
/// The [`AccountCredentials`] type is opaque, but supports deserialization.
pub async fn from_credentials_and_http(
credentials: AccountCredentials,
http: Box<dyn HttpClient>,
) -> Result<Self, Error> {
Ok(Self {
inner: Arc::new(AccountInner::from_credentials(credentials, http).await?),
pub fn builder() -> Result<AccountBuilder, Error> {
Ok(AccountBuilder {
http: Box::new(DefaultClient::try_new()?),
})
}

/// 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
/// encoded in PEM files. Use a crate like rustls-pemfile to decode from PEM to DER.
pub async fn from_parts(
id: String,
key_pkcs8_der: &[u8],
server_url: String,
http: Box<dyn HttpClient>,
) -> Result<Self, Error> {
Ok(Self {
inner: Arc::new(AccountInner {
id,
key: Key::from_pkcs8_der(key_pkcs8_der)?,
client: Arc::new(Client::new(server_url, http).await?),
}),
})
}

/// Create a new account on the `server_url` with the information in [`NewAccount`]
///
/// The returned [`AccountCredentials`] can be serialized and stored for later use.
/// Use [`Account::from_credentials()`] to restore the account from the credentials.
#[cfg(feature = "hyper-rustls")]
pub async fn create(
account: &NewAccount<'_>,
server_url: String,
external_account: Option<&ExternalAccountKey>,
) -> Result<(Account, AccountCredentials), Error> {
Self::create_inner(
account,
external_account,
Client::new(server_url, Box::new(DefaultClient::try_new()?)).await?,
)
.await
}

/// Create a new account with a custom HTTP client
///
/// The returned [`AccountCredentials`] can be serialized and stored for later use.
/// Use [`Account::from_credentials()`] to restore the account from the credentials.
pub async fn create_with_http(
account: &NewAccount<'_>,
server_url: String,
external_account: Option<&ExternalAccountKey>,
http: Box<dyn HttpClient>,
) -> Result<(Account, AccountCredentials), Error> {
Self::create_inner(
account,
external_account,
Client::new(server_url, http).await?,
)
.await
}

async fn create_inner(
account: &NewAccount<'_>,
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
.map(|eak| {
JoseJson::new(
Some(&Jwk::new(&key.inner)),
eak.header(None, &client.directory.new_account),
eak,
)
})
.transpose()?,
};

let rsp = client
.post(Some(&payload), None, &key, &client.directory.new_account)
.await?;

let account_url = rsp
.parts
.headers
.get(LOCATION)
.and_then(|hv| hv.to_str().ok())
.map(|s| s.to_owned());

// The response redirects, we don't need the body
let _ = Problem::from_response(rsp).await?;
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(),
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.
urls: None,
};

let account = AccountInner {
client: Arc::new(client),
key,
id: id.clone(),
};

Ok((
Self {
inner: Arc::new(account),
},
credentials,
))
/// Create an account builder with the given HTTP client
pub fn builder_with_http(http: Box<dyn HttpClient>) -> AccountBuilder {
AccountBuilder { http }
}

/// Create a new order based on the given [`NewOrder`]
Expand Down Expand Up @@ -480,6 +359,120 @@ impl Signer for AccountInner {
}
}

/// Builder for `Account` values
///
/// Create one via [`Account::builder()`] or [`Account::builder_with_http()`].
pub struct AccountBuilder {
http: Box<dyn HttpClient>,
}

impl AccountBuilder {
/// Restore an existing account from the given credentials
///
/// The [`AccountCredentials`] type is opaque, but supports deserialization.
#[allow(clippy::wrong_self_convention)]
pub async fn from_credentials(self, credentials: AccountCredentials) -> Result<Account, Error> {
Ok(Account {
inner: Arc::new(AccountInner::from_credentials(credentials, self.http).await?),
})
}

/// Create a new account on the `server_url` with the information in [`NewAccount`]
///
/// 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> {
Self::create_inner(
account,
external_account,
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
/// encoded in PEM files. Use a crate like rustls-pemfile to decode from PEM to DER.
#[allow(clippy::wrong_self_convention)]
pub async fn from_parts(
self,
id: String,
key_pkcs8_der: &[u8],
server_url: String,
) -> Result<Account, Error> {
Ok(Account {
inner: Arc::new(AccountInner {
id,
key: Key::from_pkcs8_der(key_pkcs8_der)?,
client: Arc::new(Client::new(server_url, self.http).await?),
}),
})
}

async fn create_inner(
account: &NewAccount<'_>,
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
.map(|eak| {
JoseJson::new(
Some(&Jwk::new(&key.inner)),
eak.header(None, &client.directory.new_account),
eak,
)
})
.transpose()?,
};

let rsp = client
.post(Some(&payload), None, &key, &client.directory.new_account)
.await?;

let account_url = rsp
.parts
.headers
.get(LOCATION)
.and_then(|hv| hv.to_str().ok())
.map(|s| s.to_owned());

// The response redirects, we don't need the body
let _ = Problem::from_response(rsp).await?;
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(),
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.
urls: None,
};

let account = AccountInner {
client: Arc::new(client),
key,
id: id.clone(),
};

Ok((
Account {
inner: Arc::new(account),
},
credentials,
))
}
}

pub(crate) struct Key {
rng: crypto::SystemRandom,
signing_algorithm: SigningAlgorithm,
Expand Down
10 changes: 7 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use serde::Serialize;

mod account;
use account::Key;
pub use account::{Account, ExternalAccountKey};
pub use account::{Account, AccountBuilder, ExternalAccountKey};
mod order;
pub use order::{
AuthorizationHandle, Authorizations, ChallengeHandle, Identifiers, KeyAuthorization, Order,
Expand Down Expand Up @@ -329,14 +329,18 @@ mod tests {
#[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::from_credentials(serde_json::from_str::<AccountCredentials>(CREDENTIALS)?).await?;
Account::builder()?
.from_credentials(serde_json::from_str::<AccountCredentials>(CREDENTIALS)?)
.await?;
Ok(())
}

#[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::from_credentials(serde_json::from_str::<AccountCredentials>(CREDENTIALS)?).await?;
Account::builder()?
.from_credentials(serde_json::from_str::<AccountCredentials>(CREDENTIALS)?)
.await?;
Ok(())
}
}
2 changes: 1 addition & 1 deletion src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@ pub(crate) struct NewAccountPayload<'a> {

/// Input data for [Account](crate::Account) creation
///
/// To be passed into [Account::create()](crate::Account::create()).
/// To be passed into [AccountBuilder::create()](crate::AccountBuilder::create()).
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NewAccount<'a> {
Expand Down
30 changes: 14 additions & 16 deletions tests/pebble.rs
Original file line number Diff line number Diff line change
Expand Up @@ -358,11 +358,9 @@ async fn update_key() -> Result<(), Box<dyn StdError>> {
);

// Change the Pebble environment to use the new ACME account key.
env.account = instant_acme::Account::from_credentials_and_http(
new_credentials,
Box::new(env.client.clone()),
)
.await?;
env.account = instant_acme::Account::builder_with_http(Box::new(env.client.clone()))
.from_credentials(new_credentials)
.await?;

// Using the new ACME account key should not produce an error.
env.account
Expand Down Expand Up @@ -476,17 +474,17 @@ impl Environment {

// Create a new `Account` with the ACME server.
debug!("creating test account");
let (account, _) = Account::create_with_http(
&NewAccount {
contact: &[],
terms_of_service_agreed: true,
only_return_existing: false,
},
format!("https://{}/dir", &config.pebble.listen_address),
config.eab_key.as_ref(),
Box::new(client.clone()),
)
.await?;
let (account, _) = Account::builder_with_http(Box::new(client.clone()))
.create(
&NewAccount {
contact: &[],
terms_of_service_agreed: true,
only_return_existing: false,
},
format!("https://{}/dir", &config.pebble.listen_address),
config.eab_key.as_ref(),
)
.await?;
info!(account_id = account.id(), "created ACME account");

Ok(Self {
Expand Down