use std::time::Duration;
use anyhow::{anyhow, Context, Error};
use base64::prelude::*;
use http::{header::CONTENT_TYPE, StatusCode};
use num_bigint::BigInt;
use rasn::types::{OctetString, Oid};
use rasn_ocsp::{
BasicOcspResponse, CertId, CertStatus, OcspRequest, OcspResponse, OcspResponseStatus, Request,
TbsRequest, Version,
};
use rasn_pkix::AlgorithmIdentifier;
use sha1::{Digest, Sha1};
use url::Url;
use x509_parser::{oid_registry::OID_PKIX_ACCESS_DESCRIPTOR_OCSP, prelude::*};
use super::Validity;
pub struct Response {
pub raw: Vec<u8>,
pub ocsp_validity: Validity,
pub cert_status: CertStatus,
}
fn extract_ocsp_url(cert: &X509Certificate) -> Option<String> {
cert.extensions()
.iter()
.find_map(|x| {
if let ParsedExtension::AuthorityInfoAccess(v) = x.parsed_extension() {
Some(v)
} else {
None
}
})?
.accessdescs
.iter()
.filter(|x| x.access_method == OID_PKIX_ACCESS_DESCRIPTOR_OCSP)
.find_map(|x| {
if let GeneralName::URI(v) = x.access_location {
Some(v.to_string())
} else {
None
}
})
}
fn prepare_ocsp_request(cert: &[u8], issuer: &[u8]) -> Result<(OcspRequest, Url), Error> {
let cert = X509Certificate::from_der(cert)
.context("unable to parse cert")?
.1;
let issuer = X509Certificate::from_der(issuer)
.context("unable to parse issuer")?
.1;
let url =
Url::parse(&extract_ocsp_url(&cert).ok_or_else(|| anyhow!("unable to extract OCSP URL"))?)
.context("unable to parse OCSP URL")?;
let hash_algorithm = AlgorithmIdentifier {
algorithm: Oid::ISO_IDENTIFIED_ORGANISATION_OIW_SECSIG_ALGORITHM_SHA1.to_owned(),
parameters: None,
};
let issuer_name_hash = OctetString::from_slice(Sha1::digest(cert.issuer.as_raw()).as_slice());
let issuer_key_hash =
OctetString::from_slice(Sha1::digest(&issuer.public_key().subject_public_key).as_slice());
let serial_number: BigInt = cert.serial.clone().into();
let req_cert = CertId {
hash_algorithm,
serial_number: serial_number.into(),
issuer_name_hash,
issuer_key_hash,
};
let request = Request {
req_cert,
single_request_extensions: None,
};
let tbs_request = TbsRequest {
version: Version::ZERO,
requestor_name: None,
request_list: vec![request],
request_extensions: None,
};
Ok((
OcspRequest {
tbs_request,
optional_signature: None,
},
url,
))
}
pub struct Client {
http_client: reqwest::Client,
}
impl Default for Client {
fn default() -> Self {
Self::new()
}
}
impl Client {
pub fn new() -> Self {
Self {
http_client: reqwest::Client::builder()
.connect_timeout(Duration::from_millis(3000))
.timeout(Duration::from_millis(6000))
.build()
.unwrap(),
}
}
pub const fn new_with_client(http_client: reqwest::Client) -> Self {
Self { http_client }
}
async fn execute(&self, url: Url, ocsp_request: OcspRequest) -> Result<OcspResponse, Error> {
let ocsp_request = rasn::der::encode(&ocsp_request)
.map_err(|e| anyhow!("unable to serialize OCSP request: {e}"))?;
let request = if ocsp_request.len() <= 255 {
let ocsp_request = BASE64_STANDARD.encode(ocsp_request);
let url = url
.join(&ocsp_request)
.context("unable to append base64 request")?;
self.http_client.get(url)
} else {
self.http_client.post(url).body(ocsp_request)
};
let response = request
.header(CONTENT_TYPE, "application/ocsp-request")
.send()
.await
.context("HTTP request failed")?;
if response.status() != StatusCode::OK {
return Err(anyhow!("Incorrect HTTP code: {}", response.status()));
}
let body = response
.bytes()
.await
.context("unable to read OCSP response body")?;
let ocsp_response: OcspResponse = rasn::der::decode(&body)
.map_err(|e| anyhow!("unable to decode OcspResponse: {e:#}"))?;
Ok(ocsp_response)
}
pub async fn query_raw(&self, cert: &[u8], issuer: &[u8]) -> Result<OcspResponse, Error> {
let (ocsp_request, url) =
prepare_ocsp_request(cert, issuer).context("unable to prepare OCSP request")?;
self.execute(url, ocsp_request).await
}
pub async fn query(&self, cert: &[u8], issuer: &[u8]) -> Result<Response, Error> {
let ocsp_response = self
.query_raw(cert, issuer)
.await
.context("Unable to perform OCSP query")?;
if ocsp_response.status != OcspResponseStatus::Successful {
return Err(anyhow!(
"Incorrect OCSP response status: {:?}",
ocsp_response.status
));
}
let raw = rasn::der::encode(&ocsp_response)
.map_err(|e| anyhow!("unable to serialize OCSP response: {e}"))?;
let ocsp_basic: BasicOcspResponse = rasn::der::decode(
&ocsp_response
.bytes
.ok_or_else(|| anyhow!("empty OCSP response"))?
.response,
)
.map_err(|e| anyhow!("unable to decode BasicOcspResponse: {e}"))?;
if ocsp_basic.tbs_response_data.responses.len() != 1 {
return Err(anyhow!(
"OCSP response should contain exactly one certificate"
));
}
let resp = ocsp_basic.tbs_response_data.responses[0].clone();
Ok(Response {
raw,
cert_status: resp.cert_status,
ocsp_validity: Validity {
not_before: resp.this_update,
not_after: resp
.next_update
.ok_or_else(|| anyhow!("No next-update field in the response"))?,
},
})
}
}
#[cfg(test)]
pub(crate) mod test {
use std::str::FromStr;
use super::*;
use hex_literal::hex;
use httptest::{matchers::*, responders::*, Expectation, Server};
use rasn::types::{Integer, OctetString};
use rustls::{crypto::ring, sign::CertifiedKey};
const OCSP_REQUEST: &[u8] = include_bytes!("../test/ocsp_request.bin");
const OCSP_RESPONSE: &[u8] = include_bytes!("../test/ocsp_response.bin");
const CHAIN: &[u8] = include_bytes!("../test/chain.pem");
const KEY: &[u8] = include_bytes!("../test/key.pem");
fn test_request() -> OcspRequest {
OcspRequest {
tbs_request: TbsRequest {
version: Version::ZERO,
requestor_name: None,
request_list: vec![Request {
req_cert: CertId {
hash_algorithm: AlgorithmIdentifier {
algorithm: Oid::ISO_IDENTIFIED_ORGANISATION_OIW_SECSIG_ALGORITHM_SHA1
.to_owned(),
parameters: None,
},
issuer_name_hash: OctetString::from(
&hex!("36175FAA02C887BDD95CA13549512D1E97FADFA9")[..],
),
issuer_key_hash: OctetString::from(
&hex!("6691287B8D8654BAF6203197AEC491E9AFB70BCB")[..],
),
serial_number: Integer::from(
num_bigint::BigInt::from_str(
"3819096869935823013274658159093914787918510",
)
.unwrap(),
),
},
single_request_extensions: None,
}],
request_extensions: None,
},
optional_signature: None,
}
}
pub(crate) fn test_ckey() -> CertifiedKey {
let certs = CHAIN.to_vec();
let certs = rustls_pemfile::certs(&mut certs.as_ref())
.collect::<Result<Vec<_>, _>>()
.unwrap();
let key = KEY.to_vec();
let key = rustls_pemfile::private_key(&mut key.as_ref())
.unwrap()
.unwrap();
let key = ring::sign::any_supported_type(&key).unwrap();
CertifiedKey::new(certs, key)
}
#[test]
fn test_extract_url() {
let certs = CHAIN.to_vec();
let certs = rustls_pemfile::certs(&mut certs.as_ref())
.collect::<Result<Vec<_>, _>>()
.unwrap();
let cert = X509Certificate::from_der(&certs[0]).unwrap().1;
assert_eq!(
extract_ocsp_url(&cert),
Some("http://stg-e5.o.lencr.org".to_string())
)
}
#[test]
fn test_prepare_request() {
let certs = CHAIN.to_vec();
let certs = rustls_pemfile::certs(&mut certs.as_ref())
.collect::<Result<Vec<_>, _>>()
.unwrap();
let (ocsp_request, url) = prepare_ocsp_request(&certs[0], &certs[1]).unwrap();
assert_eq!(url.to_string(), "http://stg-e5.o.lencr.org/");
assert_eq!(ocsp_request, test_request());
assert_eq!(rasn::der::encode(&ocsp_request).unwrap(), OCSP_REQUEST);
}
#[tokio::test]
async fn test_execute() {
ring::default_provider()
.install_default()
.unwrap_or_default();
let server = Server::run();
server.expect(
Expectation::matching(request::method("GET"))
.respond_with(status_code(200).body(OCSP_RESPONSE)),
);
let client = Client::new();
let ocsp_response = client
.execute(Url::parse(&server.url_str("/")).unwrap(), test_request())
.await
.unwrap();
assert_eq!(OCSP_RESPONSE, rasn::der::encode(&ocsp_response).unwrap());
}
}