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

Add support for OCI Image signing (spec v1.0) #158

Merged
merged 8 commits into from
Nov 24, 2022
Merged
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
12 changes: 10 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -57,18 +59,24 @@ 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"
Copy link
Member

Choose a reason for hiding this comment

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

Please file a PR against the upstream project.

tracing-subscriber = { version = "0.3.9", features = ["env-filter"] }

testcontainers = "0.14"
serial_test = "0.9.0"

# cosign example mappings

[[example]]
name = "verify"
path = "examples/cosign/verify/main.rs"

[[example]]
name = "sign"
path = "examples/cosign/sign/main.rs"

# openidconnect example mappings

[[example]]
Expand Down
57 changes: 57 additions & 0 deletions examples/cosign/sign/README.md
Original file line number Diff line number Diff line change
@@ -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
```
185 changes: 185 additions & 0 deletions examples/cosign/sign/main.rs
Original file line number Diff line number Diff line change
@@ -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<String>,

/// Password used to decrypt private key
#[clap(long, required(false))]
password: Option<String>,

/// Annotations that have to be satisfied
#[clap(short, long, required(false))]
annotations: Vec<String>,

/// 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<String, String> = 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);
}
}
}
14 changes: 13 additions & 1 deletion examples/cosign/verify/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand All @@ -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);
Expand Down
Loading