diff options
Diffstat (limited to 'cli/tools/registry/mod.rs')
-rw-r--r-- | cli/tools/registry/mod.rs | 247 |
1 files changed, 243 insertions, 4 deletions
diff --git a/cli/tools/registry/mod.rs b/cli/tools/registry/mod.rs index eadd0e44d..e01c1563b 100644 --- a/cli/tools/registry/mod.rs +++ b/cli/tools/registry/mod.rs @@ -15,11 +15,13 @@ use deno_core::error::AnyError; use deno_core::futures::FutureExt; use deno_core::serde_json; use deno_core::serde_json::json; +use deno_core::serde_json::Value; use deno_core::unsync::JoinSet; use deno_runtime::deno_fetch::reqwest; use deno_terminal::colors; use import_map::ImportMap; use lsp_types::Url; +use serde::Deserialize; use serde::Serialize; use sha2::Digest; @@ -48,6 +50,7 @@ mod auth; mod diagnostics; mod graph; mod paths; +mod provenance; mod publish_order; mod tar; mod unfurl; @@ -73,6 +76,7 @@ struct PreparedPublishPackage { version: String, tarball: PublishableTarball, config: String, + exports: HashMap<String, String>, } impl PreparedPublishPackage { @@ -161,6 +165,18 @@ async fn prepare_publish( package: name_no_scope.to_string(), version: version.to_string(), tarball, + exports: match &deno_json.json.exports { + Some(Value::Object(exports)) => exports + .into_iter() + .map(|(k, v)| (k.to_string(), v.as_str().unwrap().to_string())) + .collect(), + Some(Value::String(exports)) => { + let mut map = HashMap::new(); + map.insert(".".to_string(), exports.to_string()); + map + } + _ => HashMap::new(), + }, // the config file is always at the root of a publishing dir, // so getting the file name is always correct config: config_path @@ -454,6 +470,7 @@ async fn perform_publish( mut publish_order_graph: PublishOrderGraph, mut prepared_package_by_name: HashMap<String, Rc<PreparedPublishPackage>>, auth_method: AuthMethod, + provenance: bool, ) -> Result<(), AnyError> { let client = http_client.client()?; let registry_api_url = jsr_api_url().to_string(); @@ -514,6 +531,7 @@ async fn perform_publish( ®istry_api_url, ®istry_url, &authorization, + provenance, ) .await .with_context(|| format!("Failed to publish {}", display_name))?; @@ -540,6 +558,7 @@ async fn publish_package( registry_api_url: &str, registry_url: &str, authorization: &str, + provenance: bool, ) -> Result<(), AnyError> { let client = http_client.client()?; println!( @@ -645,6 +664,52 @@ async fn publish_package( package.package, package.version ); + + if provenance { + // Get the version manifest from JSR + let meta_url = jsr_url().join(&format!( + "@{}/{}/{}_meta.json", + package.scope, package.package, package.version + ))?; + + let meta_bytes = client.get(meta_url).send().await?.bytes().await?; + + if std::env::var("DISABLE_JSR_MANIFEST_VERIFICATION_FOR_TESTING").is_err() { + verify_version_manifest(&meta_bytes, &package)?; + } + + let subject = provenance::Subject { + name: format!( + "pkg:jsr/@{}/{}@{}", + package.scope, package.package, package.version + ), + digest: provenance::SubjectDigest { + sha256: hex::encode(sha2::Sha256::digest(&meta_bytes)), + }, + }; + let bundle = provenance::generate_provenance(subject).await?; + + let tlog_entry = &bundle.verification_material.tlog_entries[0]; + println!("{}", + colors::green(format!( + "Provenance transparency log available at https://search.sigstore.dev/?logIndex={}", + tlog_entry.log_index + )) + ); + + // Submit bundle to JSR + let provenance_url = format!( + "{}scopes/{}/packages/{}/versions/{}/provenance", + registry_api_url, package.scope, package.package, package.version + ); + client + .post(provenance_url) + .header(reqwest::header::AUTHORIZATION, authorization) + .json(&json!({ "bundle": bundle })) + .send() + .await?; + } + println!( "{}", colors::gray(format!( @@ -826,13 +891,12 @@ pub async fn publish( Arc::new(ImportMap::new(Url::parse("file:///dev/null").unwrap())) }); + let directory_path = cli_factory.cli_options().initial_cwd(); + let mapped_resolver = Arc::new(MappedSpecifierResolver::new( Some(import_map), cli_factory.package_json_deps_provider().clone(), )); - - let directory_path = cli_factory.cli_options().initial_cwd(); - let cli_options = cli_factory.cli_options(); let Some(config_file) = cli_options.maybe_config_file() else { bail!( @@ -878,6 +942,181 @@ pub async fn publish( prepared_data.publish_order_graph, prepared_data.package_by_name, auth_method, + publish_flags.provenance, ) - .await + .await?; + + Ok(()) +} + +#[derive(Deserialize)] +struct ManifestEntry { + checksum: String, +} + +#[derive(Deserialize)] +struct VersionManifest { + manifest: HashMap<String, ManifestEntry>, + exports: HashMap<String, String>, +} + +fn verify_version_manifest( + meta_bytes: &[u8], + package: &PreparedPublishPackage, +) -> Result<(), AnyError> { + let manifest = serde_json::from_slice::<VersionManifest>(meta_bytes)?; + // Check that nothing was removed from the manifest. + if manifest.manifest.len() != package.tarball.files.len() { + bail!( + "Mismatch in the number of files in the manifest: expected {}, got {}", + package.tarball.files.len(), + manifest.manifest.len() + ); + } + + for (path, entry) in manifest.manifest { + // Verify each path with the files in the tarball. + let file = package + .tarball + .files + .iter() + .find(|f| f.path_str == path.as_str()); + + if let Some(file) = file { + if file.hash != entry.checksum { + bail!( + "Checksum mismatch for {}: expected {}, got {}", + path, + entry.checksum, + file.hash + ); + } + } else { + bail!("File {} not found in the tarball", path); + } + } + + for (specifier, expected) in &manifest.exports { + let actual = package.exports.get(specifier).ok_or_else(|| { + deno_core::anyhow::anyhow!( + "Export {} not found in the package", + specifier + ) + })?; + if actual != expected { + bail!( + "Export {} mismatch: expected {}, got {}", + specifier, + expected, + actual + ); + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::tar::PublishableTarball; + use super::tar::PublishableTarballFile; + use super::verify_version_manifest; + use std::collections::HashMap; + + #[test] + fn test_verify_version_manifest() { + let meta = r#"{ + "manifest": { + "mod.ts": { + "checksum": "abc123" + } + }, + "exports": {} + }"#; + + let meta_bytes = meta.as_bytes(); + let package = super::PreparedPublishPackage { + scope: "test".to_string(), + package: "test".to_string(), + version: "1.0.0".to_string(), + tarball: PublishableTarball { + bytes: vec![].into(), + hash: "abc123".to_string(), + files: vec![PublishableTarballFile { + specifier: "file://mod.ts".try_into().unwrap(), + path_str: "mod.ts".to_string(), + hash: "abc123".to_string(), + size: 0, + }], + }, + config: "deno.json".to_string(), + exports: HashMap::new(), + }; + + assert!(verify_version_manifest(meta_bytes, &package).is_ok()); + } + + #[test] + fn test_verify_version_manifest_missing() { + let meta = r#"{ + "manifest": { + "mod.ts": {}, + }, + "exports": {} + }"#; + + let meta_bytes = meta.as_bytes(); + let package = super::PreparedPublishPackage { + scope: "test".to_string(), + package: "test".to_string(), + version: "1.0.0".to_string(), + tarball: PublishableTarball { + bytes: vec![].into(), + hash: "abc123".to_string(), + files: vec![PublishableTarballFile { + specifier: "file://mod.ts".try_into().unwrap(), + path_str: "mod.ts".to_string(), + hash: "abc123".to_string(), + size: 0, + }], + }, + config: "deno.json".to_string(), + exports: HashMap::new(), + }; + + assert!(verify_version_manifest(meta_bytes, &package).is_err()); + } + + #[test] + fn test_verify_version_manifest_invalid_hash() { + let meta = r#"{ + "manifest": { + "mod.ts": { + "checksum": "lol123" + }, + "exports": {} + } + }"#; + + let meta_bytes = meta.as_bytes(); + let package = super::PreparedPublishPackage { + scope: "test".to_string(), + package: "test".to_string(), + version: "1.0.0".to_string(), + tarball: PublishableTarball { + bytes: vec![].into(), + hash: "abc123".to_string(), + files: vec![PublishableTarballFile { + specifier: "file://mod.ts".try_into().unwrap(), + path_str: "mod.ts".to_string(), + hash: "abc123".to_string(), + size: 0, + }], + }, + config: "deno.json".to_string(), + exports: HashMap::new(), + }; + + assert!(verify_version_manifest(meta_bytes, &package).is_err()); + } } |