Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion upki/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use std::fs;
use std::fs::{self, File};
use std::io::BufReader;
use std::path::{Path, PathBuf};

use eyre::{Context, Report};
use serde::{Deserialize, Serialize};

use crate::Manifest;

/// `upki` configuration.
#[derive(Debug, Deserialize, Serialize)]
pub struct Config {
Expand Down Expand Up @@ -52,6 +55,18 @@ pub struct RevocationConfig {
pub fetch_url: String,
}

impl RevocationConfig {
pub fn manifest(&self) -> Result<Manifest, Report> {
let file_name = self.cache_dir.join("manifest.json");
serde_json::from_reader(
File::open(&file_name)
.map(BufReader::new)
.wrap_err_with(|| format!("cannot open manifest JSON {file_name:?}"))?,
)
.wrap_err("cannot parse manifest JSON")
}
}

pub enum ConfigPath {
Specified(PathBuf),
Default(PathBuf),
Expand Down
3 changes: 1 addition & 2 deletions upki/src/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ use chrono::{DateTime, Utc};
use eyre::{Context, Report, eyre};
use tempfile::NamedTempFile;
use tracing::{debug, info};
use upki::config::RevocationConfig;
use upki::{Filter, Manifest};
use upki::{Filter, Manifest, RevocationConfig};

pub(super) async fn fetch(
RevocationConfig {
Expand Down
174 changes: 133 additions & 41 deletions upki/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
use clubcard_crlite::{CRLiteKey, CRLiteStatus};
use core::error::Error as StdError;
use core::fmt;
use core::str::FromStr;
use std::fs;

use base64::Engine;
use base64::prelude::BASE64_STANDARD;
use clubcard_crlite::{CRLiteClubcard, CRLiteKey, CRLiteStatus};
use eyre::{Context, Report, eyre};
use serde::{Deserialize, Serialize};

pub mod config;
mod config;
pub use config::{Config, ConfigPath, RevocationConfig};

/// The structure contained in a manifest.json
#[derive(Clone, Debug, Deserialize, Serialize)]
Expand All @@ -18,6 +27,44 @@ pub struct Manifest {
pub filters: Vec<Filter>,
}

impl Manifest {
/// This function does a low-level revocation check.
///
/// It is assumed the caller has already done a path verification, and now wants to
/// check the revocation status of the end-entity certificate.
///
/// On success, this returns a [`RevocationStatus`] saying whether the certificate
/// is revoked, not revoked, or whether the data set cannot make that determination.
pub fn check(
&self,
input: &RevocationCheckInput,
config: &RevocationConfig,
) -> Result<RevocationStatus, Report> {
let key = input.key();
for f in &self.filters {
let bytes = fs::read(config.cache_dir.join(&f.filename))
.wrap_err_with(|| format!("cannot read filter file {}", f.filename))?;

let filter =
CRLiteClubcard::from_bytes(&bytes).map_err(|_| Error::CorruptCrliteFilter)?;

match filter.contains(
&key,
input
.sct_timestamps
.iter()
.map(|ct_ts| (&ct_ts.log_id, ct_ts.timestamp)),
) {
CRLiteStatus::Revoked => return Ok(RevocationStatus::CertainlyRevoked),
CRLiteStatus::Good => return Ok(RevocationStatus::NotRevoked),
CRLiteStatus::NotEnrolled | CRLiteStatus::NotCovered => continue,
}
}

Ok(RevocationStatus::NotCoveredByRevocationData)
}
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Filter {
/// Relative filename.
Expand All @@ -33,48 +80,83 @@ pub struct Filter {
pub hash: Vec<u8>,
}

/// This function does a low-level revocation check.
///
/// It is assumed the caller has already done a path verification, and now wants to
/// check the revocation status of the end-entity certificate.
///
/// - `filters` is the raw filter data.
/// - `cert_serial` is the big-endian bytes encoding of the end-entity certificate
/// serial number.
/// - `issuer_spki_hash` is the SHA256 hash of the `SubjectPublicKeyInfo` of the issuer
/// of the end-entity certificate.
/// - `sct_timestamps` is a list of the CT log IDs and inclusion timestamps present in
/// the end-entity certificate.
///
/// On success, this returns a [`RevocationStatus`] saying whether the certificate
/// is revoked, not revoked, or whether the data set cannot make that determination.
pub fn revocation_check<'a>(
filters: impl Iterator<Item = &'a [u8]>,
cert_serial: &[u8],
issuer_spki_hash: [u8; 32],
sct_timestamps: &[([u8; 32], u64)],
) -> Result<RevocationStatus, Error> {
let crlite_key = CRLiteKey::new(&issuer_spki_hash, cert_serial);

for filter in filters {
let filter = clubcard_crlite::CRLiteClubcard::from_bytes(filter)
.map_err(|_| Error::CorruptCrliteFilter)?;

match filter.contains(
&crlite_key,
sct_timestamps
.iter()
.map(|(log_id, ts)| (log_id, *ts)),
) {
CRLiteStatus::Revoked => return Ok(RevocationStatus::CertainlyRevoked),
CRLiteStatus::Good => return Ok(RevocationStatus::NotRevoked),
CRLiteStatus::NotEnrolled | CRLiteStatus::NotCovered => {
continue;
}
/// Input parameters for a revocation check.
#[derive(Debug)]
pub struct RevocationCheckInput {
/// Big-endian bytes encoding of the end-entity certificate serial number.
pub cert_serial: CertSerial,
/// SHA256 hash of the `SubjectPublicKeyInfo` of the issuer of the end-entity certificate.
pub issuer_spki_hash: IssuerSpkiHash,
/// CT log IDs and inclusion timestamps present in the end-entity certificate.
pub sct_timestamps: Vec<CtTimestamp>,
}

impl RevocationCheckInput {
fn key(&self) -> CRLiteKey<'_> {
CRLiteKey::new(&self.issuer_spki_hash.0, &self.cert_serial.0)
}
}

#[derive(Clone, Debug)]
pub struct CertSerial(pub Vec<u8>);

impl FromStr for CertSerial {
type Err = Report;

fn from_str(value: &str) -> Result<Self, Self::Err> {
match BASE64_STANDARD.decode(value) {
Ok(bytes) => Ok(Self(bytes)),
Err(e) => Err(e).wrap_err("cannot parse base64 serial number"),
}
}
}

#[derive(Clone, Debug)]
pub struct IssuerSpkiHash(pub [u8; 32]);

impl FromStr for IssuerSpkiHash {
type Err = Report;

fn from_str(value: &str) -> Result<Self, Self::Err> {
Ok(Self(
BASE64_STANDARD
.decode(value)
.wrap_err("cannot parse issuer SPKI hash")?
.try_into()
.map_err(|b: Vec<u8>| {
eyre!("issuer SPKI hash is wrong length (was {} bytes)", b.len())
})?,
))
}
}

Ok(RevocationStatus::NotCoveredByRevocationData)
#[derive(Clone, Debug)]
pub struct CtTimestamp {
pub log_id: [u8; 32],
pub timestamp: u64,
}

impl FromStr for CtTimestamp {
type Err = Report;

fn from_str(value: &str) -> Result<Self, Self::Err> {
let Some((log_id, issuance_timestamp)) = value.split_once(":") else {
return Err(eyre!("missing colon in CT timestamp"));
};

Ok(Self {
log_id: BASE64_STANDARD
.decode(log_id)
.wrap_err("cannot parse CT log ID")?
.try_into()
.map_err(|wrong: Vec<u8>| {
eyre!("CT log ID is wrong length (was {} bytes)", wrong.len())
})?,
timestamp: issuance_timestamp
.parse()
.wrap_err("cannot parse CT timestamp")?,
})
}
}

/// The successful outcome of a revocation check.
Expand All @@ -100,3 +182,13 @@ pub enum Error {
/// `crlite_clubcard::CRLiteClubcard` couldn't deserialize the filter data.
CorruptCrliteFilter,
}

impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::CorruptCrliteFilter => write!(f, "corrupt CRLite filter data"),
}
}
}

impl StdError for Error {}
Loading