Skip to content

Commit

Permalink
sign: init (#310)
Browse files Browse the repository at this point in the history
Add ability to sign artifacts à la sigstore-python, towards Bundle signing and verification.
Follow-up from #305, and the prerequisite motivation to #311.

Signed-off-by: Jack Leightcap <jack.leightcap@trailofbits.com>
Signed-off-by: Andrew Pan <andrew.pan@trailofbits.com>
Signed-off-by: Andrew Pan <3821575+tnytown@users.noreply.github.com>
Signed-off-by: Jack Leightcap <30168080+jleightcap@users.noreply.github.com>
Co-authored-by: Andrew Pan <a@tny.town>
Co-authored-by: Andrew Pan <3821575+tnytown@users.noreply.github.com>
Co-authored-by: Andrew Pan <andrew.pan@trailofbits.com>
Co-authored-by: Flavio Castelli <flavio@castelli.me>
  • Loading branch information
5 people authored Dec 21, 2023
1 parent 452b7d7 commit ea5d69b
Show file tree
Hide file tree
Showing 13 changed files with 1,054 additions and 191 deletions.
17 changes: 11 additions & 6 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"]
wasm = ["getrandom/js"]

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

tuf = ["tough", "regex"]

sign = []

cosign-native-tls = [
"oci-distribution/native-tls",
"cert",
Expand Down Expand Up @@ -72,7 +74,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,7 +90,7 @@ openidconnect = { version = "3.0", default-features = false, features = [
p256 = "0.13.2"
p384 = "0.13"
webbrowser = "0.8.4"
pem = "3.0"
pem = { version = "3.0", features = ["serde"] }
pkcs1 = { version = "0.7.5", features = ["std"] }
pkcs8 = { version = "0.10.2", features = [
"pem",
Expand All @@ -110,17 +112,21 @@ 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"] }
tokio-util = { version = "0.7.10", features = ["io-util"] }
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", features = ["alloc"] }
rustls-webpki = { version = "0.102.0-alpha.7", features = ["alloc"] }
rustls-pki-types = { version = "1.0.0", 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 @@ -134,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
35 changes: 35 additions & 0 deletions src/bundle/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// 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;

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

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

View workflow job for this annotation

GitHub Actions / Check WASM

variant `Bundle0_2` is never constructed
}

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_2 => "application/vnd.dev.sigstore.bundle+json;version=0.2",
})
}
}
2 changes: 1 addition & 1 deletion src/crypto/certificate_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ impl<'a> CertificatePool<'a> {
let cert_pem = pem::parse(cert_pem)?;
if cert_pem.tag() != "CERTIFICATE" {
return Err(SigstoreError::CertificatePoolError(
"PEM file is not a certificate",
"PEM file is not a certificate".into(),
));
}

Expand Down
38 changes: 36 additions & 2 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ pub enum SigstoreError {
#[error("invalid key format: {error}")]
InvalidKeyFormat { error: String },

#[error("Unable to parse identity token: {0}")]
IdentityTokenError(String),

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

Expand All @@ -70,6 +73,9 @@ pub enum SigstoreError {
#[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 @@ -101,7 +107,13 @@ pub enum SigstoreError {
CertificateWithIncompleteSubjectAlternativeName,

#[error("Certificate pool error: {0}")]
CertificatePoolError(&'static str),
CertificatePoolError(String),

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

#[error("Fulcio request unsuccessful: {0}")]
FulcioClientError(String),

#[error("Cannot fetch manifest of {image}: {error}")]
RegistryFetchManifestError { image: String, error: String },
Expand All @@ -115,9 +127,22 @@ pub enum SigstoreError {
#[error("Cannot push {image}: {error}")]
RegistryPushError { image: String, error: String },

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

#[error(transparent)]
JoinError(#[from] tokio::task::JoinError),

#[cfg(feature = "sign")]
#[error(transparent)]
ReqwestError(#[from] reqwest::Error),

#[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 All @@ -144,7 +169,7 @@ pub enum SigstoreError {
TufTargetNotFoundError(String),

#[error("{0}")]
TufMetadataError(&'static str),
TufMetadataError(String),

#[error(transparent)]
IOError(#[from] std::io::Error),
Expand All @@ -155,6 +180,9 @@ pub enum SigstoreError {
#[error("{0}")]
VerificationConstraintError(String),

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

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

Expand Down Expand Up @@ -214,4 +242,10 @@ pub enum SigstoreError {

#[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),
}
90 changes: 89 additions & 1 deletion src/fulcio/mod.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
mod models;

pub mod oauth;

use crate::crypto::signing_key::SigStoreSigner;
use crate::crypto::SigningScheme;
use crate::errors::{Result, SigstoreError};
use crate::fulcio::models::{CreateSigningCertificateRequest, SigningCertificate};
use crate::fulcio::oauth::OauthTokenProvider;
use crate::oauth::IdentityToken;
use base64::{engine::general_purpose::STANDARD as BASE64_STD_ENGINE, Engine as _};
use openidconnect::core::CoreIdToken;
use reqwest::Body;
use pkcs8::der::Decode;
use reqwest::{header, Body};
use serde::ser::SerializeStruct;
use serde::{Serialize, Serializer};
use std::convert::{TryFrom, TryInto};
use std::fmt::{Debug, Display, Formatter};
use tracing::debug;
use url::Url;
use x509_cert::Certificate;

pub use models::CertificateResponse;

/// Default public Fulcio server root.
pub const FULCIO_ROOT: &str = "https://fulcio.sigstore.dev/";

/// Path within Fulcio to obtain a signing certificate.
pub const SIGNING_CERT_PATH: &str = "api/v1/signingCert";
pub const SIGNING_CERT_V2_PATH: &str = "api/v2/signingCert";

const CONTENT_TYPE_HEADER_NAME: &str = "content-type";

Expand Down Expand Up @@ -191,4 +201,82 @@ impl FulcioClient {

Ok((signer, FulcioCert(cert)))
}

/// Request a certificate from Fulcio with the V2 endpoint.
///
/// TODO(tnytown): This (and other API clients) should be autogenerated. See sigstore-rs#209.
///
/// https://github.com/sigstore/fulcio/blob/main/fulcio.proto
///
/// Additionally, it might not be reasonable to expect callers to correctly construct and pass
/// in an X509 CSR.
pub async fn request_cert_v2(
&self,
request: x509_cert::request::CertReq,
identity: &IdentityToken,
) -> Result<CertificateResponse> {
let client = reqwest::Client::new();

macro_rules! headers {
($($key:expr => $val:expr),+) => {
{
let mut map = reqwest::header::HeaderMap::new();
$( map.insert($key, $val.parse().unwrap()); )+
map
}
}
}
let headers = headers!(
header::AUTHORIZATION => format!("Bearer {}", identity.to_string()),
header::CONTENT_TYPE => "application/json",
header::ACCEPT => "application/pem-certificate-chain"
);

let response: SigningCertificate = client
.post(self.root_url.join(SIGNING_CERT_V2_PATH)?)
.headers(headers)
.json(&CreateSigningCertificateRequest {
certificate_signing_request: request,
})
.send()
.await?
.json()
.await?;

let sct_embedded = matches!(
response,
SigningCertificate::SignedCertificateEmbeddedSct(_)
);
let certs = match response {
SigningCertificate::SignedCertificateDetachedSct(ref sc) => &sc.chain.certificates,
SigningCertificate::SignedCertificateEmbeddedSct(ref sc) => &sc.chain.certificates,
};

if certs.len() < 2 {
return Err(SigstoreError::FulcioClientError(
"Certificate chain too short: certs.len() < 2".into(),
));
}

let cert = Certificate::from_der(certs[0].contents())?;
let chain = certs[1..]
.iter()
.map(|pem| Certificate::from_der(pem.contents()))
.collect::<std::result::Result<Vec<_>, _>>()?;

// TODO(tnytown): Implement SCT extraction.
// see: https://github.com/RustCrypto/formats/pull/1134
if sct_embedded {
debug!("PrecertificateSignedCertificateTimestamps isn't implemented yet in x509_cert.");
} else {
// No embedded SCT, Fulcio instance that provides detached SCT:
if let SigningCertificate::SignedCertificateDetachedSct(_sct) = response {}
};

Ok(CertificateResponse {
cert,
chain,
// sct,
})
}
}
Loading

0 comments on commit ea5d69b

Please sign in to comment.