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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ specification.
* Support for certificate revocation
* Support for the [ACME renewal information (ARI)] extension
* Support for the [profiles] extension
* Support for account key rollover
* Support for account contacts update
* Uses hyper with rustls and Tokio for HTTP requests
* Uses *ring* or aws-lc-rs for ECDSA signing
* Minimum supported Rust version (MSRV): 1.70
Expand Down
94 changes: 92 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,8 +446,13 @@ impl ChallengeHandle<'_> {
.await?;

*self.nonce = nonce_from_response(&rsp);
let _ = Problem::check::<Challenge>(rsp).await?;
Ok(())

let response = Problem::check::<Challenge>(rsp).await?;

match response.error {
Some(details) => Err(Error::Api(details)),
None => Ok(()),
}
}

/// Create a [`KeyAuthorization`] for this challenge
Expand Down Expand Up @@ -786,6 +791,91 @@ impl Account {
pub fn id(&self) -> &str {
&self.inner.id
}

/// Account key rollover
///
/// 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. In case the key rollover succeeds the new
/// account credentials are returned for further usage. After that a new Account object with
/// the updated client key needs to be crated for further interaction with the ACME account.
///
/// See <https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.5> for more information.
pub async fn change_key(&self, server_url: &str) -> Result<AccountCredentials, Error> {
#[derive(Debug, Serialize)]
struct NewKey<'a> {
account: &'a str,
#[serde(rename = "oldKey")]
old_key: Jwk,
}

let new_key_url = match self.inner.client.directory.key_change.as_deref() {
Some(url) => url,
None => return Err("Account key rollover not supported by ACME CA".into()),
};

let (new_key, new_key_pkcs8) = Key::generate()?;

let jwk_old = Jwk::new(&self.inner.key.inner);

let payload_inner = NewKey {
account: &self.inner.id,
old_key: jwk_old,
};

let mut inner_header = new_key.header(Some("nonce"), new_key_url);
inner_header.nonce = None;

let inner_body = JoseJson::new(Some(&payload_inner), inner_header, &new_key)?;

let rsp = self
.inner
.post(Some(&inner_body), None, new_key_url)
.await?;

let _ = Problem::from_response(rsp).await?;

let credentials = AccountCredentials {
id: self.inner.id.clone(),
key_pkcs8: new_key_pkcs8.as_ref().to_vec(),
directory: Some(server_url.to_owned()),
urls: None,
};

Ok(credentials)
}

/// Updates the account contacts
///
/// This is useful if you want to update the contact information of an existing account
/// on the ACME server. The contacts argument replaces existing contacts on
/// the server. By providing an empty array the contacts are removed from the server.
///
/// See <https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.2> for more information.
pub async fn update_contacts<'a>(&self, contacts: &'a [&'a str]) -> Result<(), Error> {
#[derive(Clone, Debug, Serialize)]
struct Contacts<'a> {
contact: &'a [&'a str],
}

let payload = Contacts { contact: contacts };

let rsp = self
.inner
.post(Some(&payload), None, &self.inner.id)
.await?;

#[derive(Clone, Debug, serde::Deserialize)]
struct Account {
status: AuthorizationStatus,
}

let response = Problem::check::<Account>(rsp).await?;
match response.status {
AuthorizationStatus::Valid => Ok(()),
_ => Err("Unexpected account status after updating contact information".into()),
}
}
}

struct AccountInner {
Expand Down
65 changes: 63 additions & 2 deletions tests/pebble.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,12 +307,73 @@ async fn account_deactivate() -> Result<(), Box<dyn StdError>> {
};

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

#[tokio::test]
#[ignore]
async fn update_contacts() -> Result<(), Box<dyn StdError>> {
try_tracing_init();

// Creat an env/initial account
let env = Environment::new(EnvironmentConfig::default()).await?;

// Provide empty contacts information, this is fine for pebble
env.account.update_contacts(&[]).await?;

// Provide an email address as contacts information
env.account
.update_contacts(&["mailto:[email protected]"])
.await?;

Ok(())
}

#[tokio::test]
#[ignore]
async fn change_key() -> Result<(), Box<dyn StdError>> {
try_tracing_init();

// Creat an env/initial account
let mut env = Environment::new(EnvironmentConfig::default()).await?;

let dir = &format!("https://{}/dir", &env.config.pebble.listen_address);

// Change the account key
let new_credentials = env.account.change_key(dir).await?;

// Using the old ACME account key should now produce malformed error.
let Err(Error::Api(problem)) = env
.account
.update_contacts(&["mailto:[email protected]"])
.await
else {
panic!("unexpected error result");
};

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

// 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?;

// Using the new ACME account key should not produce an error.
env.account
.update_contacts(&["mailto:[email protected]"])
.await?;

Ok(())
}

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