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: init #310

Merged
merged 13 commits into from
Dec 21, 2023
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()),
jleightcap marked this conversation as resolved.
Show resolved Hide resolved
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 {}
};
Comment on lines +267 to +274
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Detached SCT verification is currently blocked by the linked issue. @tnytown mentioned having the availability to push that issue forward (CC @woodruffw)

Copy link
Member

Choose a reason for hiding this comment

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

Assume you meant embedded SCT verification, since that's the missing branch here 🙂

But yeah, we should get that in; xref RustCrypto/formats#1134


Ok(CertificateResponse {
cert,
chain,
// sct,
jleightcap marked this conversation as resolved.
Show resolved Hide resolved
})
}
}
Loading
Loading