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

Token fixes #412

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
6 changes: 3 additions & 3 deletions examples/bundle/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ fn sign(artifact_path: &PathBuf) {
});

let token = authorize();
let email = &token.unverified_claims().email.clone();
debug!("Signing with {}", email);
let identity = token.identity_claim.to_string();
debug!("Signing with {}", identity);

let signing_artifact = SigningContext::production().and_then(|ctx| {
ctx.blocking_signer(token)
Expand All @@ -105,7 +105,7 @@ fn sign(artifact_path: &PathBuf) {
println!(
"Created signature bundle {} with identity {}",
bundle_path.display(),
email
identity
);
}

Expand Down
10 changes: 8 additions & 2 deletions src/bundle/sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ use crate::crypto::transparency::{verify_sct, CertificateEmbeddedSCT};
use crate::errors::{Result as SigstoreResult, SigstoreError};
use crate::fulcio::oauth::OauthTokenProvider;
use crate::fulcio::{self, FulcioClient, FULCIO_ROOT};
use crate::oauth::IdentityToken;
use crate::oauth::{IdentityClaim, IdentityToken};
use crate::rekor::apis::configuration::Configuration as RekorConfiguration;
use crate::rekor::apis::entries_api::create_log_entry;
use crate::rekor::models::{hashedrekord, proposed_entry::ProposedEntry as ProposedLogEntry};
Expand Down Expand Up @@ -85,6 +85,12 @@ impl<'ctx> SigningSession<'ctx> {
fulcio: &FulcioClient,
token: &IdentityToken,
) -> SigstoreResult<(ecdsa::SigningKey<NistP256>, fulcio::CertificateResponse)> {
// NOTE: Currently both email and machine identities get wrapped in a "email" OID.
// Fulcio does not care about the content.
let identity = match &token.identity_claim {
IdentityClaim::Sub(identity) | IdentityClaim::Email(identity) => identity.as_str(),
};

let subject =
// SEQUENCE OF RelativeDistinguishedName
vec![
Expand All @@ -95,7 +101,7 @@ impl<'ctx> SigningSession<'ctx> {
oid: const_oid::db::rfc3280::EMAIL_ADDRESS,
value: AttributeValue::new(
pkcs8::der::Tag::Utf8String,
token.unverified_claims().email.as_ref(),
identity.as_ref(),
)?,
}
].try_into()?
Expand Down
1 change: 1 addition & 0 deletions src/oauth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@
pub mod openidflow;

pub mod token;
pub use token::IdentityClaim;
pub use token::IdentityToken;
111 changes: 109 additions & 2 deletions src/oauth/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use std::fmt;

use chrono::{DateTime, Utc};
use openidconnect::core::CoreIdToken;
use serde::Deserialize;
Expand All @@ -28,15 +30,41 @@ pub struct Claims {
#[serde(with = "chrono::serde::ts_seconds_option")]
#[serde(default)]
pub nbf: Option<DateTime<Utc>>,
pub email: String,
pub email: Option<String>,
pub iss: String,
pub sub: Option<String>,
}

pub type UnverifiedClaims = Claims;

/// A Sigstore token.
// The identity that should be compatible with Fulcio: Depending on the issuer it is
// either a "sub" or "email" claim.
#[derive(Debug, PartialEq)]
pub enum IdentityClaim {
Sub(String),
Email(String),
}

impl fmt::Display for IdentityClaim {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
IdentityClaim::Sub(sub) => sub.fmt(f),
IdentityClaim::Email(email) => email.fmt(f),
}
}
}

/// A Sigstore (Fulcio) authentication token
///
/// An IdentityToken is built from a OIDC JWT received from an OIDC provider and is used to
/// authenticate a signing identity to Fulcio to get a signing certificate for that identity.
///
/// The content of the token, including identity and issuer claims, come unverified from
/// the JWT: IdentityToken only makes some validity checks.
pub struct IdentityToken {
original_token: String,
claims: UnverifiedClaims,
pub identity_claim: IdentityClaim,
}

impl IdentityToken {
Expand Down Expand Up @@ -82,9 +110,35 @@ impl TryFrom<&str> for IdentityToken {
));
}

// Find the identity claim that we believe Fulcio used for this token.
// This means a few special cases and fall back on "sub" claim
let identity = match claims.iss.as_str() {
"https://accounts.google.com"
| "https://oauth2.sigstore.dev/auth"
| "https://oauth2.sigstage.dev/auth" => {
if let Some(email) = claims.email.as_ref() {
IdentityClaim::Email(email.clone())
} else {
return Err(SigstoreError::IdentityTokenError(
"Email claim not found in JWT".into(),
));
}
}
_ => {
if let Some(sub) = claims.sub.as_ref() {
IdentityClaim::Sub(sub.clone())
} else {
return Err(SigstoreError::IdentityTokenError(
"Sub claim not found in JWT".into(),
));
}
}
};

Ok(IdentityToken {
original_token: value.to_owned(),
claims,
identity_claim: identity,
})
}
}
Expand All @@ -104,3 +158,56 @@ impl std::fmt::Display for IdentityToken {
write!(f, "{}", self.original_token.clone())
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::fs;

#[test]
fn interactive_token() {
let content = fs::read_to_string("tests/data/tokens/interactive-token.txt").unwrap();
let identity_token = IdentityToken::try_from(content.as_str()).unwrap();
assert_eq!(
identity_token.claims.email,
Some(String::from("[email protected]"))
);
assert_eq!(
identity_token.identity_claim,
IdentityClaim::Email(String::from("[email protected]"))
);
assert_eq!(identity_token.claims.aud, "sigstore");
assert_eq!(
identity_token.claims.iss,
"https://oauth2.sigstore.dev/auth"
);
assert_eq!(
identity_token.claims.exp,
DateTime::parse_from_rfc3339("2024-10-21T12:15:30Z").unwrap()
);
}

#[test]
fn github_actions_token() {
let content = fs::read_to_string("tests/data/tokens/gha-token.txt").unwrap();
let identity_token = IdentityToken::try_from(content.as_str()).unwrap();
assert_eq!(identity_token.claims.email, None);
assert_eq!(
identity_token.claims.sub,
Some(String::from("repo:sigstore-conformance/extremely-dangerous-public-oidc-beacon:ref:refs/heads/main"))
Copy link
Member Author

@jku jku Oct 31, 2024

Choose a reason for hiding this comment

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

I'm going to double check if this is correct: It is the token "sub" field... but it does not seem to be what "bundle verify" uses for --identity

Copy link
Member Author

Choose a reason for hiding this comment

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

I guess that's just how it works? When we ask for the certificate we have no idea what the actual certificate subject is going to be as it's not the identity claim in the OIDC token... I really hope I'm just tired and that's not true

Copy link
Member Author

Choose a reason for hiding this comment

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

yes, I found the identity handling bits in fulcio and can confirm: before we get the certificate we ultimately do not know who we are signing as... exposing the token identity here is still useful since it happens to match the signing identity in the interactive case but it's unfortunately not as useful as I hoped.

);
assert_eq!(
identity_token.identity_claim,
IdentityClaim::Sub(String::from("repo:sigstore-conformance/extremely-dangerous-public-oidc-beacon:ref:refs/heads/main"))
);
assert_eq!(identity_token.claims.aud, "sigstore");
assert_eq!(
identity_token.claims.iss,
"https://token.actions.githubusercontent.com"
);
assert_eq!(
identity_token.claims.exp,
DateTime::parse_from_rfc3339("2024-10-21T07:29:49Z").unwrap()
);
}
}
1 change: 1 addition & 0 deletions tests/data/tokens/gha-token.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ikh5cTROQVRBanNucUM3bWRydEFoaHJDUjJfUSIsImtpZCI6IjFGMkFCODM0MDRDMDhFQzlFQTBCQjk5REFFRDAyMTg2QjA5MURCRjQifQ.eyJqdGkiOiI4NTI3ZGQyMy0zYmNjLTQzYjgtYTMyNy1kYjFkM2Q4ZTY4NGUiLCJzdWIiOiJyZXBvOnNpZ3N0b3JlLWNvbmZvcm1hbmNlL2V4dHJlbWVseS1kYW5nZXJvdXMtcHVibGljLW9pZGMtYmVhY29uOnJlZjpyZWZzL2hlYWRzL21haW4iLCJhdWQiOiJzaWdzdG9yZSIsInJlZiI6InJlZnMvaGVhZHMvbWFpbiIsInNoYSI6IjZjZjNkMWQ3MTNjZDM0MTA4ODA3NTRmNWQxZDAyNTE4MTM2OTMzNTciLCJyZXBvc2l0b3J5Ijoic2lnc3RvcmUtY29uZm9ybWFuY2UvZXh0cmVtZWx5LWRhbmdlcm91cy1wdWJsaWMtb2lkYy1iZWFjb24iLCJyZXBvc2l0b3J5X293bmVyIjoic2lnc3RvcmUtY29uZm9ybWFuY2UiLCJyZXBvc2l0b3J5X293bmVyX2lkIjoiMTMxODA0NTYzIiwicnVuX2lkIjoiMTE0MzUyMjEzNDQiLCJydW5fbnVtYmVyIjoiMTY5NDA3IiwicnVuX2F0dGVtcHQiOiIxIiwicmVwb3NpdG9yeV92aXNpYmlsaXR5IjoicHVibGljIiwicmVwb3NpdG9yeV9pZCI6IjYzMjU5Njg5NyIsImFjdG9yX2lkIjoiNDE4OTgyODIiLCJhY3RvciI6ImdpdGh1Yi1hY3Rpb25zW2JvdF0iLCJ3b3JrZmxvdyI6IkV4dHJlbWVseSBkYW5nZXJvdXMgT0lEQyBiZWFjb24iLCJoZWFkX3JlZiI6IiIsImJhc2VfcmVmIjoiIiwiZXZlbnRfbmFtZSI6IndvcmtmbG93X2Rpc3BhdGNoIiwicmVmX3Byb3RlY3RlZCI6InRydWUiLCJyZWZfdHlwZSI6ImJyYW5jaCIsIndvcmtmbG93X3JlZiI6InNpZ3N0b3JlLWNvbmZvcm1hbmNlL2V4dHJlbWVseS1kYW5nZXJvdXMtcHVibGljLW9pZGMtYmVhY29uLy5naXRodWIvd29ya2Zsb3dzL2V4dHJlbWVseS1kYW5nZXJvdXMtb2lkYy1iZWFjb24ueW1sQHJlZnMvaGVhZHMvbWFpbiIsIndvcmtmbG93X3NoYSI6IjZjZjNkMWQ3MTNjZDM0MTA4ODA3NTRmNWQxZDAyNTE4MTM2OTMzNTciLCJqb2Jfd29ya2Zsb3dfcmVmIjoic2lnc3RvcmUtY29uZm9ybWFuY2UvZXh0cmVtZWx5LWRhbmdlcm91cy1wdWJsaWMtb2lkYy1iZWFjb24vLmdpdGh1Yi93b3JrZmxvd3MvZXh0cmVtZWx5LWRhbmdlcm91cy1vaWRjLWJlYWNvbi55bWxAcmVmcy9oZWFkcy9tYWluIiwiam9iX3dvcmtmbG93X3NoYSI6IjZjZjNkMWQ3MTNjZDM0MTA4ODA3NTRmNWQxZDAyNTE4MTM2OTMzNTciLCJydW5uZXJfZW52aXJvbm1lbnQiOiJnaXRodWItaG9zdGVkIiwiaXNzIjoiaHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbSIsIm5iZiI6MTcyOTQ5NDg4OSwiZXhwIjoxNzI5NDk1Nzg5LCJpYXQiOjE3Mjk0OTU0ODl9.qMQkx6qIODt4fFvEpOFQNT7Gw-GVeoUtPNLwl-RGpJaO2EdCys4o3iPrX1-h8yvEtKpv4DFrtgNfzwTJCb9ueEWqW1Ll5oijYyd2VR7ghAYEeGV-sdwdkNQ1HO09UcRZcht00dgYayeVhbY4967dV5fNWuGib0c9BwJ2K1stzber3HgvGjjhPXoeKYdXvEE0L0MMq30b_eu1XW6ojvfeBTzujgHNxK8_drKAK1R9ENpBFBgreBeJvA1zu3hrvkby2g_sktRoHH2daOsdp4UhZjnr8IWJVMTAVHyueNJSu-UFKd5--TLKUdt_CpVW_PI_uv_xrKgJ9gU7n63w1qdbIQ
1 change: 1 addition & 0 deletions tests/data/tokens/interactive-token.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
eyJhbGciOiJSUzI1NiIsImtpZCI6IjMxNjA2OGMzM2ZhMjg2OTZhZmI5YzM5YWI2OTMxMjY1ZDk0Y2I3NTUifQ.eyJpc3MiOiJodHRwczovL29hdXRoMi5zaWdzdG9yZS5kZXYvYXV0aCIsInN1YiI6IkNnVXpNVGc0T1JJbWFIUjBjSE02SlRKR0pUSkdaMmwwYUhWaUxtTnZiU1V5Um14dloybHVKVEpHYjJGMWRHZyIsImF1ZCI6InNpZ3N0b3JlIiwiZXhwIjoxNzI5NTEyOTMwLCJpYXQiOjE3Mjk1MTI4NzAsIm5vbmNlIjoiNTI3NjM3Y2UtN2Q2MS00MDA5LThkM2EtNGNjZGM3OGJiZDg1IiwiYXRfaGFzaCI6IktmMUNPTXB5TVJDTkdzWWp1QXczclEiLCJlbWFpbCI6ImprdUBnb3RvLmZpIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImZlZGVyYXRlZF9jbGFpbXMiOnsiY29ubmVjdG9yX2lkIjoiaHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoIiwidXNlcl9pZCI6IjMxODg5In19.s27uZ3vpIzRS4eWdC3pM0FSsYkHNvScQoii_TcSRVZhtrcPAbA4D95Pw_R_UB-qRquMK1BHepKmeN1b1-CQ00jiFZgUOf9sDLC3Hy3oQejGJsYKb-7oeHs7amLz3SBzPwDwVd09e-7Yu1x9YV5k6aezqruLLt42C_kyOTsHeCIWWMEVmGp32105Jkj8YT5uEYXS-aOEvQFvAYsDfKgGuiJtGybUycVcJEfqyWI3cami7fkjU5PcCx8oFyP2E7YNRw4UeNWCTn7WFtL2onrgDm0oa2AqF3gtH4Q-9ByksVq3y6xQdoLj1ydzWcoCzsF43oZ6O6DkLmWk5fu3FxNyewg
Loading