Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

server/lib/src/pull_intake/npm.rs

//! **An npm registry as a remote source** (`kind: npm`): registry.npmjs.org,
//! Chainguard Libraries for JavaScript (`https://libraries.cgr.dev/javascript/`,
//! basic auth with a pull token), any registry that speaks npm's documents.
//!
//! A version is taken only with the `dist.integrity` (SHA-512) its packument
//! publishes, and only when the tarball's bytes hash to it. What came with it
//! travels to the provenance verifier:
//!
//! * the registry's signatures (`dist.signatures`, ECDSA P-256 over
//!   `<name>@<version>:<integrity>`) as `holger.npm-registry-signatures/v1`,
//!   checked against the floor's `rules.keys` (npm publishes its keys at
//!   `/-/npm/v1/keys`; Holger never fetches a key it then trusts);
//! * the attestations `dist.attestations.url` names (npm's SLSA provenance,
//!   keyless, and its publish attestation, key-signed).
//!
//! The packument Holger serves is a view of the AVAILABLE set: each version is its
//! sealed tarball's own `package.json` (`crate::promote::npm::packument`).

use super::http::{join, Fetch};
use super::provenance::Material;
use serde_json::Value;

/// The cap on one packument.
const MAX_DOC: u64 = 64 * 1024 * 1024;

/// One version a packument publishes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RemoteVersion {
    pub package: String,
    pub version: String,
    pub tarball: String,
    /// `sha512-<base64>`.
    pub integrity: Option<String>,
    /// `dist.signatures`, as the packument carried them.
    pub signatures: Option<Value>,
    /// `dist.attestations.url`.
    pub attestations: Option<String>,
    pub deprecated: bool,
}

/// A package name as the registry's URL spells it (`@scope%2fname`).
pub fn url_name(package: &str) -> String {
    package.replacen('/', "%2f", 1)
}

/// **Read one package's packument** from the registry at `base`. `None` when
/// the registry does not know it.
pub fn packument(
    http: &dyn Fetch,
    base: &str,
    package: &str,
    auth: Option<(&str, &str)>,
) -> anyhow::Result<Option<Value>> {
    if !crate::promote::npm::valid_package_name(package) {
        anyhow::bail!("'{package}' is not an npm package name");
    }
    let base = if base.ends_with('/') {
        base.to_string()
    } else {
        format!("{base}/")
    };
    let url = format!("{base}{}", url_name(package));
    let r = http.get(
        &url,
        &[("Accept".into(), "application/json".into())],
        auth,
        MAX_DOC,
    )?;
    match r.status {
        200 => {}
        404 | 410 => return Ok(None),
        s => anyhow::bail!("the registry {url} answered HTTP {s}"),
    }
    let v: Value = serde_json::from_slice(&r.body)
        .map_err(|e| anyhow::anyhow!("the packument {url} is not JSON: {e}"))?;
    Ok(Some(v))
}

/// The version `dist-tags.latest` names.
pub fn latest(doc: &Value) -> Option<String> {
    doc.pointer("/dist-tags/latest")
        .and_then(Value::as_str)
        .map(str::to_string)
}

/// One version of a packument, its links resolved against `page`.
pub fn version(doc: &Value, page: &str, package: &str, version: &str) -> Option<RemoteVersion> {
    let v = doc.get("versions")?.get(version)?;
    let dist = v.get("dist")?;
    let tarball = join(page, dist.get("tarball")?.as_str()?)?;
    Some(RemoteVersion {
        package: package.to_string(),
        version: version.to_string(),
        tarball,
        integrity: dist
            .get("integrity")
            .and_then(Value::as_str)
            .map(str::to_string),
        signatures: dist
            .get("signatures")
            .filter(|s| s.as_array().is_some_and(|a| !a.is_empty()))
            .cloned(),
        attestations: dist
            .pointer("/attestations/url")
            .and_then(Value::as_str)
            .and_then(|u| join(page, u)),
        deprecated: v
            .get("deprecated")
            .is_some_and(|d| !d.is_null() && d != &Value::Bool(false)),
    })
}

/// `sha512-<base64>` of `bytes`, npm's integrity.
pub fn integrity_of(bytes: &[u8]) -> String {
    crate::promote::npm::integrity_of(bytes)
}

/// **Fetch one version's tarball**, held against its published `dist.integrity`,
/// with the registry's signatures and the attestations it names.
pub fn fetch_version(
    http: &dyn Fetch,
    v: &RemoteVersion,
    auth: Option<(&str, &str)>,
    cap: u64,
) -> anyhow::Result<(Vec<u8>, Vec<Material>)> {
    let file = crate::promote::npm::tarball_file(&v.package, &v.version);
    let want = v
        .integrity
        .as_deref()
        .filter(|i| i.starts_with("sha512-"))
        .ok_or_else(|| {
            anyhow::anyhow!(
                "{}@{}: the packument publishes no sha512 integrity for it, so it is not taken",
                v.package,
                v.version
            )
        })?;
    let r = http.get(&v.tarball, &[], auth, cap)?;
    if r.status != 200 {
        anyhow::bail!("{file}: {} answered HTTP {}", v.tarball, r.status);
    }
    let got = integrity_of(&r.body);
    if got != want {
        anyhow::bail!("{file}: the bytes are {got}, the packument publishes {want}; not taken");
    }
    let mut materials = Vec::new();
    if let Some(sigs) = &v.signatures {
        let doc = serde_json::json!({
            "schema": holger_provenance::material::NPM_REGISTRY_SIGNATURES_SCHEMA,
            "name": v.package, "version": v.version, "integrity": want, "signatures": sigs,
        });
        materials.push(Material {
            name: format!("{file}.registry-signatures.json"),
            bytes: serde_json::to_vec(&doc)?,
        });
    }
    if let Some(url) = &v.attestations {
        let a = http.get(
            url,
            &[("Accept".into(), "application/json".into())],
            auth,
            MAX_DOC,
        )?;
        if a.status == 200 {
            materials.push(Material {
                name: format!("{file}.attestations.json"),
                bytes: a.body,
            });
        } else if a.status != 404 {
            anyhow::bail!("{file}: its attestations {url} answered HTTP {}", a.status);
        }
    }
    Ok((r.body, materials))
}

/// The coordinate a pulled npm version stands at: `<source>/<package>/-/<file>.tgz`,
/// the tarball's registry path below the source.
pub fn id(source: &str, package: &str, version: &str) -> traits::ArtifactId {
    let c = crate::promote::npm::coordinate(package, version);
    traits::ArtifactId {
        namespace: None,
        name: format!("{source}/{}", c.name),
        version: String::new(),
    }
}

/// The (package, version) a coordinate below `source` is the tarball of.
pub fn coords_of(source: &str, id: &traits::ArtifactId) -> Option<(String, String)> {
    let rest = id.name.strip_prefix(source)?.strip_prefix('/')?;
    crate::promote::npm::coords_of(&traits::ArtifactId {
        namespace: id.namespace.clone(),
        name: rest.to_string(),
        version: id.version.clone(),
    })
}

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

    #[test]
    fn a_scoped_name_is_spelled_as_the_registry_wants_it() {
        assert_eq!(url_name("@sigstore/core"), "@sigstore%2fcore");
        assert_eq!(url_name("lodash"), "lodash");
    }

    #[test]
    fn a_coordinate_names_the_source_and_reads_back() {
        let i = id("npmjs", "@sigstore/core", "4.0.0");
        assert_eq!(i.name, "npmjs/@sigstore/core/-/core-4.0.0.tgz");
        assert_eq!(
            coords_of("npmjs", &i),
            Some(("@sigstore/core".into(), "4.0.0".into()))
        );
        assert_eq!(coords_of("other", &i), None);
    }

    #[test]
    fn a_version_reads_its_dist() {
        let doc = serde_json::json!({"dist-tags": {"latest": "1.0.0"}, "versions": {"1.0.0": {"dist": {
            "tarball": "/a/-/a-1.0.0.tgz", "integrity": "sha512-x", "signatures": [{"keyid": "k", "sig": "s"}],
            "attestations": {"url": "https://r.example/-/npm/v1/attestations/a@1.0.0"}}}}});
        let v = version(&doc, "https://r.example/a", "a", &latest(&doc).unwrap()).unwrap();
        assert_eq!(v.tarball, "https://r.example/a/-/a-1.0.0.tgz");
        assert!(v.signatures.is_some() && v.attestations.is_some() && !v.deprecated);
    }
}