Skip to content

Commit

Permalink
verify: init
Browse files Browse the repository at this point in the history
Signed-off-by: Jack Leightcap <jack.leightcap@trailofbits.com>
  • Loading branch information
jleightcap committed Jan 5, 2024
1 parent 315d5f6 commit 5c1792a
Show file tree
Hide file tree
Showing 41 changed files with 1,906 additions and 549 deletions.
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ readme = "README.md"
repository = "https://github.com/sigstore/sigstore-rs"

[features]
default = ["full-native-tls", "cached-client", "tuf", "sign"]
default = ["full-native-tls", "cached-client", "tuf", "sign", "verify"]
wasm = ["getrandom/js"]

full-native-tls = [
Expand Down Expand Up @@ -43,6 +43,7 @@ rekor = ["reqwest"]
tuf = ["tough", "regex"]

sign = []
verify = []

cosign-native-tls = [
"oci-distribution/native-tls",
Expand Down
28 changes: 26 additions & 2 deletions src/bundle/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,45 @@
//! Useful types for Sigstore bundles.

use std::fmt::Display;
use std::str::FromStr;

pub use sigstore_protobuf_specs::Bundle;

macro_rules! required {

Check warning on line 22 in src/bundle/mod.rs

View workflow job for this annotation

GitHub Actions / Check WASM

unused macro definition: `required`
($($base:expr )? ; $first_attr:ident $( . $rest_attrs:ident)* $( , $else_err:expr)?) => {
$( $base . )? $first_attr.as_ref()
$(
.and_then(|v| v.$rest_attrs.as_ref())
)*
$( .ok_or($else_err) )?
}
}
pub(crate) use required;

Check warning on line 31 in src/bundle/mod.rs

View workflow job for this annotation

GitHub Actions / Check WASM

unused import: `required`

// Known Sigstore bundle media types.
#[derive(Clone, Copy, Debug)]
pub enum Version {
_Bundle0_1,
Bundle0_1,
Bundle0_2,
}

impl Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match &self {
Version::_Bundle0_1 => "application/vnd.dev.sigstore.bundle+json;version=0.1",
Version::Bundle0_1 => "application/vnd.dev.sigstore.bundle+json;version=0.1",
Version::Bundle0_2 => "application/vnd.dev.sigstore.bundle+json;version=0.2",
})
}
}

impl FromStr for Version {
type Err = ();

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"application/vnd.dev.sigstore.bundle+json;version=0.1" => Ok(Version::Bundle0_1),
"application/vnd.dev.sigstore.bundle+json;version=0.2" => Ok(Version::Bundle0_2),
_ => Err(()),
}
}
}
143 changes: 142 additions & 1 deletion src/crypto/certificate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
use chrono::{DateTime, NaiveDateTime, Utc};
use const_oid::db::rfc5912::ID_KP_CODE_SIGNING;
use x509_cert::{
ext::pkix::{ExtendedKeyUsage, KeyUsage, KeyUsages, SubjectAltName},
ext::pkix::{constraints, ExtendedKeyUsage, KeyUsage, KeyUsages, SubjectAltName},
Certificate,
};

Expand Down Expand Up @@ -120,6 +120,147 @@ fn verify_expiration(certificate: &Certificate, integrated_time: i64) -> Result<
Ok(())
}

/// Check if the given certificate is a leaf in the context of the Sigstore profile.
///
/// * It is not a root or intermediate CA;
/// * It has `keyUsage.digitalSignature`
/// * It has `CODE_SIGNING` as an `ExtendedKeyUsage`.
///
/// This function does not evaluate the trustworthiness of the certificate.
pub(crate) fn is_leaf(certificate: &Certificate) -> Result<bool> {
// NOTE(jl): following structure of sigstore-python over the slightly different handling found
// in `verify_key_usages`.
let tbs = &certificate.tbs_certificate;

// Only V3 certificates should appear in the context of Sigstore; earlier versions of X.509 lack
// extensions and have ambiguous CA behavior.
if tbs.version != x509_cert::Version::V3 {
return Err(SigstoreError::CertificateUnsupportedVersionError);
}

if is_ca(certificate)? {
return Ok(false);
};

let digital_signature = match tbs.get::<KeyUsage>()? {
None => {
return Err(SigstoreError::InvalidCertError(
"invalid X.509 certificate: missing KeyUsage".to_string(),
))
}
Some((_, key_usage)) => key_usage.digital_signature(),
};

if !digital_signature {
return Err(SigstoreError::InvalidCertError(
"invalid certificate for Sigstore purposes: missing digital signature usage"
.to_string(),
));
}

// Finally, we check to make sure the leaf has an `ExtendedKeyUsages`
// extension that includes a codesigning entitlement. Sigstore should
// never issue a leaf that doesn't have this extended usage.

let extended_key_usage = match tbs.get::<ExtendedKeyUsage>()? {
None => {
return Err(SigstoreError::InvalidCertError(
"invalid X.509 certificate: missing ExtendedKeyUsage".to_string(),
))
}
Some((_, extended_key_usage)) => extended_key_usage,
};

Ok(extended_key_usage.0.contains(&ID_KP_CODE_SIGNING))
}

/// Checks if the given `certificate` is a CA certificate.
///
/// This does **not** indicate trustworthiness of the given `certificate`, only if it has the
/// appropriate interior state.
///
/// This function is **not** naively invertible: users **must** use the dedicated `is_leaf`
/// utility function to determine whether a particular leaf upholds Sigstore's invariants.
pub(crate) fn is_ca(certificate: &Certificate) -> Result<bool> {
let tbs = &certificate.tbs_certificate;

// Only V3 certificates should appear in the context of Sigstore; earlier versions of X.509 lack
// extensions and have ambiguous CA behavior.
if tbs.version != x509_cert::Version::V3 {
return Err(SigstoreError::CertificateUnsupportedVersionError);
}

// Valid CA certificates must have the following set:
//
// - `BasicKeyUsage.keyCertSign`
// - `BasicConstraints.ca`
//
// Any other combination of states is inconsistent and invalid, meaning
// that we won't consider the certificate a valid non-CA leaf.

let ca = match tbs.get::<constraints::BasicConstraints>()? {
None => return Ok(false),
Some((false, _)) => {
// BasicConstraints must be marked as critical, per RFC 5280 4.2.1.9.
return Err(SigstoreError::InvalidCertError(
"invalid X.509 certificate: non-critical BasicConstraints in CA".to_string(),
));
}
Some((true, v)) => v.ca,
};

let key_cert_sign = match tbs.get::<KeyUsage>()? {
None => {
return Err(SigstoreError::InvalidCertError(
"invalid X.509 certificate: missing KeyUsage".to_string(),
))
}
Some((_, v)) => v.key_cert_sign(),
};

// both states set, this is a CA.
if ca && key_cert_sign {
return Ok(true);
}

if !(ca || key_cert_sign) {
return Ok(false);
}

// Anything else is an invalid state that should never occur.
Err(SigstoreError::InvalidCertError(format!(
"invalid certificate states: KeyUsage.keyCertSign={}, BasicConstraints.ca={}",
key_cert_sign, ca
)))
}

/// Returns `True` if and only if the given `Certificate` indicates
/// that it's a root CA.
///
/// This is **not** a verification function, and it does not establish
/// the trustworthiness of the given certificate.
pub(crate) fn is_root_ca(certificate: &Certificate) -> Result<bool> {
// NOTE(ww): This function is obnoxiously long to make the different
// states explicit.

let tbs = &certificate.tbs_certificate;

// Only V3 certificates should appear in the context of Sigstore; earlier versions of X.509 lack
// extensions and have ambiguous CA behavior.
if tbs.version != x509_cert::Version::V3 {
return Err(SigstoreError::CertificateUnsupportedVersionError);
}

// Non-CAs can't possibly be root CAs.
if !is_ca(certificate)? {
return Ok(false);
}

// A certificate that is its own issuer and signer is considered a root CA.
// TODO(jl): verify_directly_issued_by
todo!()
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
2 changes: 2 additions & 0 deletions src/crypto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ pub enum Signature<'a> {
pub(crate) mod certificate;
#[cfg(feature = "cert")]
pub(crate) mod certificate_pool;
#[cfg(feature = "cert")]
pub(crate) use certificate_pool::CertificatePool;

pub mod verification_key;

Expand Down
8 changes: 8 additions & 0 deletions src/crypto/verification.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use rustls_pki_types::CertificateDer;
use webpki::TrustAnchor;

/// Machinery for Sigstore end entity certificate verification.
struct CertificateVerificationContext<'a> {
pub trust_anchors: Vec<TrustAnchor<'a>>,
pub intermediate_certs: Vec<CertificateDer<'a>>,
}
64 changes: 63 additions & 1 deletion src/crypto/verification_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use const_oid::db::rfc5912::{ID_EC_PUBLIC_KEY, RSA_ENCRYPTION};
use ed25519::pkcs8::DecodePublicKey as ED25519DecodePublicKey;
use rsa::{pkcs1v15, pss};
use sha2::{Digest, Sha256, Sha384};
use signature::{DigestVerifier, Verifier};
use signature::{hazmat::PrehashVerifier, DigestVerifier, Verifier};
use std::convert::TryFrom;
use x509_cert::{der::referenced::OwnedToRef, spki::SubjectPublicKeyInfoOwned};

Expand Down Expand Up @@ -329,6 +329,68 @@ impl CosignVerificationKey {
}
}
}

/// Verify the signature provided has been actually generated by the given key
/// when signing the provided prehashed message.
pub fn verify_prehash(&self, signature: Signature, msg: &[u8]) -> Result<()> {
let sig = match signature {
Signature::Raw(data) => data.to_owned(),
Signature::Base64Encoded(data) => BASE64_STD_ENGINE.decode(data)?,
};

match self {
CosignVerificationKey::RSA_PSS_SHA256(inner) => {
let sig = pss::Signature::try_from(sig.as_slice())?;
inner
.verify_prehash(msg, &sig)
.map_err(|_| SigstoreError::PublicKeyVerificationError)
}
CosignVerificationKey::RSA_PSS_SHA384(inner) => {
let sig = pss::Signature::try_from(sig.as_slice())?;
inner
.verify_prehash(msg, &sig)
.map_err(|_| SigstoreError::PublicKeyVerificationError)
}
CosignVerificationKey::RSA_PSS_SHA512(inner) => {
let sig = pss::Signature::try_from(sig.as_slice())?;
inner
.verify_prehash(msg, &sig)
.map_err(|_| SigstoreError::PublicKeyVerificationError)
}
CosignVerificationKey::RSA_PKCS1_SHA256(inner) => {
let sig = pkcs1v15::Signature::try_from(sig.as_slice())?;
inner
.verify_prehash(msg, &sig)
.map_err(|_| SigstoreError::PublicKeyVerificationError)
}
CosignVerificationKey::RSA_PKCS1_SHA384(inner) => {
let sig = pkcs1v15::Signature::try_from(sig.as_slice())?;
inner
.verify_prehash(msg, &sig)
.map_err(|_| SigstoreError::PublicKeyVerificationError)
}
CosignVerificationKey::RSA_PKCS1_SHA512(inner) => {
let sig = pkcs1v15::Signature::try_from(sig.as_slice())?;
inner
.verify_prehash(msg, &sig)
.map_err(|_| SigstoreError::PublicKeyVerificationError)
}
// ECDSA signatures are encoded in der.
CosignVerificationKey::ECDSA_P256_SHA256_ASN1(inner) => {
let sig = ecdsa::Signature::from_der(&sig)?;
inner
.verify_prehash(msg, &sig)
.map_err(|_| SigstoreError::PublicKeyVerificationError)
}
CosignVerificationKey::ECDSA_P384_SHA384_ASN1(inner) => {
let sig = ecdsa::Signature::from_der(&sig)?;
inner
.verify_prehash(msg, &sig)
.map_err(|_| SigstoreError::PublicKeyVerificationError)
}
_ => unimplemented!("Ed25519 doesn't implement verify_prehash"),
}
}
}

#[cfg(test)]
Expand Down
3 changes: 3 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ pub enum SigstoreError {
#[error("Certificate pool error: {0}")]
CertificatePoolError(String),

#[error("Certificate invalid: {0}")]
InvalidCertError(String),

#[error("Signing session expired")]
ExpiredSigningSession(),

Expand Down
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,5 +288,8 @@ pub mod tuf;
mod bundle;
pub use bundle::Bundle;

#[cfg(feature = "verify")]
pub mod verify;

#[cfg(feature = "sign")]
pub mod sign;
1 change: 1 addition & 0 deletions src/tuf/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ macro_rules! tuf_resource {
}

pub(crate) const SIGSTORE_ROOT: &[u8] = tuf_resource!("prod/root.json");
pub(crate) const _SIGSTORE_TRUST_BUNDLE: &[u8] = tuf_resource!("prod/trusted_root.json");
24 changes: 24 additions & 0 deletions src/verify/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// Copyright 2023 The Sigstore Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Verifier for Sigstore bundles and associated types and policies.
mod models;
pub use models::{VerificationError, VerificationMaterials, VerificationResult};

pub mod policy;
pub use policy::VerificationPolicy;

mod verifier;
pub use verifier::Verifier;
Loading

0 comments on commit 5c1792a

Please sign in to comment.