diff --git a/Cargo.toml b/Cargo.toml index 22eea5a2ad..c34e6ec81f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,9 +10,11 @@ license = "Apache-2.0" readme = "README.md" [features] -default = ["native-tls"] +default = ["native-tls", "test-registry"] native-tls = ["oci-distribution/native-tls", "openidconnect/native-tls", "reqwest/native-tls"] rustls-tls = ["oci-distribution/rustls-tls", "openidconnect/rustls-tls", "reqwest/rustls-tls"] +# This features is used by tests that use docker to create a registry +test-registry = [] [dependencies] async-trait = "0.1.52" @@ -57,11 +59,13 @@ anyhow = { version = "1.0", features = ["backtrace"] } assert-json-diff = "2.0.2" chrono = "0.4.20" clap = { version = "4.0.8", features = ["derive"] } +docker_credential = { git = "https://github.com/Xynnn007/docker_credential", rev = "f350805"} openssl = "0.10.38" rstest = "0.15.0" tempfile = "3.3.0" tracing-subscriber = { version = "0.3.9", features = ["env-filter"] } - +testcontainers = "0.14" +serial_test = "0.9.0" # cosign example mappings @@ -69,6 +73,10 @@ tracing-subscriber = { version = "0.3.9", features = ["env-filter"] } name = "verify" path = "examples/cosign/verify/main.rs" +[[example]] +name = "sign" +path = "examples/cosign/sign/main.rs" + # openidconnect example mappings [[example]] diff --git a/examples/cosign/sign/README.md b/examples/cosign/sign/README.md new file mode 100644 index 0000000000..44d5e4528e --- /dev/null +++ b/examples/cosign/sign/README.md @@ -0,0 +1,57 @@ +This is a simple example program that shows how perform cosign signing. + +The program allows also to use annotation, in the same way as `cosign sign -a key=value` +does. + +The program prints to the standard output all the Simple Signing objects that +have been successfully pushed. + +# Key based Signing + +The implementation is in [main.rs](./main.rs). + +Create a keypair using the official cosign client: + +```console +cosign generate-key-pair +``` + +Because the default key pair generated by cosign is `ECDSA_P256` key, +so we choose to use `ECDSA_P256_SHA256_ASN1` as the signing scheme. +Suppose the password used to encrypt the private key is `123`, and the target +image to be signed is `172.17.0.2:5000/ubuntu` + +Also, let us the annotation `a=1`. + +Sign a container image: + +```console +cargo run --example sign -- \ + --key cosign.key \ + --image 172.17.0.2:5000/ubuntu \ + --signing-scheme ECDSA_P256_SHA256_ASN1 \ + --password 123 \ + --verbose \ + --http \ + --annotations a=1 +``` + +Then the image will be signed. + +Let us then verify it. + +1. Using `cosign` (golang version) +```console +cosign verify --key cosign.pub \ + -a a=1 \ + 172.17.0.2:5000/ubuntu +``` + +2. Or use `sigstore-rs` +```console +cargo run --example verify -- \ + --key cosign.pub \ + --annotations a=1 \ + --http \ + 172.17.0.2:5000/ubuntu +``` \ No newline at end of file diff --git a/examples/cosign/sign/main.rs b/examples/cosign/sign/main.rs new file mode 100644 index 0000000000..462723df31 --- /dev/null +++ b/examples/cosign/sign/main.rs @@ -0,0 +1,185 @@ +// +// Copyright 2021 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. + +use docker_credential::{CredentialRetrievalError, DockerCredential}; +use oci_distribution::Reference; +use sigstore::cosign::constraint::{AnnotationMarker, PrivateKeySigner}; +use sigstore::cosign::{Constraint, CosignCapabilities, SignatureLayer}; +use sigstore::crypto::SigningScheme; +use sigstore::registry::{Auth, ClientConfig, ClientProtocol}; +use std::convert::TryFrom; +use tracing::{debug, warn}; +use zeroize::Zeroizing; + +extern crate anyhow; +use anyhow::anyhow; + +extern crate clap; +use clap::Parser; + +use std::{collections::HashMap, fs}; + +extern crate tracing_subscriber; +use tracing_subscriber::prelude::*; +use tracing_subscriber::{fmt, EnvFilter}; + +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct Cli { + /// Verification key + #[clap(short, long, required(false))] + key: String, + + /// Signing scheme when signing and verifying + #[clap(long, required(false))] + signing_scheme: Option, + + /// Password used to decrypt private key + #[clap(long, required(false))] + password: Option, + + /// Annotations that have to be satisfied + #[clap(short, long, required(false))] + annotations: Vec, + + /// Enable verbose mode + #[clap(short, long)] + verbose: bool, + + /// Name of the image to verify + #[clap(short, long)] + image: String, + + /// Whether the registry uses HTTP + #[clap(long)] + http: bool, +} + +async fn run_app(cli: &Cli) -> anyhow::Result<()> { + let auth = &sigstore::registry::Auth::Anonymous; + + let mut oci_client_config = ClientConfig::default(); + match cli.http { + false => oci_client_config.protocol = ClientProtocol::Https, + true => oci_client_config.protocol = ClientProtocol::Http, + } + + let client_builder = + sigstore::cosign::ClientBuilder::default().with_oci_client_config(oci_client_config); + let mut client = client_builder.build()?; + + let image: &str = cli.image.as_str(); + + let (cosign_signature_image, source_image_digest) = client.triangulate(image, auth).await?; + debug!(cosign_signature_image= ?cosign_signature_image, source_image_digest= ?source_image_digest); + + let mut signature_layer = SignatureLayer::new_unsigned(image, &source_image_digest)?; + + // Try to get the auth of the image reference + let reference = + Reference::try_from(cosign_signature_image.clone()).expect("build image Reference failed"); + let auth = build_auth(&reference); + debug!(auth = ?auth, "use auth"); + + if !cli.annotations.is_empty() { + let mut values: HashMap = HashMap::new(); + for annotation in &cli.annotations { + let tmp: Vec<_> = annotation.splitn(2, '=').collect(); + if tmp.len() == 2 { + values.insert(String::from(tmp[0]), String::from(tmp[1])); + } + } + if !values.is_empty() { + let annotations_marker = AnnotationMarker { + annotations: values, + }; + annotations_marker + .add_constraint(&mut signature_layer) + .expect("add annotations failed"); + } + } + + let key = Zeroizing::new(fs::read(&cli.key).map_err(|e| anyhow!("Cannot read key: {:?}", e))?); + + let signing_scheme = if let Some(ss) = &cli.signing_scheme { + &ss[..] + } else { + "ECDSA_P256_SHA256_ASN1" + }; + let signing_scheme = SigningScheme::try_from(signing_scheme).map_err(anyhow::Error::msg)?; + let password = Zeroizing::new(cli.password.clone().unwrap_or_default().as_bytes().to_vec()); + + let signer = PrivateKeySigner::new_with_raw(key, password, &signing_scheme) + .map_err(|e| anyhow!("Cannot create private key signer: {}", e))?; + + signer + .add_constraint(&mut signature_layer) + .expect("sign image failed"); + + // Suppose there is only one SignatureLayer in the cosign image + client + .push_signature(None, &auth, &cosign_signature_image, vec![signature_layer]) + .await?; + Ok(()) +} + +/// This function helps to get the auth of the given image reference. +/// Now only `UsernamePassword` and `Anonymous` is supported. If an +/// `IdentityToken` is found, this function will return an `Anonymous` +/// auth. +/// +/// Any error will return an `Anonymous`. +fn build_auth(reference: &oci_distribution::Reference) -> Auth { + let server = reference + .resolve_registry() + .strip_suffix('/') + .unwrap_or_else(|| reference.resolve_registry()); + match docker_credential::get_credential(server) { + Err(CredentialRetrievalError::ConfigNotFound) => Auth::Anonymous, + Err(CredentialRetrievalError::NoCredentialConfigured) => Auth::Anonymous, + Err(e) => { + warn!("Error handling docker configuration file: {}", e); + Auth::Anonymous + } + Ok(DockerCredential::UsernamePassword(username, password)) => { + debug!("Found docker credentials"); + Auth::Basic(username, password) + } + Ok(DockerCredential::IdentityToken(_)) => { + warn!("Cannot use contents of docker config, identity token not supported. Using anonymous auth"); + Auth::Anonymous + } + } +} + +#[tokio::main] +pub async fn main() { + let cli = Cli::parse(); + + // setup logging + let level_filter = if cli.verbose { "debug" } else { "info" }; + let filter_layer = EnvFilter::new(level_filter); + tracing_subscriber::registry() + .with(filter_layer) + .with(fmt::layer().with_writer(std::io::stderr)) + .init(); + + match run_app(&cli).await { + Ok(_) => println!("Costraints successfully applied"), + Err(err) => { + eprintln!("Image signing failed: {:?}", err); + } + } +} diff --git a/examples/cosign/verify/main.rs b/examples/cosign/verify/main.rs index 096bb72ce6..f5a44f75e9 100644 --- a/examples/cosign/verify/main.rs +++ b/examples/cosign/verify/main.rs @@ -21,6 +21,7 @@ use sigstore::cosign::verification_constraint::{ use sigstore::cosign::{CosignCapabilities, SignatureLayer}; use sigstore::crypto::SigningScheme; use sigstore::errors::SigstoreVerifyConstraintsError; +use sigstore::registry::{ClientConfig, ClientProtocol}; use sigstore::tuf::SigstoreRepository; use std::boxed::Box; use std::convert::TryFrom; @@ -101,6 +102,10 @@ struct Cli { /// Name of the image to verify image: String, + + /// Whether the registry uses HTTP + #[clap(long)] + http: bool, } async fn run_app( @@ -120,7 +125,14 @@ async fn run_app( let auth = &sigstore::registry::Auth::Anonymous; - let mut client_builder = sigstore::cosign::ClientBuilder::default(); + let mut oci_client_config = ClientConfig::default(); + match cli.http { + false => oci_client_config.protocol = ClientProtocol::Https, + true => oci_client_config.protocol = ClientProtocol::Http, + } + + let mut client_builder = + sigstore::cosign::ClientBuilder::default().with_oci_client_config(oci_client_config); if let Some(key) = frd.rekor_pub_key.as_ref() { client_builder = client_builder.with_rekor_pub_key(key); diff --git a/src/cosign/client.rs b/src/cosign/client.rs index bcb1cf7219..add9267e38 100644 --- a/src/cosign/client.rs +++ b/src/cosign/client.rs @@ -13,21 +13,26 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::collections::HashMap; + use async_trait::async_trait; +use oci_distribution::manifest::OCI_IMAGE_MEDIA_TYPE; +use tracing::warn; -use super::{ - constants::SIGSTORE_OCI_MEDIA_TYPE, - signature_layers::{build_signature_layers, SignatureLayer}, - CosignCapabilities, -}; +use super::constants::{SIGSTORE_OCI_MEDIA_TYPE, SIGSTORE_SIGNATURE_ANNOTATION}; +use super::{CosignCapabilities, SignatureLayer}; +use crate::cosign::signature_layers::build_signature_layers; use crate::crypto::CosignVerificationKey; -use crate::registry::Auth; +use crate::registry::{Auth, PushResponse}; use crate::{ crypto::certificate_pool::CertificatePool, errors::{Result, SigstoreError}, }; use tracing::debug; +/// Used to generate an empty [OCI Configuration](https://github.com/opencontainers/image-spec/blob/v1.0.0/config.md). +pub const CONFIG_DATA: &str = "{}"; + /// Cosign Client /// /// Instances of `Client` can be built via [`sigstore::cosign::ClientBuilder`](crate::cosign::ClientBuilder). @@ -95,6 +100,58 @@ impl CosignCapabilities for Client { debug!(signature_layers=?sl, ?cosign_image, "trusted signature layers"); Ok(sl) } + + async fn push_signature( + &mut self, + annotations: Option>, + auth: &Auth, + target_reference: &str, + signature_layers: Vec, + ) -> Result { + let image_reference: oci_distribution::Reference = + target_reference + .parse() + .map_err(|_| SigstoreError::OciReferenceNotValidError { + reference: target_reference.to_string(), + })?; + + let layers: Vec = signature_layers + .iter() + .filter_map(|sl| { + match serde_json::to_vec(&sl.simple_signing) { + Ok(data) => { + let annotations = match &sl.signature { + Some(sig) => [(SIGSTORE_SIGNATURE_ANNOTATION.into(), sig.clone())].into(), + None => HashMap::new(), + }; + let image_layer = oci_distribution::client::ImageLayer::new(data, SIGSTORE_OCI_MEDIA_TYPE.into(), Some(annotations)); + Some(image_layer) + } + Err(e) => { + warn!(error = ?e, signaturelayer = ?sl, "Skipping SignatureLayer because serialization failed"); + None + } + } + }) + .collect(); + + // TODO: Do we need to support OCI Image Configuration? + let config = + oci_distribution::client::Config::oci_v1(CONFIG_DATA.as_bytes().to_vec(), None); + let mut manifest = + oci_distribution::manifest::OciImageManifest::build(&layers[..], &config, annotations); + manifest.media_type = Some(OCI_IMAGE_MEDIA_TYPE.to_string()); + self.registry_client + .push( + &image_reference, + &layers[..], + config, + &auth.into(), + Some(manifest), + ) + .await + .map(|r| r.into()) + } } impl Client { @@ -162,6 +219,7 @@ mod tests { fetch_manifest_digest_response: Some(Ok(image_digest.clone())), pull_response: None, pull_manifest_response: None, + push_response: None, }; let mut cosign_client = build_test_client(mock_client); diff --git a/src/cosign/constraint/annotation.rs b/src/cosign/constraint/annotation.rs new file mode 100644 index 0000000000..26566679ca --- /dev/null +++ b/src/cosign/constraint/annotation.rs @@ -0,0 +1,73 @@ +// +// Copyright 2022 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. + +use std::collections::HashMap; + +use serde_json::Value; +use tracing::warn; + +use crate::{cosign::SignatureLayer, errors::Result}; + +use super::Constraint; + +/// Constraint for the annotations, which can be verified by [`crate::cosign::verification_constraint::AnnotationVerifier`] +/// +/// The [`crate::cosign::payload::SimpleSigning`] object can be enriched by a signer +/// with more annotations. +/// +/// A [`AnnotationMarker`] helps to add annotations to the [`crate::cosign::payload::SimpleSigning`] +/// of the given [`SignatureLayer`]. +/// +/// Warning: The signing step must not happen until all [`AnnotationMarker`] +/// have already performed `add_constraint`. +#[derive(Debug)] +pub struct AnnotationMarker { + pub annotations: HashMap, +} + +impl AnnotationMarker { + pub fn new(annotations: HashMap) -> Self { + Self { annotations } + } +} + +impl Constraint for AnnotationMarker { + fn add_constraint(&self, signature_layer: &mut SignatureLayer) -> Result { + let mut annotations = match &signature_layer.simple_signing.optional { + Some(opt) => { + warn!(optional = ?opt, "already has an annotation field"); + opt.extra.clone() + } + None => HashMap::new(), + }; + + for (k, v) in &self.annotations { + if annotations.contains_key(k) && annotations[k] != *v { + warn!(key = ?k, "extra field already has a value"); + return Ok(false); + } + annotations.insert(k.to_owned(), Value::String(v.into())); + } + + let mut opt = signature_layer + .simple_signing + .optional + .clone() + .unwrap_or_default(); + opt.extra = annotations; + signature_layer.simple_signing.optional = Some(opt); + Ok(true) + } +} diff --git a/src/cosign/constraint/mod.rs b/src/cosign/constraint/mod.rs new file mode 100644 index 0000000000..07e6dc2616 --- /dev/null +++ b/src/cosign/constraint/mod.rs @@ -0,0 +1,73 @@ +// +// Copyright 2022 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. + +//! Structs that can be used to add constraints to [`crate::cosign::SignatureLayer`] +//! with special business logic. +//! +//! This module provides some common kinds of constraints: +//! * [`PrivateKeySigner`]: Attaching a signature +//! * [`AnnotationMarker`]: Adding extra annotations +//! +//! Developers can define ad-hoc constraint logic by creating a Struct that +//! implements the [`Constraint`] trait +//! +//! ## Warining +//! Because [`PrivateKeySigner`] will sign the whole data of a given +//! [`crate::cosign::SignatureLayer`], developers **must** ensure that +//! a [`PrivateKeySigner`] is the last constraint to be applied on a +//! [`crate::cosign::SignatureLayer`]. Before that, all constraints that +//! may modify the content of the [`crate::cosign::SignatureLayer`] should +//! have been applied already. + +use super::SignatureLayer; +use crate::errors::Result; + +pub type SignConstraintVec = Vec>; +pub type SignConstraintRefVec<'a> = Vec<&'a Box>; + +pub trait Constraint: std::fmt::Debug { + /// Given a mutable reference of [`crate::cosign::SignatureLayer`], return + /// `true` if the constraint is applied successfully. + /// + /// Developer can use the + /// [`crate::errors::SigstoreError::ApplyConstraintError`] error + /// when something goes wrong inside of the application logic. + /// + /// ``` + /// use sigstore::{ + /// cosign::constraint::Constraint, + /// cosign::signature_layers::SignatureLayer, + /// errors::{SigstoreError, Result}, + /// }; + /// + /// #[derive(Debug)] + /// struct MyConstraint{} + /// + /// impl Constraint for MyConstraint { + /// fn add_constraint(&self, _sl: &mut SignatureLayer) -> Result { + /// Err(SigstoreError::ApplyConstraintError( + /// "something went wrong!".to_string())) + /// } + /// } + /// + /// ``` + fn add_constraint(&self, signature_layer: &mut SignatureLayer) -> Result; +} + +pub mod annotation; +pub use annotation::AnnotationMarker; + +pub mod signature; +pub use self::signature::PrivateKeySigner; diff --git a/src/cosign/constraint/signature.rs b/src/cosign/constraint/signature.rs new file mode 100644 index 0000000000..870437d8ea --- /dev/null +++ b/src/cosign/constraint/signature.rs @@ -0,0 +1,73 @@ +// +// Copyright 2022 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. + +//! Structs that can be used to sign a [`crate::cosign::SignatureLayer`] + +use tracing::warn; +use zeroize::Zeroizing; + +use crate::{ + cosign::SignatureLayer, + crypto::{signing_key::SigStoreKeyPair, SigStoreSigner, SigningScheme}, + errors::{Result, SigstoreError}, +}; + +use super::Constraint; + +/// Sign the [`SignatureLayer`] with the given [`SigStoreSigner`]. +/// This constraint must be the last one to applied to a [`SignatureLayer`], +/// since all the plaintext is defined. +#[derive(Debug)] +pub struct PrivateKeySigner { + key: SigStoreSigner, +} + +impl PrivateKeySigner { + /// Create a new [PrivateKeySigner] with given raw PEM data of a + /// private key. + pub fn new_with_raw( + key_raw: Zeroizing>, + password: Zeroizing>, + signing_scheme: &SigningScheme, + ) -> Result { + let signer = match password.is_empty() { + true => SigStoreKeyPair::from_pem(&key_raw), + false => SigStoreKeyPair::from_encrypted_pem(&key_raw, &password), + } + .map_err(|e| SigstoreError::ApplyConstraintError(e.to_string()))? + .to_sigstore_signer(signing_scheme) + .map_err(|e| SigstoreError::ApplyConstraintError(e.to_string()))?; + + Ok(Self { key: signer }) + } + + pub fn new_with_signer(signer: SigStoreSigner) -> Self { + Self { key: signer } + } +} + +impl Constraint for PrivateKeySigner { + fn add_constraint(&self, signature_layer: &mut SignatureLayer) -> Result { + if signature_layer.signature.is_some() { + warn!(signature = ?signature_layer.signature, "already has signature"); + return Ok(false); + } + signature_layer.raw_data = serde_json::to_vec(&signature_layer.simple_signing)?; + let sig = self.key.sign(&signature_layer.raw_data)?; + let sig_base64 = base64::encode(&sig); + signature_layer.signature = Some(sig_base64); + Ok(true) + } +} diff --git a/src/cosign/mod.rs b/src/cosign/mod.rs index 8e5a624e4c..fb8ac0174d 100644 --- a/src/cosign/mod.rs +++ b/src/cosign/mod.rs @@ -36,11 +36,13 @@ //! In case you want to mock sigstore interactions inside of your own code, you //! can implement the [`CosignCapabilities`] trait inside of your test suite. +use std::collections::HashMap; + use async_trait::async_trait; use tracing::warn; -use crate::errors::{Result, SigstoreVerifyConstraintsError}; -use crate::registry::Auth; +use crate::errors::{Result, SigstoreApplicationConstraintsError, SigstoreVerifyConstraintsError}; +use crate::registry::{Auth, PushResponse}; mod bundle; pub(crate) mod constants; @@ -54,8 +56,13 @@ pub mod client_builder; pub use self::client_builder::ClientBuilder; pub mod verification_constraint; +pub use self::constraint::{Constraint, SignConstraintRefVec}; use self::verification_constraint::{VerificationConstraint, VerificationConstraintRefVec}; +pub mod payload; +pub use payload::simple_signing; + +pub mod constraint; #[async_trait] /// Cosign Abilities that have to be implemented by a /// Cosign client @@ -102,6 +109,34 @@ pub trait CosignCapabilities { source_image_digest: &str, cosign_image: &str, ) -> Result>; + + /// Push [`SignatureLayer`] objects to the registry. This function will do + /// the following steps: + /// * Generate a series of [`oci_distribution::client::ImageLayer`]s due to + /// the given [`Vec`]. + /// * Generate a `OciImageManifest` of [`oci_distribution::manifest::OciManifest`] + /// due to the given `source_image_digest` and `signature_layers`. It supports + /// to be extended when newly published + /// [Referrers API of OCI Registry v1.1.0](https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#listing-referrers), + /// is prepared. At that time, + /// [an artifact manifest](https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/artifact.md) + /// will be created instead of [an image manifest](https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/manifest.md). + /// * Push the generated manifest together with the layers + /// to the `target_reference`. `target_reference` contains information + /// about the registry, repository and tag. + /// + /// The parameters: + /// - `annotations`: annotations of the generated manifest + /// - `auth`: Credential used to access the registry + /// - `target_reference`: target reference to push the manifest + /// - `signature_layers`: [`SignatureLayer`] objects containing signature information + async fn push_signature( + &mut self, + annotations: Option>, + auth: &Auth, + target_reference: &str, + signature_layers: Vec, + ) -> Result; } /// Given a list of trusted `SignatureLayer`, find all the constraints that @@ -155,20 +190,67 @@ where } } +/// Given a [`SignatureLayer`], apply all the constraints to that. +/// +/// If there's any constraints that fails to apply, it means the +/// application process fails. +/// If all constraints succeed applying, it means that this layer +/// passes applying constraints process. +/// +/// Returns a `Result` with either `Ok()` for success or +/// [`SigstoreApplicationConstraintsError`](crate::errors::SigstoreApplicationConstraintsError), +/// which contains a vector of references to unapplied constraints. +/// +/// See the documentation of the [`cosign::sign_constraint`](crate::cosign::sign_constraint) module for more +/// details about how to define constraints. +pub fn apply_constraints<'a, 'b, I>( + signature_layer: &'a mut SignatureLayer, + constraints: I, +) -> std::result::Result<(), SigstoreApplicationConstraintsError<'b>> +where + I: Iterator>, +{ + let unapplied_constraints: SignConstraintRefVec = constraints + .filter(|c| match c.add_constraint(signature_layer) { + Ok(is_applied) => !is_applied, + Err(e) => { + warn!(error = ?e, constraint = ?c, "Applying constraint failed due to error"); + true + } + }) + .collect(); + + if unapplied_constraints.is_empty() { + Ok(()) + } else { + Err(SigstoreApplicationConstraintsError { + unapplied_constraints, + }) + } +} + #[cfg(test)] mod tests { use serde_json::json; use std::collections::HashMap; + use super::constraint::{AnnotationMarker, PrivateKeySigner}; use super::*; use crate::cosign::signature_layers::tests::build_correct_signature_layer_with_certificate; use crate::cosign::signature_layers::CertificateSubject; + use crate::cosign::simple_signing::Optional; use crate::cosign::verification_constraint::{ AnnotationVerifier, CertSubjectEmailVerifier, VerificationConstraintVec, }; use crate::crypto::certificate_pool::CertificatePool; use crate::crypto::{CosignVerificationKey, SigningScheme}; - use crate::simple_signing::Optional; + + #[cfg(feature = "test-registry")] + use testcontainers::{ + clients, + core::WaitFor, + images::{self, generic::GenericImage}, + }; pub(crate) const REKOR_PUB_KEY: &str = r#"-----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwr @@ -203,6 +285,9 @@ WP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9 TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ -----END CERTIFICATE-----"#; + #[cfg(feature = "test-registry")] + const SIGNED_IMAGE: &str = "busybox:1.34"; + pub(crate) fn get_fulcio_cert_pool() -> CertificatePool { let certificates = vec![ crate::registry::Certificate { @@ -372,4 +457,173 @@ TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ verify_constraints(&layers, constraints.iter()).expect_err("we should have an err"); assert_eq!(err.unsatisfied_constraints.len(), 1); } + + #[test] + fn add_constrains_all_succeed() { + let mut signature_layer = SignatureLayer::new_unsigned( + "test_image", + "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + ) + .expect("create SignatureLayer failed"); + + let signer = SigningScheme::ECDSA_P256_SHA256_ASN1 + .create_signer() + .expect("create signer failed"); + let signer = PrivateKeySigner::new_with_signer(signer); + + let annotations = [(String::from("key"), String::from("value"))].into(); + let annotations = AnnotationMarker::new(annotations); + + let constrains: Vec> = vec![Box::new(signer), Box::new(annotations)]; + apply_constraints(&mut signature_layer, constrains.iter()).expect("no error should occur"); + } + + #[test] + fn add_constrain_some_failed() { + let mut signature_layer = SignatureLayer::new_unsigned( + "test_image", + "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + ) + .expect("create SignatureLayer failed"); + + let signer = SigningScheme::ECDSA_P256_SHA256_ASN1 + .create_signer() + .expect("create signer failed"); + let signer = PrivateKeySigner::new_with_signer(signer); + let another_signer_of_same_layer = SigningScheme::ECDSA_P256_SHA256_ASN1 + .create_signer() + .expect("create signer failed"); + let another_signer_of_same_layer = + PrivateKeySigner::new_with_signer(another_signer_of_same_layer); + + let annotations = [(String::from("key"), String::from("value"))].into(); + let annotations = AnnotationMarker::new(annotations); + + let constrains: Vec> = vec![ + Box::new(signer), + Box::new(annotations), + Box::new(another_signer_of_same_layer), + ]; + apply_constraints(&mut signature_layer, constrains.iter()) + .expect_err("no error should occur"); + } + + #[cfg(feature = "test-registry")] + #[rstest::rstest] + #[case(SigningScheme::RSA_PSS_SHA256(2048))] + #[case(SigningScheme::RSA_PSS_SHA384(2048))] + #[case(SigningScheme::RSA_PSS_SHA512(2048))] + #[case(SigningScheme::RSA_PKCS1_SHA256(2048))] + #[case(SigningScheme::RSA_PKCS1_SHA384(2048))] + #[case(SigningScheme::RSA_PKCS1_SHA512(2048))] + #[case(SigningScheme::ECDSA_P256_SHA256_ASN1)] + #[case(SigningScheme::ECDSA_P384_SHA384_ASN1)] + #[case(SigningScheme::ED25519)] + #[tokio::test] + #[serial_test::serial] + async fn sign_verify_image(#[case] signing_scheme: SigningScheme) { + let docker = clients::Cli::default(); + let image = registry_image(); + let test_container = docker.run(image); + let port = test_container.get_host_port_ipv4(5000); + + let mut client = ClientBuilder::default() + .enable_registry_caching() + .with_oci_client_config(crate::registry::ClientConfig { + protocol: crate::registry::ClientProtocol::HttpsExcept(vec![format!( + "localhost:{}", + port + )]), + ..Default::default() + }) + .build() + .expect("failed to create oci client"); + + let image_ref = format!("localhost:{}/{}", port, SIGNED_IMAGE); + prepare_image_to_be_signed(&mut client, &image_ref).await; + + let (cosign_signature_image, source_image_digest) = client + .triangulate(&image_ref, &crate::registry::Auth::Anonymous) + .await + .expect("get manifest failed"); + let mut signature_layer = SignatureLayer::new_unsigned(&image_ref, &source_image_digest) + .expect("create SignatureLayer failed"); + let signer = signing_scheme + .create_signer() + .expect("create signer failed"); + let pubkey = signer + .to_sigstore_keypair() + .expect("to keypair failed") + .public_key_to_pem() + .expect("derive public key failed"); + + let signer = PrivateKeySigner::new_with_signer(signer); + if !signer + .add_constraint(&mut signature_layer) + .expect("sign SignatureLayer failed") + { + panic!("failed to sign SignatureLayer"); + }; + + client + .push_signature( + None, + &Auth::Anonymous, + &cosign_signature_image, + vec![signature_layer], + ) + .await + .expect("push signature failed"); + + dbg!("start to verify"); + + let (cosign_image, manifest_digest) = client + .triangulate(&image_ref, &Auth::Anonymous) + .await + .expect("triangulate failed"); + let signature_layers = client + .trusted_signature_layers(&Auth::Anonymous, &manifest_digest, &cosign_image) + .await + .expect("get trusted signature layers failed"); + let pk_verifier = + verification_constraint::PublicKeyVerifier::new(pubkey.as_bytes(), &signing_scheme) + .expect("create PublicKeyVerifier failed"); + assert_eq!(signature_layers.len(), 1); + let res = pk_verifier + .verify(&signature_layers[0]) + .expect("failed to verify"); + assert!(res); + } + + #[cfg(feature = "test-registry")] + async fn prepare_image_to_be_signed(client: &mut Client, image_ref: &str) { + let image_ref = image_ref.parse().expect("failed to parse image reference"); + let data = client + .registry_client + .pull( + &SIGNED_IMAGE.parse().expect("failed to parse image ref"), + &oci_distribution::secrets::RegistryAuth::Anonymous, + vec![oci_distribution::manifest::IMAGE_DOCKER_LAYER_GZIP_MEDIA_TYPE], + ) + .await + .expect("pull test image failed"); + + client + .registry_client + .push( + &image_ref, + &data.layers[..], + data.config.clone(), + &oci_distribution::secrets::RegistryAuth::Anonymous, + None, + ) + .await + .expect("push test image failed"); + } + + #[cfg(feature = "test-registry")] + fn registry_image() -> GenericImage { + images::generic::GenericImage::new("docker.io/library/registry", "2") + .with_wait_for(WaitFor::message_on_stderr("listening on ")) + } } diff --git a/src/cosign/payload/mod.rs b/src/cosign/payload/mod.rs new file mode 100644 index 0000000000..f4eb61e453 --- /dev/null +++ b/src/cosign/payload/mod.rs @@ -0,0 +1,22 @@ +// +// Copyright 2022 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. + +//! This module defines different kinds of payload to be signed +//! in cosign. Now it supports: +//! * `SimpleSigning`: Refer to +//! + +pub mod simple_signing; +pub use simple_signing::SimpleSigning; diff --git a/src/simple_signing.rs b/src/cosign/payload/simple_signing.rs similarity index 91% rename from src/simple_signing.rs rename to src/cosign/payload/simple_signing.rs index aad0307ef0..a5c8bf63ff 100644 --- a/src/simple_signing.rs +++ b/src/cosign/payload/simple_signing.rs @@ -1,5 +1,5 @@ // -// Copyright 2021 The Sigstore Authors. +// Copyright 2022 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. @@ -22,6 +22,9 @@ use serde_json::Value; use std::{collections::HashMap, fmt}; use tracing::{debug, error, info}; +/// Default type name of [`Critical`] when doing cosign signing +pub const CRITICAL_TYPE_NAME: &str = "cosign container image signature"; + #[derive(Serialize, Deserialize, Debug, Clone)] pub struct SimpleSigning { pub critical: Critical, @@ -42,6 +45,23 @@ impl fmt::Display for SimpleSigning { } impl SimpleSigning { + /// Create a new simple signing payload due to the given image reference + /// and manifefst_digest + pub fn new(image_ref: &str, manifest_digest: &str) -> Self { + Self { + critical: Critical { + type_name: CRITICAL_TYPE_NAME.to_string(), + image: Image { + docker_manifest_digest: manifest_digest.to_string(), + }, + identity: Identity { + docker_reference: image_ref.to_string(), + }, + }, + optional: None, + } + } + /// Checks whether all the provided `annotations` are satisfied pub fn satisfies_annotations(&self, annotations: &HashMap) -> bool { if annotations.is_empty() { @@ -79,11 +99,11 @@ impl SimpleSigning { #[derive(Serialize, Deserialize, Debug, Clone)] pub struct Critical { - #[serde(rename = "type")] //TODO: should we validate the contents of this attribute to ensure it's "cosign container image signature"? - pub type_name: String, - pub image: Image, pub identity: Identity, + pub image: Image, + #[serde(rename = "type")] + pub type_name: String, } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -98,9 +118,11 @@ pub struct Identity { pub docker_reference: String, } -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone, Default)] pub struct Optional { + #[serde(skip_serializing_if = "Option::is_none")] pub creator: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub timestamp: Option, #[serde(flatten)] diff --git a/src/cosign/signature_layers.rs b/src/cosign/signature_layers.rs index 98dcdc8f75..5488fd6235 100644 --- a/src/cosign/signature_layers.rs +++ b/src/cosign/signature_layers.rs @@ -13,10 +13,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +use digest::Digest; use oci_distribution::client::ImageLayer; use serde::Serialize; use std::{collections::HashMap, fmt}; -use tracing::{debug, info}; +use tracing::{debug, info, warn}; use x509_parser::{ certificate::X509Certificate, der_parser::oid::Oid, extensions::GeneralName, parse_x509_certificate, pem::parse_x509_pem, @@ -31,9 +32,9 @@ use super::constants::{ }; use crate::crypto::certificate_pool::CertificatePool; use crate::{ + cosign::simple_signing::SimpleSigning, crypto::{self, CosignVerificationKey, Signature, SigningScheme}, errors::{Result, SigstoreError}, - simple_signing::SimpleSigning, }; /// Describe the details of a certificate produced when signing artifacts @@ -138,7 +139,7 @@ pub struct SignatureLayer { /// The bundle produced by Rekor. pub bundle: Option, #[serde(skip_serializing)] - pub signature: String, + pub signature: Option, #[serde(skip_serializing)] pub raw_data: Vec, } @@ -178,6 +179,45 @@ impl fmt::Display for SignatureLayer { } impl SignatureLayer { + /// Create a [`SignatureLayer`], this function will generate a [`SimpleSigning`] + /// payload due to the given reference of image and the digest of the manifest. + /// However, the resulted [`SignatureLayer`] does not have a signature, and it + /// should be manually generated. + /// + /// ## Usage + /// ```rust,no_run + /// use sigstore::cosign::{SignatureLayer, constraint::PrivateKeySigner, Constraint}; + /// use sigstore::crypto::SigningScheme; + /// + /// async fn func() { + /// let mut signature_layer = SignatureLayer::new_unsigned("example/test", "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").expect("create SignatureLayer failed"); + /// // Now the SignatureLayer does not have a signature, we need + /// // to generate one + /// let signer = SigningScheme::ECDSA_P256_SHA256_ASN1.create_signer().expect("create signer failed"); + /// let pk_signer = PrivateKeySigner::new_with_signer(signer); + /// if pk_signer.add_constraint(&mut signature_layer).expect("unexpected error") { + /// println!("sign succeed!"); + /// } else { + /// println!("sign failed!"); + /// } + /// } + /// + /// ``` + pub fn new_unsigned(image_ref: &str, manifest_digest: &str) -> Result { + let simple_signing = SimpleSigning::new(image_ref, manifest_digest); + + let payload = serde_json::to_vec(&simple_signing)?; + let digest = format!("sha256:{:x}", sha2::Sha256::digest(&payload)); + Ok(SignatureLayer { + simple_signing, + oci_digest: digest, + certificate_signature: None, + bundle: None, + signature: None, + raw_data: payload, + }) + } + /// Create a SignatureLayer that can be considered trusted. /// /// Params: @@ -241,7 +281,7 @@ impl SignatureLayer { oci_digest: descriptor.digest.clone(), raw_data: layer.data.clone(), simple_signing, - signature, + signature: Some(signature), bundle, certificate_signature, }) @@ -317,13 +357,20 @@ impl SignatureLayer { /// Given a Cosign public key, check whether this Signature Layer has been /// signed by it pub(crate) fn is_signed_by_key(&self, verification_key: &CosignVerificationKey) -> bool { + let signature = match &self.signature { + Some(sig) => sig, + None => { + warn!(signature_layer = ?self, "signature not found in the SignatureLayer"); + return false; + } + }; match verification_key.verify_signature( - Signature::Base64Encoded(self.signature.as_bytes()), + Signature::Base64Encoded(signature.as_bytes()), &self.raw_data, ) { Ok(_) => true, Err(e) => { - debug!(signature=self.signature.as_str(), reason=?e, "Cannot verify signature with the given key"); + debug!(signature=signature.as_str(), reason=?e, "Cannot verify signature with the given key"); false } } @@ -517,7 +564,7 @@ OSWS1X9vPavpiQOoTTGC0xX57OojUadxF1cdQmrsiReWg2Wn4FneJfa8xw== SignatureLayer { simple_signing: serde_json::from_value(ss_value.clone()).unwrap(), oci_digest: String::from("digest"), - signature, + signature: Some(signature), bundle: None, certificate_signature: None, raw_data: serde_json::to_vec(&ss_value).unwrap(), @@ -581,7 +628,7 @@ oXqqo/C9QnOHTto= SignatureLayer { simple_signing: serde_json::from_value(ss_value.clone()).unwrap(), oci_digest: String::from("sha256:5f481572d088dc4023afb35fced9530ced3d9b03bf7299c6f492163cb9f0452e"), - signature: String::from("MEUCIGqWScz7s9aP2sGXNFKeqivw3B6kPRs56AITIHnvd5igAiEA1kzbaV2Y5yPE81EN92NUFOl31LLJSvwsjFQ07m2XqaA="), + signature: Some(String::from("MEUCIGqWScz7s9aP2sGXNFKeqivw3B6kPRs56AITIHnvd5igAiEA1kzbaV2Y5yPE81EN92NUFOl31LLJSvwsjFQ07m2XqaA=")), bundle: Some(bundle), certificate_signature: Some(certificate_signature), raw_data: serde_json::to_vec(&ss_value).unwrap(), diff --git a/src/cosign/verification_constraint/certificate_verifier.rs b/src/cosign/verification_constraint/certificate_verifier.rs index 74fb31ee93..49dc3816ec 100644 --- a/src/cosign/verification_constraint/certificate_verifier.rs +++ b/src/cosign/verification_constraint/certificate_verifier.rs @@ -198,7 +198,7 @@ RAIgPixAn47x4qLpu7Y/d0oyvbnOGtD5cY7rywdMOO7LYRsCIDsCyGUZIYMFfSrt let signature_layer = SignatureLayer { simple_signing: serde_json::from_value(ss_value.clone()).unwrap(), oci_digest: String::from("sha256:f9b817c013972c75de8689d55c0d441c3eb84f6233ac75f6a9c722ea5db0058b"), - signature: String::from("MEYCIQCIqLEe6hnjEXP/YC2P9OIwEr2yMmwPNHLzvCPaoaXFOQIhALyTouhKNKc2ZVrR0GUQ7J0U5AtlyDZDLGnasAi7XnV/"), + signature: Some(String::from("MEYCIQCIqLEe6hnjEXP/YC2P9OIwEr2yMmwPNHLzvCPaoaXFOQIhALyTouhKNKc2ZVrR0GUQ7J0U5AtlyDZDLGnasAi7XnV/")), bundle: Some(bundle), certificate_signature: None, raw_data: serde_json::to_vec(&ss_value).unwrap(), diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index d27b83e082..da50593809 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -47,7 +47,7 @@ pub use verification_key::CosignVerificationKey; /// signature format please refer /// to [RFC 8032](https://www.rfc-editor.org/rfc/rfc8032.html#section-5.1.6). #[allow(non_camel_case_types)] -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum SigningScheme { RSA_PSS_SHA256(usize), RSA_PSS_SHA384(usize), @@ -60,6 +60,23 @@ pub enum SigningScheme { ED25519, } +impl ToString for SigningScheme { + fn to_string(&self) -> String { + let str = match self { + SigningScheme::RSA_PSS_SHA256(_) => "RSA_PSS_SHA256", + SigningScheme::RSA_PSS_SHA384(_) => "RSA_PSS_SHA384", + SigningScheme::RSA_PSS_SHA512(_) => "RSA_PSS_SHA512", + SigningScheme::RSA_PKCS1_SHA256(_) => "RSA_PKCS1_SHA256", + SigningScheme::RSA_PKCS1_SHA384(_) => "RSA_PKCS1_SHA384", + SigningScheme::RSA_PKCS1_SHA512(_) => "RSA_PKCS1_SHA512", + SigningScheme::ECDSA_P256_SHA256_ASN1 => "ECDSA_P256_SHA256_ASN1", + SigningScheme::ECDSA_P384_SHA384_ASN1 => "ECDSA_P384_SHA384_ASN1", + SigningScheme::ED25519 => "ED25519", + }; + String::from(str) + } +} + impl TryFrom<&str> for SigningScheme { type Error = String; diff --git a/src/crypto/signing_key/ecdsa/ec.rs b/src/crypto/signing_key/ecdsa/ec.rs index d91c703652..f27201b4ac 100644 --- a/src/crypto/signing_key/ecdsa/ec.rs +++ b/src/crypto/signing_key/ecdsa/ec.rs @@ -108,7 +108,7 @@ use super::ECDSAKeys; /// /// More elliptic curves, please refer to /// . -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct EcdsaKeys where C: Curve + ProjectiveArithmetic + pkcs8::AssociatedOid, @@ -287,7 +287,7 @@ where /// /// For concrete digest algorithms, please refer to /// . -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct EcdsaSigner where C: PrimeCurve + ProjectiveArithmetic + AssociatedOid, diff --git a/src/crypto/signing_key/ecdsa/mod.rs b/src/crypto/signing_key/ecdsa/mod.rs index 78cb86d6a7..e43a81b6c4 100644 --- a/src/crypto/signing_key/ecdsa/mod.rs +++ b/src/crypto/signing_key/ecdsa/mod.rs @@ -88,6 +88,16 @@ pub enum ECDSAKeys { P384(EcdsaKeys), } +impl ToString for ECDSAKeys { + fn to_string(&self) -> String { + let str = match self { + ECDSAKeys::P256(_) => "ECDSA P256", + ECDSAKeys::P384(_) => "ECDSA P384", + }; + String::from(str) + } +} + /// The types of supported elliptic curves: /// * `P256`: `P-256`, also known as `secp256r1` or `prime256v1`. /// * `P384`: `P-384`, also known as `secp384r1`. diff --git a/src/crypto/signing_key/ed25519.rs b/src/crypto/signing_key/ed25519.rs index 52cfb6e4dc..f0be674044 100644 --- a/src/crypto/signing_key/ed25519.rs +++ b/src/crypto/signing_key/ed25519.rs @@ -77,6 +77,7 @@ use super::{ SIGSTORE_PRIVATE_KEY_PEM_LABEL, }; +#[derive(Debug)] pub struct Ed25519Keys { key_pair: ed25519_dalek_fiat::Keypair, key_pair_bytes: KeypairBytes, @@ -266,6 +267,7 @@ impl KeyPair for Ed25519Keys { } } +#[derive(Debug)] pub struct Ed25519Signer { key_pair: Ed25519Keys, } diff --git a/src/crypto/signing_key/mod.rs b/src/crypto/signing_key/mod.rs index 35cd526355..efc34b60ac 100644 --- a/src/crypto/signing_key/mod.rs +++ b/src/crypto/signing_key/mod.rs @@ -75,7 +75,7 @@ use crate::errors::*; use self::{ ecdsa::{ec::EcdsaSigner, ECDSAKeys}, ed25519::{Ed25519Keys, Ed25519Signer}, - rsa::{keypair::RSAKeys, RSASigner}, + rsa::{keypair::RSAKeys, DigestAlgorithm, PaddingScheme, RSASigner}, }; use super::{verification_key::CosignVerificationKey, SigningScheme}; @@ -139,6 +139,16 @@ pub enum SigStoreKeyPair { RSA(RSAKeys), } +impl ToString for SigStoreKeyPair { + fn to_string(&self) -> String { + match self { + SigStoreKeyPair::ECDSA(_) => String::from("EC Key"), + SigStoreKeyPair::ED25519(_) => String::from("Ed25519 Key"), + SigStoreKeyPair::RSA(_) => String::from("RSA Key"), + } + } +} + /// This macro helps to reduce duplicated code. macro_rules! sigstore_keypair_from { ($func: ident ($($args:expr),*)) => { @@ -146,8 +156,10 @@ macro_rules! sigstore_keypair_from { Ok(SigStoreKeyPair::ECDSA(keys)) } else if let Ok(keys) = Ed25519Keys::$func($($args,)*) { Ok(SigStoreKeyPair::ED25519(keys)) + } else if let Ok(keys) = RSAKeys::$func($($args,)*) { + Ok(SigStoreKeyPair::RSA(keys)) } else { - Err(SigstoreError::KeyParseError("SigStoreKeys".to_string())) + Err(SigstoreError::KeyParseError("Unsupported key type".to_string())) } } } @@ -214,6 +226,74 @@ impl SigStoreKeyPair { ) -> Result { sigstore_keypair_code!(to_verification_key(signing_scheme), self) } + + /// Convert this KeyPair into a [`SigStoreSigner`] due to the given + /// signing scheme. If the key type does not match the given + /// signing scheme, an error will occur. + pub fn to_sigstore_signer(&self, signing_scheme: &SigningScheme) -> Result { + match self { + SigStoreKeyPair::ECDSA(keys) => match signing_scheme { + SigningScheme::ECDSA_P256_SHA256_ASN1 => match keys { + ECDSAKeys::P256(key) => { + let signer = EcdsaSigner::from_ecdsa_keys(key)?; + Ok(SigStoreSigner::ECDSA_P256_SHA256_ASN1(signer)) + } + ECDSAKeys::P384(_) => Err(SigstoreError::UnmatchedKeyAndSigningScheme { + key_typ: keys.to_string(), + scheme: signing_scheme.to_string(), + }), + }, + SigningScheme::ECDSA_P384_SHA384_ASN1 => match keys { + ECDSAKeys::P384(key) => { + let signer = EcdsaSigner::from_ecdsa_keys(key)?; + Ok(SigStoreSigner::ECDSA_P384_SHA384_ASN1(signer)) + } + ECDSAKeys::P256(_) => Err(SigstoreError::UnmatchedKeyAndSigningScheme { + key_typ: keys.to_string(), + scheme: signing_scheme.to_string(), + }), + }, + _ => Err(SigstoreError::UnmatchedKeyAndSigningScheme { + key_typ: self.to_string(), + scheme: signing_scheme.to_string(), + }), + }, + SigStoreKeyPair::ED25519(keys) => { + if *signing_scheme != SigningScheme::ED25519 { + Err(SigstoreError::UnmatchedKeyAndSigningScheme { + key_typ: self.to_string(), + scheme: signing_scheme.to_string(), + }) + } else { + keys.to_sigstore_signer() + } + } + SigStoreKeyPair::RSA(keys) => match signing_scheme { + SigningScheme::RSA_PSS_SHA256(_) => { + keys.to_sigstore_signer(DigestAlgorithm::Sha256, PaddingScheme::PSS) + } + SigningScheme::RSA_PSS_SHA384(_) => { + keys.to_sigstore_signer(DigestAlgorithm::Sha384, PaddingScheme::PSS) + } + SigningScheme::RSA_PSS_SHA512(_) => { + keys.to_sigstore_signer(DigestAlgorithm::Sha512, PaddingScheme::PSS) + } + SigningScheme::RSA_PKCS1_SHA256(_) => { + keys.to_sigstore_signer(DigestAlgorithm::Sha256, PaddingScheme::PKCS1v15) + } + SigningScheme::RSA_PKCS1_SHA384(_) => { + keys.to_sigstore_signer(DigestAlgorithm::Sha384, PaddingScheme::PKCS1v15) + } + SigningScheme::RSA_PKCS1_SHA512(_) => { + keys.to_sigstore_signer(DigestAlgorithm::Sha512, PaddingScheme::PKCS1v15) + } + _ => Err(SigstoreError::UnmatchedKeyAndSigningScheme { + key_typ: self.to_string(), + scheme: signing_scheme.to_string(), + }), + }, + } + } } /// `Signer` trait is an abstraction of a specific set of asymmetric @@ -227,6 +307,7 @@ pub trait Signer { fn sign(&self, msg: &[u8]) -> Result>; } +#[derive(Debug)] #[allow(non_camel_case_types)] pub enum SigStoreSigner { RSA_PSS_SHA256(RSASigner), diff --git a/src/crypto/signing_key/rsa/keypair.rs b/src/crypto/signing_key/rsa/keypair.rs index cdbfb810be..0041a3baab 100644 --- a/src/crypto/signing_key/rsa/keypair.rs +++ b/src/crypto/signing_key/rsa/keypair.rs @@ -59,7 +59,7 @@ use crate::crypto::signing_key::{ use super::{DigestAlgorithm, PaddingScheme, RSASigner}; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct RSAKeys { pub(crate) private_key: RsaPrivateKey, public_key: RsaPublicKey, diff --git a/src/crypto/signing_key/rsa/mod.rs b/src/crypto/signing_key/rsa/mod.rs index 3d15998e97..463ea2a631 100644 --- a/src/crypto/signing_key/rsa/mod.rs +++ b/src/crypto/signing_key/rsa/mod.rs @@ -89,6 +89,7 @@ pub enum PaddingScheme { /// * `Sha256` /// * `Sha384` /// * `Sha512` +#[derive(Debug)] #[allow(non_camel_case_types)] pub enum RSASigner { RSA_PSS_SHA256(BlindedSigningKey, RSAKeys), diff --git a/src/errors.rs b/src/errors.rs index 9a7f35d86b..ef4ad277a2 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -17,7 +17,9 @@ use thiserror::Error; -use crate::cosign::verification_constraint::VerificationConstraintRefVec; +use crate::cosign::{ + constraint::SignConstraintRefVec, verification_constraint::VerificationConstraintRefVec, +}; #[derive(Error, Debug)] #[error("Several Signature Layers failed verification")] @@ -25,6 +27,12 @@ pub struct SigstoreVerifyConstraintsError<'a> { pub unsatisfied_constraints: VerificationConstraintRefVec<'a>, } +#[derive(Error, Debug)] +#[error("Several Constraints failed to apply on the SignatureLayer")] +pub struct SigstoreApplicationConstraintsError<'a> { + pub unapplied_constraints: SignConstraintRefVec<'a>, +} + pub type Result = std::result::Result; #[derive(Error, Debug)] @@ -41,6 +49,9 @@ pub enum SigstoreError { #[error("invalid key format: {error}")] InvalidKeyFormat { error: String }, + #[error("unmatched key type {key_typ} and signing scheme {scheme}")] + UnmatchedKeyAndSigningScheme { key_typ: String, scheme: String }, + #[error(transparent)] PEMParseError(#[from] x509_parser::nom::Err), @@ -49,6 +60,7 @@ pub enum SigstoreError { #[error(transparent)] X509ParseError(#[from] x509_parser::nom::Err), + #[error(transparent)] X509Error(#[from] x509_parser::error::X509Error), @@ -106,6 +118,9 @@ pub enum SigstoreError { #[error("Cannot pull {image}: {error}")] RegistryPullError { image: String, error: String }, + #[error("Cannot push {image}: {error}")] + RegistryPushError { image: String, error: String }, + #[error("OCI reference not valid: {reference}")] OciReferenceNotValidError { reference: String }, @@ -142,6 +157,9 @@ pub enum SigstoreError { #[error("{0}")] VerificationConstraintError(String), + #[error("{0}")] + ApplyConstraintError(String), + #[error("Verification of OIDC claims received from OpenIdProvider failed")] ClaimsVerificationError, diff --git a/src/lib.rs b/src/lib.rs index ec0ae557ff..b8b9061b91 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -235,5 +235,4 @@ pub mod fulcio; pub mod oauth; pub mod registry; pub mod rekor; -pub mod simple_signing; pub mod tuf; diff --git a/src/mock_client.rs b/src/mock_client.rs index ed525980de..8be823f2d8 100644 --- a/src/mock_client.rs +++ b/src/mock_client.rs @@ -19,7 +19,10 @@ pub(crate) mod test { use async_trait::async_trait; use oci_distribution::{ - client::ImageData, manifest::OciManifest, secrets::RegistryAuth, Reference, + client::{ImageData, PushResponse}, + manifest::OciManifest, + secrets::RegistryAuth, + Reference, }; #[derive(Default)] @@ -27,6 +30,7 @@ pub(crate) mod test { pub fetch_manifest_digest_response: Option>, pub pull_response: Option>, pub pull_manifest_response: Option>, + pub push_response: Option>, } #[async_trait] @@ -96,5 +100,33 @@ pub(crate) mod test { }), } } + + async fn push( + &mut self, + image_ref: &oci_distribution::Reference, + _layers: &[oci_distribution::client::ImageLayer], + _config: oci_distribution::client::Config, + _auth: &oci_distribution::secrets::RegistryAuth, + _manifest: Option, + ) -> Result { + let mock_response = + self.push_response + .as_ref() + .ok_or_else(|| SigstoreError::RegistryPushError { + image: image_ref.whole(), + error: String::from("No push_response provided!"), + })?; + + match mock_response { + Ok(r) => Ok(PushResponse { + config_url: r.config_url.clone(), + manifest_url: r.manifest_url.clone(), + }), + Err(e) => Err(SigstoreError::RegistryPushError { + image: image_ref.whole(), + error: e.to_string(), + }), + } + } } } diff --git a/src/registry/config.rs b/src/registry/config.rs index f553a198e5..2aa1bd213d 100644 --- a/src/registry/config.rs +++ b/src/registry/config.rs @@ -161,7 +161,7 @@ impl Default for ClientConfig { impl From for oci_distribution::client::ClientConfig { fn from(config: ClientConfig) -> Self { oci_distribution::client::ClientConfig { - protocol: oci_distribution::client::ClientProtocol::Https, + protocol: config.protocol.into(), accept_invalid_certificates: config.accept_invalid_certificates, #[cfg(feature = "native-tls")] accept_invalid_hostnames: config.accept_invalid_hostnames, @@ -174,3 +174,30 @@ impl From for oci_distribution::client::ClientConfig { } } } + +/// A client configuration +#[derive(Debug, Clone)] +pub struct PushResponse { + /// Pullable url for the config. + pub config_url: String, + /// Pullable url for the manifest. + pub manifest_url: String, +} + +impl From for oci_distribution::client::PushResponse { + fn from(pr: PushResponse) -> Self { + oci_distribution::client::PushResponse { + config_url: pr.config_url, + manifest_url: pr.manifest_url, + } + } +} + +impl From for PushResponse { + fn from(pr: oci_distribution::client::PushResponse) -> Self { + PushResponse { + config_url: pr.config_url, + manifest_url: pr.manifest_url, + } + } +} diff --git a/src/registry/mod.rs b/src/registry/mod.rs index 38dab356ac..565ab5fa59 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -47,4 +47,13 @@ pub(crate) trait ClientCapabilities: Send + Sync { image: &oci_distribution::Reference, auth: &oci_distribution::secrets::RegistryAuth, ) -> Result<(oci_distribution::manifest::OciManifest, String)>; + + async fn push( + &mut self, + image_ref: &oci_distribution::Reference, + layers: &[oci_distribution::client::ImageLayer], + config: oci_distribution::client::Config, + auth: &oci_distribution::secrets::RegistryAuth, + manifest: Option, + ) -> Result; } diff --git a/src/registry/oci_caching_client.rs b/src/registry/oci_caching_client.rs index 17dc16c141..edc1e6389a 100644 --- a/src/registry/oci_caching_client.rs +++ b/src/registry/oci_caching_client.rs @@ -297,4 +297,21 @@ impl ClientCapabilities for OciCachingClient { data.value }) } + + async fn push( + &mut self, + image_ref: &oci_distribution::Reference, + layers: &[oci_distribution::client::ImageLayer], + config: oci_distribution::client::Config, + auth: &oci_distribution::secrets::RegistryAuth, + manifest: Option, + ) -> Result { + self.registry_client + .push(image_ref, layers, config, auth, manifest) + .await + .map_err(|e| SigstoreError::RegistryPushError { + image: image_ref.whole(), + error: e.to_string(), + }) + } } diff --git a/src/registry/oci_client.rs b/src/registry/oci_client.rs index abad24561d..d22d117a92 100644 --- a/src/registry/oci_client.rs +++ b/src/registry/oci_client.rs @@ -71,4 +71,21 @@ impl ClientCapabilities for OciClient { error: e.to_string(), }) } + + async fn push( + &mut self, + image_ref: &oci_distribution::Reference, + layers: &[oci_distribution::client::ImageLayer], + config: oci_distribution::client::Config, + auth: &oci_distribution::secrets::RegistryAuth, + manifest: Option, + ) -> Result { + self.registry_client + .push(image_ref, layers, config, auth, manifest) + .await + .map_err(|e| SigstoreError::RegistryPushError { + image: image_ref.whole(), + error: e.to_string(), + }) + } }