From 7740ccd0df9f0d2c89dfb75353fef9835795e10b Mon Sep 17 00:00:00 2001 From: Deftware <14980525+Moudoux@users.noreply.github.com> Date: Thu, 25 Jan 2024 00:41:48 +0100 Subject: [PATCH 1/4] Add certificate revocation --- src/lib.rs | 16 ++++++++++++++-- src/types.rs | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4910066..3ce2cc7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,8 +23,9 @@ use serde::Serialize; mod types; pub use types::{ - AccountCredentials, Authorization, AuthorizationStatus, Challenge, ChallengeType, Error, - Identifier, LetsEncrypt, NewAccount, NewOrder, OrderState, OrderStatus, Problem, ZeroSsl, + AccountCredentials, Authorization, AuthorizationStatus, CertificateRevocation, Challenge, + ChallengeType, Error, Identifier, LetsEncrypt, NewAccount, NewOrder, OrderState, OrderStatus, + Problem, RevocationReason, ZeroSsl, }; use types::{ DirectoryUrls, Empty, FinalizeRequest, Header, JoseJson, Jwk, KeyOrKeyId, NewAccountPayload, @@ -362,6 +363,17 @@ impl Account { url: order_url.ok_or("no order URL found")?, }) } + + /// Revokes a previously issued certificate + pub async fn revoke<'a>(&'a self, payload: &'a CertificateRevocation) -> Result<(), Error> { + let rsp = self + .inner + .post(Some(payload), None, &self.inner.client.urls.revoke_cert) + .await?; + // The body is empty if the request was successful + let _ = Problem::from_response(rsp).await?; + Ok(()) + } } struct AccountInner { diff --git a/src/types.rs b/src/types.rs index faa6d90..ecbd48d 100644 --- a/src/types.rs +++ b/src/types.rs @@ -271,6 +271,52 @@ pub struct NewOrder<'a> { pub identifiers: &'a [Identifier], } +/// The reason for a certificate revocation +/// Defined in +#[allow(missing_docs)] +pub enum RevocationReason { + Unspecified, + KeyCompromise, + CaCompromise, + AffiliationChanged, + Superseded, + CessationOfOperation, + CertificateHold, + RemoveFromCrl, + PrivilegeWithdrawn, + AaCompromise, +} + +/// Payload for a certificate revocation request +/// Defined in +#[derive(Debug, Serialize)] +pub struct CertificateRevocation { + /// URL safe base64 encoded certificate (not a chain) + /// The certificate should not contain a header or footer or line breaks + pub certificate: String, + /// Reason for revocation + pub reason: Option, +} + +impl CertificateRevocation { + /// Create a new revocation request from a certificate. + /// The certificate cannot be a chain. + pub fn new(certificate: &str, reason: Option) -> CertificateRevocation { + let reason_code = reason.map(|reason| reason as i32); + // Strip header, remove line breaks, and make it URL safe + let body = certificate + .replace("-----BEGIN CERTIFICATE-----", "") + .replace("-----END CERTIFICATE-----", "") + .replace('+', "-") + .replace('/', "_") + .replace(['\n', '\r'], ""); + CertificateRevocation { + certificate: body, + reason: reason_code, + } + } +} + #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct NewAccountPayload<'a> { @@ -302,6 +348,7 @@ pub(crate) struct DirectoryUrls { pub(crate) new_nonce: String, pub(crate) new_account: String, pub(crate) new_order: String, + pub(crate) revoke_cert: String, } #[derive(Serialize)] From 457bbb604f94d399a24c8caf03239f2dc7132734 Mon Sep 17 00:00:00 2001 From: Deftware <14980525+Moudoux@users.noreply.github.com> Date: Thu, 25 Jan 2024 00:48:24 +0100 Subject: [PATCH 2/4] Fix test failing --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 3ce2cc7..1bf3d45 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -709,7 +709,7 @@ 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"}}"#; + 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::(CREDENTIALS)?).await?; Ok(()) } From 1763a9ee1d5de9779aca285d680858fdcee4b8c0 Mon Sep 17 00:00:00 2001 From: Deftware <14980525+Moudoux@users.noreply.github.com> Date: Sat, 27 Jan 2024 13:06:41 +0100 Subject: [PATCH 3/4] Tidy up --- Cargo.toml | 1 + src/lib.rs | 8 +++--- src/types.rs | 70 ++++++++++++++++++++++++++++------------------------ 3 files changed, 43 insertions(+), 36 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 865be14..c45e304 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ base64 = "0.21.0" hyper = { version = "0.14.18", features = ["client", "http1", "http2"] } hyper-rustls = { version = "0.24", default-features = false, features = ["http1", "http2", "native-tokio", "tls12"], optional = true } ring = { version = "0.17", features = ["std"] } +rustls-pki-types = "1.1.0" serde = { version = "1.0.104", features = ["derive"] } serde_json = "1.0.78" thiserror = "1.0.30" diff --git a/src/lib.rs b/src/lib.rs index 1bf3d45..3bbb6eb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,9 +23,9 @@ use serde::Serialize; mod types; pub use types::{ - AccountCredentials, Authorization, AuthorizationStatus, CertificateRevocation, Challenge, - ChallengeType, Error, Identifier, LetsEncrypt, NewAccount, NewOrder, OrderState, OrderStatus, - Problem, RevocationReason, ZeroSsl, + AccountCredentials, Authorization, AuthorizationStatus, Challenge, ChallengeType, Error, + Identifier, LetsEncrypt, NewAccount, NewOrder, OrderState, OrderStatus, Problem, + RevocationReason, RevocationRequest, ZeroSsl, }; use types::{ DirectoryUrls, Empty, FinalizeRequest, Header, JoseJson, Jwk, KeyOrKeyId, NewAccountPayload, @@ -365,7 +365,7 @@ impl Account { } /// Revokes a previously issued certificate - pub async fn revoke<'a>(&'a self, payload: &'a CertificateRevocation) -> Result<(), Error> { + pub async fn revoke<'a>(&'a self, payload: &'a RevocationRequest) -> Result<(), Error> { let rsp = self .inner .post(Some(payload), None, &self.inner.client.urls.revoke_cert) diff --git a/src/types.rs b/src/types.rs index ecbd48d..04fe7db 100644 --- a/src/types.rs +++ b/src/types.rs @@ -4,6 +4,7 @@ use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD}; use hyper::{Body, Response}; use ring::digest::{digest, Digest, SHA256}; use ring::signature::{EcdsaKeyPair, KeyPair}; +use rustls_pki_types::CertificateDer; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -271,52 +272,57 @@ pub struct NewOrder<'a> { pub identifiers: &'a [Identifier], } -/// The reason for a certificate revocation -/// Defined in -#[allow(missing_docs)] -pub enum RevocationReason { - Unspecified, - KeyCompromise, - CaCompromise, - AffiliationChanged, - Superseded, - CessationOfOperation, - CertificateHold, - RemoveFromCrl, - PrivilegeWithdrawn, - AaCompromise, -} - /// Payload for a certificate revocation request /// Defined in #[derive(Debug, Serialize)] -pub struct CertificateRevocation { +pub struct RevocationRequest { /// URL safe base64 encoded certificate (not a chain) /// The certificate should not contain a header or footer or line breaks pub certificate: String, /// Reason for revocation - pub reason: Option, + pub reason: Option, } -impl CertificateRevocation { +impl RevocationRequest { /// Create a new revocation request from a certificate. - /// The certificate cannot be a chain. - pub fn new(certificate: &str, reason: Option) -> CertificateRevocation { - let reason_code = reason.map(|reason| reason as i32); - // Strip header, remove line breaks, and make it URL safe - let body = certificate - .replace("-----BEGIN CERTIFICATE-----", "") - .replace("-----END CERTIFICATE-----", "") - .replace('+', "-") - .replace('/', "_") - .replace(['\n', '\r'], ""); - CertificateRevocation { - certificate: body, - reason: reason_code, + /// The CertificateDer is the certificate to be revoked. + /// The rustls_pemfile crate can be used to obtain a CertificateDer from a PEM file. + pub fn new( + certificate: &CertificateDer, + reason: Option, + ) -> RevocationRequest { + let base64 = BASE64_URL_SAFE_NO_PAD.encode(certificate); + RevocationRequest { + certificate: base64, + reason, } } } +/// The reason for a certificate revocation +/// Defined in +#[allow(missing_docs)] +#[derive(Debug, Clone)] +#[repr(u8)] +pub enum RevocationReason { + Unspecified = 0, + KeyCompromise = 1, + CaCompromise = 2, + AffiliationChanged = 3, + Superseded = 4, + CessationOfOperation = 5, + CertificateHold = 6, + RemoveFromCrl = 8, + PrivilegeWithdrawn = 9, + AaCompromise = 10, +} + +impl Serialize for RevocationReason { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_u8(self.clone() as u8) + } +} + #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct NewAccountPayload<'a> { From dcdc1eb7362ed0f95ea1b9793f549c38761348bd Mon Sep 17 00:00:00 2001 From: Moudoux <14980525+Moudoux@users.noreply.github.com> Date: Mon, 29 Jan 2024 16:11:58 +0100 Subject: [PATCH 4/4] Implement serialize for RevocationRequest --- src/lib.rs | 2 +- src/types.rs | 30 +++++++++++++----------------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3bbb6eb..36e9ac2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -365,7 +365,7 @@ impl Account { } /// Revokes a previously issued certificate - pub async fn revoke<'a>(&'a self, payload: &'a RevocationRequest) -> Result<(), Error> { + pub async fn revoke<'a>(&'a self, payload: &RevocationRequest<'a>) -> Result<(), Error> { let rsp = self .inner .post(Some(payload), None, &self.inner.client.urls.revoke_cert) diff --git a/src/types.rs b/src/types.rs index 04fe7db..541d4b4 100644 --- a/src/types.rs +++ b/src/types.rs @@ -6,6 +6,7 @@ use ring::digest::{digest, Digest, SHA256}; use ring::signature::{EcdsaKeyPair, KeyPair}; use rustls_pki_types::CertificateDer; use serde::de::DeserializeOwned; +use serde::ser::SerializeMap; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -274,28 +275,23 @@ pub struct NewOrder<'a> { /// Payload for a certificate revocation request /// Defined in -#[derive(Debug, Serialize)] -pub struct RevocationRequest { - /// URL safe base64 encoded certificate (not a chain) - /// The certificate should not contain a header or footer or line breaks - pub certificate: String, +#[derive(Debug)] +pub struct RevocationRequest<'a> { + /// The certificate to revoke + pub certificate: &'a CertificateDer<'a>, /// Reason for revocation pub reason: Option, } -impl RevocationRequest { - /// Create a new revocation request from a certificate. - /// The CertificateDer is the certificate to be revoked. - /// The rustls_pemfile crate can be used to obtain a CertificateDer from a PEM file. - pub fn new( - certificate: &CertificateDer, - reason: Option, - ) -> RevocationRequest { - let base64 = BASE64_URL_SAFE_NO_PAD.encode(certificate); - RevocationRequest { - certificate: base64, - reason, +impl<'a> Serialize for RevocationRequest<'a> { + fn serialize(&self, serializer: S) -> Result { + let base64 = BASE64_URL_SAFE_NO_PAD.encode(self.certificate); + let mut map = serializer.serialize_map(Some(2))?; + map.serialize_entry("certificate", &base64)?; + if let Some(reason) = &self.reason { + map.serialize_entry("reason", reason)?; } + map.end() } }