Skip to content
Merged
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
15 changes: 13 additions & 2 deletions extension/src/webcat/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,15 @@ class ClaimPolicy implements VerificationPolicy {
private expected: string,
) {}

private claimMatches(got: string): boolean {
if (this.expected.startsWith("^")) {
const expectedPrefix = this.expected.slice(1);
return got.startsWith(expectedPrefix);
}

return got === this.expected;
}

verify(cert: X509Certificate): void {
// Special case: SubjectAltName (2.5.29.17) ---
if (this.oid === "2.5.29.17") {
Expand All @@ -730,7 +739,9 @@ class ClaimPolicy implements VerificationPolicy {
const other = sanExt.otherName(EXTENSION_OID_OTHERNAME);
if (other) allSans.add(other);

if (!allSans.has(this.expected)) {
const sanMatches = [...allSans].some((san) => this.claimMatches(san));

if (!sanMatches) {
throw new PolicyError(
`SAN mismatch for 2.5.29.17: expected '${this.expected}'`,
);
Expand Down Expand Up @@ -775,7 +786,7 @@ class ClaimPolicy implements VerificationPolicy {
throw new PolicyError(`Unable to decode extension ${this.oid}`);
}

if (got !== this.expected) {
if (!this.claimMatches(got)) {
throw new PolicyError(
`Extension ${this.oid} mismatch: got '${got}', expected '${this.expected}'`,
);
Expand Down
146 changes: 146 additions & 0 deletions extension/tests/webcat/sigstore-claims.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { describe, expect, it, vi } from "vitest";

const mockVerifyArtifactPolicy = vi.fn();

vi.mock("@freedomofpress/sigstore-browser", () => {
class PolicyError extends Error {}

class AllOf {
constructor(private policies: Array<{ verify: (cert: unknown) => void }>) {}

verify(cert: unknown) {
for (const policy of this.policies) {
policy.verify(cert);
}
}
}

class SigstoreVerifier {
async loadSigstoreRoot() {
return;
}

async verifyArtifactPolicy(
policy: { verify: (cert: unknown) => void },
bundle: { cert: unknown },
) {
mockVerifyArtifactPolicy(policy, bundle);
policy.verify(bundle.cert);
return true;
}
}

return {
AllOf,
EXTENSION_OID_OTHERNAME: "othername-oid",
PolicyError,
SigstoreVerifier,
};
});

import {
Manifest,
SigstoreEnrollment,
SigstoreSignatures,
} from "../../src/webcat/interfaces/bundle";
import { verifySigstoreManifest } from "../../src/webcat/validators";

function createSanCert(san: string) {
return {
extSubjectAltName: {
uri: san,
otherName: () => undefined,
},
extension: () => undefined,
notBefore: new Date(),
};
}

function createExtensionCert(oid: string, value: string) {
return {
extSubjectAltName: undefined,
extension: (extOid: string) => {
if (extOid !== oid) {
return undefined;
}

return {
value: new TextEncoder().encode(value),
valueObj: {},
};
},
notBefore: new Date(),
};
}

function baseEnrollment(claims: Record<string, string>): SigstoreEnrollment {
return {
trusted_root: "dHJ1c3Qtcm9vdA",
max_age: 3600,
claims,
};
}

const manifest: Manifest = {
version: 1,
timestamp: "0",
hashes: { a: "b" },
};

describe("verifySigstoreManifest claim matching", () => {
it("matches SAN prefixes when claim is prefixed with ^", async () => {
const enrollment = baseEnrollment({
"2.5.29.17": "^https://github.com/example/",
});

const signatures = [
{ cert: createSanCert("https://github.com/example/repo") },
] as unknown as SigstoreSignatures;

const result = await verifySigstoreManifest(
enrollment,
manifest,
signatures,
);

expect(result).toBeNull();
expect(mockVerifyArtifactPolicy).toHaveBeenCalled();
});

it("matches extension prefixes when claim is prefixed with ^", async () => {
const oid = "1.2.3.4";
const enrollment = baseEnrollment({
[oid]: "^https://issuer.example/",
});

const signatures = [
{ cert: createExtensionCert(oid, "https://issuer.example/value") },
] as unknown as SigstoreSignatures;

const result = await verifySigstoreManifest(
enrollment,
manifest,
signatures,
);

expect(result).toBeNull();
});

it("keeps exact matching when claim is not wrapped", async () => {
const enrollment = baseEnrollment({
"2.5.29.17": "https://github.com/example",
});

const signatures = [
{ cert: createSanCert("https://github.com/example/repo") },
] as unknown as SigstoreSignatures;

const result = await verifySigstoreManifest(
enrollment,
manifest,
signatures,
);

expect(result).not.toBeNull();
});
});