use openssl::sha::sha256;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use crate::acc::AccountInner;
use crate::acc::AcmeKey;
use crate::api::{ApiAuth, ApiChallenge, ApiEmptyObject, ApiEmptyString};
use crate::jwt::*;
use crate::persist::Persist;
use crate::util::{base64url, read_json};
use crate::Result;
#[derive(Debug)]
pub struct Auth<P: Persist> {
inner: Arc<AccountInner<P>>,
api_auth: ApiAuth,
auth_url: String,
}
impl<P: Persist> Auth<P> {
pub(crate) fn new(inner: &Arc<AccountInner<P>>, api_auth: ApiAuth, auth_url: &str) -> Self {
Auth {
inner: inner.clone(),
api_auth,
auth_url: auth_url.into(),
}
}
pub fn domain_name(&self) -> &str {
&self.api_auth.identifier.value
}
pub fn need_challenge(&self) -> bool {
!self.api_auth.is_status_valid()
}
pub fn http_challenge(&self) -> Challenge<P, Http> {
self.api_auth
.http_challenge()
.map(|c| Challenge::new(&self.inner, c.clone(), &self.auth_url))
.expect("http-challenge")
}
pub fn dns_challenge(&self) -> Challenge<P, Dns> {
self.api_auth
.dns_challenge()
.map(|c| Challenge::new(&self.inner, c.clone(), &self.auth_url))
.expect("dns-challenge")
}
pub fn tls_alpn_challenge(&self) -> Challenge<P, TlsAlpn> {
self.api_auth
.tls_alpn_challenge()
.map(|c| Challenge::new(&self.inner, c.clone(), &self.auth_url))
.expect("tls-alpn-challenge")
}
pub fn api_auth(&self) -> &ApiAuth {
&self.api_auth
}
}
#[doc(hidden)]
pub struct Http;
#[doc(hidden)]
pub struct Dns;
#[doc(hidden)]
pub struct TlsAlpn;
pub struct Challenge<P: Persist, A> {
inner: Arc<AccountInner<P>>,
api_challenge: ApiChallenge,
auth_url: String,
_ph: std::marker::PhantomData<A>,
}
impl<P: Persist> Challenge<P, Http> {
pub fn http_token(&self) -> &str {
&self.api_challenge.token
}
pub fn http_proof(&self) -> String {
let acme_key = self.inner.transport.acme_key();
key_authorization(&self.api_challenge.token, acme_key, false)
}
}
impl<P: Persist> Challenge<P, Dns> {
pub fn dns_proof(&self) -> String {
let acme_key = self.inner.transport.acme_key();
key_authorization(&self.api_challenge.token, acme_key, true)
}
}
impl<P: Persist> Challenge<P, TlsAlpn> {
pub fn tls_alpn_proof(&self) -> [u8; 32] {
let acme_key = self.inner.transport.acme_key();
sha256(key_authorization(&self.api_challenge.token, acme_key, false).as_bytes())
}
}
impl<P: Persist, A> Challenge<P, A> {
fn new(inner: &Arc<AccountInner<P>>, api_challenge: ApiChallenge, auth_url: &str) -> Self {
Challenge {
inner: inner.clone(),
api_challenge,
auth_url: auth_url.into(),
_ph: std::marker::PhantomData,
}
}
pub fn need_validate(&self) -> bool {
self.api_challenge.is_status_pending()
}
pub fn validate(self, delay_millis: u64) -> Result<()> {
let url_chall = &self.api_challenge.url;
let res = self.inner.transport.call(url_chall, &ApiEmptyObject)?;
let _: ApiChallenge = read_json(res)?;
let auth = wait_for_auth_status(&self.inner, &self.auth_url, delay_millis)?;
if !auth.is_status_valid() {
let error = auth
.challenges
.iter()
.filter_map(|c| c.error.as_ref())
.next();
let reason = if let Some(error) = error {
format!(
"Failed: {}",
error.detail.clone().unwrap_or_else(|| error._type.clone())
)
} else {
"Validation failed and no error found".into()
};
return Err(reason.into());
}
Ok(())
}
pub fn api_challenge(&self) -> &ApiChallenge {
&self.api_challenge
}
}
fn key_authorization(token: &str, key: &AcmeKey, extra_sha256: bool) -> String {
let jwk: Jwk = key.into();
let jwk_thumb: JwkThumb = (&jwk).into();
let jwk_json = serde_json::to_string(&jwk_thumb).expect("jwk_thumb");
let digest = base64url(&sha256(jwk_json.as_bytes()));
let key_auth = format!("{}.{}", token, digest);
if extra_sha256 {
base64url(&sha256(key_auth.as_bytes()))
} else {
key_auth
}
}
fn wait_for_auth_status<P: Persist>(
inner: &Arc<AccountInner<P>>,
auth_url: &str,
delay_millis: u64,
) -> Result<ApiAuth> {
let auth = loop {
let res = inner.transport.call(auth_url, &ApiEmptyString)?;
let auth: ApiAuth = read_json(res)?;
if !auth.is_status_pending() {
break auth;
}
thread::sleep(Duration::from_millis(delay_millis));
};
Ok(auth)
}
#[cfg(test)]
mod test {
use crate::persist::*;
use crate::*;
#[test]
fn test_get_challenges() -> Result<()> {
let server = crate::test::with_directory_server();
let url = DirectoryUrl::Other(&server.dir_url);
let persist = MemoryPersist::new();
let dir = Directory::from_url(persist, url)?;
let acc = dir.account("foo@bar.com")?;
let ord = acc.new_order("acmetest.example.com", &[])?;
let authz = ord.authorizations()?;
assert!(authz.len() == 1);
let auth = &authz[0];
{
let http = auth.http_challenge();
assert!(http.need_validate());
}
{
let dns = auth.dns_challenge();
assert!(dns.need_validate());
}
Ok(())
}
}