Skip to content
Open
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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ include = [
"src/end_entity.rs",
"src/error.rs",
"src/rpk_entity.rs",
"src/sct.rs",
"src/subject_name/dns_name.rs",
"src/subject_name/ip_address.rs",
"src/subject_name/mod.rs",
Expand Down
9 changes: 9 additions & 0 deletions src/cert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub struct Cert<'a> {
pub(crate) name_constraints: Option<untrusted::Input<'a>>,
pub(crate) subject_alt_name: Option<untrusted::Input<'a>>,
pub(crate) crl_distribution_points: Option<untrusted::Input<'a>>,
pub(crate) scts: Option<untrusted::Input<'a>>,

der: CertificateDer<'a>,
}
Expand Down Expand Up @@ -102,6 +103,7 @@ impl<'a> Cert<'a> {
name_constraints: None,
subject_alt_name: None,
crl_distribution_points: None,
scts: None,

der: CertificateDer::from(cert_der.as_slice_less_safe()),
};
Expand Down Expand Up @@ -328,6 +330,9 @@ fn remember_cert_extension<'a>(
// id-ce-extKeyUsage 2.5.29.37
Standard(37) => &mut cert.eku,

// signedCertificateTimestampList 1.3.6.1.4.1.11129.2.4.2
SignedCertificateTimestampList => &mut cert.scts,

// Unsupported extension
_ => return extension.unsupported(),
};
Expand All @@ -337,6 +342,10 @@ fn remember_cert_extension<'a>(
// Unlike the other extensions we remember KU is a BitString and not a Sequence. We
// read the raw bytes here and parse at the time of use.
Standard(15) => Ok(value.read_bytes_to_end()),

// signedCertificateTimestampList is an OCTET STRING
SignedCertificateTimestampList => der::expect_tag(value, Tag::OctetString),

// All other remembered certificate extensions are wrapped in a Sequence.
_ => der::expect_tag(value, Tag::Sequence),
})
Expand Down
17 changes: 16 additions & 1 deletion src/end_entity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use crate::crl::RevocationOptions;
use crate::error::Error;
use crate::subject_name::{verify_dns_names, verify_ip_address_names};
use crate::verify_cert::{self, ExtendedKeyUsageValidator, VerifiedPath};
use crate::{cert, signed_data};
use crate::{cert, sct, signed_data};

/// An end-entity certificate.
///
Expand Down Expand Up @@ -167,6 +167,21 @@ impl EndEntityCert<'_> {
untrusted::Input::from(signature),
)
}

/// Returns the SCT logs that contributed to the SCTs included in the certificate.
///
/// Note this method does not verify the SCTs themselves.
///
/// If the certificate does not contain an SCT extension, this method returns an empty
/// iterator.
pub fn sct_log_timestamps<'a>(
&'a self,
) -> Result<
impl Iterator<Item = Result<(sct::LogId, sct::Timestamp), sct::Error>> + 'a,
sct::Error,
> {
Ok(sct::SctParser::new(self.scts)?.map(|sct| sct.map(|sct| (sct.log_id, sct.timestamp))))
}
}

impl<'a> Deref for EndEntityCert<'a> {
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ mod error;
#[cfg(feature = "ring")]
mod ring_algs;
mod rpk_entity;
#[allow(missing_docs, dead_code)]
pub mod sct;
mod signed_data;
mod subject_name;
mod time;
Expand Down
136 changes: 136 additions & 0 deletions src/sct.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/// Reads a `SignedCertificateTimestampList` encoding, yielding each `SignedCertificateTimestamp`.
pub(crate) fn iter_scts<'a>(
bytes: untrusted::Input<'a>,
) -> Result<impl Iterator<Item = Result<SignedCertificateTimestamp<'a>, Error>> + 'a, Error> {
let items_body = bytes.read_all(Error::MalformedSct, |rd| read_field(rd, u16_field_len, 1))?;

let mut reader = untrusted::Reader::new(items_body);

Ok(core::iter::from_fn(move || {
let item = read_field(&mut reader, u16_field_len, 1).ok()?;
Some(SignedCertificateTimestamp::try_from(
item.as_slice_less_safe(),
))
}))
}

pub(crate) struct SctParser<'a> {
reader: untrusted::Reader<'a>,
}

impl<'a> SctParser<'a> {
pub(crate) fn new(input: Option<untrusted::Input<'a>>) -> Result<Self, Error> {
Ok(SctParser {
reader: match input {
Some(input) => untrusted::Reader::new(
input.read_all(Error::MalformedSct, |rd| read_field(rd, u16_field_len, 1))?,
),
None => untrusted::Reader::new(untrusted::Input::from(&[])),
},
})
}
}

impl<'a> Iterator for SctParser<'a> {
type Item = Result<SignedCertificateTimestamp<'a>, Error>;

fn next(&mut self) -> Option<Self::Item> {
if self.reader.at_end() {
return None;
}

Some(match read_field(&mut self.reader, u16_field_len, 1) {
Ok(item) => SignedCertificateTimestamp::try_from(item.as_slice_less_safe()),
Err(_) => Err(Error::MalformedSct),
})
}
}

/// This is `SignedCertificateTimestamp` defined in [RFC6962][].
///
/// [RFC6962]: https://www.rfc-editor.org/rfc/rfc6962.html#section-3.2
#[derive(Debug)]
pub(crate) struct SignedCertificateTimestamp<'a> {
pub(crate) log_id: LogId,
pub(crate) timestamp: Timestamp,
#[allow(dead_code)] // pending sct verification
extensions: untrusted::Input<'a>,
#[allow(dead_code)] // pending sct verification
signature_algorithm: u16,
#[allow(dead_code)] // pending sct verification
signature: untrusted::Input<'a>,
Comment on lines +56 to +61
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What kind of verification needs to happen? Thoughts on a testing strategy?

}

impl<'a> TryFrom<&'a [u8]> for SignedCertificateTimestamp<'a> {
type Error = Error;

fn try_from(bytes: &'a [u8]) -> Result<Self, Self::Error> {
let input = untrusted::Input::from(bytes);
input.read_all(Error::MalformedSct, |rd| {
match read_array(rd)? {
[0] => {}
_ => return Err(Error::UnsupportedSctVersion),
};

let log_id = LogId(read_array(rd)?);
let timestamp = Timestamp(u64::from_be_bytes(read_array(rd)?));
let extensions = read_field(rd, u16_field_len, 0)?;
let signature_algorithm = u16::from_be_bytes(read_array(rd)?);
let signature = read_field(rd, u16_field_len, 1)?;

Ok(SignedCertificateTimestamp {
log_id,
timestamp,
extensions,
signature_algorithm,
signature,
})
})
}
}

#[derive(Debug)]
pub struct LogId(pub [u8; 32]);

#[derive(Debug)]
pub struct Timestamp(pub u64);

/// Read a length-prefixed field from `rd`.
///
/// The length is encoded in `N` bytes and those bytes are decoded by `into_size`,
/// and must be at least `min_size` bytes.
fn read_field<'a, const N: usize>(
rd: &mut untrusted::Reader<'a>,
into_size: fn([u8; N]) -> usize,
min_size: usize,
) -> Result<untrusted::Input<'a>, Error> {
let len = into_size(read_array::<N>(rd)?);
if len < min_size {
return Err(Error::MalformedSct);
}
rd.read_bytes(len).map_err(|_| Error::MalformedSct)
}

/// Read `N` bytes from `rd` as an array.
fn read_array<const N: usize>(rd: &mut untrusted::Reader<'_>) -> Result<[u8; N], Error> {
rd.read_bytes(N)
.map_err(|_| Error::MalformedSct)?
.as_slice_less_safe()
.try_into()
.map_err(|_| Error::MalformedSct)
}

fn u16_field_len(bytes: [u8; 2]) -> usize {
usize::from(u16::from_be_bytes(bytes))
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Error {
/// The SCT was somehow misencoded, truncated or otherwise corrupt.
MalformedSct,

/// An unsupported SCT version was encountered.
///
/// This library only supports `v1(0)`.
UnsupportedSctVersion,
}
30 changes: 30 additions & 0 deletions src/verify_cert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@ use alloc::vec::Vec;
use core::fmt;
use core::ops::ControlFlow;

#[cfg(feature = "alloc")]
use pki_types::SubjectPublicKeyInfoDer;
use pki_types::{CertificateDer, SignatureVerificationAlgorithm, TrustAnchor, UnixTime};

use crate::cert::Cert;
use crate::crl::RevocationOptions;
use crate::der::{self, FromDer};
use crate::end_entity::EndEntityCert;
use crate::error::Error;
use crate::sct::{self, LogId, SctParser, Timestamp};
use crate::{public_values_eq, subject_name};

// Use `'a` for lifetimes that we don't care about, `'p` for lifetimes that become a part of
Expand Down Expand Up @@ -195,6 +198,21 @@ impl<'p> VerifiedPath<'p> {
}
}

/// Returns the SCT logs that contributed to the SCTs included in the certificate.
///
/// Note this method does not verify the SCTs themselves, but does require that
/// the certificate chain was previously verified by the caller. This is demonstrated
/// by the `verified_path` parameter.
///
/// If the certificate does not contain an SCT extension, this method returns
/// `Ok(Vec::new())`.
pub fn sct_log_timestamps(
&self,
) -> Result<impl Iterator<Item = Result<(LogId, Timestamp), sct::Error>> + 'p, sct::Error> {
Ok(SctParser::new(self.end_entity.scts)?
.map(|sct| sct.map(|sct| (sct.log_id, sct.timestamp))))
}

/// Yields a (double-ended) iterator over the intermediate certificates in this path.
pub fn intermediate_certificates(&'p self) -> IntermediateIterator<'p> {
IntermediateIterator {
Expand All @@ -211,6 +229,18 @@ impl<'p> VerifiedPath<'p> {
pub fn anchor(&self) -> &'p TrustAnchor<'p> {
self.anchor
}

/// Get the `SubjectPublicKeyInfo` of the issuer of the end-entity certificate.
#[cfg(feature = "alloc")]
pub fn issuer_spki(&self) -> SubjectPublicKeyInfoDer<'p> {
match self.intermediate_certificates().next() {
Some(issuer) => issuer.subject_public_key_info(),
None => SubjectPublicKeyInfoDer::from(der::asn1_wrap(
crate::der::Tag::Sequence,
self.anchor.subject_public_key_info.as_ref(),
)),
}
}
}

/// Iterator over a path's intermediate certificates.
Expand Down
18 changes: 18 additions & 0 deletions src/x509.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,17 +106,35 @@ impl<'a> FromDer<'a> for DistributionPointName<'a> {
pub(crate) enum ExtensionOid {
/// Extensions whose OID is under `id-ce` arc.
Standard(u8),
/// The OID given by `SCT_LIST_OID`.
SignedCertificateTimestampList,
}

impl ExtensionOid {
fn lookup(id: untrusted::Input<'_>) -> Option<Self> {
match id.as_slice_less_safe() {
v if v == SCT_LIST_OID => Some(Self::SignedCertificateTimestampList),
[first, second, x] if [*first, *second] == ID_CE => Some(Self::Standard(*x)),
_ => None,
}
}
}

/// This is 1.3.6.1.4.1.11129.2.4.2, as defined in RFC6962
///
/// In full this is:
///
/// ```text
/// {iso(1) identified-organization(3) dod(6) internet(1)
/// private(4) enterprise(1) google(11129) 2 4 2}
/// ```
///
/// Note that the `oid!` macro doesn't work for OIDs with any
/// limb greater than 7 bits, so this is a manual expansion.
///
/// <https://datatracker.ietf.org/doc/html/rfc6962#section-3.3>
const SCT_LIST_OID: [u8; 10] = [40 + 3, 6, 1, 4, 1, 214, 121, 2, 4, 2];

/// ISO arc for standard certificate and CRL extensions.
///
/// ```text
Expand Down
Loading