use std::{collections::HashMap, time::Duration};
use openssl::{bn::BigNum, x509::X509Crl};
use serde::{Deserialize, Serialize};
use url::Url;
use super::{CertBackend, CertBackendInner, path_config::Config};
use crate::{
errors::RvError,
logical::{Backend, Field, FieldType, Operation, Path, Request, Response},
storage::StorageEntry,
utils::{deserialize_duration, serialize_duration},
};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct RevokedSerialInfo;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CDPInfo {
pub url: String,
#[serde(
serialize_with = "serialize_duration",
deserialize_with = "deserialize_duration"
)]
pub valid_until: Duration,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CRLInfo {
pub cdp: Option<CDPInfo>,
pub serials: HashMap<String, RevokedSerialInfo>,
}
impl CertBackend {
pub fn crl_path(&self) -> Path {
let cert_backend_ref1 = self.inner.clone();
let cert_backend_ref2 = self.inner.clone();
let cert_backend_ref3 = self.inner.clone();
Path::builder()
.pattern(r"crls/(?P<name>\w[\w-]+\w)")
.field(
"name",
Field::builder()
.field_type(FieldType::Str)
.required(true)
.description("The name of the certificate."),
)
.field(
"crl",
Field::builder()
.field_type(FieldType::Str)
.description(
r#"The public CRL that should be trusted to attest to certificates' validity statuses.
May be DER or PEM encoded. Note: the expiration time
is ignored; if the CRL is no longer valid, delete it
using the same name as specified here."#,
),
)
.field(
"url",
Field::builder()
.field_type(FieldType::Str)
.description(
"The URL of a CRL distribution point. Only one of 'crl' or 'url' parameters should be specified.",
),
)
.operation(Operation::Read, {
let handler = cert_backend_ref1.clone();
move |backend, req| {
let handler = handler.clone();
Box::pin(async move { handler.read_crl(backend, req).await })
}
})
.operation(Operation::Write, {
let handler = cert_backend_ref2.clone();
move |backend, req| {
let handler = handler.clone();
Box::pin(async move { handler.write_crl(backend, req).await })
}
})
.operation(Operation::Delete, {
let handler = cert_backend_ref3.clone();
move |backend, req| {
let handler = handler.clone();
Box::pin(async move { handler.delete_crl(backend, req).await })
}
})
.help(
r#"
This endpoint allows you to list, create, read, update, and delete the Certificate
Revocation Lists checked during authentication, and/or CRL Distribution Point
URLs.
When any CRLs are in effect, any login will check the trust chains sent by a
client against the submitted or retrieved CRLs. Any chain containing a serial number revoked
by one or more of the CRLs causes that chain to be marked as invalid for the
authentication attempt. Conversely, *any* valid chain -- that is, a chain
in which none of the serials are revoked by any CRL -- allows authentication.
This allows authentication to succeed when interim parts of one chain have been
revoked; for instance, if a certificate is signed by two intermediate CAs due to
one of them expiring.
"#,
)
.build()
}
pub fn crl_list_path(&self) -> Path {
let cert_backend_ref = self.inner.clone();
Path::builder()
.pattern(r"crls/?$")
.operation(Operation::List, {
let handler = cert_backend_ref.clone();
move |backend, req| {
let handler = handler.clone();
Box::pin(async move { handler.list_crl(backend, req).await })
}
})
.help(
r#"
This endpoint allows you to list, create, read, update, and delete the Certificate
Revocation Lists checked during authentication, and/or CRL Distribution Point
URLs.
When any CRLs are in effect, any login will check the trust chains sent by a
client against the submitted or retrieved CRLs. Any chain containing a serial number revoked
by one or more of the CRLs causes that chain to be marked as invalid for the
authentication attempt. Conversely, *any* valid chain -- that is, a chain
in which none of the serials are revoked by any CRL -- allows authentication.
This allows authentication to succeed when interim parts of one chain have been
revoked; for instance, if a certificate is signed by two intermediate CAs due to
one of them expiring.
"#,
)
.build()
}
}
impl CertBackendInner {
pub async fn get_crl(&self, req: &mut Request) -> Result<Option<Config>, RvError> {
let storage_entry = req.storage_get("crls").await?;
if storage_entry.is_none() {
return Ok(None);
}
let entry = storage_entry.unwrap();
let config: Config = serde_json::from_slice(entry.value.as_slice())?;
Ok(Some(config))
}
pub async fn set_crl(
&self,
req: &mut Request,
x509crl: Option<X509Crl>,
name: &str,
cdp: Option<CDPInfo>,
) -> Result<(), RvError> {
self.update_crl_cache(req).await?;
let mut crl_info = CRLInfo {
cdp,
..Default::default()
};
if let Some(crl) = x509crl
&& let Some(revoked_stack) = crl.get_revoked()
{
for revoked in revoked_stack.iter() {
let serial = revoked.serial_number().to_bn()?;
let serial_str = serial.to_dec_str()?;
crl_info
.serials
.insert(serial_str.to_lowercase(), RevokedSerialInfo {});
}
}
let entry = StorageEntry::new(format!("crls/{name}").as_str(), &crl_info)?;
req.storage_put(&entry).await?;
self.crls.insert(name.to_string(), crl_info);
Ok(())
}
pub async fn fetch_crl(
&self,
req: &mut Request,
name: &str,
crl: CRLInfo,
) -> Result<(), RvError> {
if crl.cdp.is_none() {
return Err(RvError::ErrRequestInvalid);
}
let url = crl.cdp.as_ref().unwrap().url.as_str();
let body: String = ureq::get(url).call()?.into_string()?;
let x509crl = X509Crl::from_pem(body.as_bytes())?;
self.set_crl(req, Some(x509crl), name, crl.cdp).await?;
Ok(())
}
pub async fn list_crl(
&self,
_backend: &dyn Backend,
req: &mut Request,
) -> Result<Option<Response>, RvError> {
let crls = req.storage_list("crls/").await?;
let resp = Response::list_response(&crls);
Ok(Some(resp))
}
pub async fn read_crl(
&self,
_backend: &dyn Backend,
req: &mut Request,
) -> Result<Option<Response>, RvError> {
let name = req.get_data_as_str("name")?.to_lowercase();
if name.is_empty() {
return Err(RvError::ErrRequestNoDataField);
}
self.update_crl_cache(req).await?;
let crl = self.crls.get(&name);
if crl.is_none() {
log::error!("no such CRL {name}");
return Err(RvError::ErrRequestInvalid);
};
let crl_info = crl.unwrap();
let crl_data = serde_json::to_value(&*crl_info)?;
Ok(Some(Response::data_response(Some(
crl_data.as_object().unwrap().clone(),
))))
}
pub async fn write_crl(
&self,
_backend: &dyn Backend,
req: &mut Request,
) -> Result<Option<Response>, RvError> {
let name = req.get_data_as_str("name")?.to_lowercase();
if name.is_empty() {
return Err(RvError::ErrRequestNoDataField);
}
if let Ok(crl_value) = req.get_data("crl") {
let crl = crl_value.as_str().ok_or(RvError::ErrRequestFieldInvalid)?;
let x509crl = X509Crl::from_pem(crl.as_bytes())?;
self.set_crl(req, Some(x509crl), &name, None).await?;
} else if let Ok(url_value) = req.get_data("url") {
let url = url_value.as_str().ok_or(RvError::ErrRequestFieldInvalid)?;
if url.is_empty() {
return Err(RvError::ErrRequestInvalid);
}
let _ = Url::parse(url)?;
let cdp_info = CDPInfo {
url: url.to_string(),
..Default::default()
};
let crl_info = CRLInfo {
cdp: Some(cdp_info),
..Default::default()
};
self.fetch_crl(req, &name, crl_info).await?;
} else {
return Err(RvError::ErrRequestNoDataField);
}
Ok(None)
}
pub async fn delete_crl(
&self,
_backend: &dyn Backend,
req: &mut Request,
) -> Result<Option<Response>, RvError> {
let name = req.get_data_as_str("name")?.to_lowercase();
if name.is_empty() {
return Err(RvError::ErrRequestNoDataField);
}
self.update_crl_cache(req).await?;
if self.crls.get(&name).is_none() {
log::error!("no such CRL {name}");
return Err(RvError::ErrRequestInvalid);
}
req.storage_delete(format!("crls/{}", name.to_lowercase()).as_str())
.await?;
self.crls.remove(&name);
Ok(None)
}
pub fn find_serial_in_crls(
&self,
serial: BigNum,
) -> Result<HashMap<String, RevokedSerialInfo>, RvError> {
let serial_str = serial.to_dec_str()?;
let mut ret: HashMap<String, RevokedSerialInfo> = HashMap::new();
for item in self.crls.iter() {
let crl = item.value();
if let Some(info) = crl.serials.get(&serial_str.to_lowercase()) {
ret.insert(item.key().clone(), info.clone());
}
}
Ok(ret)
}
async fn update_crl_cache(&self, req: &Request) -> Result<(), RvError> {
if !self.crls.is_empty() {
return Ok(());
}
let keys = req.storage_list("crls/").await?;
if keys.is_empty() {
return Ok(());
}
for key in &keys {
let entry = match req.storage_get(&format!("crls/{key}")).await {
Ok(None) => continue,
Ok(Some(entry)) => entry,
Err(err) => {
self.crls.clear();
return Err(err);
}
};
let crl_info: CRLInfo = match serde_json::from_slice(entry.value.as_slice()) {
Ok(crl_info) => crl_info,
Err(err) => {
self.crls.clear();
return Err(RvError::SerdeJson { source: err });
}
};
self.crls.insert(key.to_string(), crl_info);
}
Ok(())
}
}