diff --git a/Cargo.toml b/Cargo.toml index 704975e0b0..393a9be973 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ readme = "README.md" repository = "https://github.com/sigstore/sigstore-rs" [features] -default = ["full-native-tls", "cached-client", "tuf"] +default = ["full-native-tls", "cached-client", "tuf", "sign"] wasm = ["getrandom/js"] full-native-tls = [ @@ -42,6 +42,8 @@ rekor = ["reqwest"] tuf = ["tough", "regex"] +sign = [] + cosign-native-tls = [ "oci-distribution/native-tls", "cert", @@ -72,7 +74,7 @@ async-trait = "0.1.52" base64 = "0.21.0" cached = { version = "0.46.0", optional = true, features = ["async"] } cfg-if = "1.0.0" -chrono = { version = "0.4.27", default-features = false } +chrono = { version = "0.4.27", default-features = false, features = ["serde"] } const-oid = "0.9.1" digest = { version = "0.10.3", default-features = false } ecdsa = { version = "0.16.7", features = ["pkcs8", "digest", "der", "signing"] } @@ -88,10 +90,7 @@ openidconnect = { version = "3.0", default-features = false, features = [ p256 = "0.13.2" p384 = "0.13" webbrowser = "0.8.4" -pem = "3.0" -picky = { version = "7.0.0-rc.8", default-features = false, features = [ - "x509", -] } +pem = { version = "3.0", features = ["serde"] } pkcs1 = { version = "0.7.5", features = ["std"] } pkcs8 = { version = "0.10.2", features = [ "pem", @@ -113,17 +112,20 @@ serde_json = "1.0.79" serde_with = { version = "3.4.0", features = ["base64"] } sha2 = { version = "0.10.6", features = ["oid"] } signature = { version = "2.0" } +sigstore_protobuf_specs = "0.1.0-rc.2" thiserror = "1.0.30" tokio = { version = "1.17.0", features = ["rt"] } tough = { version = "0.14", features = ["http"], optional = true } tracing = "0.1.31" url = "2.2.2" -x509-cert = { version = "0.2.2", features = ["pem", "std"] } +x509-cert = { version = "0.2.2", features = ["builder", "pem", "std"] } crypto_secretbox = "0.1.1" zeroize = "1.5.7" rustls-webpki = { version = "0.102.0-alpha.4", features = ["alloc"] } rustls-pki-types = { version = "0.2.1", features = ["std"] } serde_repr = "0.1.16" +hex = "0.4.3" +json-syntax = { version = "0.9.6", features = ["canonicalize", "serde"] } [dev-dependencies] anyhow = { version = "1.0", features = ["backtrace"] } @@ -137,7 +139,6 @@ serial_test = "2.0.0" tempfile = "3.3.0" testcontainers = "0.15" tracing-subscriber = { version = "0.3.9", features = ["env-filter"] } -hex = "0.4.3" # cosign example mappings diff --git a/src/bundle/mod.rs b/src/bundle/mod.rs new file mode 100644 index 0000000000..dabef9770f --- /dev/null +++ b/src/bundle/mod.rs @@ -0,0 +1,54 @@ +// Copyright 2023 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. + +//! Useful types for Sigstore bundles. + +use std::fmt::Display; + +pub use sigstore_protobuf_specs::Bundle; + +// Known Sigstore bundle media types. +#[derive(Clone, Copy, Debug)] +pub enum Version { + Bundle0_1, + Bundle0_2, +} + +impl TryFrom<&str> for Version { + type Error = (); + + fn try_from(value: &str) -> Result { + match value { + "application/vnd.dev.sigstore.bundle+json;version=0.1" => Ok(Version::Bundle0_1), + "application/vnd.dev.sigstore.bundle+json;version=0.2" => Ok(Version::Bundle0_2), + _ => Err(()), + } + } +} + +impl From for &str { + fn from(value: Version) -> Self { + match value { + Version::Bundle0_1 => "application/vnd.dev.sigstore.bundle+json;version=0.1", + Version::Bundle0_2 => "application/vnd.dev.sigstore.bundle+json;version=0.2", + } + } +} + +impl Display for Version { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str((*self).into())?; + Ok(()) + } +} diff --git a/src/errors.rs b/src/errors.rs index 430066cc65..77290945ba 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -52,6 +52,9 @@ pub enum SigstoreError { #[error("invalid key format: {error}")] InvalidKeyFormat { error: String }, + #[error("Unable to parse identity token: {0}")] + IdentityTokenError(&'static str), + #[error("unmatched key type {key_typ} and signing scheme {scheme}")] UnmatchedKeyAndSigningScheme { key_typ: String, scheme: String }, @@ -70,6 +73,9 @@ pub enum SigstoreError { #[error("Public key verification error")] PublicKeyVerificationError, + #[error("X.509 certificate version is not V3")] + CertificateUnsupportedVersionError, + #[error("Certificate validity check failed: cannot be used before {0}")] CertificateValidityError(String), @@ -103,6 +109,12 @@ pub enum SigstoreError { #[error("Certificate pool error: {0}")] CertificatePoolError(&'static str), + #[error("Signing session expired")] + ExpiredSigningSession(), + + #[error("Fulcio request unsuccessful: {0}")] + FulcioClientError(&'static str), + #[error("Cannot fetch manifest of {image}: {error}")] RegistryFetchManifestError { image: String, error: String }, @@ -115,9 +127,18 @@ pub enum SigstoreError { #[error("Cannot push {image}: {error}")] RegistryPushError { image: String, error: String }, + #[error("Rekor request unsuccessful: {0}")] + RekorClientError(String), + + #[error(transparent)] + ReqwestError(#[from] reqwest::Error), + #[error("OCI reference not valid: {reference}")] OciReferenceNotValidError { reference: String }, + #[error("Sigstore bundle malformed: {0}")] + SigstoreBundleMalformedError(String), + #[error("Layer doesn't have Sigstore media type")] SigstoreMediaTypeNotFoundError, @@ -155,6 +176,9 @@ pub enum SigstoreError { #[error("{0}")] VerificationConstraintError(String), + #[error("{0}")] + VerificationMaterialError(String), + #[error("{0}")] ApplyConstraintError(String), @@ -214,4 +238,10 @@ pub enum SigstoreError { #[error(transparent)] Ed25519PKCS8Error(#[from] ed25519_dalek::pkcs8::spki::Error), + + #[error(transparent)] + X509ParseError(#[from] x509_cert::der::Error), + + #[error(transparent)] + X509BuilderError(#[from] x509_cert::builder::Error), } diff --git a/src/fulcio/mod.rs b/src/fulcio/mod.rs index 2570a15f15..9a2ab089f3 100644 --- a/src/fulcio/mod.rs +++ b/src/fulcio/mod.rs @@ -1,23 +1,32 @@ +mod models; + pub mod oauth; use crate::crypto::signing_key::SigStoreSigner; use crate::crypto::SigningScheme; use crate::errors::{Result, SigstoreError}; +use crate::fulcio::models::{CreateSigningCertificateRequest, SigningCertificate}; use crate::fulcio::oauth::OauthTokenProvider; +use crate::oauth::IdentityToken; use base64::{engine::general_purpose::STANDARD as BASE64_STD_ENGINE, Engine as _}; use openidconnect::core::CoreIdToken; -use reqwest::Body; +use pkcs8::der::Decode; +use reqwest::{header, Body}; use serde::ser::SerializeStruct; use serde::{Serialize, Serializer}; use std::convert::{TryFrom, TryInto}; use std::fmt::{Debug, Display, Formatter}; use url::Url; +use x509_cert::Certificate; + +pub use models::CertificateResponse; /// Default public Fulcio server root. pub const FULCIO_ROOT: &str = "https://fulcio.sigstore.dev/"; /// Path within Fulcio to obtain a signing certificate. pub const SIGNING_CERT_PATH: &str = "api/v1/signingCert"; +pub const SIGNING_CERT_V2_PATH: &str = "api/v2/signingCert"; const CONTENT_TYPE_HEADER_NAME: &str = "content-type"; @@ -191,4 +200,89 @@ impl FulcioClient { Ok((signer, FulcioCert(cert))) } + + /// Request a certificate from Fulcio with the V2 endpoint. + /// + /// TODO(tnytown): This (and other API clients) probably be autogenerated. See sigstore-rs#209. + /// + /// https://github.com/sigstore/fulcio/blob/main/fulcio.proto + /// + /// Additionally, it might not be reasonable to expect callers to correctly construct and pass + /// in an X509 CSR. + pub fn request_cert_v2( + &self, + request: x509_cert::request::CertReq, + identity: &IdentityToken, + ) -> Result { + let client = reqwest::blocking::Client::new(); + + macro_rules! headers { + ($($key:expr => $val:expr),+) => { + { + let mut map = reqwest::header::HeaderMap::new(); + $( map.insert($key, $val.parse().unwrap()); )+ + map + } + } + } + let headers = headers!( + header::AUTHORIZATION => format!("Bearer {}", identity.to_string()), + header::CONTENT_TYPE => "application/json", + header::ACCEPT => "application/pem-certificate-chain" + ); + + let response: SigningCertificate = client + .post(self.root_url.join(SIGNING_CERT_V2_PATH)?) + .headers(headers) + .json(&CreateSigningCertificateRequest { + certificate_signing_request: request, + }) + .send()? + .json()?; + + let _sct_embedded = matches!( + response, + SigningCertificate::SignedCertificateEmbeddedSct(_) + ); + let certs = match response { + SigningCertificate::SignedCertificateDetachedSct(ref sc) => &sc.chain.certificates, + SigningCertificate::SignedCertificateEmbeddedSct(ref sc) => &sc.chain.certificates, + }; + + if certs.len() < 2 { + return Err(SigstoreError::FulcioClientError( + "Certificate chain too short: certs.len() < 2", + )); + } + + let mut chain = certs + .iter() + .map(|pem| Certificate::from_der(pem.contents())) + .collect::, _>>()?; + let cert = chain + .drain(..1) + .next() + .expect("failed to drain certificates of checked length!"); + + // TODO(tnytown): Implement SCT extraction. + /* + let sct = if sct_embedded { + // PrecertificateSignedCertificateTimestamps isn't implemented yet in x509_cert + // let 😭: ... = cert.tbs_certificate.get(); + todo!("tnytown: embedded SCT extraction"); + } else { + if let SigningCertificate::SignedCertificateDetachedSct(cert) = response { + cert.signed_certificate_timestamp + } else { + unreachable!() + } + }; + */ + + Ok(CertificateResponse { + cert, + chain, + // sct, + }) + } } diff --git a/src/fulcio/models.rs b/src/fulcio/models.rs new file mode 100644 index 0000000000..b4e7dc367d --- /dev/null +++ b/src/fulcio/models.rs @@ -0,0 +1,116 @@ +// Copyright 2023 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. + +//! Models for interfacing with Fulcio. +//! +//! https://github.com/sigstore/fulcio/blob/9da27be4fb64b85c907ab9ddd8a5d3cbd38041d4/fulcio.proto + +use base64::{engine::general_purpose::STANDARD as BASE64_STD_ENGINE, Engine as _}; +use pem::Pem; +use pkcs8::der::EncodePem; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde_repr::Deserialize_repr; +use x509_cert::Certificate; + +fn serialize_x509_csr( + input: &x509_cert::request::CertReq, + ser: S, +) -> std::result::Result +where + S: Serializer, +{ + let encoded = input + .to_pem(pkcs8::LineEnding::CRLF) + .map_err(serde::ser::Error::custom)?; + let encoded = BASE64_STD_ENGINE.encode(encoded); + + ser.serialize_str(&encoded) +} + +fn deserialize_base64<'de, D>(de: D) -> std::result::Result, D::Error> +where + D: Deserializer<'de>, +{ + let buf: &str = Deserialize::deserialize(de)?; + + BASE64_STD_ENGINE + .decode(buf) + .map_err(serde::de::Error::custom) +} + +fn deserialize_inner_detached_sct<'de, D>(de: D) -> std::result::Result +where + D: Deserializer<'de>, +{ + let buf = deserialize_base64(de)?; + + serde_json::from_slice(&buf).map_err(serde::de::Error::custom) +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateSigningCertificateRequest { + #[serde(serialize_with = "serialize_x509_csr")] + pub certificate_signing_request: x509_cert::request::CertReq, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum SigningCertificate { + SignedCertificateDetachedSct(SigningCertificateDetachedSCT), + SignedCertificateEmbeddedSct(SigningCertificateEmbeddedSCT), +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SigningCertificateDetachedSCT { + pub chain: CertificateChain, + #[serde(deserialize_with = "deserialize_inner_detached_sct")] + pub signed_certificate_timestamp: InnerDetachedSCT, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SigningCertificateEmbeddedSCT { + pub chain: CertificateChain, +} + +#[derive(Deserialize)] +pub struct CertificateChain { + pub certificates: Vec, +} + +#[derive(Deserialize)] +pub struct InnerDetachedSCT { + pub sct_version: SCTVersion, + #[serde(deserialize_with = "deserialize_base64")] + pub id: Vec, + pub timestamp: u64, + #[serde(deserialize_with = "deserialize_base64")] + pub signature: Vec, + #[serde(deserialize_with = "deserialize_base64")] + pub extensions: Vec, +} + +#[derive(Deserialize_repr, PartialEq, Debug)] +#[repr(u8)] +pub enum SCTVersion { + V1 = 0, +} + +pub struct CertificateResponse { + pub cert: Certificate, + pub chain: Vec, + // pub sct: InnerDetachedSCT, +} diff --git a/src/lib.rs b/src/lib.rs index afea5441a0..0c648b5f62 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -283,3 +283,10 @@ pub mod rekor; #[cfg(feature = "tuf")] pub mod tuf; + +// Don't export yet -- these types should only be useful internally. +mod bundle; +pub use bundle::Bundle; + +#[cfg(feature = "sign")] +pub mod sign; diff --git a/src/oauth/mod.rs b/src/oauth/mod.rs index 24d240b35f..a5419e7fce 100644 --- a/src/oauth/mod.rs +++ b/src/oauth/mod.rs @@ -14,3 +14,6 @@ // limitations under the License. pub mod openidflow; + +mod token; +pub use token::IdentityToken; diff --git a/src/oauth/token.rs b/src/oauth/token.rs new file mode 100644 index 0000000000..3c61731d87 --- /dev/null +++ b/src/oauth/token.rs @@ -0,0 +1,101 @@ +// Copyright 2023 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 chrono::{DateTime, Utc}; +use openidconnect::core::CoreIdToken; +use serde::Deserialize; + +use base64::{engine::general_purpose::STANDARD_NO_PAD as base64, Engine as _}; + +use crate::errors::SigstoreError; + +#[derive(Deserialize)] +pub struct Claims { + pub aud: String, + #[serde(with = "chrono::serde::ts_seconds")] + pub exp: DateTime, + pub email: String, +} + +pub type UnverifiedClaims = Claims; + +/// A Sigstore token. +pub struct IdentityToken { + original_token: String, + // header + claims: UnverifiedClaims, + // signature +} + +impl IdentityToken { + /// Returns the **unverified** claim set for the token. + /// + /// The [UnverifiedClaims] returned from this method should not be used to enforce security + /// invariants. + pub fn unverified_claims(&self) -> &UnverifiedClaims { + &self.claims + } + + pub fn appears_to_be_expired(&self) -> bool { + Utc::now() > self.claims.exp + } +} + +impl TryFrom<&str> for IdentityToken { + type Error = SigstoreError; + + fn try_from(value: &str) -> Result { + let parts: Vec<_> = value.split('.').take(3).collect(); + + if parts.len() != 3 { + return Err(SigstoreError::IdentityTokenError("Malformed JWT")); + } + let &[_, claims, _] = &parts[..] else { + unreachable!() + }; + + let claims = base64 + .decode(claims) + .or(Err(SigstoreError::IdentityTokenError( + "Malformed JWT: Unable to decode claims", + )))?; + let claims: Claims = serde_json::from_slice(&claims).or(Err( + SigstoreError::IdentityTokenError("Malformed JWT: claims JSON malformed"), + ))?; + if claims.aud != "sigstore" { + return Err(SigstoreError::IdentityTokenError("Not a Sigstore JWT")); + } + + Ok(IdentityToken { + original_token: value.to_owned(), + claims, + }) + } +} + +impl From for IdentityToken { + fn from(value: CoreIdToken) -> Self { + value + .to_string() + .as_str() + .try_into() + .expect("Token conversion failed") + } +} + +impl ToString for IdentityToken { + fn to_string(&self) -> String { + self.original_token.clone() + } +} diff --git a/src/rekor/models/log_entry.rs b/src/rekor/models/log_entry.rs index b3b86f2fef..5caadd3db2 100644 --- a/src/rekor/models/log_entry.rs +++ b/src/rekor/models/log_entry.rs @@ -1,3 +1,18 @@ +// +// Copyright 2023 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 crate::errors::SigstoreError; use crate::rekor::TreeSize; use base64::{engine::general_purpose::STANDARD as BASE64_STD_ENGINE, Engine as _}; @@ -14,7 +29,7 @@ use super::{ /// Stores the response returned by Rekor after making a new entry #[derive(Default, Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields, rename_all = "camelCase")] pub struct LogEntry { pub uuid: String, #[serde(skip_serializing_if = "Option::is_none")] @@ -95,4 +110,10 @@ pub struct InclusionProof { pub log_index: i64, pub root_hash: String, pub tree_size: TreeSize, + + /// A snapshot of the transparency log's state at a specific point in time, + /// in [Signed Note format]. + /// + /// [Signed Note format]: https://github.com/transparency-dev/formats/blob/main/log/README.md + pub checkpoint: String, } diff --git a/src/sign.rs b/src/sign.rs new file mode 100644 index 0000000000..80f39919a4 --- /dev/null +++ b/src/sign.rs @@ -0,0 +1,324 @@ +// Copyright 2023 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::cell::OnceCell; +use std::io::{self, Read}; +use std::str::FromStr; +use std::time::SystemTime; + +use base64::{engine::general_purpose::STANDARD as base64, Engine as _}; +use hex; +use json_syntax::Print; +use p256::NistP256; +use pkcs8::der::{Encode, EncodePem}; +use sha2::{Digest, Sha256}; +use signature::DigestSigner; +use sigstore_protobuf_specs::{ + Bundle, DevSigstoreBundleV1VerificationMaterial, DevSigstoreCommonV1HashOutput, + DevSigstoreCommonV1LogId, DevSigstoreCommonV1MessageSignature, + DevSigstoreCommonV1X509Certificate, DevSigstoreCommonV1X509CertificateChain, + DevSigstoreRekorV1Checkpoint, DevSigstoreRekorV1InclusionPromise, + DevSigstoreRekorV1InclusionProof, DevSigstoreRekorV1KindVersion, + DevSigstoreRekorV1TransparencyLogEntry, +}; +use url::Url; +use x509_cert::builder::{Builder, RequestBuilder as CertRequestBuilder}; +use x509_cert::{ext::pkix as x509_ext, name::Name as X509Name}; + +use crate::bundle::Version; +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::rekor::apis::configuration::Configuration as RekorConfiguration; +use crate::rekor::apis::entries_api::create_log_entry; +use crate::rekor::models::LogEntry; +use crate::rekor::models::{hashedrekord, proposed_entry::ProposedEntry as ProposedLogEntry}; + +/// A Sigstore signing session. +/// +/// Sessions hold a provided user identity and key materials tied to that identity. A single +/// session may be used to sign multiple items. For more information, see [`Self::sign()`]. +pub struct SigningSession<'ctx> { + context: &'ctx SigningContext, + identity_token: IdentityToken, + private_key: ecdsa::SigningKey, + certs: OnceCell, +} + +impl<'ctx> SigningSession<'ctx> { + fn new(context: &'ctx SigningContext, identity_token: IdentityToken) -> Self { + Self { + context, + identity_token, + private_key: Self::private_key(), + certs: Default::default(), + } + } + + fn private_key() -> ecdsa::SigningKey { + let mut rng = rand::thread_rng(); + let secret_key = p256::SecretKey::random(&mut rng); + ecdsa::SigningKey::from(secret_key) + } + + fn certs(&self) -> SigstoreResult<&fulcio::CertificateResponse> { + fn init_certs( + fulcio: &FulcioClient, + identity: &IdentityToken, + private_key: &ecdsa::SigningKey, + ) -> SigstoreResult { + let subject = X509Name::from_str(&format!( + "emailAddress={}", + identity.unverified_claims().email + )) + .expect("failed to initialize constant X509Name!"); + + let mut builder = CertRequestBuilder::new(subject, private_key)?; + builder + .add_extension(&x509_ext::BasicConstraints { + ca: false, + path_len_constraint: None, + }) + .expect("failed to initialize constant BasicConstaints!"); + + let cert_req = builder + .build::() + .expect("CSR signing failed"); + fulcio.request_cert_v2(cert_req, identity) + } + + let resp = init_certs( + &self.context.fulcio, + &self.identity_token, + &self.private_key, + )?; + Ok(self.certs.get_or_init(|| resp)) + } + + /// Check if the session's identity token or key material is expired. + /// + /// If the session is expired, it cannot be used for signing operations, and a new session + /// must be created with a fresh identity token. + pub fn is_expired(&self) -> bool { + self.identity_token.appears_to_be_expired() + || self.certs().is_ok_and(|certs| { + let not_after = certs + .cert + .tbs_certificate + .validity + .not_after + .to_system_time(); + + SystemTime::now() > not_after + }) + } + + /// Signs for the input with the session's identity. If the identity is expired, + /// [SigstoreError::ExpiredSigningSession] is returned. + /// + /// TODO(tnytown): Make this async safe. We may need to make the underlying trait functions + /// implementations async and wrap them with executors for the sync variants. Our async + /// variants would also need to use async variants of common traits (AsyncRead? AsyncHasher?) + pub fn sign(&self, input: &mut R) -> SigstoreResult { + if self.is_expired() { + return Err(SigstoreError::ExpiredSigningSession()); + } + + let mut hasher = Sha256::new(); + io::copy(input, &mut hasher)?; + + let certs = self.certs()?; + // TODO(tnytown): Verify SCT here. + + // Sign artifact. + let input_hash: &[u8] = &hasher.clone().finalize(); + let artifact_signature: p256::ecdsa::Signature = self.private_key.sign_digest(hasher); + + // Prepare inputs. + let b64_artifact_signature = base64.encode(artifact_signature.to_der()); + let cert = &certs.cert; + + // Create the transparency log entry. + let proposed_entry = ProposedLogEntry::Hashedrekord { + api_version: "0.0.1".to_owned(), + spec: hashedrekord::Spec { + signature: hashedrekord::Signature { + content: b64_artifact_signature.clone(), + public_key: hashedrekord::PublicKey::new( + base64.encode(cert.to_pem(pkcs8::LineEnding::CRLF)?), + ), + }, + data: hashedrekord::Data { + hash: hashedrekord::Hash { + algorithm: hashedrekord::AlgorithmKind::sha256, + value: hex::encode(input_hash), + }, + }, + }, + }; + + // HACK(tnytown): We aren't async yet. + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + let entry = rt + .block_on(create_log_entry(&self.context.rekor_config, proposed_entry)) + .map_err(|err| { + eprintln!("original: {err:?}"); + SigstoreError::RekorClientError(err.to_string()) + })?; + + // TODO(tnytown): Maybe run through the verification flow here? See sigstore-rs#296. + + Ok(SigningArtifact { + input_digest: base64.encode(input_hash), + cert: cert.to_der()?, + b64_signature: b64_artifact_signature, + log_entry: entry, + }) + } +} + +/// A Sigstore signing context. +/// +/// Contexts hold Fulcio (CA) and Rekor (CT) configurations which signing sessions can be +/// constructed against. Use [`Self::production()`] to create a context against the public-good +/// Sigstore infrastructure. +pub struct SigningContext { + fulcio: FulcioClient, + rekor_config: RekorConfiguration, +} + +impl SigningContext { + /// Manually constructs a [SigningContext] from its constituent data. + pub fn new(fulcio: FulcioClient, rekor_config: RekorConfiguration) -> Self { + Self { + fulcio, + rekor_config, + } + } + + /// Returns a [SigningContext] configured against the public-good production Sigstore + /// infrastructure. + pub fn production() -> Self { + Self::new( + FulcioClient::new( + Url::parse(FULCIO_ROOT).expect("constant FULCIO root fails to parse!"), + crate::fulcio::TokenProvider::Oauth(OauthTokenProvider::default()), + ), + Default::default(), + ) + } + + /// Configures and returns a [SigningSession] with the held context. + pub fn signer(&self, identity_token: IdentityToken) -> SigningSession { + SigningSession::new(self, identity_token) + } +} + +/// A signature and its associated metadata. +pub struct SigningArtifact { + input_digest: String, + cert: Vec, + b64_signature: String, + log_entry: LogEntry, +} + +impl SigningArtifact { + /// Consumes the signing artifact and produces a Sigstore [Bundle]. + /// + /// The resulting bundle can be serialized with with [serde_json]. + pub fn to_bundle(self) -> Bundle { + #[inline] + fn hex_to_base64>(hex: S) -> String { + let decoded = hex::decode(hex.as_ref()).expect("Malformed data in Rekor response"); + base64.encode(decoded) + } + + // NOTE: We explicitly only include the leaf certificate in the bundle's "chain" + // here: the specs explicitly forbid the inclusion of the root certificate, + // and discourage inclusion of any intermediates (since they're in the root of + // trust already). + let x_509_certificate_chain = Some(DevSigstoreCommonV1X509CertificateChain { + certificates: Some(vec![DevSigstoreCommonV1X509Certificate { + raw_bytes: Some(base64.encode(&self.cert)), + }]), + }); + + let inclusion_proof = if let Some(proof) = self.log_entry.verification.inclusion_proof { + let hashes = proof.hashes.iter().map(hex_to_base64).collect(); + Some(DevSigstoreRekorV1InclusionProof { + checkpoint: Some(DevSigstoreRekorV1Checkpoint { + envelope: Some(proof.checkpoint), + }), + hashes: Some(hashes), + log_index: Some(proof.log_index.to_string()), + root_hash: Some(hex_to_base64(proof.root_hash)), + tree_size: Some(proof.tree_size.to_string()), + }) + } else { + None + }; + + let canonicalized_body = { + let mut body = json_syntax::to_value(self.log_entry.body) + .expect("failed to parse constructed Body!"); + body.canonicalize(); + Some(base64.encode(body.compact_print().to_string())) + }; + + // TODO(tnytown): When we fix `sigstore_protobuf_specs`, have the Rekor client APIs convert + // responses into types from the specs as opposed to returning the raw `LogEntry` model type. + let tlog_entry = DevSigstoreRekorV1TransparencyLogEntry { + canonicalized_body, + inclusion_promise: Some(DevSigstoreRekorV1InclusionPromise { + // XX: sigstore-python deserializes the SET from base64 here because their protobuf + // library transparently serializes `bytes` fields as base64. + signed_entry_timestamp: Some(self.log_entry.verification.signed_entry_timestamp), + }), + inclusion_proof, + integrated_time: Some(self.log_entry.integrated_time.to_string()), + kind_version: Some(DevSigstoreRekorV1KindVersion { + kind: Some("hashedrekord".to_owned()), + version: Some("0.0.1".to_owned()), + }), + log_id: Some(DevSigstoreCommonV1LogId { + key_id: Some(hex_to_base64(self.log_entry.log_i_d)), + }), + log_index: Some(self.log_entry.log_index.to_string()), + }; + + let verification_material = Some(DevSigstoreBundleV1VerificationMaterial { + public_key: None, + timestamp_verification_data: None, + tlog_entries: Some(vec![tlog_entry]), + x_509_certificate_chain, + }); + + let message_signature = Some(DevSigstoreCommonV1MessageSignature { + message_digest: Some(DevSigstoreCommonV1HashOutput { + algorithm: Some("SHA2_256".to_owned()), + digest: Some(self.input_digest), + }), + signature: Some(self.b64_signature), + }); + Bundle { + dsse_envelope: None, + media_type: Some(Version::Bundle0_2.to_string()), + message_signature, + verification_material, + } + } +} diff --git a/src/tuf/constants.rs b/src/tuf/constants.rs index 325989706c..0529d1128b 100644 --- a/src/tuf/constants.rs +++ b/src/tuf/constants.rs @@ -23,3 +23,4 @@ macro_rules! tuf_resource { } pub(crate) const SIGSTORE_ROOT: &[u8] = tuf_resource!("prod/root.json"); +pub(crate) const _SIGSTORE_TRUST_BUNDLE: &[u8] = tuf_resource!("prod/trusted_root.json"); diff --git a/src/tuf/repository_helper.rs b/src/tuf/repository_helper.rs index 74747d363e..e821c230e4 100644 --- a/src/tuf/repository_helper.rs +++ b/src/tuf/repository_helper.rs @@ -154,6 +154,111 @@ impl RepositoryHelper { } } +/// Given a `range`, checks that the the current time is not before `start`. If +/// `allow_expired` is `false`, also checks that the current time is not after +/// `end`. +fn is_timerange_valid(range: Option, allow_expired: bool) -> bool { + let time = chrono::Utc::now(); + + match range { + // If there was no validity period specified, the key is always valid. + None => true, + // Active: if the current time is before the starting period, we are not yet valid. + Some(range) if time < range.start => false, + // If we want Expired keys, then the key is valid at this point. + _ if allow_expired => true, + // Otherwise, check that we are in range if the range has an end. + Some(range) => match range.end { + None => true, + Some(end) => time <= end, + }, + } +} + +/// Download a file stored inside of a TUF repository, try to reuse a local +/// cache when possible. +/// +/// * `repository`: TUF repository holding the file +/// * `target_name`: TUF representation of the file to be downloaded +/// * `local_file`: location where the file should be downloaded +/// +/// This function will reuse the local copy of the file if contents +/// didn't change. +/// This check is done by comparing the digest of the local file, if found, +/// with the digest reported inside of the TUF repository metadata. +/// +/// **Note well:** the `local_file` is updated whenever its contents are +/// outdated. +fn fetch_target_or_reuse_local_cache( + repository: &tough::Repository, + target_name: &TargetName, + local_file: Option<&PathBuf>, +) -> Result> { + let (local_file_outdated, local_file_contents) = if let Some(path) = local_file { + is_local_file_outdated(repository, target_name, path) + } else { + Ok((true, None)) + }?; + + let data = if local_file_outdated { + let data = fetch_target(repository, target_name)?; + if let Some(path) = local_file { + // update the local file to have latest data from the TUF repo + fs::write(path, data.clone())?; + } + data + } else { + local_file_contents + .expect("local file contents to not be 'None'") + .as_bytes() + .to_owned() + }; + + Ok(data) +} + +/// Download a file from a TUF repository +fn fetch_target(repository: &tough::Repository, target_name: &TargetName) -> Result> { + let data: Vec; + match repository.read_target(target_name).map_err(Box::new)? { + None => Err(SigstoreError::TufTargetNotFoundError( + target_name.raw().to_string(), + )), + Some(reader) => { + data = read_to_end(reader)?; + Ok(data) + } + } +} + +/// Compares the checksum of a local file, with the digest reported inside of +/// TUF repository metadata +fn is_local_file_outdated( + repository: &tough::Repository, + target_name: &TargetName, + local_file: &Path, +) -> Result<(bool, Option)> { + let target = repository + .targets() + .signed + .targets + .get(target_name) + .ok_or_else(|| SigstoreError::TufTargetNotFoundError(target_name.raw().to_string()))?; + + if local_file.exists() { + let data = fs::read_to_string(local_file)?; + let local_checksum = Sha256::digest(data.clone()); + let expected_digest: Vec = target.hashes.sha256.to_vec(); + + if local_checksum.as_slice() == expected_digest.as_slice() { + // local data is not outdated + Ok((false, Some(data))) + } else { + Ok(keys) + } + } +} + #[cfg(test)] mod tests { use super::super::constants::*; diff --git a/tests/data/repository/1.registry.npmjs.org.json b/tests/data/repository/1.registry.npmjs.org.json new file mode 100644 index 0000000000..1c8ec2165b --- /dev/null +++ b/tests/data/repository/1.registry.npmjs.org.json @@ -0,0 +1,23 @@ +{ + "signed": { + "_type": "targets", + "spec_version": "1.0", + "version": 1, + "expires": "2024-09-29T16:47:20Z", + "targets": { + "registry.npmjs.org/keys.json": { + "length": 1017, + "hashes": { + "sha256": "7a8ec9678ad824cdccaa7a6dc0961caf8f8df61bc7274189122c123446248426", + "sha512": "881a853ee92d8cf513b07c164fea36b22a7305c256125bdfffdc5c65a4205c4c3fc2b5bcc98964349167ea68d40b8cd02551fcaa870a30d4601ba1caf6f63699" + } + } + } + }, + "signatures": [ + { + "keyid": "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600", + "sig": "3044022059bf01a64dd2793d5b630e26d7b6e455b0d6d8b47c23049ae856a122e5cec2ab022068b99b8bb39457e53d500f698cb43f9e640958ed26e5d3a47c29619df61889bc" + } + ] +} \ No newline at end of file diff --git a/tests/data/repository/1.snapshot.json b/tests/data/repository/1.snapshot.json new file mode 100644 index 0000000000..fcb179878c --- /dev/null +++ b/tests/data/repository/1.snapshot.json @@ -0,0 +1,32 @@ +{ + "signed": { + "_type": "snapshot", + "spec_version": "1.0", + "version": 1, + "expires": "2024-04-19T16:47:48Z", + "meta": { + "registry.npmjs.org.json": { + "length": 713, + "hashes": { + "sha256": "17b361687dbb401c2d51d7ce21688d13547eae7f8e7b2183b7dd6d94fa675705", + "sha512": "3f60a08cdbab650ece48ded43b54943dc816580fdb2f5a2a20c30e878eb2489ab817f0308666cac80da03d75d6f5b71959431b1ba7794335fece8a4ed635eb4d" + }, + "version": 1 + }, + "targets.json": { + "length": 4518, + "hashes": { + "sha256": "cc62e5fb1644717c7429c82b6a1cbd085008f9a2e07aad38573f8fdf9d55386c", + "sha512": "5709bc76bc35da403a9a0a5ec96890db49e797c986eda9e5f7973938dbccad96838c8136617c91f5218cfd919d93745d3942ca6d50a52b5fd0e662e6876b395f" + }, + "version": 1 + } + } + }, + "signatures": [ + { + "keyid": "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600", + "sig": "304602210082d244d5dab0c20ee07b3229964beffaa8bb0bdf4c5107e2f764619878d124a2022100e7c50116ef636c41348ec49a7502f1c98037238b9c717ee781b62c5154f5a1f0" + } + ] +} \ No newline at end of file diff --git a/tests/data/repository/1.targets.json b/tests/data/repository/1.targets.json new file mode 100644 index 0000000000..6844bad771 --- /dev/null +++ b/tests/data/repository/1.targets.json @@ -0,0 +1,148 @@ +{ + "signed": { + "_type": "targets", + "spec_version": "1.0", + "version": 1, + "expires": "2024-09-29T16:47:20Z", + "targets": { + "artifact.pub": { + "length": 177, + "hashes": { + "sha256": "59ebf97a9850aecec4bc39c1f5c1dc46e6490a6b5fd2a6cacdcac0c3a6fc4cbf", + "sha512": "308fd1d1d95d7f80aa33b837795251cc3e886792982275e062409e13e4e236ffc34d676682aa96fdc751414de99c864bf132dde71581fa651c6343905e3bf988" + }, + "custom": { + "sigstore": { + "status": "Active", + "usage": "Unknown" + } + } + }, + "ctfe.pub": { + "length": 177, + "hashes": { + "sha256": "7fcb94a5d0ed541260473b990b99a6c39864c1fb16f3f3e594a5a3cebbfe138a", + "sha512": "4b20747d1afe2544238ad38cc0cc3010921b177d60ac743767e0ef675b915489bd01a36606c0ff83c06448622d7160f0d866c83d20f0c0f44653dcc3f9aa0bd4" + }, + "custom": { + "sigstore": { + "status": "Active", + "uri": "https://ctfe.sigstore.dev/test", + "usage": "CTFE" + } + } + }, + "ctfe_2022.pub": { + "length": 178, + "hashes": { + "sha256": "270488a309d22e804eeb245493e87c667658d749006b9fee9cc614572d4fbbdc", + "sha512": "e83fa4f427b24ee7728637fad1b4aa45ebde2ba02751fa860694b1bb16059a490328f9985e51cc70e4d237545315a1bc866dc4fdeef2f6248d99cc7a6077bf85" + }, + "custom": { + "sigstore": { + "status": "Active", + "uri": "https://ctfe.sigstore.dev/2022", + "usage": "CTFE" + } + } + }, + "fulcio.crt.pem": { + "length": 744, + "hashes": { + "sha256": "f360c53b2e13495a628b9b8096455badcb6d375b185c4816d95a5d746ff29908", + "sha512": "0713252a7fd17f7f3ab12f88a64accf2eb14b8ad40ca711d7fe8b4ecba3b24db9e9dffadb997b196d3867b8f9ff217faf930d80e4dab4e235c7fc3f07be69224" + }, + "custom": { + "sigstore": { + "status": "Expired", + "uri": "https://fulcio.sigstore.dev", + "usage": "Fulcio" + } + } + }, + "fulcio_intermediate_v1.crt.pem": { + "length": 789, + "hashes": { + "sha256": "f8cbecf186db7714624a5f4e99da31a917cbef70a94dd6921f5c3ca969dfe30a", + "sha512": "0f99f47dbc26c5f1e3cba0bfd9af4245a26e5cb735d6ef005792ec7e603f66fdb897de985973a6e50940ca7eff5e1849719e967b5ad2dac74a29115a41cf6f21" + }, + "custom": { + "sigstore": { + "status": "Active", + "uri": "https://fulcio.sigstore.dev", + "usage": "Fulcio" + } + } + }, + "fulcio_v1.crt.pem": { + "length": 740, + "hashes": { + "sha256": "f989aa23def87c549404eadba767768d2a3c8d6d30a8b793f9f518a8eafd2cf5", + "sha512": "f2e33a6dc208cee1f51d33bbea675ab0f0ced269617497985f9a0680689ee7073e4b6f8fef64c91bda590d30c129b3070dddce824c05bc165ac9802f0705cab6" + }, + "custom": { + "sigstore": { + "status": "Active", + "uri": "https://fulcio.sigstore.dev", + "usage": "Fulcio" + } + } + }, + "rekor.pub": { + "length": 178, + "hashes": { + "sha256": "dce5ef715502ec9f3cdfd11f8cc384b31a6141023d3e7595e9908a81cb6241bd", + "sha512": "0ae7705e02db33e814329746a4a0e5603c5bdcd91c96d072158d71011a2695788866565a2fec0fe363eb72cbcaeda39e54c5fe8d416daf9f3101fdba4217ef35" + }, + "custom": { + "sigstore": { + "status": "Active", + "uri": "https://rekor.sigstore.dev", + "usage": "Rekor" + } + } + }, + "trusted_root.json": { + "length": 4567, + "hashes": { + "sha256": "cec894ad77f79b1cb324150f6363012bcef7492954f3ab9134f932e6aa2e2e20", + "sha512": "08be2fd75c19e654caad30852847c566f97e6245f2bbcc54d347d6bdec7e879135e3395b5633b9e3b85d739fdb9b4eb8c09ddc70495792bc2ea65c8caf770d27" + } + } + }, + "delegations": { + "keys": { + "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600": { + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEXMZ7rD8tWDE4lK/+naJN7INMxNC7\nbMMANDqTQE7WpzyzffWOg59hc/MwbvJtvuxhO9mEu3GD3Cn0HffFlmVRiA==\n-----END PUBLIC KEY-----\n" + } + } + }, + "roles": [ + { + "name": "registry.npmjs.org", + "keyids": [ + "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600" + ], + "threshold": 1, + "terminating": true, + "paths": [ + "registry.npmjs.org/*" + ] + } + ] + } + }, + "signatures": [ + { + "keyid": "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda", + "sig": "304402201662b260e99e59f7271bd9e3fb01aa47a399bef8c5ec808bea6d40ae2d93625d022042fd2a275d84196dc50e17ca9c9408a34349372410febc7217415b11eb978bbb" + } + ] +} \ No newline at end of file diff --git a/tests/data/repository/2.registry.npmjs.org.json b/tests/data/repository/2.registry.npmjs.org.json new file mode 100644 index 0000000000..d53f15267b --- /dev/null +++ b/tests/data/repository/2.registry.npmjs.org.json @@ -0,0 +1,23 @@ +{ + "signed": { + "_type": "targets", + "spec_version": "1.0", + "version": 2, + "expires": "2028-09-29T21:10:55Z", + "targets": { + "registry.npmjs.org/keys.json": { + "length": 1017, + "hashes": { + "sha256": "7a8ec9678ad824cdccaa7a6dc0961caf8f8df61bc7274189122c123446248426", + "sha512": "881a853ee92d8cf513b07c164fea36b22a7305c256125bdfffdc5c65a4205c4c3fc2b5bcc98964349167ea68d40b8cd02551fcaa870a30d4601ba1caf6f63699" + } + } + } + }, + "signatures": [ + { + "keyid": "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600", + "sig": "3045022057b9fc8afd9feaf45cf3173d3420fdcd6b68c22e4ef7b47e80a6887e1f20246c0221009f39c42fac630ab354c5197288c9a82ab6d46a59b423f81fff719da57cff16ab" + } + ] +} \ No newline at end of file diff --git a/tests/data/repository/2.snapshot.json b/tests/data/repository/2.snapshot.json new file mode 100644 index 0000000000..6c1e4dd147 --- /dev/null +++ b/tests/data/repository/2.snapshot.json @@ -0,0 +1,32 @@ +{ + "signed": { + "_type": "snapshot", + "spec_version": "1.0", + "version": 2, + "expires": "2028-04-19T21:11:16Z", + "meta": { + "registry.npmjs.org.json": { + "length": 715, + "hashes": { + "sha256": "4dc55b2b468b0d1c9629c457c5cfce2cc1c330c59c5a7cf71cb7549f1ef76f1d", + "sha512": "278f4b6112db9d4bd9366e1717cf710ad7eacf44605fd4f894c3374fc5dff850a1a03c24c4a885d050a4ac1a86fa6929537fae12d8c2864c8e0c239b382d5556" + }, + "version": 2 + }, + "targets.json": { + "length": 4120, + "hashes": { + "sha256": "095d093de09350cec021828f49361688b5dd692486ad7bfb03d4150b3269ef8a", + "sha512": "97b9c75f49fb41eaf2f33c5f58b125febc3bbecd4c97f6edd0901423a231e4d0c5760d4780bc180a364d7198b5e0710f07ee0abf84dcd163fe3348d6bce26fab" + }, + "version": 2 + } + } + }, + "signatures": [ + { + "keyid": "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600", + "sig": "3044022013bf1032cf0a37d9f88ab9d33d0abb7a932efd95fadbc354fc21a807c7be29ef0220677e651c3e67e0728591faa20c5a09b8a16953038c3ceeffb7d9cfec766b3245" + } + ] +} \ No newline at end of file diff --git a/tests/data/repository/2.targets.json b/tests/data/repository/2.targets.json new file mode 100644 index 0000000000..dea42487f0 --- /dev/null +++ b/tests/data/repository/2.targets.json @@ -0,0 +1,135 @@ +{ + "signed": { + "_type": "targets", + "spec_version": "1.0", + "version": 2, + "expires": "2028-09-29T21:10:55Z", + "targets": { + "ctfe.pub": { + "length": 775, + "hashes": { + "sha256": "bd7a6812a1f239dfddbbb19d36c7423d21510da56d466ba5018401959cd66037", + "sha512": "b861189e48df51186a39612230fba6b02af951f7b35ad9375e8ca182d0e085d470e26d69f7cd4d7450a0f223991e8e5a4ddf8f1968caa15255de8e37035af43a" + }, + "custom": { + "sigstore": { + "status": "Active", + "uri": "https://ctfe.sigstage.dev/test", + "usage": "CTFE" + } + } + }, + "ctfe_2022.pub": { + "length": 178, + "hashes": { + "sha256": "910d899c7763563095a0fe684c8477573fedc19a78586de6ecfbfd8f289f5423", + "sha512": "ab975a75600fc366a837536d0dcba841b755552d21bb114498ff8ac9d2403f76643f5b91269bce5d124a365514719a3edee9dcc2b046cb173f51af659911fcd3" + }, + "custom": { + "sigstore": { + "status": "Active", + "uri": "https://ctfe.sigstage.dev/2022", + "usage": "CTFE" + } + } + }, + "ctfe_2022_2.pub": { + "length": 178, + "hashes": { + "sha256": "7054b4f15f969daca1c242bb9e77527abaf0b9acf9818a2a35144e4b32b20dc6", + "sha512": "3d035f94e1b14ac84627a28afdbed9a34861fb84239f76d73aa1a99f52262bfd95c4fa0ee71f1fd7e3bfb998d89cd5e0f0eafcff9fa7fa87c6e23484fc1e0cec" + }, + "custom": { + "sigstore": { + "status": "Active", + "uri": "https://ctfe.sigstage.dev/2022-2", + "usage": "CTFE" + } + } + }, + "fulcio.crt.pem": { + "length": 741, + "hashes": { + "sha256": "0e6b0442485ad552bea5f62f11c29e2acfda35307d7538430b4cc1dbef49bff1", + "sha512": "c69ae618883a0c89c282c0943a1ad0c16b0a7788f74e47a1adefc631dac48a0c4449d8c3de7455ae7d772e43c4a87e341f180b0614a46a86006969f8a7b84532" + }, + "custom": { + "sigstore": { + "status": "Active", + "uri": "https://fulcio.sigstage.dev", + "usage": "Fulcio" + } + } + }, + "fulcio_intermediate.crt.pem": { + "length": 790, + "hashes": { + "sha256": "782868913fe13c385105ddf33e827191386f58da40a931f2075a7e27b1b6ac7b", + "sha512": "90659875a02f73d1026055427c6d857c556e410e23748ff88aeb493227610fd2f5fbdd95ef2a21565f91438dfb3e073f50c4c9dd06f9a601b5d9b064d5cb60b4" + }, + "custom": { + "sigstore": { + "status": "Active", + "uri": "https://fulcio.sigstage.dev", + "usage": "Fulcio" + } + } + }, + "rekor.pub": { + "length": 178, + "hashes": { + "sha256": "1d80b8f72505a43e65e6e125247cd508f61b459dc457c1d1bcb78d96e1760959", + "sha512": "09ab08698a67354a95d3b8897d9ce7eaef05f06f5ed5f0202d79c228579858ecc5816b7e1b7cc6786abe7d6aaa758e1fcb05900cb749235186c3bf9522d6d7ce" + }, + "custom": { + "sigstore": { + "status": "Active", + "uri": "https://rekor.sigstage.dev", + "usage": "Rekor" + } + } + }, + "trusted_root.json": { + "length": 4521, + "hashes": { + "sha256": "6494317303d0e04509a30b239bf8290057164fba67072b6f89ddf1032273a78b", + "sha512": "fa2ca05656176f993fd616fa8586f3deeaacfb891dfb6f58e02b26073cb0233a52b7e66338d0053c8549f551485581141094c2de40ca812d8ac47a128bf84963" + } + } + }, + "delegations": { + "keys": { + "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600": { + "keytype": "ecdsa-sha2-nistp256", + "scheme": "ecdsa-sha2-nistp256", + "keyid_hash_algorithms": [ + "sha256", + "sha512" + ], + "keyval": { + "public": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEXMZ7rD8tWDE4lK/+naJN7INMxNC7\nbMMANDqTQE7WpzyzffWOg59hc/MwbvJtvuxhO9mEu3GD3Cn0HffFlmVRiA==\n-----END PUBLIC KEY-----\n" + } + } + }, + "roles": [ + { + "name": "registry.npmjs.org", + "keyids": [ + "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600" + ], + "threshold": 1, + "terminating": true, + "paths": [ + "registry.npmjs.org/*" + ] + } + ] + } + }, + "signatures": [ + { + "keyid": "c8e09a68b5821b75462ae0df52151c81deb7f1838246dc1da8c34cc91ec12bda", + "sig": "304502210090b089087d1b17b2517c464b7774d76d3ea558ffca874eed63ccbee8f6bc3b76022022b56f551bcd0ac8a9c35cd0724ac5b00b4984544cbf812f47f276a9b48db8db" + } + ] +} \ No newline at end of file diff --git a/tests/data/repository/registry.npmjs.org.json b/tests/data/repository/registry.npmjs.org.json new file mode 100644 index 0000000000..d53f15267b --- /dev/null +++ b/tests/data/repository/registry.npmjs.org.json @@ -0,0 +1,23 @@ +{ + "signed": { + "_type": "targets", + "spec_version": "1.0", + "version": 2, + "expires": "2028-09-29T21:10:55Z", + "targets": { + "registry.npmjs.org/keys.json": { + "length": 1017, + "hashes": { + "sha256": "7a8ec9678ad824cdccaa7a6dc0961caf8f8df61bc7274189122c123446248426", + "sha512": "881a853ee92d8cf513b07c164fea36b22a7305c256125bdfffdc5c65a4205c4c3fc2b5bcc98964349167ea68d40b8cd02551fcaa870a30d4601ba1caf6f63699" + } + } + } + }, + "signatures": [ + { + "keyid": "314ae73abd3012fc73bfcc3783e31d03852716597642b891d6a33155c4baf600", + "sig": "3045022057b9fc8afd9feaf45cf3173d3420fdcd6b68c22e4ef7b47e80a6887e1f20246c0221009f39c42fac630ab354c5197288c9a82ab6d46a59b423f81fff719da57cff16ab" + } + ] +} \ No newline at end of file diff --git a/tests/data/repository/targets/6494317303d0e04509a30b239bf8290057164fba67072b6f89ddf1032273a78b.trusted_root.json b/tests/data/repository/targets/6494317303d0e04509a30b239bf8290057164fba67072b6f89ddf1032273a78b.trusted_root.json new file mode 100644 index 0000000000..6a1c1f5a40 --- /dev/null +++ b/tests/data/repository/targets/6494317303d0e04509a30b239bf8290057164fba67072b6f89ddf1032273a78b.trusted_root.json @@ -0,0 +1,86 @@ +{ + "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", + "tlogs": [ + { + "baseUrl": "https://rekor.sigstage.dev", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDODRU688UYGuy54mNUlaEBiQdTE9nYLr0lg6RXowI/QV/RE1azBn4Eg5/2uTOMbhB1/gfcHzijzFi9Tk+g1Prg==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2021-01-12T11:53:27.000Z" + } + }, + "logId": { + "keyId": "0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=" + } + } + ], + "certificateAuthorities": [ + { + "subject": { + "organization": "sigstore.dev", + "commonName": "sigstore" + }, + "uri": "https://fulcio.sigstage.dev", + "certChain": { + "certificates": [ + { + "rawBytes": "MIIB9jCCAXugAwIBAgITDdEJvluliE0AzYaIE4jTMdnFTzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDMyNTE2NTA0NloXDTMyMDMyMjE2NTA0NVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMo9BUNk9QIYisYysC24+2OytoV72YiLonYcqR3yeVnYziPt7Xv++CYE8yoCTiwedUECCWKOcvQKRCJZb9ht4Hzy+VvBx36hK+C6sECCSR0x6pPSiz+cTk1f788ZjBlUZaNjMGEwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP9CMrpofas6cK/cDNQa4j6Hj2ZlMB8GA1UdIwQYMBaAFP9CMrpofas6cK/cDNQa4j6Hj2ZlMAoGCCqGSM49BAMDA2kAMGYCMQD+kojuzMwztNay9Ibzjuk//ZL5m6T2OCsm45l1lY004pcb984L926BowodoirFMcMCMQDIJtFHhP/1D3a+M3dAGomOb6O4CmTry3TTPbPsAFnv22YA0Y+P21NVoxKDjdu0tkw=" + }, + { + "rawBytes": "MIICGTCCAaCgAwIBAgITJta/okfgHvjabGm1BOzuhrwA1TAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDQxNDIxMzg0MFoXDTMyMDMyMjE2NTA0NVowNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASosAySWJQ/tK5r8T5aHqavk0oI+BKQbnLLdmOMRXHQF/4Hx9KtNfpcdjH9hNKQSBxSlLFFN3tvFCco0qFBzWYwZtsYsBe1l91qYn/9VHFTaEVwYQWIJEEvrs0fvPuAqjajezB5MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEFBQcDAzASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRxhjCmFHxib/n31vQFGn9f/+tvrDAfBgNVHSMEGDAWgBT/QjK6aH2rOnCv3AzUGuI+h49mZTAKBggqhkjOPQQDAwNnADBkAjAM1lbKkcqQlE/UspMTbWNo1y2TaJ44tx3l/FJFceTSdDZ+0W1OHHeU4twie/lq8XgCMHQxgEv26xNNiAGyPXbkYgrDPvbOqp0UeWX4mJnLSrBr3aN/KX1SBrKQu220FmVL0Q==" + } + ] + }, + "validFor": { + "start": "2022-03-25T16:50:46.000Z" + } + } + ], + "ctlogs": [ + { + "baseUrl": "https://ctfe.sigstage.dev/test", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MIICCgKCAgEA27A2MPQXm0I0v7/Ly5BIauDjRZF5Jor9vU+QheoE2UIIsZHcyYq3slHzSSHy2lLj1ZD2d91CtJ492ZXqnBmsr4TwZ9jQ05tW2mGIRI8u2DqN8LpuNYZGz/f9SZrjhQQmUttqWmtu3UoLfKz6NbNXUnoo+NhZFcFRLXJ8VporVhuiAmL7zqT53cXR3yQfFPCUDeGnRksnlhVIAJc3AHZZSHQJ8DEXMhh35TVv2nYhTI3rID7GwjXXw4ocz7RGDD37ky6p39Tl5NB71gT1eSqhZhGHEYHIPXraEBd5+3w9qIuLWlp5Ej/K6Mu4ELioXKCUimCbwy+Cs8UhHFlqcyg4AysOHJwIadXIa8LsY51jnVSGrGOEBZevopmQPNPtyfFY3dmXSS+6Z3RD2Gd6oDnNGJzpSyEk410Ag5uvNDfYzJLCWX9tU8lIxNwdFYmIwpd89HijyRyoGnoJ3entd63cvKfuuix5r+GHyKp1Xm1L5j5AWM6P+z0xigwkiXnt+adexAl1J9wdDxv/pUFEESRF4DG8DFGVtbdH6aR1A5/vD4krO4tC1QYUSeyL5Mvsw8WRqIFHcXtgybtxylljvNcGMV1KXQC8UFDmpGZVDSHx6v3e/BHMrZ7gjoCCfVMZ/cFcQi0W2AIHPYEMH/C95J2r4XbHMRdYXpovpOoT5Ca78gsCAwEAAQ==", + "keyDetails": "PKCS1_RSA_PKCS1V5", + "validFor": { + "start": "2021-03-14T00:00:00.000Z" + } + }, + "logId": { + "keyId": "s9AOb93xWxr+a4ztxJnxxJCX7VZ0V3IF4jTu/OoL84A=" + } + }, + { + "baseUrl": "https://ctfe.sigstage.dev/2022", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEh99xuRi6slBFd8VUJoK/rLigy4bYeSYWO/fE6Br7r0D8NpMI94+A63LR/WvLxpUUGBpY8IJA3iU2telag5CRpA==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2022-07-01T00:00:00.000Z" + } + }, + "logId": { + "keyId": "++JKOMQt7SJ3ynUHnCfnDhcKP8/58J4TueMqXuk3HmA=" + } + }, + { + "baseUrl": "https://ctfe.sigstage.dev/2022-2", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8gEDKNme8AnXuPBgHjrtXdS6miHqc24CRblNEOFpiJRngeq8Ko73Y+K18yRYVf1DXD4AVLwvKyzdNdl5n0jUSQ==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2022-07-01T00:00:00.000Z" + } + }, + "logId": { + "keyId": "KzC83GiIyeLh2CYpXnQfSDkxlgLynDPLXkNA/rKshno=" + } + } + ], + "timestampAuthorities": [] +} diff --git a/tests/data/repository/targets/fa2ca05656176f993fd616fa8586f3deeaacfb891dfb6f58e02b26073cb0233a52b7e66338d0053c8549f551485581141094c2de40ca812d8ac47a128bf84963.trusted_root.json b/tests/data/repository/targets/fa2ca05656176f993fd616fa8586f3deeaacfb891dfb6f58e02b26073cb0233a52b7e66338d0053c8549f551485581141094c2de40ca812d8ac47a128bf84963.trusted_root.json new file mode 100644 index 0000000000..6a1c1f5a40 --- /dev/null +++ b/tests/data/repository/targets/fa2ca05656176f993fd616fa8586f3deeaacfb891dfb6f58e02b26073cb0233a52b7e66338d0053c8549f551485581141094c2de40ca812d8ac47a128bf84963.trusted_root.json @@ -0,0 +1,86 @@ +{ + "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", + "tlogs": [ + { + "baseUrl": "https://rekor.sigstage.dev", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDODRU688UYGuy54mNUlaEBiQdTE9nYLr0lg6RXowI/QV/RE1azBn4Eg5/2uTOMbhB1/gfcHzijzFi9Tk+g1Prg==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2021-01-12T11:53:27.000Z" + } + }, + "logId": { + "keyId": "0y8wo8MtY5wrdiIFohx7sHeI5oKDpK5vQhGHI6G+pJY=" + } + } + ], + "certificateAuthorities": [ + { + "subject": { + "organization": "sigstore.dev", + "commonName": "sigstore" + }, + "uri": "https://fulcio.sigstage.dev", + "certChain": { + "certificates": [ + { + "rawBytes": "MIIB9jCCAXugAwIBAgITDdEJvluliE0AzYaIE4jTMdnFTzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDMyNTE2NTA0NloXDTMyMDMyMjE2NTA0NVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMo9BUNk9QIYisYysC24+2OytoV72YiLonYcqR3yeVnYziPt7Xv++CYE8yoCTiwedUECCWKOcvQKRCJZb9ht4Hzy+VvBx36hK+C6sECCSR0x6pPSiz+cTk1f788ZjBlUZaNjMGEwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP9CMrpofas6cK/cDNQa4j6Hj2ZlMB8GA1UdIwQYMBaAFP9CMrpofas6cK/cDNQa4j6Hj2ZlMAoGCCqGSM49BAMDA2kAMGYCMQD+kojuzMwztNay9Ibzjuk//ZL5m6T2OCsm45l1lY004pcb984L926BowodoirFMcMCMQDIJtFHhP/1D3a+M3dAGomOb6O4CmTry3TTPbPsAFnv22YA0Y+P21NVoxKDjdu0tkw=" + }, + { + "rawBytes": "MIICGTCCAaCgAwIBAgITJta/okfgHvjabGm1BOzuhrwA1TAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIyMDQxNDIxMzg0MFoXDTMyMDMyMjE2NTA0NVowNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASosAySWJQ/tK5r8T5aHqavk0oI+BKQbnLLdmOMRXHQF/4Hx9KtNfpcdjH9hNKQSBxSlLFFN3tvFCco0qFBzWYwZtsYsBe1l91qYn/9VHFTaEVwYQWIJEEvrs0fvPuAqjajezB5MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEFBQcDAzASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRxhjCmFHxib/n31vQFGn9f/+tvrDAfBgNVHSMEGDAWgBT/QjK6aH2rOnCv3AzUGuI+h49mZTAKBggqhkjOPQQDAwNnADBkAjAM1lbKkcqQlE/UspMTbWNo1y2TaJ44tx3l/FJFceTSdDZ+0W1OHHeU4twie/lq8XgCMHQxgEv26xNNiAGyPXbkYgrDPvbOqp0UeWX4mJnLSrBr3aN/KX1SBrKQu220FmVL0Q==" + } + ] + }, + "validFor": { + "start": "2022-03-25T16:50:46.000Z" + } + } + ], + "ctlogs": [ + { + "baseUrl": "https://ctfe.sigstage.dev/test", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MIICCgKCAgEA27A2MPQXm0I0v7/Ly5BIauDjRZF5Jor9vU+QheoE2UIIsZHcyYq3slHzSSHy2lLj1ZD2d91CtJ492ZXqnBmsr4TwZ9jQ05tW2mGIRI8u2DqN8LpuNYZGz/f9SZrjhQQmUttqWmtu3UoLfKz6NbNXUnoo+NhZFcFRLXJ8VporVhuiAmL7zqT53cXR3yQfFPCUDeGnRksnlhVIAJc3AHZZSHQJ8DEXMhh35TVv2nYhTI3rID7GwjXXw4ocz7RGDD37ky6p39Tl5NB71gT1eSqhZhGHEYHIPXraEBd5+3w9qIuLWlp5Ej/K6Mu4ELioXKCUimCbwy+Cs8UhHFlqcyg4AysOHJwIadXIa8LsY51jnVSGrGOEBZevopmQPNPtyfFY3dmXSS+6Z3RD2Gd6oDnNGJzpSyEk410Ag5uvNDfYzJLCWX9tU8lIxNwdFYmIwpd89HijyRyoGnoJ3entd63cvKfuuix5r+GHyKp1Xm1L5j5AWM6P+z0xigwkiXnt+adexAl1J9wdDxv/pUFEESRF4DG8DFGVtbdH6aR1A5/vD4krO4tC1QYUSeyL5Mvsw8WRqIFHcXtgybtxylljvNcGMV1KXQC8UFDmpGZVDSHx6v3e/BHMrZ7gjoCCfVMZ/cFcQi0W2AIHPYEMH/C95J2r4XbHMRdYXpovpOoT5Ca78gsCAwEAAQ==", + "keyDetails": "PKCS1_RSA_PKCS1V5", + "validFor": { + "start": "2021-03-14T00:00:00.000Z" + } + }, + "logId": { + "keyId": "s9AOb93xWxr+a4ztxJnxxJCX7VZ0V3IF4jTu/OoL84A=" + } + }, + { + "baseUrl": "https://ctfe.sigstage.dev/2022", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEh99xuRi6slBFd8VUJoK/rLigy4bYeSYWO/fE6Br7r0D8NpMI94+A63LR/WvLxpUUGBpY8IJA3iU2telag5CRpA==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2022-07-01T00:00:00.000Z" + } + }, + "logId": { + "keyId": "++JKOMQt7SJ3ynUHnCfnDhcKP8/58J4TueMqXuk3HmA=" + } + }, + { + "baseUrl": "https://ctfe.sigstage.dev/2022-2", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8gEDKNme8AnXuPBgHjrtXdS6miHqc24CRblNEOFpiJRngeq8Ko73Y+K18yRYVf1DXD4AVLwvKyzdNdl5n0jUSQ==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2022-07-01T00:00:00.000Z" + } + }, + "logId": { + "keyId": "KzC83GiIyeLh2CYpXnQfSDkxlgLynDPLXkNA/rKshno=" + } + } + ], + "timestampAuthorities": [] +} diff --git a/tests/data/repository/targets/registry.npmjs.org/7a8ec9678ad824cdccaa7a6dc0961caf8f8df61bc7274189122c123446248426.keys.json b/tests/data/repository/targets/registry.npmjs.org/7a8ec9678ad824cdccaa7a6dc0961caf8f8df61bc7274189122c123446248426.keys.json new file mode 100644 index 0000000000..f5667a5f0e --- /dev/null +++ b/tests/data/repository/targets/registry.npmjs.org/7a8ec9678ad824cdccaa7a6dc0961caf8f8df61bc7274189122c123446248426.keys.json @@ -0,0 +1,26 @@ +{ + "keys": [ + { + "keyId": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA", + "keyUsage": "npm:signatures", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "1999-01-01T00:00:00.000Z" + } + } + }, + { + "keyId": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA", + "keyUsage": "npm:attestations", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2022-12-01T00:00:00.000Z" + } + } + } + ] +} diff --git a/tests/data/repository/targets/registry.npmjs.org/881a853ee92d8cf513b07c164fea36b22a7305c256125bdfffdc5c65a4205c4c3fc2b5bcc98964349167ea68d40b8cd02551fcaa870a30d4601ba1caf6f63699.keys.json b/tests/data/repository/targets/registry.npmjs.org/881a853ee92d8cf513b07c164fea36b22a7305c256125bdfffdc5c65a4205c4c3fc2b5bcc98964349167ea68d40b8cd02551fcaa870a30d4601ba1caf6f63699.keys.json new file mode 100644 index 0000000000..f5667a5f0e --- /dev/null +++ b/tests/data/repository/targets/registry.npmjs.org/881a853ee92d8cf513b07c164fea36b22a7305c256125bdfffdc5c65a4205c4c3fc2b5bcc98964349167ea68d40b8cd02551fcaa870a30d4601ba1caf6f63699.keys.json @@ -0,0 +1,26 @@ +{ + "keys": [ + { + "keyId": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA", + "keyUsage": "npm:signatures", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "1999-01-01T00:00:00.000Z" + } + } + }, + { + "keyId": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA", + "keyUsage": "npm:attestations", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2022-12-01T00:00:00.000Z" + } + } + } + ] +}