Skip to content
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
387 changes: 387 additions & 0 deletions .github/workflows/test-standalone-install.yml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/vp_setup/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ vt_str = { workspace = true }
junction = { workspace = true }

[dev-dependencies]
httpmock = { workspace = true }
tempfile = { workspace = true }

[lib]
Expand Down
5 changes: 5 additions & 0 deletions crates/vp_setup/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,9 @@ pub enum Error {

#[error("Unsupported integrity format: {0} (only sha512 is supported)")]
UnsupportedIntegrity(Str),

#[error(
"Refusing to install {package}@{version}: the package does not contain supported npm provenance metadata. Vite+ only installs release binaries published with npm provenance."
)]
UnsupportedPlatformPackageProvenance { package: Str, version: Str },
}
223 changes: 223 additions & 0 deletions crates/vp_setup/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ pub struct PackageVersionMetadata {
pub struct DistInfo {
pub tarball: String,
pub integrity: String,
#[serde(default)]
pub attestations: Option<NpmAttestations>,
}

/// npm attestations attached to a package version.
#[derive(Debug, Deserialize)]
pub struct NpmAttestations {
#[serde(default)]
pub provenance: Option<NpmProvenance>,
}

/// npm provenance metadata used to identify the attestation predicate.
#[derive(Debug, Deserialize)]
pub struct NpmProvenance {
#[serde(rename = "predicateType", default)]
pub predicate_type: Option<String>,
}

/// Resolved version info with URLs and integrity for the platform package.
Expand All @@ -33,6 +49,32 @@ pub struct ResolvedVersion {
const MAIN_PACKAGE_NAME: &str = "vite-plus";
const PLATFORM_PACKAGE_SCOPE: &str = "@voidzero-dev";
const CLI_PACKAGE_NAME_PREFIX: &str = "vite-plus-cli";
const SUPPORTED_PROVENANCE_PREDICATE_TYPES: [&str; 2] =
["https://slsa.dev/provenance/v1", "https://slsa.dev/provenance/v0.2"];

fn validate_platform_package_provenance(
package_name: &str,
version: &str,
dist: &DistInfo,
) -> Result<(), Error> {
let predicate_type = dist
.attestations
.as_ref()
.and_then(|attestations| attestations.provenance.as_ref())
.and_then(|provenance| provenance.predicate_type.as_deref())
.filter(|predicate_type| !predicate_type.is_empty());

if predicate_type.is_some_and(|predicate_type| {
SUPPORTED_PROVENANCE_PREDICATE_TYPES.contains(&predicate_type)
}) {
return Ok(());
}

Err(Error::UnsupportedPlatformPackageProvenance {
package: package_name.into(),
version: version.into(),
})
}

/// Resolve a version string from the npm registry.
///
Expand Down Expand Up @@ -86,6 +128,11 @@ pub async fn resolve_platform_package(
)
})?;

// npm registry signatures only prove that registry metadata was signed. The
// provenance object separately binds the package to its supported build
// attestation, so reject before exposing the tarball URL to any caller.
validate_platform_package_provenance(&cli_package_name, version, &cli_meta.dist)?;

Ok(ResolvedVersion {
version: version.to_owned(),
platform_tarball_url: cli_meta.dist.tarball,
Expand All @@ -109,15 +156,191 @@ pub async fn resolve_version(

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

use super::*;

const TEST_PACKAGE_NAME: &str = "@voidzero-dev/vite-plus-cli-darwin-arm64";
const TEST_VERSION: &str = "1.2.3";

fn parse_metadata(dist: serde_json::Value) -> PackageVersionMetadata {
serde_json::from_value(serde_json::json!({
"version": TEST_VERSION,
"dist": dist,
}))
.unwrap()
}

fn dist_with_provenance(predicate_type: serde_json::Value) -> serde_json::Value {
serde_json::json!({
"tarball": "https://registry.example.test/platform.tgz",
"integrity": "sha512-test",
"signatures": [{ "keyid": "registry-signature-is-not-provenance" }],
"attestations": {
"provenance": {
"predicateType": predicate_type,
}
}
})
}

#[test]
fn test_cli_package_name_construction() {
let suffix = "darwin-arm64";
let name = format!("{PLATFORM_PACKAGE_SCOPE}/{CLI_PACKAGE_NAME_PREFIX}-{suffix}");
assert_eq!(name, "@voidzero-dev/vite-plus-cli-darwin-arm64");
}

#[test]
fn test_platform_package_accepts_supported_provenance_predicates() {
for predicate_type in SUPPORTED_PROVENANCE_PREDICATE_TYPES {
let metadata = parse_metadata(dist_with_provenance(predicate_type.into()));
assert!(
validate_platform_package_provenance(
TEST_PACKAGE_NAME,
TEST_VERSION,
&metadata.dist,
)
.is_ok(),
"expected {predicate_type} to be accepted"
);
}
}

#[test]
fn test_platform_package_rejects_missing_or_unsupported_provenance() {
let cases = [
serde_json::json!({
"tarball": "https://registry.example.test/platform.tgz",
"integrity": "sha512-test",
}),
serde_json::json!({
"tarball": "https://registry.example.test/platform.tgz",
"integrity": "sha512-test",
"attestations": {},
}),
serde_json::json!({
"tarball": "https://registry.example.test/platform.tgz",
"integrity": "sha512-test",
"attestations": { "provenance": {} },
}),
dist_with_provenance("".into()),
dist_with_provenance(" https://slsa.dev/provenance/v1 ".into()),
dist_with_provenance("https://example.test/unknown-provenance/v1".into()),
serde_json::json!({
"tarball": "https://registry.example.test/platform.tgz",
"integrity": "sha512-test",
"signatures": [{ "keyid": "signature-only" }],
}),
];

for dist in cases {
let metadata = parse_metadata(dist);
let error = validate_platform_package_provenance(
TEST_PACKAGE_NAME,
TEST_VERSION,
&metadata.dist,
)
.unwrap_err();

match error {
Error::UnsupportedPlatformPackageProvenance { package, version } => {
assert_eq!(package.as_str(), TEST_PACKAGE_NAME);
assert_eq!(version.as_str(), TEST_VERSION);
}
other => panic!("unexpected error: {other:?}"),
}
}
}

#[test]
fn test_platform_package_ignores_top_level_attestations() {
let metadata: PackageVersionMetadata = serde_json::from_value(serde_json::json!({
"version": TEST_VERSION,
"attestations": {
"provenance": {
"predicateType": "https://slsa.dev/provenance/v1"
}
},
"dist": {
"tarball": "https://registry.example.test/platform.tgz",
"integrity": "sha512-test"
}
}))
.unwrap();

assert!(matches!(
validate_platform_package_provenance(TEST_PACKAGE_NAME, TEST_VERSION, &metadata.dist,),
Err(Error::UnsupportedPlatformPackageProvenance { .. })
));
}

#[test]
fn test_platform_package_metadata_rejects_malformed_provenance_shape() {
let result = serde_json::from_value::<PackageVersionMetadata>(serde_json::json!({
"version": TEST_VERSION,
"dist": {
"tarball": "https://registry.example.test/platform.tgz",
"integrity": "sha512-test",
"attestations": { "provenance": "not-an-object" }
}
}));

assert!(result.is_err());
}

#[tokio::test]
async fn test_resolve_platform_package_returns_verified_distribution() {
let server = MockServer::start();
let metadata_mock = server.mock(|when, then| {
when.method(GET).path("/@voidzero-dev/vite-plus-cli-darwin-arm64/1.2.3");
then.status(200).json_body(serde_json::json!({
"version": TEST_VERSION,
"dist": dist_with_provenance("https://slsa.dev/provenance/v1".into()),
}));
});

let resolved =
resolve_platform_package(TEST_VERSION, "darwin-arm64", Some(&server.base_url()))
.await
.unwrap();

metadata_mock.assert();
assert_eq!(resolved.version, TEST_VERSION);
assert_eq!(resolved.platform_tarball_url, "https://registry.example.test/platform.tgz");
assert_eq!(resolved.platform_integrity, "sha512-test");
}

#[tokio::test]
async fn test_resolve_platform_package_rejects_before_returning_distribution() {
let server = MockServer::start();
let metadata_mock = server.mock(|when, then| {
when.method(GET).path("/@voidzero-dev/vite-plus-cli-darwin-arm64/1.2.3");
then.status(200).json_body(serde_json::json!({
"version": TEST_VERSION,
"dist": {
"tarball": format!("{}/platform.tgz", server.base_url()),
"integrity": "sha512-test",
}
}));
});
let tarball_mock = server.mock(|when, then| {
when.method(GET).path("/platform.tgz");
then.status(200).body("must not be downloaded");
});

let error =
resolve_platform_package(TEST_VERSION, "darwin-arm64", Some(&server.base_url()))
.await
.unwrap_err();

metadata_mock.assert();
assert_eq!(tarball_mock.hits(), 0);
assert!(matches!(error, Error::UnsupportedPlatformPackageProvenance { .. }));
assert!(error.to_string().contains(TEST_PACKAGE_NAME));
assert!(error.to_string().contains(TEST_VERSION));
}

#[test]
fn test_all_platform_suffixes_match_published_cli_packages() {
// These are the actual published CLI package suffixes
Expand Down
73 changes: 72 additions & 1 deletion packages/cli/install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ $PrVersion = $env:VP_PR_VERSION
# pulls a coherent, clearly-defined test build.
$BridgeDownloadBase = "https://registry-bridge.viteplus.dev/voidzero-dev/vite-plus"
$BridgeRegistry = "https://registry-bridge.viteplus.dev/"
$SupportedProvenancePredicateTypes = @(
"https://slsa.dev/provenance/v1",
"https://slsa.dev/provenance/v0.2"
)

function Write-Info {
param([string]$Message)
Expand Down Expand Up @@ -482,6 +486,72 @@ function Get-VersionFromMetadata {
return $metadata.version
}

function Get-PlatformPackageMetadata {
param(
[string]$PackageName,
[string]$Version
)

$encodedPackageName = [System.Uri]::EscapeDataString($PackageName)
$metadataUrl = "$NpmRegistry/$encodedPackageName/$Version"
try {
$metadata = Invoke-RestMethod -Uri $metadataUrl -Headers @{ Accept = "application/json" }
} catch {
if (Test-IsInstallStopException $_) { throw }
$errorMsg = $_.ErrorDetails.Message
if ($errorMsg) {
try {
$errorJson = $errorMsg | ConvertFrom-Json
if ($errorJson.error) {
Write-Error-Exit "Failed to fetch CLI package metadata '${PackageName}@${Version}': $($errorJson.error)`n URL: $metadataUrl"
}
} catch {
if (Test-IsInstallStopException $_) { throw }
# JSON parsing failed, fall through to the generic network error.
}
}
Write-Error-Exit "Failed to fetch CLI package metadata from: $metadataUrl`nError: $_"
}

# Some custom registries return JSON using a non-JSON content type. Match
# Get-PackageMetadata by parsing that raw string before inspecting fields.
if ($metadata -is [string]) {
try {
$metadata = $metadata | ConvertFrom-Json
} catch {
if (Test-IsInstallStopException $_) { throw }
Write-Error-Exit "Failed to parse CLI package metadata '${PackageName}@${Version}'`n URL: $metadataUrl"
}
}
if ($metadata.error) {
Write-Error-Exit "Failed to fetch CLI package metadata '${PackageName}@${Version}': $($metadata.error)`n URL: $metadataUrl"
}

return $metadata
}

function Get-VerifiedPlatformTarballUrl {
param(
[object]$Metadata,
[string]$PackageName,
[string]$Version
)

# Registry signatures and trusted-publisher labels are not substitutes for
# npm provenance. Check the typed object path so package-defined top-level
# fields cannot satisfy the gate, and deny unknown predicates before download.
$predicateType = $Metadata.dist.attestations.provenance.predicateType
if (-not $predicateType -or $SupportedProvenancePredicateTypes -notcontains $predicateType) {
Write-Error-Exit "Refusing to install ${PackageName}@${Version}: the package does not contain supported npm provenance metadata. Vite+ only installs release binaries published with npm provenance."
}

$tarballUrl = $Metadata.dist.tarball
if (-not $tarballUrl) {
Write-Error-Exit "CLI package metadata for ${PackageName}@${Version} does not include dist.tarball"
}
return [string]$tarballUrl
}

function Get-PlatformSuffix {
param([string]$Platform)
# Windows needs -msvc suffix, other platforms map directly
Expand Down Expand Up @@ -811,7 +881,8 @@ function Main {
$platformUrl = "$BridgeDownloadBase/@voidzero-dev/vite-plus-cli-$platformSuffix@$PrVersion"
} else {
$packageName = "@voidzero-dev/vite-plus-cli-$platformSuffix"
$platformUrl = "$NpmRegistry/$packageName/-/vite-plus-cli-$platformSuffix-$ViteVersion.tgz"
$platformMetadata = Get-PlatformPackageMetadata -PackageName $packageName -Version $ViteVersion
$platformUrl = Get-VerifiedPlatformTarballUrl -Metadata $platformMetadata -PackageName $packageName -Version $ViteVersion
}

$platformTempFile = New-TemporaryFile
Expand Down
Loading
Loading