-
Notifications
You must be signed in to change notification settings - Fork 52
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
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
6a170a1
fix: From sigstore ClientConfig to oci_distribution ClientConfig
Xynnn007 a3e77b9
feat: Add to_sigstore_signer to SigStoreKeyPair
Xynnn007 c0b1e2e
fix: add RSA case when trying to parse a key pair
Xynnn007 b488af5
feat: Add useful macros for crypto module
Xynnn007 13ac0b6
feat: add push trait func for oci_client
Xynnn007 cefd239
refactor: refactor SimpleSigning to a submodule
Xynnn007 6f1bd05
feat: Add OCI Image signing support
Xynnn007 36629c2
feat: add HTTP/HTTPS switch for verify example
Xynnn007 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.