// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Net;
using System.Text;
using System.Text.Json;
using Aspire.Cli.Npm;
using Aspire.Cli.Tests.Utils;
using Microsoft.Extensions.Logging.Abstractions;
using Sigstore;
namespace Aspire.Cli.Tests.Agents;
public class SigstoreNpmProvenanceCheckerTests
{
private const string GitHubReleaseAssetBuildType = "https://actions.github.io/buildtypes/workflow/v1";
#region ExtractSlsaBundleJson Tests
[Fact]
public void ExtractSlsaBundleJson_WithValidSlsaAttestation_ReturnsBundleJson()
{
var json = BuildAttestationJsonWithBundle("https://github.com/microsoft/playwright-cli");
var bundleJson = SigstoreNpmProvenanceChecker.ExtractSlsaBundleJson(json, out _);
Assert.NotNull(bundleJson);
var bundleDoc = JsonDocument.Parse(bundleJson);
Assert.True(bundleDoc.RootElement.TryGetProperty("dsseEnvelope", out _));
}
[Fact]
public void ExtractSlsaBundleJson_WithNoSlsaPredicate_ReturnsNull()
{
var json = """
{
"attestations": [
{
"predicateType": "https://github.com/npm/attestation/tree/main/specs/publish/v0.1",
"bundle": {
"dsseEnvelope": {
"payload": ""
}
}
}
]
}
""";
var bundleJson = SigstoreNpmProvenanceChecker.ExtractSlsaBundleJson(json, out _);
Assert.Null(bundleJson);
}
[Fact]
public void ExtractSlsaBundleJson_WithEmptyAttestations_ReturnsNull()
{
var bundleJson = SigstoreNpmProvenanceChecker.ExtractSlsaBundleJson("""{"attestations": []}""", out _);
Assert.Null(bundleJson);
}
[Fact]
public void ExtractSlsaBundleJson_WithInvalidJson_ReturnsNullAndSetsParseFailed()
{
var bundleJson = SigstoreNpmProvenanceChecker.ExtractSlsaBundleJson("not valid json {{{", out var parseFailed);
Assert.Null(bundleJson);
Assert.True(parseFailed);
}
[Fact]
public void ExtractSlsaBundleJson_WithMultipleMixedAttestations_FindsSlsaPredicate()
{
var json = $$"""
{
"attestations": [
{
"predicateType": "https://github.com/npm/attestation/tree/main/specs/publish/v0.1",
"bundle": { "wrong": true }
},
{
"predicateType": "https://slsa.dev/provenance/v1",
"bundle": {
"dsseEnvelope": { "payload": "dGVzdA==", "payloadType": "application/vnd.in-toto+json" }
}
}
]
}
""";
var bundleJson = SigstoreNpmProvenanceChecker.ExtractSlsaBundleJson(json, out _);
Assert.NotNull(bundleJson);
var doc = JsonDocument.Parse(bundleJson);
Assert.True(doc.RootElement.TryGetProperty("dsseEnvelope", out _));
}
[Fact]
public void ExtractSlsaBundleJson_WithNoBundleProperty_ReturnsNull()
{
var json = """
{
"attestations": [
{
"predicateType": "https://slsa.dev/provenance/v1"
}
]
}
""";
var bundleJson = SigstoreNpmProvenanceChecker.ExtractSlsaBundleJson(json, out _);
Assert.Null(bundleJson);
}
#endregion
#region VerifyProvenanceAsync Tests
[Theory]
[InlineData(
"pkg:npm/%40playwright/cli@0.1.2",
"00112233445566778899aabbccddeeff",
nameof(ProvenanceVerificationOutcome.PackageIdentityMismatch))]
[InlineData(
"pkg:npm/%40playwright/cli@0.1.1",
"ffeeddccbbaa99887766554433221100",
nameof(ProvenanceVerificationOutcome.PackageDigestMismatch))]
public async Task VerifyProvenanceAsync_WithMismatchedSignedSubject_ReturnsMismatch(
string subjectName,
string subjectDigest,
string expectedOutcome)
{
var result = await VerifyThroughCheckerAsync(
subjectName,
subjectDigest,
workflowPath: ".github/workflows/publish.yml");
Assert.Equal(Enum.Parse<ProvenanceVerificationOutcome>(expectedOutcome), result.Outcome);
}
[Fact]
public async Task VerifyProvenanceAsync_WithMatchingSignedSubject_AppliesProvenanceFieldValidation()
{
var result = await VerifyThroughCheckerAsync(
"pkg:npm/%40playwright/cli@0.1.1",
"00112233445566778899aabbccddeeff",
workflowPath: ".github/workflows/unexpected.yml");
Assert.Equal(ProvenanceVerificationOutcome.WorkflowMismatch, result.Outcome);
}
#endregion
#region ExtractProvenanceFromResult Tests
[Fact]
public void ExtractProvenanceFromResult_WithStatementAndExtensions_ReturnsProvenance()
{
var result = BuildVerificationResult(
sourceRepoUri: "https://github.com/microsoft/playwright-cli",
sourceRepoRef: "refs/tags/v0.1.1",
workflowPath: ".github/workflows/publish.yml",
buildType: "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
builderId: "https://github.com/actions/runner/github-hosted",
sourceRepoInPredicate: "https://github.com/microsoft/playwright-cli");
var provenance = SigstoreNpmProvenanceChecker.ExtractProvenanceFromResult(result);
Assert.NotNull(provenance);
Assert.Equal("https://github.com/microsoft/playwright-cli", provenance.SourceRepository);
Assert.Equal(".github/workflows/publish.yml", provenance.WorkflowPath);
Assert.Equal("refs/tags/v0.1.1", provenance.WorkflowRef);
Assert.Equal("https://github.com/actions/runner/github-hosted", provenance.BuilderId);
Assert.Equal("https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1", provenance.BuildType);
}
[Fact]
public void ExtractProvenanceFromResult_PrefersExtensionsOverPredicate()
{
var result = BuildVerificationResult(
sourceRepoUri: "https://github.com/microsoft/playwright-cli",
sourceRepoRef: "refs/tags/v0.1.1",
workflowPath: ".github/workflows/publish.yml",
buildType: "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
builderId: "https://github.com/actions/runner/github-hosted",
sourceRepoInPredicate: "https://github.com/evil/repo",
workflowRefInPredicate: "refs/heads/main");
var provenance = SigstoreNpmProvenanceChecker.ExtractProvenanceFromResult(result);
Assert.NotNull(provenance);
// Certificate extensions should win over predicate values
Assert.Equal("https://github.com/microsoft/playwright-cli", provenance.SourceRepository);
Assert.Equal("refs/tags/v0.1.1", provenance.WorkflowRef);
}
[Fact]
public void ExtractProvenanceFromResult_WithNoStatement_ReturnsPartialProvenance()
{
var result = new VerificationResult
{
SignerIdentity = new VerifiedIdentity
{
SubjectAlternativeName = "https://github.com/microsoft/playwright-cli/.github/workflows/publish.yml@refs/tags/v0.1.1",
Issuer = "https://token.actions.githubusercontent.com",
Extensions = new FulcioCertificateExtensions
{
SourceRepositoryUri = "https://github.com/microsoft/playwright-cli",
SourceRepositoryRef = "refs/tags/v0.1.1"
}
},
Statement = null
};
var provenance = SigstoreNpmProvenanceChecker.ExtractProvenanceFromResult(result);
Assert.NotNull(provenance);
Assert.Equal("https://github.com/microsoft/playwright-cli", provenance.SourceRepository);
Assert.Equal("refs/tags/v0.1.1", provenance.WorkflowRef);
Assert.Null(provenance.WorkflowPath);
Assert.Null(provenance.BuildType);
Assert.Null(provenance.BuilderId);
}
[Fact]
public void ExtractProvenanceFromResult_WithNoExtensions_FallsToPredicate()
{
var result = BuildVerificationResult(
sourceRepoUri: null,
sourceRepoRef: null,
workflowPath: ".github/workflows/publish.yml",
buildType: "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
builderId: "https://github.com/actions/runner/github-hosted",
sourceRepoInPredicate: "https://github.com/microsoft/playwright-cli",
workflowRefInPredicate: "refs/tags/v0.1.1",
includeExtensions: false);
var provenance = SigstoreNpmProvenanceChecker.ExtractProvenanceFromResult(result);
Assert.NotNull(provenance);
Assert.Equal("https://github.com/microsoft/playwright-cli", provenance.SourceRepository);
Assert.Equal("refs/tags/v0.1.1", provenance.WorkflowRef);
}
[Fact]
public void ExtractProvenanceFromResult_WithWrongPredicateType_ReturnsNullFields()
{
var predicateJson = """
{
"_type": "https://in-toto.io/Statement/v1",
"predicateType": "https://example.com/custom/v1",
"subject": [],
"predicate": { "custom": true }
}
""";
var statement = InTotoStatement.Parse(predicateJson);
var result = new VerificationResult
{
SignerIdentity = new VerifiedIdentity
{
SubjectAlternativeName = "test",
Issuer = "test",
Extensions = new FulcioCertificateExtensions()
},
Statement = statement
};
var provenance = SigstoreNpmProvenanceChecker.ExtractProvenanceFromResult(result);
Assert.NotNull(provenance);
Assert.Null(provenance.WorkflowPath);
Assert.Null(provenance.BuildType);
Assert.Null(provenance.BuilderId);
}
#endregion
#region VerifyProvenanceFields Tests
[Fact]
public void VerifyProvenanceFields_WithAllFieldsMatching_ReturnsVerified()
{
var provenance = new NpmProvenanceData
{
SourceRepository = "https://github.com/microsoft/playwright-cli",
WorkflowPath = ".github/workflows/publish.yml",
BuildType = "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
WorkflowRef = "refs/tags/v0.1.1",
BuilderId = "https://github.com/actions/runner/github-hosted"
};
var result = SigstoreNpmProvenanceChecker.VerifyProvenanceFields(
provenance,
"https://github.com/microsoft/playwright-cli",
".github/workflows/publish.yml",
"https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
refInfo => refInfo.Kind == "tags");
Assert.Equal(ProvenanceVerificationOutcome.Verified, result.Outcome);
}
[Fact]
public void VerifyProvenanceFields_WithGitHubReleaseAssetBuildType_ReturnsVerified()
{
var provenance = new NpmProvenanceData
{
SourceRepository = "https://github.com/microsoft/aspire-skills",
WorkflowPath = ".github/workflows/publish.yml",
BuildType = GitHubReleaseAssetBuildType,
WorkflowRef = "refs/tags/v0.0.1",
BuilderId = "https://github.com/microsoft/aspire-skills/.github/workflows/publish.yml@refs/tags/v0.0.1"
};
var result = SigstoreNpmProvenanceChecker.VerifyProvenanceFields(
provenance,
"https://github.com/microsoft/aspire-skills",
".github/workflows/publish.yml",
GitHubReleaseAssetBuildType,
refInfo => string.Equals(refInfo.Kind, "tags", StringComparison.Ordinal) &&
string.Equals(refInfo.Name, "v0.0.1", StringComparison.Ordinal));
Assert.Equal(ProvenanceVerificationOutcome.Verified, result.Outcome);
}
[Theory]
[InlineData("https://github.com/evil/aspire-skills", ".github/workflows/publish.yml", "refs/tags/v0.0.1", nameof(ProvenanceVerificationOutcome.SourceRepositoryMismatch))]
[InlineData("https://github.com/microsoft/aspire-skills", ".github/workflows/evil.yml", "refs/tags/v0.0.1", nameof(ProvenanceVerificationOutcome.WorkflowMismatch))]
[InlineData("https://github.com/microsoft/aspire-skills", ".github/workflows/publish.yml", "refs/heads/main", nameof(ProvenanceVerificationOutcome.WorkflowRefMismatch))]
public void VerifyProvenanceFields_WithGitHubReleaseAssetBuildType_RejectsUnexpectedProvenance(
string sourceRepository,
string workflowPath,
string workflowRef,
string expectedOutcome)
{
var provenance = new NpmProvenanceData
{
SourceRepository = sourceRepository,
WorkflowPath = workflowPath,
BuildType = GitHubReleaseAssetBuildType,
WorkflowRef = workflowRef,
BuilderId = "https://github.com/microsoft/aspire-skills/.github/workflows/publish.yml@refs/tags/v0.0.1"
};
var result = SigstoreNpmProvenanceChecker.VerifyProvenanceFields(
provenance,
"https://github.com/microsoft/aspire-skills",
".github/workflows/publish.yml",
GitHubReleaseAssetBuildType,
refInfo => string.Equals(refInfo.Kind, "tags", StringComparison.Ordinal) &&
string.Equals(refInfo.Name, "v0.0.1", StringComparison.Ordinal));
Assert.Equal(Enum.Parse<ProvenanceVerificationOutcome>(expectedOutcome), result.Outcome);
}
[Fact]
public void VerifyProvenanceFields_WithSourceRepoMismatch_ReturnsSourceRepositoryMismatch()
{
var provenance = new NpmProvenanceData
{
SourceRepository = "https://github.com/evil/repo",
WorkflowPath = ".github/workflows/publish.yml",
BuildType = "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
};
var result = SigstoreNpmProvenanceChecker.VerifyProvenanceFields(
provenance,
"https://github.com/microsoft/playwright-cli",
".github/workflows/publish.yml",
"https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
null);
Assert.Equal(ProvenanceVerificationOutcome.SourceRepositoryMismatch, result.Outcome);
}
[Fact]
public void VerifyProvenanceFields_WithWorkflowMismatch_ReturnsWorkflowMismatch()
{
var provenance = new NpmProvenanceData
{
SourceRepository = "https://github.com/microsoft/playwright-cli",
WorkflowPath = ".github/workflows/evil.yml",
BuildType = "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
};
var result = SigstoreNpmProvenanceChecker.VerifyProvenanceFields(
provenance,
"https://github.com/microsoft/playwright-cli",
".github/workflows/publish.yml",
"https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
null);
Assert.Equal(ProvenanceVerificationOutcome.WorkflowMismatch, result.Outcome);
}
[Fact]
public void VerifyProvenanceFields_WithBuildTypeMismatch_ReturnsBuildTypeMismatch()
{
var provenance = new NpmProvenanceData
{
SourceRepository = "https://github.com/microsoft/playwright-cli",
WorkflowPath = ".github/workflows/publish.yml",
BuildType = "https://evil.example.com/build/v1",
};
var result = SigstoreNpmProvenanceChecker.VerifyProvenanceFields(
provenance,
"https://github.com/microsoft/playwright-cli",
".github/workflows/publish.yml",
"https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
null);
Assert.Equal(ProvenanceVerificationOutcome.BuildTypeMismatch, result.Outcome);
}
[Fact]
public void VerifyProvenanceFields_WithWorkflowRefValidationFailure_ReturnsWorkflowRefMismatch()
{
var provenance = new NpmProvenanceData
{
SourceRepository = "https://github.com/microsoft/playwright-cli",
WorkflowPath = ".github/workflows/publish.yml",
BuildType = "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
WorkflowRef = "refs/heads/main"
};
var result = SigstoreNpmProvenanceChecker.VerifyProvenanceFields(
provenance,
"https://github.com/microsoft/playwright-cli",
".github/workflows/publish.yml",
"https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
refInfo => refInfo.Kind == "tags");
Assert.Equal(ProvenanceVerificationOutcome.WorkflowRefMismatch, result.Outcome);
}
#endregion
#region VerifyNpmSubject Tests
[Fact]
public void VerifyNpmSubject_WithMatchingPackageAndDigest_ReturnsVerified()
{
var statement = BuildStatementWithSubject(
"""
[
{
"name": "pkg:npm/%40playwright/cli@0.1.1",
"digest": { "sha512": "00112233445566778899aabbccddeeff" }
}
]
""");
var outcome = SigstoreNpmProvenanceChecker.VerifyNpmSubject(
statement,
"@playwright/cli",
"0.1.1",
ToSha512Sri("00112233445566778899aabbccddeeff"));
Assert.Equal(ProvenanceVerificationOutcome.Verified, outcome);
}
[Fact]
public void VerifyNpmSubject_WithDifferentPackageIdentity_ReturnsPackageIdentityMismatch()
{
var statement = BuildStatementWithSubject(
"""
[
{
"name": "pkg:npm/%40playwright/other@0.1.1",
"digest": { "sha512": "00112233445566778899aabbccddeeff" }
}
]
""");
var outcome = SigstoreNpmProvenanceChecker.VerifyNpmSubject(
statement,
"@playwright/cli",
"0.1.1",
ToSha512Sri("00112233445566778899aabbccddeeff"));
Assert.Equal(ProvenanceVerificationOutcome.PackageIdentityMismatch, outcome);
}
[Fact]
public void VerifyNpmSubject_WithDifferentDigest_ReturnsPackageDigestMismatch()
{
var statement = BuildStatementWithSubject(
"""
[
{
"name": "pkg:npm/%40playwright/cli@0.1.1",
"digest": { "sha512": "ffeeddccbbaa99887766554433221100" }
}
]
""");
var outcome = SigstoreNpmProvenanceChecker.VerifyNpmSubject(
statement,
"@playwright/cli",
"0.1.1",
ToSha512Sri("00112233445566778899aabbccddeeff"));
Assert.Equal(ProvenanceVerificationOutcome.PackageDigestMismatch, outcome);
}
[Theory]
[InlineData("""[]""", nameof(ProvenanceVerificationOutcome.PackageIdentityMismatch))]
[InlineData("""[{ "name": "pkg:npm/%40playwright/cli@0.1.1" }]""", nameof(ProvenanceVerificationOutcome.PackageDigestMismatch))]
[InlineData("""[{ "digest": { "sha512": "00112233445566778899aabbccddeeff" } }]""", nameof(ProvenanceVerificationOutcome.PackageIdentityMismatch))]
[InlineData(
"""
[
{
"name": "pkg:npm/%40playwright/cli@0.1.1",
"digest": { "sha512": "00112233445566778899aabbccddeeff" }
},
{
"name": "pkg:npm/%40playwright/cli@0.1.1",
"digest": { "sha512": "00112233445566778899aabbccddeeff" }
}
]
""",
nameof(ProvenanceVerificationOutcome.PackageIdentityMismatch))]
public void VerifyNpmSubject_WithMalformedSubject_ReturnsMismatch(string subjectJson, string expectedOutcome)
{
var statement = BuildStatementWithSubject(subjectJson);
var outcome = SigstoreNpmProvenanceChecker.VerifyNpmSubject(
statement,
"@playwright/cli",
"0.1.1",
ToSha512Sri("00112233445566778899aabbccddeeff"));
Assert.Equal(Enum.Parse<ProvenanceVerificationOutcome>(expectedOutcome), outcome);
}
#endregion
#region TryParseGitHubOwnerRepo Tests
[Theory]
[InlineData("https://github.com/microsoft/playwright-cli", "microsoft", "playwright-cli")]
[InlineData("https://github.com/microsoft/aspire", "microsoft", "aspire")]
[InlineData("https://github.com/owner/repo", "owner", "repo")]
public void TryParseGitHubOwnerRepo_WithValidUrl_ReturnsTrueAndParsesComponents(string url, string expectedOwner, string expectedRepo)
{
var result = SigstoreNpmProvenanceChecker.TryParseGitHubOwnerRepo(url, out var owner, out var repo);
Assert.True(result);
Assert.Equal(expectedOwner, owner);
Assert.Equal(expectedRepo, repo);
}
[Theory]
[InlineData("not-a-url")]
[InlineData("https://github.com/")]
[InlineData("https://github.com/only-owner")]
public void TryParseGitHubOwnerRepo_WithInvalidUrl_ReturnsFalse(string url)
{
var result = SigstoreNpmProvenanceChecker.TryParseGitHubOwnerRepo(url, out _, out _);
Assert.False(result);
}
#endregion
#region Adversarial Tests - Malformed JSON
[Fact]
public void ExtractSlsaBundleJson_WithDeeplyNestedJson_ReturnsNull()
{
// Build a valid deeply-nested JSON object to test stack safety.
// Each level wraps the previous in {"key": ...}.
var depth = 200;
var inner = """{"attestations":[]}""";
for (var i = 0; i < depth; i++)
{
inner = $$"""{"level{{i}}":{{inner}}}""";
}
// Should either return null or handle gracefully (no exception)
var bundleJson = SigstoreNpmProvenanceChecker.ExtractSlsaBundleJson(inner, out _);
// The deeply nested JSON has "attestations" buried inside — not at root level
Assert.Null(bundleJson);
}
[Fact]
public void ExtractSlsaBundleJson_WithTruncatedJson_ReturnsNull()
{
var json = """{"attestations": [{"predicateType": "https://slsa.dev/provenance/v1", "bundle": {"dsse""";
var bundleJson = SigstoreNpmProvenanceChecker.ExtractSlsaBundleJson(json, out _);
Assert.Null(bundleJson);
}
[Fact]
public void ExtractSlsaBundleJson_WithWrongJsonTypes_ReturnsNull()
{
// attestations is a string instead of array
var json = """{"attestations": "not an array"}""";
var bundleJson = SigstoreNpmProvenanceChecker.ExtractSlsaBundleJson(json, out _);
Assert.Null(bundleJson);
}
[Fact]
public void ExtractSlsaBundleJson_WithNullAttestations_ReturnsNull()
{
var json = """{"attestations": null}""";
var bundleJson = SigstoreNpmProvenanceChecker.ExtractSlsaBundleJson(json, out _);
Assert.Null(bundleJson);
}
[Fact]
public void ExtractSlsaBundleJson_WithEmptyObject_ReturnsNullWithoutParseFailed()
{
var bundleJson = SigstoreNpmProvenanceChecker.ExtractSlsaBundleJson("{}", out var parseFailed);
Assert.Null(bundleJson);
Assert.False(parseFailed);
}
[Fact]
public void ExtractSlsaBundleJson_WithNonObjectArrayElements_SkipsThem()
{
// Array contains string, number, and null elements instead of objects
var json = """
{
"attestations": [
"not an object",
42,
null,
{
"predicateType": "https://slsa.dev/provenance/v1",
"bundle": { "dsseEnvelope": {} }
}
]
}
""";
var bundleJson = SigstoreNpmProvenanceChecker.ExtractSlsaBundleJson(json, out var parseFailed);
Assert.NotNull(bundleJson);
Assert.False(parseFailed);
}
[Fact]
public void ExtractSlsaBundleJson_WithNonStringPredicateType_SkipsElement()
{
// predicateType is a number instead of a string
var json = """
{
"attestations": [
{
"predicateType": 42,
"bundle": { "dsseEnvelope": {} }
}
]
}
""";
var bundleJson = SigstoreNpmProvenanceChecker.ExtractSlsaBundleJson(json, out var parseFailed);
Assert.Null(bundleJson);
Assert.False(parseFailed);
}
[Fact]
public void ExtractSlsaBundleJson_WithEmptyString_ReturnsNullAndSetsParseFailed()
{
var bundleJson = SigstoreNpmProvenanceChecker.ExtractSlsaBundleJson("", out var parseFailed);
Assert.Null(bundleJson);
Assert.True(parseFailed);
}
#endregion
#region Adversarial Tests - Provenance Spoofing
[Theory]
[InlineData("https://github.com/micr0soft/playwright-cli")] // Homoglyph: zero instead of 'o'
[InlineData("https://github.com/microsofт/playwright-cli")] // Homoglyph: Turkish dotless t
[InlineData("https://github.com/microsoft-/playwright-cli")] // Trailing dash
[InlineData("https://github.com/MICROSOFT/playwright-cli")] // Case should match (OrdinalIgnoreCase)
public void VerifyProvenanceFields_WithSimilarRepositoryUrls_ChecksCorrectly(string spoofedUrl)
{
var provenance = new NpmProvenanceData
{
SourceRepository = spoofedUrl,
WorkflowPath = ".github/workflows/publish.yml",
BuildType = "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
};
var result = SigstoreNpmProvenanceChecker.VerifyProvenanceFields(
provenance,
"https://github.com/microsoft/playwright-cli",
".github/workflows/publish.yml",
"https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
null);
// MICROSOFT should match (OrdinalIgnoreCase), all others should fail
if (string.Equals(spoofedUrl, "https://github.com/microsoft/playwright-cli", StringComparison.OrdinalIgnoreCase))
{
Assert.Equal(ProvenanceVerificationOutcome.Verified, result.Outcome);
}
else
{
Assert.Equal(ProvenanceVerificationOutcome.SourceRepositoryMismatch, result.Outcome);
}
}
[Theory]
[InlineData("../../.github/workflows/evil.yml")]
[InlineData(".github/workflows/../../../evil.yml")]
[InlineData(".github/workflows/publish.yml\0evil")]
[InlineData("")]
public void VerifyProvenanceFields_WithWorkflowPathManipulation_RejectsInvalid(string spoofedPath)
{
var provenance = new NpmProvenanceData
{
SourceRepository = "https://github.com/microsoft/playwright-cli",
WorkflowPath = spoofedPath,
BuildType = "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
};
var result = SigstoreNpmProvenanceChecker.VerifyProvenanceFields(
provenance,
"https://github.com/microsoft/playwright-cli",
".github/workflows/publish.yml",
"https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
null);
Assert.Equal(ProvenanceVerificationOutcome.WorkflowMismatch, result.Outcome);
}
[Theory]
[InlineData("refs/heads/main")] // Branch instead of tag
[InlineData("refs/tags/v0.1.1/../../heads/main")] // Path traversal in ref
[InlineData("refs/tags/")] // Empty tag name
[InlineData("refs/")] // No kind or name
[InlineData("tags/v0.1.1")] // Missing refs/ prefix
[InlineData("")] // Empty string
public void VerifyProvenanceFields_WithRefManipulation_RejectsInvalidRefs(string spoofedRef)
{
var provenance = new NpmProvenanceData
{
SourceRepository = "https://github.com/microsoft/playwright-cli",
WorkflowPath = ".github/workflows/publish.yml",
BuildType = "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
WorkflowRef = spoofedRef
};
var result = SigstoreNpmProvenanceChecker.VerifyProvenanceFields(
provenance,
"https://github.com/microsoft/playwright-cli",
".github/workflows/publish.yml",
"https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
refInfo => string.Equals(refInfo.Kind, "tags", StringComparison.Ordinal) &&
string.Equals(refInfo.Name, "v0.1.1", StringComparison.Ordinal));
Assert.Equal(ProvenanceVerificationOutcome.WorkflowRefMismatch, result.Outcome);
}
[Fact]
public void VerifyProvenanceFields_WithNullWorkflowRef_ReturnsWorkflowRefMismatch()
{
var provenance = new NpmProvenanceData
{
SourceRepository = "https://github.com/microsoft/playwright-cli",
WorkflowPath = ".github/workflows/publish.yml",
BuildType = "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
WorkflowRef = null
};
var result = SigstoreNpmProvenanceChecker.VerifyProvenanceFields(
provenance,
"https://github.com/microsoft/playwright-cli",
".github/workflows/publish.yml",
"https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
refInfo => refInfo.Kind == "tags");
Assert.Equal(ProvenanceVerificationOutcome.WorkflowRefMismatch, result.Outcome);
}
#endregion
#region Adversarial Tests - Build Type Spoofing
[Theory]
[InlineData("https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1?inject=true")]
[InlineData("https://evil.com/github-actions-buildtypes/workflow/v1")]
[InlineData("")]
public void VerifyProvenanceFields_WithBuildTypeSpoofing_Rejects(string spoofedBuildType)
{
var provenance = new NpmProvenanceData
{
SourceRepository = "https://github.com/microsoft/playwright-cli",
WorkflowPath = ".github/workflows/publish.yml",
BuildType = spoofedBuildType,
};
var result = SigstoreNpmProvenanceChecker.VerifyProvenanceFields(
provenance,
"https://github.com/microsoft/playwright-cli",
".github/workflows/publish.yml",
"https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
null);
Assert.Equal(ProvenanceVerificationOutcome.BuildTypeMismatch, result.Outcome);
}
[Fact]
public void VerifyProvenanceFields_WithNullBuildType_ReturnsBuildTypeMismatch()
{
var provenance = new NpmProvenanceData
{
SourceRepository = "https://github.com/microsoft/playwright-cli",
WorkflowPath = ".github/workflows/publish.yml",
BuildType = null,
};
var result = SigstoreNpmProvenanceChecker.VerifyProvenanceFields(
provenance,
"https://github.com/microsoft/playwright-cli",
".github/workflows/publish.yml",
"https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
null);
Assert.Equal(ProvenanceVerificationOutcome.BuildTypeMismatch, result.Outcome);
}
#endregion
#region Adversarial Tests - URL Parsing Edge Cases
[Theory]
[InlineData("https://github.com.evil.com/microsoft/playwright-cli")] // Subdomain attack
[InlineData("https://githüb.com/microsoft/playwright-cli")] // Unicode domain
[InlineData("ftp://github.com/microsoft/playwright-cli")] // Wrong scheme
public void TryParseGitHubOwnerRepo_WithSuspiciousUrls_HandlesCorrectly(string url)
{
var result = SigstoreNpmProvenanceChecker.TryParseGitHubOwnerRepo(url, out var owner, out var repo);
// These are syntactically valid URLs so TryParseGitHubOwnerRepo will succeed,
// but the domain mismatch would be caught by Sigstore's certificate identity check
// (SAN pattern matching against github.com). TryParseGitHubOwnerRepo only extracts
// the path segments — the security boundary is in the VerificationPolicy.
if (result)
{
// Verify it at least parsed the path segments
Assert.False(string.IsNullOrEmpty(owner));
Assert.False(string.IsNullOrEmpty(repo));
}
}
[Theory]
[InlineData("https://github.com/microsoft/playwright-cli/../evil-repo")]
[InlineData("https://github.com/microsoft/playwright-cli/extra/segments")]
public void TryParseGitHubOwnerRepo_WithExtraPathSegments_ExtractsFirstTwo(string url)
{
var result = SigstoreNpmProvenanceChecker.TryParseGitHubOwnerRepo(url, out var owner, out _);
Assert.True(result);
Assert.Equal("microsoft", owner);
// URI normalization resolves ".." so the path may differ
}
[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData("relative/path")]
public void TryParseGitHubOwnerRepo_WithNonAbsoluteUri_ReturnsFalse(string url)
{
var result = SigstoreNpmProvenanceChecker.TryParseGitHubOwnerRepo(url, out _, out _);
Assert.False(result);
}
#endregion
#region Adversarial Tests - Statement Extraction Edge Cases
[Fact]
public void ExtractProvenanceFromResult_WithMissingPredicateFields_ReturnsPartialData()
{
var predicateJson = """
{
"_type": "https://in-toto.io/Statement/v1",
"predicateType": "https://slsa.dev/provenance/v1",
"subject": [],
"predicate": {
"buildDefinition": {
"buildType": "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1"
}
}
}
""";
var statement = InTotoStatement.Parse(predicateJson);
var result = new VerificationResult
{
SignerIdentity = new VerifiedIdentity
{
SubjectAlternativeName = "test",
Issuer = "test",
Extensions = new FulcioCertificateExtensions()
},
Statement = statement
};
var provenance = SigstoreNpmProvenanceChecker.ExtractProvenanceFromResult(result);
Assert.NotNull(provenance);
Assert.Equal("https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1", provenance.BuildType);
Assert.Null(provenance.WorkflowPath);
Assert.Null(provenance.BuilderId);
}
[Fact]
public void ExtractProvenanceFromResult_WithEmptyPredicate_ReturnsNullFields()
{
var predicateJson = """
{
"_type": "https://in-toto.io/Statement/v1",
"predicateType": "https://slsa.dev/provenance/v1",
"subject": [],
"predicate": {}
}
""";
var statement = InTotoStatement.Parse(predicateJson);
var result = new VerificationResult
{
SignerIdentity = new VerifiedIdentity
{
SubjectAlternativeName = "test",
Issuer = "test",
Extensions = new FulcioCertificateExtensions()
},
Statement = statement
};
var provenance = SigstoreNpmProvenanceChecker.ExtractProvenanceFromResult(result);
Assert.NotNull(provenance);
Assert.Null(provenance.BuildType);
Assert.Null(provenance.WorkflowPath);
}
[Fact]
public void ExtractProvenanceFromResult_WithNullSignerIdentity_ReturnsProvenanceFromPredicate()
{
var result = BuildVerificationResult(
sourceRepoUri: null,
sourceRepoRef: null,
workflowPath: ".github/workflows/publish.yml",
buildType: "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
builderId: "https://github.com/actions/runner/github-hosted",
sourceRepoInPredicate: "https://github.com/microsoft/playwright-cli",
includeIdentity: false);
var provenance = SigstoreNpmProvenanceChecker.ExtractProvenanceFromResult(result);
Assert.NotNull(provenance);
Assert.Equal("https://github.com/microsoft/playwright-cli", provenance.SourceRepository);
}
#endregion
#region Adversarial Tests - Predicate Type Safety
[Fact]
public void ExtractProvenanceFromResult_WithWrongTypedPredicateValues_ReturnsNullFields()
{
// buildType is a number, workflow fields are arrays/booleans — all wrong types
var predicateJson = """
{
"_type": "https://in-toto.io/Statement/v1",
"predicateType": "https://slsa.dev/provenance/v1",
"subject": [],
"predicate": {
"buildDefinition": {
"buildType": 42,
"externalParameters": {
"workflow": {
"repository": true,
"path": [],
"ref": {}
}
}
},
"runDetails": {
"builder": {
"id": 999
}
}
}
}
""";
var statement = InTotoStatement.Parse(predicateJson);
var result = new VerificationResult
{
SignerIdentity = new VerifiedIdentity
{
SubjectAlternativeName = "test",
Issuer = "test",
Extensions = new FulcioCertificateExtensions()
},
Statement = statement
};
var provenance = SigstoreNpmProvenanceChecker.ExtractProvenanceFromResult(result);
Assert.NotNull(provenance);
Assert.Null(provenance.BuildType);
Assert.Null(provenance.SourceRepository);
Assert.Null(provenance.WorkflowPath);
Assert.Null(provenance.WorkflowRef);
Assert.Null(provenance.BuilderId);
}
#endregion
#region Adversarial Tests - Attestation Structure
[Fact]
public void ExtractSlsaBundleJson_WithNullPredicateType_ReturnsNull()
{
var json = """
{
"attestations": [
{
"bundle": { "dsseEnvelope": {} }
}
]
}
""";
var bundleJson = SigstoreNpmProvenanceChecker.ExtractSlsaBundleJson(json, out _);
Assert.Null(bundleJson);
}
[Fact]
public void ExtractSlsaBundleJson_WithCaseSensitivePredicateType_ReturnsNull()
{
// Predicate type comparison is case-sensitive (Ordinal)
var json = """
{
"attestations": [
{
"predicateType": "HTTPS://SLSA.DEV/PROVENANCE/V1",
"bundle": { "dsseEnvelope": {} }
}
]
}
""";
var bundleJson = SigstoreNpmProvenanceChecker.ExtractSlsaBundleJson(json, out _);
Assert.Null(bundleJson);
}
#endregion
#region WorkflowRefInfo Adversarial Tests
[Theory]
[InlineData("refs/tags/v1.0.0", true, "tags", "v1.0.0")]
[InlineData("refs/heads/main", true, "heads", "main")]
[InlineData("refs/tags/@scope/pkg@1.0.0", true, "tags", "@scope/pkg@1.0.0")]
[InlineData("refs/tags/", false, null, null)] // Empty name
[InlineData("refs/", false, null, null)] // No kind/name
[InlineData("", false, null, null)] // Empty string
[InlineData("heads/main", false, null, null)] // Missing refs/ prefix
[InlineData("refs/tags/v1/../../../etc/passwd", true, "tags", "v1/../../../etc/passwd")] // Path traversal in name (accepted as-is)
public void WorkflowRefInfo_TryParse_HandlesEdgeCases(string? input, bool expectedSuccess, string? expectedKind, string? expectedName)
{
var result = WorkflowRefInfo.TryParse(input, out var refInfo);
Assert.Equal(expectedSuccess, result);
if (expectedSuccess)
{
Assert.NotNull(refInfo);
Assert.Equal(expectedKind, refInfo.Kind);
Assert.Equal(expectedName, refInfo.Name);
}
else
{
Assert.Null(refInfo);
}
}
#endregion
#region Test Helpers
private static VerificationResult BuildVerificationResult(
string? sourceRepoUri,
string? sourceRepoRef,
string? workflowPath,
string? buildType,
string? builderId,
string? sourceRepoInPredicate = null,
string? workflowRefInPredicate = null,
bool includeExtensions = true,
bool includeIdentity = true)
{
var predicateJson = BuildSlsaPredicateStatementJson(
sourceRepoInPredicate ?? sourceRepoUri ?? "https://github.com/test/repo",
workflowPath ?? ".github/workflows/test.yml",
workflowRefInPredicate ?? sourceRepoRef ?? "refs/tags/v0.0.1",
buildType ?? "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
builderId ?? "https://github.com/actions/runner/github-hosted");
var statement = InTotoStatement.Parse(predicateJson);
VerifiedIdentity? identity = null;
if (includeIdentity)
{
identity = new VerifiedIdentity
{
SubjectAlternativeName = "https://github.com/test/repo/.github/workflows/test.yml@refs/tags/v0.0.1",
Issuer = "https://token.actions.githubusercontent.com",
Extensions = includeExtensions ? new FulcioCertificateExtensions
{
SourceRepositoryUri = sourceRepoUri,
SourceRepositoryRef = sourceRepoRef
} : null
};
}
return new VerificationResult
{
SignerIdentity = identity,
Statement = statement
};
}
private static async Task<ProvenanceVerificationResult> VerifyThroughCheckerAsync(
string subjectName,
string subjectDigest,
string workflowPath)
{
const string sourceRepository = "https://github.com/microsoft/playwright-cli";
const string workflowRef = "refs/tags/v0.1.1";
const string buildType = "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1";
const string builderId = "https://github.com/actions/runner/github-hosted";
var statementJson = BuildSlsaPredicateStatementJson(
sourceRepository,
workflowPath,
workflowRef,
buildType,
builderId,
subjectName,
subjectDigest);
var statement = InTotoStatement.Parse(statementJson);
Assert.NotNull(statement);
var bundle = new SigstoreBundle
{
DsseEnvelope = new DsseEnvelope
{
PayloadType = "application/vnd.in-toto+json",
Payload = Encoding.UTF8.GetBytes(statementJson)
}
};
var attestationJson = $$"""
{
"attestations": [
{
"predicateType": "https://slsa.dev/provenance/v1",
"bundle": {{bundle.Serialize()}}
}
]
}
""";
using var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(attestationJson, Encoding.UTF8, "application/json")
};
using var handler = new MockHttpMessageHandler(response);
using var httpClient = new HttpClient(handler);
var verificationResult = new VerificationResult
{
SignerIdentity = new VerifiedIdentity
{
SubjectAlternativeName = "https://github.com/microsoft/playwright-cli/.github/workflows/publish.yml@refs/tags/v0.1.1",
Issuer = "https://token.actions.githubusercontent.com",
Extensions = new FulcioCertificateExtensions
{
SourceRepositoryUri = sourceRepository,
SourceRepositoryRef = workflowRef
}
},
Statement = statement
};
var checker = new SigstoreNpmProvenanceChecker(
httpClient,
NullLogger<SigstoreNpmProvenanceChecker>.Instance,
(_, _, _, _, _) => Task.FromResult((true, (VerificationResult?)verificationResult)));
return await checker.VerifyProvenanceAsync(
"@playwright/cli",
"0.1.1",
sourceRepository,
".github/workflows/publish.yml",
buildType,
refInfo => refInfo is { Kind: "tags", Name: "v0.1.1" },
ToSha512Sri("00112233445566778899aabbccddeeff"),
CancellationToken.None);
}
private static string BuildSlsaPredicateStatementJson(
string sourceRepository,
string workflowPath,
string workflowRef,
string buildType,
string builderId,
string subjectName = "pkg:npm/%40playwright/cli@0.1.1",
string subjectDigest = "abc123")
{
return $$"""
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [
{
"name": "{{subjectName}}",
"digest": { "sha512": "{{subjectDigest}}" }
}
],
"predicateType": "https://slsa.dev/provenance/v1",
"predicate": {
"buildDefinition": {
"buildType": "{{buildType}}",
"externalParameters": {
"workflow": {
"ref": "{{workflowRef}}",
"repository": "{{sourceRepository}}",
"path": "{{workflowPath}}"
}
}
},
"runDetails": {
"builder": {
"id": "{{builderId}}"
}
}
}
}
""";
}
private static InTotoStatement BuildStatementWithSubject(string subjectJson)
{
var statement = InTotoStatement.Parse(
$$"""
{
"_type": "https://in-toto.io/Statement/v1",
"subject": {{subjectJson}},
"predicateType": "https://slsa.dev/provenance/v1",
"predicate": {}
}
""");
Assert.NotNull(statement);
return statement;
}
private static string ToSha512Sri(string hexDigest)
=> $"sha512-{Convert.ToBase64String(Convert.FromHexString(hexDigest))}";
private static string BuildAttestationJsonWithBundle(string sourceRepository)
{
var payload = BuildSlsaPredicateStatementJson(
sourceRepository,
".github/workflows/publish.yml",
"refs/tags/v0.1.1",
"https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
"https://github.com/actions/runner/github-hosted");
var payloadBase64 = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(payload));
return $$"""
{
"attestations": [
{
"predicateType": "https://slsa.dev/provenance/v1",
"bundle": {
"mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
"dsseEnvelope": {
"payload": "{{payloadBase64}}",
"payloadType": "application/vnd.in-toto+json",
"signatures": [
{
"sig": "MEUCIQC+fake+signature",
"keyid": ""
}
]
},
"verificationMaterial": {
"certificate": {
"rawBytes": "MIIFake..."
},
"tlogEntries": [
{
"logIndex": "12345",
"logId": {
"keyId": "fake-key-id"
},
"kindVersion": {
"kind": "dsse",
"version": "0.0.1"
},
"integratedTime": "1700000000",
"inclusionPromise": {
"signedEntryTimestamp": "MEUC..."
},
"canonicalizedBody": "eyJ..."
}
]
}
}
}
]
}
""";
}
#endregion
}