Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sign/verify: add bundle support #309

Closed
wants to merge 2 commits into from
Closed
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
18 changes: 10 additions & 8 deletions 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"]
default = ["full-native-tls", "cached-client", "tuf", "sign", "verify"]
wasm = ["getrandom/js"]

full-native-tls = [
Expand Down Expand Up @@ -42,6 +42,9 @@ rekor = ["reqwest"]

tuf = ["tough", "regex"]

sign = []
verify = []

cosign-native-tls = [
"oci-distribution/native-tls",
"cert",
Expand Down Expand Up @@ -72,7 +75,7 @@ async-trait = "0.1.52"
base64 = "0.21.0"
cached = { version = "0.46.0", optional = true, features = ["async"] }
cfg-if = "1.0.0"
chrono = { version = "0.4.27", default-features = false }
chrono = { version = "0.4.27", default-features = false, features = ["serde"] }
const-oid = "0.9.1"
digest = { version = "0.10.3", default-features = false }
ecdsa = { version = "0.16.7", features = ["pkcs8", "digest", "der", "signing"] }
Expand All @@ -88,10 +91,7 @@ openidconnect = { version = "3.0", default-features = false, features = [
p256 = "0.13.2"
p384 = "0.13"
webbrowser = "0.8.4"
pem = "3.0"
picky = { version = "7.0.0-rc.8", default-features = false, features = [
"x509",
] }
pem = { version = "3.0", features = ["serde"] }
pkcs1 = { version = "0.7.5", features = ["std"] }
pkcs8 = { version = "0.10.2", features = [
"pem",
Expand All @@ -113,17 +113,20 @@ serde_json = "1.0.79"
serde_with = { version = "3.4.0", features = ["base64"] }
sha2 = { version = "0.10.6", features = ["oid"] }
signature = { version = "2.0" }
sigstore_protobuf_specs = "0.1.0-rc.2"
thiserror = "1.0.30"
tokio = { version = "1.17.0", features = ["rt"] }
tough = { version = "0.14", features = ["http"], optional = true }
tracing = "0.1.31"
url = "2.2.2"
x509-cert = { version = "0.2.2", features = ["pem", "std"] }
x509-cert = { version = "0.2.2", features = ["builder", "pem", "std"] }
crypto_secretbox = "0.1.1"
zeroize = "1.5.7"
rustls-webpki = { version = "0.102.0-alpha.4", features = ["alloc"] }
rustls-pki-types = { version = "0.2.1", features = ["std"] }
serde_repr = "0.1.16"
hex = "0.4.3"
json-syntax = { version = "0.9.6", features = ["canonicalize", "serde"] }

[dev-dependencies]
anyhow = { version = "1.0", features = ["backtrace"] }
Expand All @@ -137,7 +140,6 @@ serial_test = "2.0.0"
tempfile = "3.3.0"
testcontainers = "0.15"
tracing-subscriber = { version = "0.3.9", features = ["env-filter"] }
hex = "0.4.3"

# cosign example mappings

Expand Down
65 changes: 65 additions & 0 deletions src/bundle/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// 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.

//! Useful types for Sigstore bundles.

use std::fmt::Display;

pub use sigstore_protobuf_specs::Bundle;

macro_rules! required {

Check warning on line 21 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 30 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_2,
}

impl TryFrom<&str> for Version {
type Error = ();

fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"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(()),
}
}
}

impl From<Version> for &str {
fn from(value: Version) -> Self {
match value {
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 Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str((*self).into())?;
Ok(())
}
}
28 changes: 28 additions & 0 deletions src/crypto/certificate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,34 @@ 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<()> {
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);
}

// TODO(tnytown): cert_is_ca

verify_key_usages(certificate)?;

Ok(())
}

pub(crate) fn is_root_ca(_certificate: &Certificate) -> Result<()> {
// TODO(tnytown)
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
12 changes: 12 additions & 0 deletions src/crypto/verification.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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>>,
}

impl CertificateVerificationContext<'_> {
pub fn new() {}
}
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
30 changes: 30 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@
#[error("invalid key format: {error}")]
InvalidKeyFormat { error: String },

#[error("Unable to parse identity token: {0}")]
IdentityTokenError(&'static str),

#[error("unmatched key type {key_typ} and signing scheme {scheme}")]
UnmatchedKeyAndSigningScheme { key_typ: String, scheme: String },

Expand All @@ -70,6 +73,9 @@
#[error("Public key verification error")]
PublicKeyVerificationError,

#[error("X.509 certificate version is not V3")]
CertificateUnsupportedVersionError,

#[error("Certificate validity check failed: cannot be used before {0}")]
CertificateValidityError(String),

Expand Down Expand Up @@ -103,6 +109,12 @@
#[error("Certificate pool error: {0}")]
CertificatePoolError(&'static str),

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

#[error("Fulcio request unsuccessful: {0}")]
FulcioClientError(&'static str),

#[error("Cannot fetch manifest of {image}: {error}")]
RegistryFetchManifestError { image: String, error: String },

Expand All @@ -115,9 +127,18 @@
#[error("Cannot push {image}: {error}")]
RegistryPushError { image: String, error: String },

#[error("Rekor request unsuccessful: {0}")]
RekorClientError(String),

#[error(transparent)]
ReqwestError(#[from] reqwest::Error),

Check failure on line 134 in src/errors.rs

View workflow job for this annotation

GitHub Actions / Check WASM

failed to resolve: use of undeclared crate or module `reqwest`

#[error("OCI reference not valid: {reference}")]
OciReferenceNotValidError { reference: String },

#[error("Sigstore bundle malformed: {0}")]
SigstoreBundleMalformedError(String),

#[error("Layer doesn't have Sigstore media type")]
SigstoreMediaTypeNotFoundError,

Expand Down Expand Up @@ -155,6 +176,9 @@
#[error("{0}")]
VerificationConstraintError(String),

#[error("{0}")]
VerificationMaterialError(String),

#[error("{0}")]
ApplyConstraintError(String),

Expand Down Expand Up @@ -214,4 +238,10 @@

#[error(transparent)]
Ed25519PKCS8Error(#[from] ed25519_dalek::pkcs8::spki::Error),

#[error(transparent)]
X509ParseError(#[from] x509_cert::der::Error),

#[error(transparent)]
X509BuilderError(#[from] x509_cert::builder::Error),
}
Loading
Loading