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

Fulcio client #132

Merged
merged 7 commits into from
Oct 5, 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
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,7 @@ path = "examples/rekor/search_index/main.rs"
[[example]]
name = "search_log_query"
path = "examples/rekor/search_log_query/main.rs"

[[example]]
name = "fulcio_cert"
path = "examples/fulcio/cert/main.rs"
35 changes: 35 additions & 0 deletions examples/fulcio/cert/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use sigstore::crypto::signing_key::SigningScheme;
use sigstore::fulcio::oauth::OauthTokenProvider;
use sigstore::fulcio::{FulcioClient, TokenProvider, FULCIO_ROOT};
use url::Url;
use x509_parser::pem::Pem;

#[tokio::main]
async fn main() {
let fulcio = FulcioClient::new(
Url::parse(FULCIO_ROOT).unwrap(),
TokenProvider::Oauth(OauthTokenProvider::default()),
);

if let Ok((_signer, cert)) = fulcio
.request_cert(SigningScheme::ECDSA_P256_SHA256_ASN1)
.await
{
println!("Received certificate chain");

for cert in Pem::iter_from_buffer(cert.as_ref()) {
if let Ok(cert) = cert {
if let Ok(result) = cert.parse_x509() {
if let Ok(san) = result.subject_alternative_name() {
if let Some(san) = san {
let san = san.value;
for name in &san.general_names {
println!("SAN: {}", name);
}
}
}
}
}
}
}
}
162 changes: 162 additions & 0 deletions src/fulcio/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
pub mod oauth;

use crate::crypto::signing_key::{SigStoreSigner, SigningScheme};
use crate::errors::{Result, SigstoreError};
use crate::fulcio::oauth::OauthTokenProvider;
use openidconnect::core::CoreIdToken;
use reqwest::header::HeaderName;
use reqwest::Body;
use serde::ser::SerializeStruct;
use serde::{Serialize, Serializer};
use std::convert::{TryFrom, TryInto};
use std::fmt::{Debug, Display, Formatter};
use url::Url;

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

Choose a reason for hiding this comment

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

This should be configurable , someone could be using their own instance of fulcio.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The client takes a URL in the ctor. Const just handy for folks who want to use public fulcio.


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

const CONTENT_TYPE_HEADER_NAME: HeaderName = HeaderName::from_static("content-type");

/// Fulcio certificate signing request
///
/// Used to present a public key and signed challenge/proof-of-key in exchange
/// for a signed X509 certificate in return.
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
struct Csr {
public_key: Option<PublicKey>,
signed_email_address: Option<String>,
}

impl TryFrom<Csr> for Body {
type Error = serde_json::Error;

fn try_from(csr: Csr) -> std::result::Result<Self, Self::Error> {
Ok(Body::from(serde_json::to_string(&csr)?))
}
}

/// Internal newtype to control serde jsonification.
#[derive(Debug)]
struct PublicKey(String, SigningScheme);

impl Serialize for PublicKey {
fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut pk = serializer.serialize_struct("PublicKey", 2)?;
pk.serialize_field("content", &self.0)?;
pk.serialize_field(
"algorithm",
match self.1 {
SigningScheme::ECDSA_P256_SHA256_ASN1 | SigningScheme::ECDSA_P384_SHA384_ASN1 => {
"ecdsa"
}
SigningScheme::ED25519 => "ed25519",
},
)?;
pk.end()
}
}

/// The PEM-encoded certificate chain returned by Fulcio.
pub struct FulcioCert(String);

impl AsRef<[u8]> for FulcioCert {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}

impl Display for FulcioCert {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}

/// Provider for Fulcio token.
#[allow(clippy::large_enum_variant)]
pub enum TokenProvider {
Static((CoreIdToken, String)),
Oauth(OauthTokenProvider),
}

impl TokenProvider {
/// Retrieve a token and the challenge-to-sign from the provider.
pub async fn get_token(&self) -> Result<(CoreIdToken, String)> {
match self {
TokenProvider::Static(inner) => Ok(inner.clone()),
TokenProvider::Oauth(auth) => auth.get_token().await,
}
}
}

/// Client for creating and holding ephemeral key pairs, and easily
/// getting a Fulcio-signed certificate chain.
pub struct FulcioClient {
root_url: Url,
token_provider: TokenProvider,
}

impl FulcioClient {
/// Create a new Fulcio client.
///
/// * root_url: The root Fulcio server URL.
/// * token_provider: Provider capable of providing a CoreIdToken and the challenge to sign.
///
/// Returns a configured Fulcio client.
pub fn new(root_url: Url, token_provider: TokenProvider) -> Self {
Self {
root_url,
token_provider,
}
}

/// Request a certificate from Fulcio
///
/// * signing_scheme: The signing scheme to use.
///
/// Returns a tuple of the appropriately-configured sigstore signer and the Fulcio-issued certificate chain.
pub async fn request_cert(
self,
signing_scheme: SigningScheme,
) -> Result<(SigStoreSigner, FulcioCert)> {
let (token, challenge) = self.token_provider.get_token().await?;

let signer = signing_scheme.create_signer()?;
let signature = signer.sign(challenge.as_bytes())?;
let signature = base64::encode(signature);

let key_pair = signer.to_sigstore_keypair()?;
let public_key = key_pair.public_key_to_der()?;
let public_key = base64::encode(public_key);

let csr = Csr {
public_key: Some(PublicKey(public_key, signing_scheme)),
signed_email_address: Some(signature),
};

let csr: Body = csr.try_into()?;

let client = reqwest::Client::new();
let response = client
.post(self.root_url.join(SIGNING_CERT_PATH)?)
.header(CONTENT_TYPE_HEADER_NAME, "application/json")
.bearer_auth(token.to_string())
.body(csr)
.send()
.await
.map_err(|_| SigstoreError::SigstoreFulcioCertificatesNotProvidedError)?;

let cert = response
.text()
.await
.map_err(|_| SigstoreError::SigstoreFulcioCertificatesNotProvidedError)?;

Ok((signer, FulcioCert(cert)))
}
}
130 changes: 130 additions & 0 deletions src/fulcio/oauth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
use crate::errors::Result;
use crate::errors::SigstoreError;
use crate::oauth::openidflow::{OpenIDAuthorize, RedirectListener};
use openidconnect::core::CoreIdToken;

/// Default client id ("sigstore").
pub const DEFAULT_CLIENT_ID: &str = "sigstore";

/// Default client secret (the empty string)
pub const DEFAULT_CLIENT_SECRET: &str = "";

/// Default issuer (Oauth provider at sigstore.dev)
pub const DEFAULT_ISSUER: &str = "https://oauth2.sigstore.dev/auth";

/// Default local redirect port (8080)
pub const DEFAULT_REDIRECT_PORT: u32 = 8080;

/// Token provider that performs a human-involved OIDC flow to acquire a token id.
#[derive(Default)]
pub struct OauthTokenProvider {
client_id: Option<String>,
client_secret: Option<String>,
issuer: Option<String>,
redirect_port: Option<u32>,
}

impl OauthTokenProvider {
/// Set a non-default client-id.
pub fn with_client_id(self, client_id: &str) -> Self {
Self {
client_id: Some(client_id.to_string()),
client_secret: self.client_secret,
issuer: self.issuer,
redirect_port: self.redirect_port,
}
}

/// Set a non-default client secret.
pub fn with_client_secret(self, client_secret: &str) -> Self {
Self {
client_id: self.client_id,
client_secret: Some(client_secret.to_string()),
issuer: self.issuer,
redirect_port: self.redirect_port,
}
}

/// Set a non-default issuer.
pub fn with_issuer(self, issuer: &str) -> Self {
Self {
client_id: self.client_id,
client_secret: self.client_secret,
issuer: Some(issuer.to_string()),
redirect_port: self.redirect_port,
}
}

/// Set a non-default redirect port.
pub fn with_redirect_port(self, port: u32) -> Self {
Self {
client_id: self.client_id,
client_secret: self.client_secret,
issuer: self.issuer,
redirect_port: Some(port),
}
}

fn redirect_url(&self) -> String {
format!(
"http://localhost:{}",
self.redirect_port.unwrap_or(DEFAULT_REDIRECT_PORT)
)
}

/// Perform human-involved OIDC flow to acquire an id token, along with
/// the extracted email claim value for used in signed challenge with Fulcio.
pub async fn get_token(&self) -> Result<(CoreIdToken, String)> {
let oidc_url = OpenIDAuthorize::new(
self.client_id
.as_ref()
.unwrap_or(&DEFAULT_CLIENT_ID.to_string()),
self.client_secret
.as_ref()
.unwrap_or(&DEFAULT_CLIENT_SECRET.to_string()),
self.issuer.as_ref().unwrap_or(&DEFAULT_ISSUER.to_string()),
&self.redirect_url(),
)
.auth_url_async()
.await;

match oidc_url.as_ref() {
Ok(url) => {
open::that(url.0.to_string())?;
println!(
"Open this URL in a browser if it does not automatically open for you:\n{}\n",
url.0,
);
}
Err(e) => println!("{}", e),
}

let oidc_url = oidc_url?;
let result = RedirectListener::new(
&*format!(
"127.0.0.1:{}",
self.redirect_port.unwrap_or(DEFAULT_REDIRECT_PORT)
),
oidc_url.1.clone(), // client
oidc_url.2.clone(), // nonce
oidc_url.3, // pkce_verifier
)
.redirect_listener_async()
.await;

if let Ok((_, id_token)) = result {
let verifier = oidc_url.1.id_token_verifier();
let nonce = &oidc_url.2;

let claims = id_token.claims(&verifier, nonce);
if let Ok(claims) = claims {
if let Some(email) = claims.email() {
let email = &**email;
return Ok((id_token.clone(), email.clone()));
}
}
}

Err(SigstoreError::NoIDToken)
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ mod mock_client;

pub mod cosign;
pub mod errors;
pub mod fulcio;
pub mod oauth;
pub mod registry;
pub mod rekor;
Expand Down
Loading