1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
/// Common types and structures for the ACME protocol.
/// This module defines the core data structures used throughout the library,
/// including JWS headers, identifiers, and status enumerations.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// Represents the header of a JSON Web Signature (JWS).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JwsHeader {
/// The signature algorithm (e.g., "EdDSA", "RS256").
pub alg: String,
/// The JSON Web Key (JWK) used for signing, typically included in the first request.
#[serde(skip_serializing_if = "Option::is_none")]
pub jwk: Option<serde_json::Value>,
/// The Key ID (KID), used in subsequent requests after the account is registered.
#[serde(skip_serializing_if = "Option::is_none")]
pub kid: Option<String>,
/// An anti-replay nonce provided by the ACME server.
pub nonce: String,
/// The URL of the resource being accessed.
pub url: String,
}
/// A representation of a JSON Web Key (JWK).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Jwk {
/// The key type (e.g., "RSA", "EC", "OKP").
pub kty: String,
/// The intended use of the key (typically "sig" for signing).
#[serde(skip_serializing_if = "Option::is_none")]
pub use_: Option<String>,
/// Allowed key operations (e.g., "sign", "verify").
#[serde(skip_serializing_if = "Option::is_none")]
pub key_ops: Option<Vec<String>>,
/// Additional key-specific parameters (e.g., "crv", "x" for Ed25519).
#[serde(flatten)]
pub params: HashMap<String, serde_json::Value>,
}
/// Detailed error information returned by the ACME server (RFC 7807).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AcmeErrorDetail {
/// A URI reference that identifies the problem type.
#[serde(rename = "type")]
pub error_type: String,
/// A human-readable explanation specific to this occurrence of the problem.
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
/// The HTTP status code generated by the origin server.
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<u16>,
/// A short, human-readable summary of the problem type.
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
/// A URI reference that identifies the specific occurrence of the problem.
#[serde(skip_serializing_if = "Option::is_none")]
pub instance: Option<String>,
/// Optional sub-problems providing more granular error details.
#[serde(skip_serializing_if = "Option::is_none")]
pub subproblems: Option<Vec<AcmeSubproblem>>,
}
/// A sub-problem providing additional context for an ACME error.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AcmeSubproblem {
/// The type of the sub-problem.
#[serde(rename = "type")]
pub error_type: String,
/// Detailed explanation of the sub-problem.
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
/// The identifier (e.g., domain) associated with this sub-problem.
#[serde(skip_serializing_if = "Option::is_none")]
pub identifier: Option<Identifier>,
}
/// An identifier used in ACME authorizations (e.g., a DNS domain name).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Identifier {
/// The type of identifier (e.g., "dns" or "ip").
#[serde(rename = "type")]
pub id_type: String,
/// The value of the identifier (e.g., "example.com").
pub value: String,
}
impl Identifier {
/// Creates a new DNS identifier for the given domain.
pub fn dns(domain: impl Into<String>) -> Self {
Self {
id_type: "dns".to_string(),
value: domain.into(),
}
}
/// Creates a new IP identifier for the given IP address.
pub fn ip(ip: impl Into<String>) -> Self {
Self {
id_type: "ip".to_string(),
value: ip.into(),
}
}
}
/// Reasons for revoking a certificate (RFC 5280).
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[repr(u8)]
pub enum RevocationReason {
/// No specific reason given.
Unspecified = 0,
/// The private key has been compromised.
KeyCompromise = 1,
/// The Certificate Authority has been compromised.
CaCompromise = 2,
/// The subject's affiliation has changed.
AffiliationChanged = 3,
/// The certificate has been superseded by a new one.
Superseded = 4,
/// The subject has ceased operations.
CessationOfOperation = 5,
/// The certificate is temporarily on hold.
CertificateHold = 6,
/// The certificate should be removed from the CRL.
RemoveFromCRL = 8,
/// The subject's privileges have been withdrawn.
PrivilegeWithdrawn = 9,
/// The Attribute Authority has been compromised.
AACompromise = 10,
}
impl RevocationReason {
/// Returns the numeric value of the revocation reason.
pub fn as_u8(self) -> u8 {
self as u8
}
}
/// Contact information for an ACME account.
#[derive(Debug, Clone)]
pub struct Contact {
/// An optional email address.
pub email: Option<String>,
/// An optional phone number.
pub phone: Option<String>,
/// An optional generic URL.
pub url: Option<String>,
}
impl Contact {
/// Creates a new contact with an email address.
pub fn email(email: impl Into<String>) -> Self {
Self {
email: Some(email.into()),
phone: None,
url: None,
}
}
/// Creates a new contact with a phone number.
pub fn phone(phone: impl Into<String>) -> Self {
Self {
email: None,
phone: Some(phone.into()),
url: None,
}
}
/// Creates a new contact with a generic URL.
pub fn url(url: impl Into<String>) -> Self {
Self {
email: None,
phone: None,
url: Some(url.into()),
}
}
/// Converts the contact information into an ACME-compatible URI string.
pub fn to_uri(&self) -> String {
if let Some(email) = &self.email {
format!("mailto:{}", email)
} else if let Some(phone) = &self.phone {
format!("tel:{}", phone)
} else if let Some(url) = &self.url {
url.clone()
} else {
tracing::warn!("Attempted to convert an empty Contact to URI");
String::new()
}
}
}
/// Supported ACME challenge types.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ChallengeType {
/// Validation via a file served over HTTP.
Http01,
/// Validation via a TXT record in DNS.
Dns01,
/// Validation via a specific TLS extension.
TlsAlpn01,
}
impl ChallengeType {
/// Returns the string representation of the challenge type.
pub fn as_str(&self) -> &'static str {
match self {
ChallengeType::Http01 => "http-01",
ChallengeType::Dns01 => "dns-01",
ChallengeType::TlsAlpn01 => "tls-alpn-01",
}
}
}
impl std::str::FromStr for ChallengeType {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"http-01" => Ok(ChallengeType::Http01),
"dns-01" => Ok(ChallengeType::Dns01),
"tls-alpn-01" => Ok(ChallengeType::TlsAlpn01),
_ => Err(format!("Unknown challenge type: {}", s)),
}
}
}
impl std::fmt::Display for ChallengeType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
/// The current status of a certificate order.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OrderStatus {
/// The order is waiting for authorizations to be completed.
Pending,
/// All authorizations are valid; the order is ready for finalization.
Ready,
/// The server is processing the CSR and issuing the certificate.
Processing,
/// The certificate has been issued and is ready for download.
Valid,
/// The order has failed or been invalidated.
Invalid,
/// The order has expired.
Expired,
/// The order was deactivated by the user.
Deactivated,
}
impl std::str::FromStr for OrderStatus {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"pending" => Ok(OrderStatus::Pending),
"ready" => Ok(OrderStatus::Ready),
"processing" => Ok(OrderStatus::Processing),
"valid" => Ok(OrderStatus::Valid),
"invalid" => Ok(OrderStatus::Invalid),
"expired" => Ok(OrderStatus::Expired),
"deactivated" => Ok(OrderStatus::Deactivated),
_ => Err(format!("Unknown order status: {}", s)),
}
}
}
impl OrderStatus {
/// Returns the string representation of the order status.
pub fn as_str(&self) -> &'static str {
match self {
OrderStatus::Pending => "pending",
OrderStatus::Ready => "ready",
OrderStatus::Processing => "processing",
OrderStatus::Valid => "valid",
OrderStatus::Invalid => "invalid",
OrderStatus::Expired => "expired",
OrderStatus::Deactivated => "deactivated",
}
}
}
impl std::fmt::Display for OrderStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
/// The current status of an authorization.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthorizationStatus {
/// The authorization is waiting for challenges to be completed.
Pending,
/// The authorization has been successfully validated.
Valid,
/// The authorization has failed or been invalidated.
Invalid,
/// The authorization was deactivated by the user.
Deactivated,
/// The authorization has expired.
Expired,
}
impl std::str::FromStr for AuthorizationStatus {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"pending" => Ok(AuthorizationStatus::Pending),
"valid" => Ok(AuthorizationStatus::Valid),
"invalid" => Ok(AuthorizationStatus::Invalid),
"deactivated" => Ok(AuthorizationStatus::Deactivated),
"expired" => Ok(AuthorizationStatus::Expired),
_ => Err(format!("Unknown authorization status: {}", s)),
}
}
}
impl AuthorizationStatus {
/// Returns the string representation of the authorization status.
pub fn as_str(&self) -> &'static str {
match self {
AuthorizationStatus::Pending => "pending",
AuthorizationStatus::Valid => "valid",
AuthorizationStatus::Invalid => "invalid",
AuthorizationStatus::Deactivated => "deactivated",
AuthorizationStatus::Expired => "expired",
}
}
}
impl std::fmt::Display for AuthorizationStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_identifier_dns() {
let id = Identifier::dns("example.com");
assert_eq!(id.id_type, "dns");
assert_eq!(id.value, "example.com");
}
#[test]
fn test_contact_email() {
let contact = Contact::email("test@example.com");
assert_eq!(contact.to_uri(), "mailto:test@example.com");
}
#[test]
fn test_challenge_type() {
assert_eq!(ChallengeType::Http01.as_str(), "http-01");
assert_eq!("dns-01".parse::<ChallengeType>(), Ok(ChallengeType::Dns01));
}
#[test]
fn test_order_status() {
assert_eq!("pending".parse::<OrderStatus>(), Ok(OrderStatus::Pending));
assert_eq!(OrderStatus::Valid.as_str(), "valid");
}
}