mirror of https://github.com/breard-r/acmed.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
346 lines
8.5 KiB
346 lines
8.5 KiB
use crate::acme_proto::structs::{ApiError, HttpApiError, Identifier};
|
|
use acme_common::b64_encode;
|
|
use acme_common::crypto::{HashFunction, KeyPair};
|
|
use acme_common::error::Error;
|
|
use serde::Deserialize;
|
|
use std::fmt;
|
|
use std::str::FromStr;
|
|
|
|
const ACME_OID: &str = "1.3.6.1.5.5.7.1";
|
|
const ID_PE_ACME_ID: usize = 31;
|
|
const DER_OCTET_STRING_ID: usize = 0x04;
|
|
const DER_STRUCT_NAME: &str = "DER";
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct Authorization {
|
|
pub identifier: Identifier,
|
|
pub status: AuthorizationStatus,
|
|
pub expires: Option<String>,
|
|
pub challenges: Vec<Challenge>,
|
|
pub wildcard: Option<bool>,
|
|
}
|
|
|
|
impl FromStr for Authorization {
|
|
type Err = Error;
|
|
|
|
fn from_str(data: &str) -> Result<Self, Self::Err> {
|
|
let mut res: Self = serde_json::from_str(data)?;
|
|
res.challenges.retain(|c| *c != Challenge::Unknown);
|
|
Ok(res)
|
|
}
|
|
}
|
|
|
|
impl ApiError for Authorization {
|
|
fn get_error(&self) -> Option<Error> {
|
|
for challenge in self.challenges.iter() {
|
|
let err = challenge.get_error();
|
|
if err.is_some() {
|
|
return err;
|
|
}
|
|
}
|
|
None
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq, Deserialize)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum AuthorizationStatus {
|
|
Pending,
|
|
Valid,
|
|
Invalid,
|
|
Deactivated,
|
|
Expired,
|
|
Revoked,
|
|
}
|
|
|
|
impl fmt::Display for AuthorizationStatus {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
let s = match self {
|
|
AuthorizationStatus::Pending => "pending",
|
|
AuthorizationStatus::Valid => "valid",
|
|
AuthorizationStatus::Invalid => "invalid",
|
|
AuthorizationStatus::Deactivated => "deactivated",
|
|
AuthorizationStatus::Expired => "expired",
|
|
AuthorizationStatus::Revoked => "revoked",
|
|
};
|
|
write!(f, "{}", s)
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq, Deserialize)]
|
|
#[serde(tag = "type")]
|
|
pub enum Challenge {
|
|
#[serde(rename = "http-01")]
|
|
Http01(TokenChallenge),
|
|
#[serde(rename = "dns-01")]
|
|
Dns01(TokenChallenge),
|
|
#[serde(rename = "tls-alpn-01")]
|
|
TlsAlpn01(TokenChallenge),
|
|
#[serde(other)]
|
|
Unknown,
|
|
}
|
|
|
|
deserialize_from_str!(Challenge);
|
|
|
|
impl Challenge {
|
|
pub fn get_url(&self) -> String {
|
|
match self {
|
|
Challenge::Http01(tc) | Challenge::Dns01(tc) | Challenge::TlsAlpn01(tc) => {
|
|
tc.url.to_owned()
|
|
}
|
|
Challenge::Unknown => String::new(),
|
|
}
|
|
}
|
|
|
|
pub fn get_proof(&self, key_pair: &KeyPair) -> Result<String, Error> {
|
|
match self {
|
|
Challenge::Http01(tc) => tc.key_authorization(key_pair),
|
|
Challenge::Dns01(tc) => {
|
|
let ka = tc.key_authorization(key_pair)?;
|
|
let a = HashFunction::Sha256.hash(ka.as_bytes());
|
|
let a = b64_encode(&a);
|
|
Ok(a)
|
|
}
|
|
Challenge::TlsAlpn01(tc) => {
|
|
let acme_ext_name = format!("{}.{}", ACME_OID, ID_PE_ACME_ID);
|
|
let ka = tc.key_authorization(key_pair)?;
|
|
let proof = HashFunction::Sha256.hash(ka.as_bytes());
|
|
let proof_str = proof
|
|
.iter()
|
|
.map(|e| format!("{:02x}", e))
|
|
.collect::<Vec<String>>()
|
|
.join(":");
|
|
let value = format!(
|
|
"critical,{}:{:02x}:{:02x}:{}",
|
|
DER_STRUCT_NAME,
|
|
DER_OCTET_STRING_ID,
|
|
proof.len(),
|
|
proof_str
|
|
);
|
|
let acme_ext = format!("{}={}", acme_ext_name, value);
|
|
Ok(acme_ext)
|
|
}
|
|
Challenge::Unknown => Ok(String::new()),
|
|
}
|
|
}
|
|
|
|
pub fn get_file_name(&self) -> String {
|
|
match self {
|
|
Challenge::Http01(tc) => tc.token.to_owned(),
|
|
Challenge::Dns01(_) | Challenge::TlsAlpn01(_) => String::new(),
|
|
Challenge::Unknown => String::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ApiError for Challenge {
|
|
fn get_error(&self) -> Option<Error> {
|
|
match self {
|
|
Challenge::Http01(tc) | Challenge::Dns01(tc) | Challenge::TlsAlpn01(tc) => {
|
|
tc.error.to_owned().map(Error::from)
|
|
}
|
|
Challenge::Unknown => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(PartialEq, Deserialize)]
|
|
pub struct TokenChallenge {
|
|
pub url: String,
|
|
pub status: Option<ChallengeStatus>,
|
|
pub validated: Option<String>,
|
|
pub error: Option<HttpApiError>,
|
|
pub token: String,
|
|
}
|
|
|
|
impl TokenChallenge {
|
|
fn key_authorization(&self, key_pair: &KeyPair) -> Result<String, Error> {
|
|
let thumbprint = key_pair.jwk_public_key_thumbprint()?;
|
|
let thumbprint = HashFunction::Sha256.hash(thumbprint.to_string().as_bytes());
|
|
let thumbprint = b64_encode(&thumbprint);
|
|
let auth = format!("{}.{}", self.token, thumbprint);
|
|
Ok(auth)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq, Deserialize)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum ChallengeStatus {
|
|
Pending,
|
|
Processing,
|
|
Valid,
|
|
Invalid,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{Authorization, AuthorizationStatus, Challenge, ChallengeStatus};
|
|
use crate::identifier::IdentifierType;
|
|
use std::str::FromStr;
|
|
|
|
#[test]
|
|
fn test_authorization() {
|
|
let data = "{
|
|
\"status\": \"pending\",
|
|
\"identifier\": {
|
|
\"type\": \"dns\",
|
|
\"value\": \"example.com\"
|
|
},
|
|
\"challenges\": []
|
|
}";
|
|
let a = Authorization::from_str(data);
|
|
assert!(a.is_ok());
|
|
let a = a.unwrap();
|
|
assert_eq!(a.status, AuthorizationStatus::Pending);
|
|
assert!(a.challenges.is_empty());
|
|
let i = a.identifier;
|
|
assert_eq!(i.id_type, IdentifierType::Dns);
|
|
assert_eq!(i.value, "example.com".to_string());
|
|
}
|
|
|
|
#[test]
|
|
fn test_authorization_challenge() {
|
|
let data = "{
|
|
\"status\": \"pending\",
|
|
\"identifier\": {
|
|
\"type\": \"dns\",
|
|
\"value\": \"example.com\"
|
|
},
|
|
\"challenges\": [
|
|
{
|
|
\"type\": \"dns-01\",
|
|
\"status\": \"pending\",
|
|
\"url\": \"https://example.com/chall/jYWxob3N0OjE\",
|
|
\"token\": \"1y9UVMUvkqQVljCsnwlRLsbJcwN9nx-qDd6JHzXQQsw\"
|
|
}
|
|
]
|
|
}";
|
|
let a = Authorization::from_str(data);
|
|
assert!(a.is_ok());
|
|
let a = a.unwrap();
|
|
assert_eq!(a.status, AuthorizationStatus::Pending);
|
|
assert_eq!(a.challenges.len(), 1);
|
|
let i = a.identifier;
|
|
assert_eq!(i.id_type, IdentifierType::Dns);
|
|
assert_eq!(i.value, "example.com".to_string());
|
|
}
|
|
|
|
#[test]
|
|
fn test_authorization_unknown_challenge() {
|
|
let data = "{
|
|
\"status\": \"pending\",
|
|
\"identifier\": {
|
|
\"type\": \"dns\",
|
|
\"value\": \"example.com\"
|
|
},
|
|
\"challenges\": [
|
|
{
|
|
\"type\": \"invalid-challenge-01\",
|
|
\"status\": \"pending\",
|
|
\"url\": \"https://example.com/chall/jYWxob3N0OjE\",
|
|
\"token\": \"1y9UVMUvkqQVljCsnwlRLsbJcwN9nx-qDd6JHzXQQsw\"
|
|
}
|
|
]
|
|
}";
|
|
let a = Authorization::from_str(data);
|
|
assert!(a.is_ok());
|
|
let a = a.unwrap();
|
|
assert_eq!(a.status, AuthorizationStatus::Pending);
|
|
assert!(a.challenges.is_empty());
|
|
let i = a.identifier;
|
|
assert_eq!(i.id_type, IdentifierType::Dns);
|
|
assert_eq!(i.value, "example.com".to_string());
|
|
}
|
|
|
|
#[test]
|
|
fn test_invalid_authorization() {
|
|
let data = "{
|
|
\"status\": \"pending\",
|
|
\"identifier\": {
|
|
\"type\": \"foo\",
|
|
\"value\": \"bar\"
|
|
},
|
|
\"challenges\": []
|
|
}";
|
|
let a = Authorization::from_str(data);
|
|
assert!(a.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_http01_challenge() {
|
|
let data = "{
|
|
\"type\": \"http-01\",
|
|
\"url\": \"https://example.com/acme/chall/prV_B7yEyA4\",
|
|
\"status\": \"pending\",
|
|
\"token\": \"LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0\"
|
|
}";
|
|
let challenge = Challenge::from_str(data);
|
|
assert!(challenge.is_ok());
|
|
let challenge = challenge.unwrap();
|
|
let c = match challenge {
|
|
Challenge::Http01(c) => c,
|
|
_ => {
|
|
assert!(false);
|
|
return;
|
|
}
|
|
};
|
|
assert_eq!(
|
|
c.url,
|
|
"https://example.com/acme/chall/prV_B7yEyA4".to_string()
|
|
);
|
|
assert_eq!(c.status, Some(ChallengeStatus::Pending));
|
|
assert_eq!(
|
|
c.token,
|
|
"LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0".to_string()
|
|
);
|
|
assert!(c.validated.is_none());
|
|
assert!(c.error.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_dns01_challenge() {
|
|
let data = "{
|
|
\"type\": \"http-01\",
|
|
\"url\": \"https://example.com/acme/chall/prV_B7yEyA4\",
|
|
\"status\": \"valid\",
|
|
\"token\": \"LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0\"
|
|
}";
|
|
let challenge = Challenge::from_str(data);
|
|
assert!(challenge.is_ok());
|
|
let challenge = challenge.unwrap();
|
|
let c = match challenge {
|
|
Challenge::Http01(c) => c,
|
|
_ => {
|
|
assert!(false);
|
|
return;
|
|
}
|
|
};
|
|
assert_eq!(
|
|
c.url,
|
|
"https://example.com/acme/chall/prV_B7yEyA4".to_string()
|
|
);
|
|
assert_eq!(c.status, Some(ChallengeStatus::Valid));
|
|
assert_eq!(
|
|
c.token,
|
|
"LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0".to_string()
|
|
);
|
|
assert!(c.validated.is_none());
|
|
assert!(c.error.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_unknown_challenge_type() {
|
|
let data = "{
|
|
\"type\": \"invalid-01\",
|
|
\"url\": \"https://example.com/acme/chall/prV_B7yEyA4\",
|
|
\"status\": \"pending\",
|
|
\"token\": \"LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0\"
|
|
}";
|
|
let challenge = Challenge::from_str(data);
|
|
assert!(challenge.is_ok());
|
|
match challenge.unwrap() {
|
|
Challenge::Unknown => assert!(true),
|
|
_ => assert!(false),
|
|
}
|
|
}
|
|
}
|