package gatewayrelease

import (
	"crypto"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"math/big"
	"os"
	"strings"
	"testing"
	"time"
)

const (
	testCommit            = "0123456789abcdef0123456789abcdef01234567"
	testWorkflowCommit    = "1111111111111111111111111111111111111111"
	testDigest            = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
	testPolicy            = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
	cosignFixtureFile     = "testdata/cosign-v3.0.6-legacy-simple-signing.json"
	provenanceFixtureFile = "testdata/cosign-v3.0.6-slsa-v1-attestation.json"
	cosignFixturePolicy   = "sha256:64388365f7a48810d39901f22f8b6100019f8e35f05b982a5c6d1d3c6182411a"
	cosignFixtureImage    = "sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662"
)

func testOwnerPolicy() Policy {
	return Policy{
		ImageRepository:             "ghcr.io/pingcap-inc/nexuscrm-api-host-gateway",
		KMSKeyARN:                   "arn:aws:kms:us-west-2:604175033442:key/5d8648a4-770c-4383-8f7a-9f0780522293",
		OIDCAudiencePrefix:          "urn:nexuscrm:gateway-image:",
		OIDCIssuer:                  "https://token.actions.githubusercontent.com",
		OIDCJWKSURI:                 "https://token.actions.githubusercontent.com/.well-known/jwks",
		OIDCMaxTokenLifetimeSeconds: 600,
		OIDCSubject:                 "repo:pingcap-inc/nexuscrm-api-host:ref:refs/heads/main",
		OIDCTokenAnnotation:         "io.nexuscrm.github-oidc-token",
		OwnerRepository:             "pingcap-inc/nexuscrm-api-host",
		ProvenanceBuilderID:         "https://github.com/pingcap-inc/nexuscrm-api-host/.github/workflows/trusted-publish-gateway-image.yml@" + testWorkflowCommit,
		ProvenanceBuildType:         "https://github.com/pingcap-inc/nexuscrm-api-host/blob/main/docs/gateway-image-build-type-v1.md",
		ProvenancePredicateType:     "https://slsa.dev/provenance/v1",
		RepositoryID:                "1279604404",
		RepositoryVisibility:        "private",
		RunnerEnvironment:           "github-hosted",
		PublisherState:              publisherActive,
		SchemaVersion:               "nexuscrm.oci-owner-signing-policy/v4",
		SourceRef:                   "refs/heads/main",
		WorkflowName:                "publish-gateway-image",
		WorkflowRef:                 "pingcap-inc/nexuscrm-api-host/.github/workflows/publish-gateway-image.yml@refs/heads/main",
	}
}

func TestPublisherStateSeparatesBootstrapFromReleaseAdmission(t *testing.T) {
	inactive := testOwnerPolicy()
	inactive.PublisherState = publisherInactive
	inactive.ProvenanceBuilderID = ""
	if err := inactive.validate(); err != nil {
		t.Fatalf("explicit inactive bootstrap policy is malformed: %v", err)
	}
	if _, err := VerifyToken(inactive, testCommit, testDigest, "not-evaluated", nil, time.Now(), false); err == nil || !strings.Contains(err.Error(), "not active") {
		t.Fatalf("inactive publisher reached release verification: %v", err)
	}

	for _, builder := range []string{
		"https://github.com/pingcap-inc/nexuscrm-api-host/.github/workflows/trusted-publish-gateway-image.yml@refs/heads/main",
		"https://github.com/pingcap-inc/other/.github/workflows/trusted-publish-gateway-image.yml@" + testWorkflowCommit,
	} {
		active := testOwnerPolicy()
		active.ProvenanceBuilderID = builder
		if err := active.validate(); err == nil {
			t.Fatalf("untrusted active workflow identity passed policy validation: %s", builder)
		}
	}
}

type tokenFixture struct {
	privateKey *rsa.PrivateKey
	kid        string
	jwks       []byte
	claims     map[string]any
	now        time.Time
}

func newTokenFixture(t *testing.T) tokenFixture {
	t.Helper()
	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		t.Fatal(err)
	}
	now := time.Unix(1_800_000_000, 0)
	policy := testOwnerPolicy()
	fixture := tokenFixture{
		privateKey: privateKey,
		kid:        "github-test-key",
		now:        now,
		claims: map[string]any{
			"iss":                   policy.OIDCIssuer,
			"aud":                   policy.OIDCAudiencePrefix + testDigest,
			"sub":                   policy.OIDCSubject,
			"exp":                   now.Add(5 * time.Minute).Unix(),
			"iat":                   now.Add(-5 * time.Second).Unix(),
			"nbf":                   now.Add(-5 * time.Second).Unix(),
			"jti":                   "immutable-jwt-id",
			"repository":            policy.OwnerRepository,
			"repository_id":         policy.RepositoryID,
			"repository_visibility": policy.RepositoryVisibility,
			"ref":                   policy.SourceRef,
			"ref_type":              "branch",
			"sha":                   testCommit,
			"workflow":              policy.WorkflowName,
			"workflow_ref":          policy.WorkflowRef,
			"workflow_sha":          testCommit,
			"job_workflow_ref":      strings.TrimPrefix(policy.ProvenanceBuilderID, "https://github.com/"),
			"job_workflow_sha":      testWorkflowCommit,
			"event_name":            "push",
			"runner_environment":    policy.RunnerEnvironment,
			"run_id":                "123456789",
			"run_attempt":           "1",
			"actor":                 "an-irrelevant-extra-github-claim",
		},
	}
	fixture.jwks = mustJSON(t, map[string]any{
		"keys": []any{map[string]any{
			"kid": fixture.kid,
			"kty": "RSA",
			"use": "sig",
			"alg": "RS256",
			"n":   base64.RawURLEncoding.EncodeToString(privateKey.N.Bytes()),
			"e":   base64.RawURLEncoding.EncodeToString(big.NewInt(int64(privateKey.E)).Bytes()),
			"x5c": []string{"ignored GitHub-owned certificate material"},
		}},
	})
	return fixture
}

func (fixture tokenFixture) token(t *testing.T, claims map[string]any, headerExtra map[string]any) string {
	t.Helper()
	header := map[string]any{"alg": "RS256", "typ": "JWT", "kid": fixture.kid, "x5t": "ignored"}
	for name, value := range headerExtra {
		header[name] = value
	}
	headerPart := base64.RawURLEncoding.EncodeToString(mustJSON(t, header))
	claimsPart := base64.RawURLEncoding.EncodeToString(mustJSON(t, claims))
	signingInput := headerPart + "." + claimsPart
	digest := sha256.Sum256([]byte(signingInput))
	signature, err := rsa.SignPKCS1v15(rand.Reader, fixture.privateKey, crypto.SHA256, digest[:])
	if err != nil {
		t.Fatal(err)
	}
	return signingInput + "." + base64.RawURLEncoding.EncodeToString(signature)
}

func (fixture tokenFixture) cosignEvidence(t *testing.T, token string, annotations map[string]string) []byte {
	t.Helper()
	policy := testOwnerPolicy()
	optional := map[string]any{
		"io.nexuscrm.source.repository":    policy.OwnerRepository,
		"io.nexuscrm.source.commit":        testCommit,
		"io.nexuscrm.source.ref":           policy.SourceRef,
		"io.nexuscrm.signer.workflow":      policy.WorkflowRef,
		"io.nexuscrm.signer.key-arn":       policy.KMSKeyARN,
		"io.nexuscrm.signer.policy-sha256": testPolicy,
		policy.OIDCTokenAnnotation:         token,
	}
	for name, value := range annotations {
		optional[name] = value
	}
	return mustJSON(t, []any{map[string]any{
		"critical": map[string]any{
			"identity": map[string]any{"docker-reference": policy.ImageRepository},
			"image":    map[string]any{"docker-manifest-digest": testDigest},
			"type":     cosignPayloadType,
		},
		"optional":           optional,
		"cosign_owned_extra": "ignored",
	}})
}

func TestPinnedCosignVerificationFixtureMatchesSelectedPayloadContract(t *testing.T) {
	evidence, err := os.ReadFile(cosignFixtureFile)
	if err != nil {
		t.Fatal(err)
	}
	var payloads []cosignPayload
	if err := decodeJSON(evidence, &payloads); err != nil {
		t.Fatalf("decode real Cosign v3.0.6 verification output: %v", err)
	}
	if len(payloads) != 1 {
		t.Fatalf("real Cosign fixture contains %d payloads, want 1", len(payloads))
	}
	token, err := payloads[0].identityToken(
		testOwnerPolicy(),
		testCommit,
		cosignFixtureImage,
		cosignFixturePolicy,
	)
	if err != nil {
		t.Fatalf("real Cosign v3.0.6 payload does not match verifier contract: %v", err)
	}
	if token != "header.payload.signature" {
		t.Fatalf("real Cosign fixture identity token=%q", token)
	}
}

func TestPinnedCosignSLSAV1FixtureMatchesSelectedAttestationContract(t *testing.T) {
	evidence, err := os.ReadFile(provenanceFixtureFile)
	if err != nil {
		t.Fatal(err)
	}
	var envelope dsseEnvelope
	if err := decodeJSON(evidence, &envelope); err != nil {
		t.Fatalf("decode real Cosign v3.0.6 attestation output: %v", err)
	}
	payload, err := base64.StdEncoding.DecodeString(envelope.Payload)
	if err != nil {
		t.Fatal(err)
	}
	var statement map[string]any
	if err := json.Unmarshal(payload, &statement); err != nil {
		t.Fatal(err)
	}
	if statement["_type"] != inTotoStatementV01 || statement["predicateType"] != slsaProvenanceV1 {
		t.Fatalf("real Cosign envelope versions drifted: %#v", statement)
	}
	// Cosign created the fixture against a public immutable image and the
	// pre-bootstrap builder identity so the real CLI could be exercised without
	// publishing repository state. Normalize those two policy-owned values
	// before testing the statement parser; Cosign owns signature verification in
	// production, while this package owns statement semantics.
	subject := statement["subject"].([]any)[0].(map[string]any)
	subject["name"] = testOwnerPolicy().ImageRepository
	subject["digest"] = map[string]any{"sha256": strings.TrimPrefix(testDigest, "sha256:")}
	statement["predicate"].(map[string]any)["runDetails"].(map[string]any)["builder"].(map[string]any)["id"] = testOwnerPolicy().ProvenanceBuilderID
	envelope.Payload = base64.StdEncoding.EncodeToString(mustJSON(t, statement))
	normalized := mustJSON(t, envelope)
	if _, err := VerifyProvenanceEvidence(testOwnerPolicy(), testCommit, testDigest, "123456789", "1", normalized); err != nil {
		t.Fatalf("real Cosign v3.0.6 attestation shape failed policy verification: %v", err)
	}
}

func cloneClaims(source map[string]any) map[string]any {
	clone := make(map[string]any, len(source))
	for name, value := range source {
		clone[name] = value
	}
	return clone
}

func mustJSON(t *testing.T, value any) []byte {
	t.Helper()
	content, err := json.Marshal(value)
	if err != nil {
		t.Fatal(err)
	}
	return content
}

func provenanceEvidence(t *testing.T, policy Policy, commit, digest, runID, runAttempt string, mutate func(map[string]any)) []byte {
	t.Helper()
	predicate, err := BuildProvenancePredicate(policy, commit, runID, runAttempt)
	if err != nil {
		t.Fatal(err)
	}
	var predicateValue map[string]any
	if err := json.Unmarshal(predicate, &predicateValue); err != nil {
		t.Fatal(err)
	}
	statement := map[string]any{
		"_type": inTotoStatementV01,
		"subject": []any{map[string]any{
			"name": policy.ImageRepository,
			"digest": map[string]any{
				"sha256": strings.TrimPrefix(digest, "sha256:"),
			},
		}},
		"predicateType": policy.ProvenancePredicateType,
		"predicate":     predicateValue,
	}
	if mutate != nil {
		mutate(statement)
	}
	payload := mustJSON(t, statement)
	return mustJSON(t, map[string]any{
		"payloadType": dssePayloadType,
		"payload":     base64.StdEncoding.EncodeToString(payload),
		"signatures":  []any{map[string]any{"keyid": "", "sig": base64.StdEncoding.EncodeToString([]byte("verified-by-cosign"))}},
	})
}

func TestOwnerSignedProvenanceBindsBuildSourceSubjectAndGitHubRun(t *testing.T) {
	policy := testOwnerPolicy()
	evidence := provenanceEvidence(t, policy, testCommit, testDigest, "123456789", "1", nil)
	digest, err := VerifyProvenanceEvidence(policy, testCommit, testDigest, "123456789", "1", evidence)
	if err != nil {
		t.Fatal(err)
	}
	if !strings.HasPrefix(digest, "sha256:") {
		t.Fatalf("provenance digest is not canonical: %q", digest)
	}

	tests := []struct {
		name   string
		mutate func(map[string]any)
	}{
		{name: "different subject", mutate: func(statement map[string]any) {
			statement["subject"].([]any)[0].(map[string]any)["name"] = "ghcr.io/pingcap-inc/other"
		}},
		{name: "different digest", mutate: func(statement map[string]any) {
			statement["subject"].([]any)[0].(map[string]any)["digest"].(map[string]any)["sha256"] = strings.Repeat("b", 64)
		}},
		{name: "different predicate type", mutate: func(statement map[string]any) {
			statement["predicateType"] = "https://attacker.invalid/provenance"
		}},
		{name: "different builder", mutate: func(statement map[string]any) {
			statement["predicate"].(map[string]any)["runDetails"].(map[string]any)["builder"].(map[string]any)["id"] = "https://attacker.invalid/builder"
		}},
		{name: "different source", mutate: func(statement map[string]any) {
			statement["predicate"].(map[string]any)["buildDefinition"].(map[string]any)["externalParameters"].(map[string]any)["source"].(map[string]any)["commit"] = strings.Repeat("c", 40)
		}},
		{name: "different run", mutate: func(statement map[string]any) {
			statement["predicate"].(map[string]any)["buildDefinition"].(map[string]any)["internalParameters"].(map[string]any)["runId"] = "999"
		}},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			bad := provenanceEvidence(t, policy, testCommit, testDigest, "123456789", "1", test.mutate)
			if _, err := VerifyProvenanceEvidence(policy, testCommit, testDigest, "123456789", "1", bad); err == nil {
				t.Fatal("unbound provenance passed verification")
			}
		})
	}
}

func TestOwnerSignedProvenanceRejectsCandidateMetadataAndUnsignedEvidence(t *testing.T) {
	policy := testOwnerPolicy()
	unsigned := provenanceEvidence(t, policy, testCommit, testDigest, "123456789", "1", nil)
	var envelope map[string]any
	if err := json.Unmarshal(unsigned, &envelope); err != nil {
		t.Fatal(err)
	}
	envelope["signatures"] = nil
	if _, err := VerifyProvenanceEvidence(policy, testCommit, testDigest, "123456789", "1", mustJSON(t, envelope)); err == nil {
		t.Fatal("unsigned candidate provenance passed verification")
	}

	conflicting := append(
		provenanceEvidence(t, policy, testCommit, testDigest, "123456789", "1", nil),
		provenanceEvidence(t, policy, testCommit, testDigest, "123456789", "2", nil)...,
	)
	if _, err := VerifyProvenanceEvidence(policy, testCommit, testDigest, "123456789", "1", conflicting); err == nil {
		t.Fatal("conflicting owner-signed provenance passed verification")
	}
}

func TestVerifyTokenBindsGitHubWorkflowToExactDigest(t *testing.T) {
	fixture := newTokenFixture(t)
	token := fixture.token(t, fixture.claims, nil)
	receipt, err := VerifyToken(testOwnerPolicy(), testCommit, testDigest, token, fixture.jwks, fixture.now, true)
	if err != nil {
		t.Fatal(err)
	}
	if receipt.ImageDigest != testDigest || receipt.SourceCommit != testCommit || receipt.RunID != "123456789" ||
		receipt.JobWorkflowSHA != testWorkflowCommit {
		t.Fatalf("unexpected receipt: %#v", receipt)
	}
	if !strings.HasPrefix(receipt.OIDCKeySHA256, "sha256:") || receipt.OIDCKeyID != fixture.kid {
		t.Fatalf("receipt did not pin the independently fetched GitHub key: %#v", receipt)
	}
}

func TestVerifyTokenRejectsEveryUnboundGitHubIdentityFact(t *testing.T) {
	fixture := newTokenFixture(t)
	tests := []struct {
		name  string
		claim string
		value any
	}{
		{name: "issuer", claim: "iss", value: "https://attacker.invalid"},
		{name: "audience", claim: "aud", value: "urn:nexuscrm:gateway-image:sha256:" + strings.Repeat("b", 64)},
		{name: "multiple audiences", claim: "aud", value: []string{testOwnerPolicy().OIDCAudiencePrefix + testDigest, "other"}},
		{name: "subject", claim: "sub", value: "repo:pingcap-inc/nexuscrm-api-host:ref:refs/heads/feature"},
		{name: "repository", claim: "repository", value: "pingcap-inc/other"},
		{name: "repository ID", claim: "repository_id", value: "999"},
		{name: "visibility", claim: "repository_visibility", value: "public"},
		{name: "ref", claim: "ref", value: "refs/heads/feature"},
		{name: "ref type", claim: "ref_type", value: "tag"},
		{name: "source SHA", claim: "sha", value: strings.Repeat("c", 40)},
		{name: "workflow", claim: "workflow", value: "other"},
		{name: "workflow ref", claim: "workflow_ref", value: "pingcap-inc/other/.github/workflows/publish.yml@refs/heads/main"},
		{name: "malformed workflow SHA", claim: "workflow_sha", value: "not-a-full-sha"},
		{name: "job workflow ref", claim: "job_workflow_ref", value: "pingcap-inc/other/.github/workflows/publish.yml@" + testWorkflowCommit},
		{name: "job workflow SHA", claim: "job_workflow_sha", value: strings.Repeat("e", 40)},
		{name: "event", claim: "event_name", value: "pull_request_target"},
		{name: "runner", claim: "runner_environment", value: "self-hosted"},
		{name: "missing run ID", claim: "run_id", value: ""},
		{name: "missing attempt", claim: "run_attempt", value: ""},
		{name: "missing JWT ID", claim: "jti", value: ""},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			claims := cloneClaims(fixture.claims)
			claims[test.claim] = test.value
			token := fixture.token(t, claims, nil)
			if _, err := VerifyToken(testOwnerPolicy(), testCommit, testDigest, token, fixture.jwks, fixture.now, true); err == nil {
				t.Fatal("unbound GitHub identity passed verification")
			}
		})
	}
}

func TestVerifyTokenPreservesWorkflowRevisionAsASeparateGitHubFact(t *testing.T) {
	fixture := newTokenFixture(t)
	claims := cloneClaims(fixture.claims)
	workflowCommit := strings.Repeat("d", 40)
	claims["workflow_sha"] = workflowCommit
	token := fixture.token(t, claims, nil)
	receipt, err := VerifyToken(testOwnerPolicy(), testCommit, testDigest, token, fixture.jwks, fixture.now, true)
	if err != nil {
		t.Fatal(err)
	}
	if receipt.SourceCommit != testCommit || receipt.WorkflowSHA != workflowCommit {
		t.Fatalf("source and workflow revisions were not preserved independently: %#v", receipt)
	}
}

func TestVerifyTokenRejectsUntrustedKeySelectionAndSignature(t *testing.T) {
	fixture := newTokenFixture(t)
	for _, test := range []struct {
		name   string
		header map[string]any
	}{
		{name: "embedded JWK", header: map[string]any{"jwk": map[string]string{"kty": "RSA"}}},
		{name: "remote JKU", header: map[string]any{"jku": "https://attacker.invalid/jwks"}},
		{name: "critical extension", header: map[string]any{"crit": []string{"attacker"}}},
	} {
		t.Run(test.name, func(t *testing.T) {
			token := fixture.token(t, fixture.claims, test.header)
			if _, err := VerifyToken(testOwnerPolicy(), testCommit, testDigest, token, fixture.jwks, fixture.now, true); err == nil {
				t.Fatal("untrusted JWT key selection passed verification")
			}
		})
	}

	other := newTokenFixture(t)
	token := other.token(t, other.claims, nil)
	if _, err := VerifyToken(testOwnerPolicy(), testCommit, testDigest, token, fixture.jwks, fixture.now, true); err == nil {
		t.Fatal("JWT signed by a different key passed verification")
	}

	var set map[string]any
	if err := json.Unmarshal(fixture.jwks, &set); err != nil {
		t.Fatal(err)
	}
	keys := set["keys"].([]any)
	set["keys"] = append(keys, keys[0])
	if _, err := VerifyToken(testOwnerPolicy(), testCommit, testDigest, fixture.token(t, fixture.claims, nil), mustJSON(t, set), fixture.now, true); err == nil {
		t.Fatal("duplicate GitHub key ID passed verification")
	}
}

func TestVerifyTokenAllowsHistoricalProofButFreshPublicationRequiresCurrentToken(t *testing.T) {
	fixture := newTokenFixture(t)
	claims := cloneClaims(fixture.claims)
	claims["iat"] = fixture.now.Add(-10 * time.Minute).Unix()
	claims["nbf"] = fixture.now.Add(-10 * time.Minute).Unix()
	claims["exp"] = fixture.now.Add(-5 * time.Minute).Unix()
	token := fixture.token(t, claims, nil)
	if _, err := VerifyToken(testOwnerPolicy(), testCommit, testDigest, token, fixture.jwks, fixture.now, false); err != nil {
		t.Fatalf("historical digest-bound proof failed: %v", err)
	}
	if _, err := VerifyToken(testOwnerPolicy(), testCommit, testDigest, token, fixture.jwks, fixture.now, true); err == nil {
		t.Fatal("expired token was accepted for a new stable publication")
	}
}

func TestVerifyTokenRejectsMalformedValidityIntervals(t *testing.T) {
	fixture := newTokenFixture(t)
	tests := []struct {
		name   string
		mutate func(map[string]any)
	}{
		{name: "missing not-before", mutate: func(claims map[string]any) { claims["nbf"] = int64(0) }},
		{name: "not-before after issued-at", mutate: func(claims map[string]any) {
			claims["nbf"] = fixture.now.Add(2 * time.Minute).Unix()
		}},
		{name: "expires before issued-at", mutate: func(claims map[string]any) {
			claims["exp"] = fixture.now.Add(-10 * time.Second).Unix()
		}},
		{name: "excessive lifetime", mutate: func(claims map[string]any) {
			claims["exp"] = fixture.now.Add(20 * time.Minute).Unix()
		}},
		{name: "not yet current", mutate: func(claims map[string]any) {
			claims["iat"] = fixture.now.Add(5 * time.Minute).Unix()
			claims["nbf"] = fixture.now.Add(5 * time.Minute).Unix()
			claims["exp"] = fixture.now.Add(9 * time.Minute).Unix()
		}},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			claims := cloneClaims(fixture.claims)
			test.mutate(claims)
			token := fixture.token(t, claims, nil)
			if _, err := VerifyToken(testOwnerPolicy(), testCommit, testDigest, token, fixture.jwks, fixture.now, true); err == nil {
				t.Fatal("malformed JWT validity interval passed verification")
			}
		})
	}
}

func TestVerifyCosignEvidenceRequiresKMSAndGitHubProofOnSamePayload(t *testing.T) {
	fixture := newTokenFixture(t)
	token := fixture.token(t, fixture.claims, nil)
	evidence := fixture.cosignEvidence(t, token, nil)
	receipt, err := VerifyCosignEvidence(testOwnerPolicy(), testCommit, testDigest, testPolicy, evidence, fixture.jwks, fixture.now, true)
	if err != nil {
		t.Fatal(err)
	}
	if receipt.OwnerPolicySHA256 != testPolicy || receipt.KMSKeyARN != testOwnerPolicy().KMSKeyARN {
		t.Fatalf("receipt lost owner proof: %#v", receipt)
	}

	for _, test := range []struct {
		name        string
		annotations map[string]string
	}{
		{name: "different source", annotations: map[string]string{"io.nexuscrm.source.commit": strings.Repeat("c", 40)}},
		{name: "different key", annotations: map[string]string{"io.nexuscrm.signer.key-arn": "arn:aws:kms:us-west-2:604175033442:key/00000000-0000-0000-0000-000000000000"}},
		{name: "different policy", annotations: map[string]string{"io.nexuscrm.signer.policy-sha256": "sha256:" + strings.Repeat("c", 64)}},
		{name: "missing GitHub proof", annotations: map[string]string{testOwnerPolicy().OIDCTokenAnnotation: ""}},
	} {
		t.Run(test.name, func(t *testing.T) {
			badEvidence := fixture.cosignEvidence(t, token, test.annotations)
			if _, err := VerifyCosignEvidence(testOwnerPolicy(), testCommit, testDigest, testPolicy, badEvidence, fixture.jwks, fixture.now, true); err == nil {
				t.Fatal("unbound KMS/GitHub evidence passed verification")
			}
		})
	}
}

func TestVerifyTokenRejectsDigestReplayAndOversizedEvidence(t *testing.T) {
	fixture := newTokenFixture(t)
	token := fixture.token(t, fixture.claims, nil)
	otherDigest := "sha256:" + strings.Repeat("b", 64)
	if _, err := VerifyToken(testOwnerPolicy(), testCommit, otherDigest, token, fixture.jwks, fixture.now, true); err == nil {
		t.Fatal("digest-bound GitHub proof replayed to another image")
	}
	if _, err := VerifyCosignEvidence(testOwnerPolicy(), testCommit, testDigest, testPolicy, []byte(strings.Repeat("x", maxEvidenceBytes+1)), fixture.jwks, fixture.now, true); err == nil {
		t.Fatal("oversized Cosign evidence passed verification")
	}
}

func TestLoadPolicyRejectsUnknownFacts(t *testing.T) {
	policyBytes, err := os.ReadFile("../../release/gateway-image-signing-policy.json")
	if err != nil {
		t.Fatal(err)
	}
	var policy map[string]any
	if err := json.Unmarshal(policyBytes, &policy); err != nil {
		t.Fatal(err)
	}
	policy["second_authority"] = true
	content := mustJSON(t, policy)
	if _, err := LoadPolicy(strings.NewReader(string(content))); err == nil {
		t.Fatal("unknown owner-policy fact passed verification")
	}
}
