diff --git a/.github/workflows/production-lifecycle-ref.yml b/.github/workflows/production-lifecycle-ref.yml new file mode 100644 index 0000000..399f532 --- /dev/null +++ b/.github/workflows/production-lifecycle-ref.yml @@ -0,0 +1,147 @@ +name: Production lifecycle feed ref + +on: + workflow_dispatch: + inputs: + feed_update_json: + description: Closed openadapt.production-lifecycle-feed-update/v1 JSON + required: true + type: string + +permissions: + contents: read + attestations: read + +concurrency: + group: production-lifecycle-feed-ref + cancel-in-progress: false + +jobs: + authorize: + runs-on: ubuntu-latest + timeout-minutes: 2 + permissions: {} + steps: + - name: Require the registered lifecycle App dispatcher + env: + ACTOR: ${{ github.actor }} + ACTOR_ID: ${{ github.actor_id }} + TRIGGERING_ACTOR: ${{ github.triggering_actor }} + shell: bash + run: | + set -euo pipefail + test "$ACTOR" = 'openadapt-lifecycle[bot]' + test "$ACTOR_ID" = '321544198' + test "$TRIGGERING_ACTOR" = 'openadapt-lifecycle[bot]' + + update: + needs: authorize + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: production-lifecycle-feed + steps: + - name: Check out current protected main + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 2 + persist-credentials: false + + - name: Mint the lifecycle App token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + app-id: '4730735' + private-key: ${{ secrets.OPENADAPT_LIFECYCLE_APP_PRIVATE_KEY }} + owner: OpenAdaptAI + repositories: .github + + - name: Install the pinned GitHub attestation verifier + shell: bash + run: | + set -euo pipefail + archive="$RUNNER_TEMP/gh_2.98.0_linux_amd64.tar.gz" + curl --fail --location --silent --show-error \ + --output "$archive" \ + https://github.com/cli/cli/releases/download/v2.98.0/gh_2.98.0_linux_amd64.tar.gz + echo "3b8ac6b30336802fc1a858d7c084e11cdf24ac1a761ca90b68022d7d729208de $archive" \ + | sha256sum --check --strict + tar --extract --gzip --file "$archive" --directory "$RUNNER_TEMP" + echo "$RUNNER_TEMP/gh_2.98.0_linux_amd64/bin" >> "$GITHUB_PATH" + + - name: Install the pinned message-signature verifier + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.0 + with: + cosign-release: v3.1.3 + + - name: Bind Cosign and the Sigstore trust material + shell: bash + run: | + set -euo pipefail + cosign_path=$(command -v cosign) + test "$(wc -c < "$cosign_path" | tr -d ' ')" = '141178250' + echo "4629c757b7618056f8ddd7e2625ae9fdd94c0372a65049520bc7d9df9efc7f71 $cosign_path" \ + | sha256sum --check --strict + trusted_root="$RUNNER_TEMP/sigstore-trusted-root.json" + signing_config="$RUNNER_TEMP/sigstore-signing-config.json" + curl --fail --location --silent --show-error \ + --output "$trusted_root" \ + https://raw.githubusercontent.com/sigstore/root-signing/c6f23ff62645fb0c46ebc7945675835b52f91aa8/targets/trusted_root.json + test "$(wc -c < "$trusted_root" | tr -d ' ')" = '6787' + echo "6494e21ea73fa7ee769f85f57d5a3e6a08725eae1e38c755fc3517c9e6bc0b66 $trusted_root" \ + | sha256sum --check --strict + curl --fail --location --silent --show-error \ + --output "$signing_config" \ + https://raw.githubusercontent.com/sigstore/root-signing/c6f23ff62645fb0c46ebc7945675835b52f91aa8/targets/signing_config.json + test "$(wc -c < "$signing_config" | tr -d ' ')" = '219' + echo "d358c75d032833f4193500f5b01b5760409410558fac962c599439adbb268b0f $signing_config" \ + | sha256sum --check --strict + echo "OPENADAPT_SIGSTORE_TRUSTED_ROOT=$trusted_root" >> "$GITHUB_ENV" + + - name: Validate the exact feed commit + env: + FEED_UPDATE_JSON: ${{ inputs.feed_update_json }} + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + new_commit=$(python3 -c 'import json,os; print(json.loads(os.environ["FEED_UPDATE_JSON"])["new_commit"])') + expected_old=$(python3 -c 'import json,os; print(json.loads(os.environ["FEED_UPDATE_JSON"])["expected_old_commit"] or "")') + git fetch --no-tags origin "$new_commit" + if [ -n "$expected_old" ]; then + git fetch --no-tags origin "$expected_old" + fi + python3 scripts/validate_production_lifecycle_ref.py \ + --update-json "$FEED_UPDATE_JSON" \ + --trusted-main "${{ github.sha }}" + + - name: Verify lifecycle App installation + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + shell: bash + run: | + set -euo pipefail + test "$(gh api /installation --jq '.app_id|tostring')" = '4730735' + test "$(gh api /installation --jq '.id|tostring')" = '156835618' + test "$(gh api /installation --jq '.account.id|tostring')" = '132681217' + + - name: Compare and swap the protected feed ref + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + FEED_UPDATE_JSON: ${{ inputs.feed_update_json }} + shell: bash + run: | + set -euo pipefail + expected_old=$(python3 -c 'import json,os; print(json.loads(os.environ["FEED_UPDATE_JSON"])["expected_old_commit"] or "")') + new_commit=$(python3 -c 'import json,os; print(json.loads(os.environ["FEED_UPDATE_JSON"])["new_commit"])') + current=$(gh api /repos/OpenAdaptAI/.github/git/ref/heads/production-lifecycle-feed \ + --jq .object.sha 2>/dev/null || true) + test "$current" = "$expected_old" + if [ -z "$expected_old" ]; then + gh api --method POST /repos/OpenAdaptAI/.github/git/refs \ + -f ref=refs/heads/production-lifecycle-feed -f sha="$new_commit" >/dev/null + else + test "$(gh api /repos/OpenAdaptAI/.github/git/ref/heads/production-lifecycle-feed --jq .object.sha)" = "$expected_old" + gh api --method PATCH /repos/OpenAdaptAI/.github/git/refs/heads/production-lifecycle-feed \ + -f sha="$new_commit" -F force=false >/dev/null + fi + test "$(gh api /repos/OpenAdaptAI/.github/git/ref/heads/production-lifecycle-feed --jq .object.sha)" = "$new_commit" diff --git a/.github/workflows/profile-consistency.yml b/.github/workflows/profile-consistency.yml index bdf6ebb..a81bfdb 100644 --- a/.github/workflows/profile-consistency.yml +++ b/.github/workflows/profile-consistency.yml @@ -66,7 +66,9 @@ jobs: else printf '{"admissions":[]}\n' > "$RUNNER_TEMP/previous-production-lifecycle-admissions.json" fi - python3 scripts/validate_production_lifecycle.py --previous-admissions "$RUNNER_TEMP/previous-production-lifecycle-admissions.json" + python3 scripts/validate_production_lifecycle.py \ + --history-only \ + --previous-admissions "$RUNNER_TEMP/previous-production-lifecycle-admissions.json" - name: Validate product truth and links run: python3 scripts/check_profile.py diff --git a/.github/workflows/verify-production-release-admission.yml b/.github/workflows/verify-production-release-admission.yml new file mode 100644 index 0000000..4452210 --- /dev/null +++ b/.github/workflows/verify-production-release-admission.yml @@ -0,0 +1,184 @@ +name: Verify Production release admission + +on: + workflow_call: + inputs: + admission_reference_json: {required: true, type: string} + artifact_inventory_json: {required: true, type: string} + candidate_artifact_name: {required: true, type: string} + expected_target: {required: true, type: string} + expected_repository: {required: true, type: string} + expected_repository_id: {required: true, type: string} + expected_source_commit: {required: true, type: string} + expected_version: {required: true, type: string} + expected_tag: {required: true, type: string} + central_verifier_sha: {required: true, type: string} + outputs: + admission_object_sha256: + value: ${{ jobs.verify.outputs.admission_object_sha256 }} + release_sha256: + value: ${{ jobs.verify.outputs.release_sha256 }} + artifact_inventory_sha256: + value: ${{ jobs.verify.outputs.artifact_inventory_sha256 }} + publication_staging_json: + value: ${{ jobs.verify.outputs.publication_staging_json }} + publication_staging_sha256: + value: ${{ jobs.verify.outputs.publication_staging_sha256 }} + draft_release_id: + value: ${{ jobs.verify.outputs.draft_release_id }} + expires_at: + value: ${{ jobs.verify.outputs.expires_at }} + registry_source_commit: + value: ${{ jobs.verify.outputs.registry_source_commit }} + +permissions: + contents: read + attestations: read + id-token: write + +jobs: + verify: + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + admission_object_sha256: ${{ steps.verify.outputs.admission_object_sha256 }} + release_sha256: ${{ steps.verify.outputs.release_sha256 }} + artifact_inventory_sha256: ${{ steps.verify.outputs.artifact_inventory_sha256 }} + publication_staging_json: ${{ steps.verify.outputs.publication_staging_json }} + publication_staging_sha256: ${{ steps.verify.outputs.publication_staging_sha256 }} + draft_release_id: ${{ steps.verify.outputs.draft_release_id }} + expires_at: ${{ steps.verify.outputs.expires_at }} + registry_source_commit: ${{ steps.verify.outputs.registry_source_commit }} + steps: + - name: Bind this reusable call to the requested central commit + shell: bash + env: + CENTRAL_VERIFIER_SHA: ${{ inputs.central_verifier_sha }} + run: | + set -euo pipefail + if [[ ! "$CENTRAL_VERIFIER_SHA" =~ ^[0-9a-f]{40}$ ]]; then + echo "central_verifier_sha must be one exact commit" >&2 + exit 1 + fi + audience="openadapt-production-release-verifier" + token_json="$(curl --fail --silent --show-error \ + --header "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ + "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=${audience}")" + TOKEN_JSON="$token_json" AUDIENCE="$audience" python3 - <<'PY' + import base64 + import json + import os + + response = json.loads(os.environ["TOKEN_JSON"]) + token = response.get("value") + if not isinstance(token, str) or token.count(".") != 2: + raise SystemExit("GitHub did not return one OIDC token") + payload = token.split(".")[1] + payload += "=" * (-len(payload) % 4) + claims = json.loads(base64.urlsafe_b64decode(payload)) + commit = os.environ["CENTRAL_VERIFIER_SHA"] + expected = { + "aud": os.environ["AUDIENCE"], + "job_workflow_ref": ( + "OpenAdaptAI/.github/.github/workflows/" + f"verify-production-release-admission.yml@{commit}" + ), + "job_workflow_sha": commit, + "repository_owner_id": "132681217", + "runner_environment": "github-hosted", + } + for name, value in expected.items(): + if claims.get(name) != value: + raise SystemExit(f"OIDC claim {name} differs from the central verifier contract") + PY + + - name: Check out the exact central verifier + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + repository: OpenAdaptAI/.github + ref: ${{ inputs.central_verifier_sha }} + path: central-trust + persist-credentials: false + + - name: Download the exact candidate artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: ${{ inputs.candidate_artifact_name }} + path: candidate + + - name: Install the pinned GitHub attestation verifier + shell: bash + run: | + set -euo pipefail + archive="$RUNNER_TEMP/gh_2.98.0_linux_amd64.tar.gz" + curl --fail --location --silent --show-error \ + --output "$archive" \ + https://github.com/cli/cli/releases/download/v2.98.0/gh_2.98.0_linux_amd64.tar.gz + echo "3b8ac6b30336802fc1a858d7c084e11cdf24ac1a761ca90b68022d7d729208de $archive" \ + | sha256sum --check --strict + tar --extract --gzip --file "$archive" --directory "$RUNNER_TEMP" + echo "$RUNNER_TEMP/gh_2.98.0_linux_amd64/bin" >> "$GITHUB_PATH" + + - name: Install the pinned message-signature verifier + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.0 + with: + cosign-release: v3.1.3 + + - name: Bind Cosign and the Sigstore trust material + shell: bash + run: | + set -euo pipefail + cosign_path=$(command -v cosign) + test "$(wc -c < "$cosign_path" | tr -d ' ')" = '141178250' + echo "4629c757b7618056f8ddd7e2625ae9fdd94c0372a65049520bc7d9df9efc7f71 $cosign_path" \ + | sha256sum --check --strict + trusted_root="$RUNNER_TEMP/sigstore-trusted-root.json" + signing_config="$RUNNER_TEMP/sigstore-signing-config.json" + curl --fail --location --silent --show-error \ + --output "$trusted_root" \ + https://raw.githubusercontent.com/sigstore/root-signing/c6f23ff62645fb0c46ebc7945675835b52f91aa8/targets/trusted_root.json + test "$(wc -c < "$trusted_root" | tr -d ' ')" = '6787' + echo "6494e21ea73fa7ee769f85f57d5a3e6a08725eae1e38c755fc3517c9e6bc0b66 $trusted_root" \ + | sha256sum --check --strict + curl --fail --location --silent --show-error \ + --output "$signing_config" \ + https://raw.githubusercontent.com/sigstore/root-signing/c6f23ff62645fb0c46ebc7945675835b52f91aa8/targets/signing_config.json + test "$(wc -c < "$signing_config" | tr -d ' ')" = '219' + echo "d358c75d032833f4193500f5b01b5760409410558fac962c599439adbb268b0f $signing_config" \ + | sha256sum --check --strict + echo "OPENADAPT_SIGSTORE_TRUSTED_ROOT=$trusted_root" >> "$GITHUB_ENV" + + - name: Materialize closed verifier inputs + shell: bash + env: + ADMISSION_REFERENCE_JSON: ${{ inputs.admission_reference_json }} + ARTIFACT_INVENTORY_JSON: ${{ inputs.artifact_inventory_json }} + run: | + set -euo pipefail + printf '%s' "$ADMISSION_REFERENCE_JSON" > "$RUNNER_TEMP/admission-reference.json" + printf '%s' "$ARTIFACT_INVENTORY_JSON" > "$RUNNER_TEMP/artifact-inventory.json" + + - name: Verify admission and exact candidate bytes + id: verify + shell: bash + env: + GH_TOKEN: ${{ github.token }} + EXPECTED_TARGET: ${{ inputs.expected_target }} + EXPECTED_REPOSITORY: ${{ inputs.expected_repository }} + EXPECTED_REPOSITORY_ID: ${{ inputs.expected_repository_id }} + EXPECTED_SOURCE_COMMIT: ${{ inputs.expected_source_commit }} + EXPECTED_VERSION: ${{ inputs.expected_version }} + EXPECTED_TAG: ${{ inputs.expected_tag }} + run: | + set -euo pipefail + python3 central-trust/scripts/verify_production_release_admission.py \ + --admission-reference "$RUNNER_TEMP/admission-reference.json" \ + --artifact-inventory "$RUNNER_TEMP/artifact-inventory.json" \ + --artifact-root candidate \ + --expected-target "$EXPECTED_TARGET" \ + --expected-repository "$EXPECTED_REPOSITORY" \ + --expected-repository-id "$EXPECTED_REPOSITORY_ID" \ + --expected-source-commit "$EXPECTED_SOURCE_COMMIT" \ + --expected-version "$EXPECTED_VERSION" \ + --expected-tag "$EXPECTED_TAG" \ + --github-output "$GITHUB_OUTPUT" diff --git a/evidence-registry.json b/evidence-registry.json index 4594757..53dcce7 100644 --- a/evidence-registry.json +++ b/evidence-registry.json @@ -1,6 +1,13 @@ { "$schema": "schemas/evidence-registry.schema.json", - "schema_version": "openadapt.production-evidence-registry/v1", - "head_entry_sha256": null, + "schema_version": "openadapt.production-evidence-registry/v2", + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "revision": 1, + "previous_registry_head_sha256": null, + "registry_head_sha256": "sha256:3c88a4e945f1aa51ee080f05331c0d7a36b907ad288cf8a24b2fef08b1681a97", + "signer_registry": null, + "signer_registry_history": [], "entries": [] } diff --git a/production-evidence-policy.json b/production-evidence-policy.json new file mode 100644 index 0000000..6de9428 --- /dev/null +++ b/production-evidence-policy.json @@ -0,0 +1,130 @@ +{ + "schema_version": "openadapt.production-evidence-verification-policy/v1", + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "reference_schema_version": "openadapt.production-evidence-object-reference/v2", + "transport": "exact-commit-raw-github", + "signer_registry_schema_version": "openadapt.qualification-signer-registry/v2", + "embedded_signatures": [ + { + "kind": "qualification-authority-state-receipt", + "algorithm": "ed25519", + "profile": "ed25519-canonical-signing-statement", + "statement_schema_version": "openadapt.qualification-evidence-signing-statement/v1", + "signature_domain": "OpenAdapt qualification authority state receipt v2\u0000", + "projection": "canonical-object-minus-signature-and-signing-statement", + "encoding": "base64-rfc4648-padded", + "key_source": "active-qualification-signer-registry-v2" + }, + { + "kind": "qualification-evidence-decision-receipt", + "algorithm": "ed25519", + "profile": "ed25519-canonical-signing-statement", + "statement_schema_version": "openadapt.qualification-evidence-signing-statement/v1", + "signature_domain": "OpenAdapt qualification evidence decision receipt v1\u0000", + "projection": "canonical-object-minus-signature-and-signing-statement", + "encoding": "base64-rfc4648-padded", + "key_source": "active-qualification-signer-registry-v2" + }, + { + "kind": "qualification-revocation-state-receipt", + "algorithm": "ed25519", + "profile": "ed25519-canonical-signing-statement", + "statement_schema_version": "openadapt.qualification-evidence-signing-statement/v1", + "signature_domain": "OpenAdapt qualification revocation state receipt v1\u0000", + "projection": "canonical-object-minus-signature-and-signing-statement", + "encoding": "base64-rfc4648-padded", + "key_source": "active-qualification-signer-registry-v2" + } + ], + "message_signature": { + "profile": "sigstore-message-signature", + "tool": "cosign", + "version": "3.1.3", + "runner": "ubuntu-24.04", + "architecture": "linux-amd64", + "installer_action": "sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6", + "url": "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/sigstore/cosign/releases/download/v3.1.3/cosign-linux-amd64", + "sha256": "sha256:4629c757b7618056f8ddd7e2625ae9fdd94c0372a65049520bc7d9df9efc7f71", + "size_bytes": 141178250, + "trusted_root": { + "repository": "sigstore/root-signing", + "commit": "c6f23ff62645fb0c46ebc7945675835b52f91aa8", + "path": "targets/trusted_root.json", + "sha256": "sha256:6494e21ea73fa7ee769f85f57d5a3e6a08725eae1e38c755fc3517c9e6bc0b66", + "size_bytes": 6787 + }, + "signing_config": { + "repository": "sigstore/root-signing", + "commit": "c6f23ff62645fb0c46ebc7945675835b52f91aa8", + "path": "targets/signing_config.json", + "sha256": "sha256:d358c75d032833f4193500f5b01b5760409410558fac962c599439adbb268b0f", + "size_bytes": 219 + } + }, + "sigstore": { + "tool": "gh", + "version": "2.98.0", + "cert_oidc_issuer": "https://token.actions.githubusercontent.com", + "deny_self_hosted_runners": true, + "allow_public_good": false, + "archives": [ + {"os":"linux","arch":"amd64","url":"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/cli/cli/releases/download/v2.98.0/gh_2.98.0_linux_amd64.tar.gz","sha256":"sha256:3b8ac6b30336802fc1a858d7c084e11cdf24ac1a761ca90b68022d7d729208de"}, + {"os":"linux","arch":"arm64","url":"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/cli/cli/releases/download/v2.98.0/gh_2.98.0_linux_arm64.tar.gz","sha256":"sha256:cf689084f3a3618f7eae4a2420d335d74626d65f5e594b9828d125d69f800d86"}, + {"os":"macos","arch":"amd64","url":"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/cli/cli/releases/download/v2.98.0/gh_2.98.0_macOS_amd64.zip","sha256":"sha256:734c7bbd0bc56a3974500ee9aea74d60f0e5b89be09e92b9d9148939a3a1e0e6"}, + {"os":"macos","arch":"arm64","url":"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/cli/cli/releases/download/v2.98.0/gh_2.98.0_macOS_arm64.zip","sha256":"sha256:8cfb027cc5310675f2b830eac8f9865c1155a45ffcf9757f699fdd5a22046ca4"} + ], + "certificate_identities": [ + {"kind":"production-acceptance-manifest","bundle_profile":"github-attestation","issuer_repository":"OpenAdaptAI/openadapt-evals","certificate_identity":"/OpenAdaptAI/openadapt-evals/.github/workflows/issue-production-acceptance.yml@refs/heads/main"}, + {"kind":"production-acceptance-summary","bundle_profile":"github-attestation","issuer_repository":"OpenAdaptAI/openadapt-evals","certificate_identity":"/OpenAdaptAI/openadapt-evals/.github/workflows/issue-production-acceptance.yml@refs/heads/main"}, + {"kind":"production-current-default","bundle_profile":"github-attestation","issuer_repository":"OpenAdaptAI/openadapt-ops","certificate_identity":"/OpenAdaptAI/openadapt-ops/.github/workflows/production-current-default.yml@refs/heads/main"}, + {"kind":"production-lifecycle-checkpoint","bundle_profile":"github-attestation","issuer_repository":"OpenAdaptAI/openadapt-ops","certificate_identity":"/OpenAdaptAI/openadapt-ops/.github/workflows/production-lifecycle-checkpoint.yml@refs/heads/main"}, + {"kind":"qualification-admission","bundle_profile":"github-attestation","issuer_repository":"OpenAdaptAI/.github","certificate_identity":"/OpenAdaptAI/.github/.github/workflows/issue-qualification-admission.yml@refs/heads/main"}, + {"kind":"qualification-authority-state-receipt","bundle_profile":"github-attestation","issuer_repository":"OpenAdaptAI/openadapt-ops","certificate_identity":"/OpenAdaptAI/openadapt-ops/.github/workflows/qualification-authority-state.yml@refs/heads/main"}, + {"kind":"qualification-evidence-decision-receipt","bundle_profile":"sigstore-message-signature","issuer_repository":"OpenAdaptAI/openadapt-internal","certificate_identity":"/OpenAdaptAI/openadapt-internal/.github/workflows/issue-private-qualification-evidence-decision.yml@refs/heads/main"}, + {"kind":"qualification-release","bundle_profile":"github-attestation","issuer_repository":"OpenAdaptAI/.github","certificate_identity":"/OpenAdaptAI/.github/.github/workflows/issue-production-release-admission.yml@refs/heads/main"}, + {"kind":"qualification-revocation-state-receipt","bundle_profile":"github-attestation","issuer_repository":"OpenAdaptAI/openadapt-ops","certificate_identity":"/OpenAdaptAI/openadapt-ops/.github/workflows/qualification-revocation-state.yml@refs/heads/main"}, + {"kind":"support-release-admission","bundle_profile":"github-attestation","issuer_repository":"OpenAdaptAI/.github","certificate_identity":"/OpenAdaptAI/.github/.github/workflows/issue-support-release-admission.yml@refs/heads/main"} + ] + }, + "public_trust_dsse": { + "profile": "aws-kms-p256-dsse-v1", + "bundle_media_type": "application/vnd.dev.sigstore.bundle.v0.3+json", + "statement_schema_version": "openadapt.production-public-trust-signing-statement/v1", + "statement_media_type": "application/vnd.openadapt.production-public-trust-signing-statement+json;version=1", + "signature_encoding": "asn1-der-low-s-base64-rfc4648-padded", + "key_origin": "aws-kms", + "aws_account_id": "992382684924", + "aws_region": "us-east-1", + "kms_key_spec": "ECC_NIST_P256", + "kms_signing_algorithm": "ECDSA_SHA_256", + "kms_message_type": "DIGEST", + "role_arn": "arn:aws:iam::992382684924:role/openadapt-public-trust-signer", + "signing_workflow": "/OpenAdaptAI/.github/.github/workflows/sign-production-evidence.yml@refs/heads/main", + "signing_environment": "public-trust-signing", + "key_source": "active-qualification-signer-registry-v2", + "offline_verification_required": true, + "alias_allowed": false, + "activation_state": "inactive-until-all-consumers-pin-exact-verifier", + "allowed_kinds": [ + "production-acceptance-manifest", + "production-acceptance-summary", + "production-cloud-deploy-authorization", + "production-cloud-deployment-result", + "production-current-default", + "production-deployment-observation", + "production-lifecycle-checkpoint", + "qualification-admission", + "qualification-authority-state-receipt", + "qualification-campaign-permit", + "qualification-campaign-permit-policy", + "qualification-campaign-permit-receipt", + "qualification-campaign-permit-request", + "qualification-evidence-decision-receipt", + "qualification-release", + "qualification-revocation-state-receipt", + "support-release-admission" + ] + } +} diff --git a/production-lifecycle-policy.json b/production-lifecycle-policy.json index dbd02cf..f510dbd 100644 --- a/production-lifecycle-policy.json +++ b/production-lifecycle-policy.json @@ -1,121 +1,22 @@ { "$schema": "schemas/production-lifecycle-policy.schema.json", - "schema_version": "openadapt.production-lifecycle-policy/v1", - "revision": 1, - "maximum_admission_days": 30, - "summary_authority": { - "repository": "OpenAdaptAI/openadapt-evals", - "workflow": ".github/workflows/production-lifecycle-evidence.yml", - "source_ref": "refs/heads/main", - "oidc_issuer": "https://token.actions.githubusercontent.com", - "certificate_identity": "/OpenAdaptAI/openadapt-evals/.github/workflows/production-lifecycle-evidence.yml@refs/heads/main", - "summary_schema_version": "openadapt.production-lifecycle-evidence-summary/v1", - "private_certificate_schema_version": "openadapt.execute-live-acceptance-record/v2", - "evidence_manifest_schema_version": "openadapt.production-acceptance/v1", - "acceptance_policy_sha256": "sha256:9b1fe55bc6796ae0a46960ca4aa335d88de60b0562c383afa1e85fa0a0c204b8", - "release_identity_schema_version": "openadapt.monotonic-production-release/v1", - "production_channel": "production", - "signer_provenance_digest_domain": "OpenAdapt production certificate signer provenance v1\u0000" - }, + "schema_version": "openadapt.production-lifecycle-policy/v2", + "revision": 2, + "maximum_release_admission_days": 30, + "maximum_workflow_admission_days": 7, + "object_reference_schema_version": "openadapt.production-evidence-object-reference/v2", + "release_admission_schema_version": "openadapt.qualification-release/v1", + "workflow_admission_schema_version": "openadapt.qualification-admission/v3", + "lifecycle_checkpoint_schema_version": "openadapt.production-lifecycle-checkpoint/v1", + "lifecycle_feed_schema_version": "openadapt.production-lifecycle-feed/v1", + "lifecycle_feed_ref": "refs/heads/production-lifecycle-feed", "targets": [ - { - "id": "agent", - "display_name": "OpenAdapt Agent", - "lifecycle_scope": "repository", - "lifecycle_subject": "openadapt-agent", - "source_repository": "OpenAdaptAI/openadapt-agent", - "release_kind": "public_package", - "required_claim_scope": "qualified_agent_bridge_release", - "required_artifact_kinds": ["sdist", "wheel"], - "package_index_project": "openadapt-agent", - "artifact_authority_by_kind": {"sdist": "pypi", "wheel": "pypi"} - }, - { - "id": "capture", - "display_name": "OpenAdapt Capture", - "lifecycle_scope": "repository", - "lifecycle_subject": "openadapt-capture", - "source_repository": "OpenAdaptAI/openadapt-capture", - "release_kind": "public_package", - "required_claim_scope": "qualified_native_recorder_release", - "required_artifact_kinds": ["sdist", "wheel"], - "package_index_project": "openadapt-capture", - "artifact_authority_by_kind": {"sdist": "pypi", "wheel": "pypi"} - }, - { - "id": "cloud", - "display_name": "OpenAdapt Cloud", - "lifecycle_scope": "repository", - "lifecycle_subject": "openadapt-cloud", - "source_repository": "OpenAdaptAI/openadapt-cloud", - "release_kind": "private_deployment", - "required_claim_scope": "qualified_workflow_control_plane_deployment", - "required_artifact_kinds": [], - "package_index_project": null, - "artifact_authority_by_kind": {} - }, - { - "id": "desktop", - "display_name": "OpenAdapt Desktop", - "lifecycle_scope": "repository", - "lifecycle_subject": "openadapt-desktop", - "source_repository": "OpenAdaptAI/openadapt-desktop", - "release_kind": "public_package", - "required_claim_scope": "qualified_native_workflow_desktop_release", - "required_artifact_kinds": [ - "linux-installer", - "macos-installer", - "sdist", - "wheel", - "windows-installer" - ], - "package_index_project": "openadapt-desktop", - "artifact_authority_by_kind": { - "linux-installer": "github_release", - "macos-installer": "github_release", - "sdist": "pypi", - "wheel": "pypi", - "windows-installer": "github_release" - } - }, - { - "id": "docs", - "display_name": "OpenAdapt Documentation", - "lifecycle_scope": "public_surface", - "lifecycle_subject": "docs.openadapt.ai", - "source_repository": "OpenAdaptAI/openadapt-ops", - "release_kind": "public_deployment", - "required_claim_scope": "production_documentation_deployment", - "required_artifact_kinds": ["deployment-manifest", "site-archive"], - "package_index_project": null, - "artifact_authority_by_kind": { - "deployment-manifest": "managed_evidence", - "site-archive": "managed_evidence" - } - }, - { - "id": "flow", - "display_name": "OpenAdapt Flow", - "lifecycle_scope": "repository", - "lifecycle_subject": "openadapt-flow", - "source_repository": "OpenAdaptAI/openadapt-flow", - "release_kind": "public_package", - "required_claim_scope": "qualified_workflow_runtime_release", - "required_artifact_kinds": ["sdist", "wheel"], - "package_index_project": "openadapt-flow", - "artifact_authority_by_kind": {"sdist": "pypi", "wheel": "pypi"} - }, - { - "id": "openadapt", - "display_name": "OpenAdapt", - "lifecycle_scope": "repository", - "lifecycle_subject": "OpenAdapt", - "source_repository": "OpenAdaptAI/OpenAdapt", - "release_kind": "public_package", - "required_claim_scope": "qualified_workflow_launcher_release", - "required_artifact_kinds": ["sdist", "wheel"], - "package_index_project": "openadapt", - "artifact_authority_by_kind": {"sdist": "pypi", "wheel": "pypi"} - } + {"id":"agent","display_name":"OpenAdapt Agent","source_repository":"OpenAdaptAI/openadapt-agent","source_repository_id":"1136136670","release_kind":"package","claim_scope":"production_agent","required_artifact_kinds":["python-sdist","python-wheel"],"package_index_project":"openadapt-agent"}, + {"id":"capture","display_name":"OpenAdapt Capture","source_repository":"OpenAdaptAI/openadapt-capture","source_repository_id":"1115283835","release_kind":"package","claim_scope":"production_capture","required_artifact_kinds":["chrome-extension-zip","python-sdist","python-wheel","spdx-sbom"],"package_index_project":"openadapt-capture"}, + {"id":"cloud","display_name":"OpenAdapt Cloud","source_repository":"OpenAdaptAI/openadapt-cloud","source_repository_id":"1300570990","release_kind":"deployment","claim_scope":"production_cloud","required_artifact_kinds":["deployment-manifest"],"package_index_project":null}, + {"id":"desktop","display_name":"OpenAdapt Desktop","source_repository":"OpenAdaptAI/openadapt-desktop","source_repository_id":"1171291730","release_kind":"hybrid","claim_scope":"production_desktop","required_artifact_kinds":["linux-appimage","linux-deb","macos-dmg-arm64","macos-dmg-x86-64","python-sdist","python-wheel","windows-msi","windows-nsis"],"package_index_project":"openadapt-desktop"}, + {"id":"docs","display_name":"OpenAdapt Documentation","source_repository":"OpenAdaptAI/openadapt-ops","source_repository_id":"1172011294","release_kind":"deployment","claim_scope":"production_docs","required_artifact_kinds":["deployment-manifest","site-archive"],"package_index_project":null}, + {"id":"flow","display_name":"OpenAdapt Flow","source_repository":"OpenAdaptAI/openadapt-flow","source_repository_id":"1291376938","release_kind":"package","claim_scope":"production_flow","required_artifact_kinds":["python-sdist","python-wheel"],"package_index_project":"openadapt-flow"}, + {"id":"openadapt","display_name":"OpenAdapt","source_repository":"OpenAdaptAI/OpenAdapt","source_repository_id":"627024850","release_kind":"package","claim_scope":"production_openadapt","required_artifact_kinds":["python-sdist","python-wheel"],"package_index_project":"openadapt"} ] } diff --git a/production-release-authority-policy.json b/production-release-authority-policy.json new file mode 100644 index 0000000..d79fa8e --- /dev/null +++ b/production-release-authority-policy.json @@ -0,0 +1 @@ +{"$schema":"schemas/production-release-authority-policy.schema.json","schema_version":"openadapt.production-release-authority-policy/v1","release_app":{"app_id":"4730708","app_slug":"openadapt-release","installation_id":"156835568","bot_user_id":"321543906","bot_login":"openadapt-release[bot]"},"repository_selection":"selected","permissions":{"administration":"read","contents":"write","metadata":"read"},"repositories":[{"target":"agent","repository":"OpenAdaptAI/openadapt-agent","repository_id":"1136136670"},{"target":"capture","repository":"OpenAdaptAI/openadapt-capture","repository_id":"1115283835"},{"target":"cloud","repository":"OpenAdaptAI/openadapt-cloud","repository_id":"1300570990"},{"target":"desktop","repository":"OpenAdaptAI/openadapt-desktop","repository_id":"1171291730"},{"target":"docs","repository":"OpenAdaptAI/openadapt-ops","repository_id":"1172011294"},{"target":"flow","repository":"OpenAdaptAI/openadapt-flow","repository_id":"1291376938"},{"target":"openadapt","repository":"OpenAdaptAI/OpenAdapt","repository_id":"627024850"}],"tag_rulesets":{"creation_authority":"OpenAdapt policy: release tag creation","immutability":"OpenAdapt policy: immutable release tags"},"support_release_authorities":[{"id":"desktop-managed-ffmpeg-runtime","lifecycle":"Support","repository":"OpenAdaptAI/openadapt-desktop","repository_id":"1171291730","tag_pattern":"refs/tags/ffmpeg-runtime-v*","creation_ruleset":"OpenAdapt policy: FFmpeg runtime tag creation","immutability_ruleset":"OpenAdapt policy: immutable FFmpeg runtime tags"}]} diff --git a/qualification-trial-receipt-authority-policy.json b/qualification-trial-receipt-authority-policy.json new file mode 100644 index 0000000..e1644cf --- /dev/null +++ b/qualification-trial-receipt-authority-policy.json @@ -0,0 +1 @@ +{"$schema":"schemas/qualification-trial-receipt-authority-policy.schema.json","schema_version":"openadapt.qualification-trial-receipt-authority-policy/v1","repository":"OpenAdaptAI/openadapt-evals","repository_id":"1135998197","repository_owner_id":"132681217","workflow":".github/workflows/issue-qualification-trial-receipts.yml","ref":"refs/heads/main","trigger":"workflow_dispatch","input":{"name":"campaign_request_handle","schema_version":"openadapt.opaque-one-use-campaign-request-handle/v1","pattern":"^qcrh_[A-Za-z0-9_-]{43}$","atomically_consumed":true},"signing":{"algorithm":"ed25519","signer_registry_usage":"qualification-trial-receipt","distinct_key_per_receipt_type":true,"outer_sigstore_bundle":false},"authorities":[{"receipt_type":"runner","environment":"qualification-trial-runner","runner_labels":["self-hosted","evidence-authority","qualification-trial-runner"]},{"receipt_type":"observer","environment":"qualification-trial-observer","runner_labels":["self-hosted","evidence-authority","qualification-trial-observer"]},{"receipt_type":"delivery","environment":"qualification-trial-delivery","runner_labels":["self-hosted","evidence-authority","qualification-trial-delivery"]},{"receipt_type":"decision","environment":"qualification-trial-decision","runner_labels":["self-hosted","evidence-authority","qualification-trial-decision"]},{"receipt_type":"policy","environment":"qualification-trial-policy","runner_labels":["self-hosted","evidence-authority","qualification-trial-policy"]},{"receipt_type":"repair","environment":"qualification-trial-repair","runner_labels":["self-hosted","evidence-authority","qualification-trial-repair"]},{"receipt_type":"cleanup","environment":"qualification-trial-cleanup","runner_labels":["self-hosted","evidence-authority","qualification-trial-cleanup"]},{"receipt_type":"cleanup_absence","environment":"qualification-trial-cleanup-absence","runner_labels":["self-hosted","evidence-authority","qualification-trial-cleanup-absence"]}]} diff --git a/schemas/evidence-registry.schema.json b/schemas/evidence-registry.schema.json index 3628351..047ef46 100644 --- a/schemas/evidence-registry.schema.json +++ b/schemas/evidence-registry.schema.json @@ -1,97 +1,82 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://openadapt.ai/schemas/evidence-registry.schema.json", - "title": "OpenAdapt production evidence registry", - "description": "Content-addressed, append-only, hash-chained registry of remote-safe acceptance evidence references. Entries bind an HTTPS URL to its exact SHA-256 digest; each entry commits to the digest of its predecessor.", + "$id": "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/OpenAdaptAI/.github/blob/main/schemas/evidence-registry.schema.json", + "title": "OpenAdapt production evidence registry v2", "type": "object", "additionalProperties": false, "required": [ - "$schema", - "schema_version", - "head_entry_sha256", - "entries" + "$schema", "schema_version", "repository", "repository_id", + "repository_owner_id", "revision", "previous_registry_head_sha256", + "registry_head_sha256", "signer_registry", "signer_registry_history", "entries" ], "properties": { - "$schema": { - "const": "schemas/evidence-registry.schema.json" - }, - "schema_version": { - "const": "openadapt.production-evidence-registry/v1" - }, - "head_entry_sha256": { - "description": "Chained digest of the last entry, or null while the registry is empty.", + "$schema": {"const": "schemas/evidence-registry.schema.json"}, + "schema_version": {"const": "openadapt.production-evidence-registry/v2"}, + "repository": {"const": "OpenAdaptAI/.github"}, + "repository_id": {"const": "858454062"}, + "repository_owner_id": {"const": "132681217"}, + "revision": {"type": "integer", "minimum": 1}, + "previous_registry_head_sha256": {"oneOf": [{"$ref": "#/$defs/digest"}, {"type": "null"}]}, + "registry_head_sha256": {"$ref": "#/$defs/digest"}, + "signer_registry": { "oneOf": [ + {"type": "null"}, { - "type": "null" - }, - { - "type": "string", - "pattern": "^sha256:[0-9a-f]{64}$" + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "object_path", "object_sha256", "registry_identity_sha256", "registry_revision"], + "properties": { + "schema_version": {"const": "openadapt.qualification-signer-registry-pointer/v1"}, + "object_path": {"type": "string", "pattern": "^production-evidence/signer-registries/sha256/[0-9a-f]{2}/[0-9a-f]{64}\\.qualification-signer-registry\\.json$"}, + "object_sha256": {"$ref": "#/$defs/digest"}, + "registry_identity_sha256": {"$ref": "#/$defs/digest"}, + "registry_revision": {"type": "integer", "minimum": 1} + } } ] }, + "signer_registry_history": { + "type": "array", + "uniqueItems": true, + "items": {"$ref": "#/$defs/signer_registry_pointer"} + }, "entries": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": [ - "sequence", - "kind", - "url", - "sha256", - "size_bytes", - "recorded_at", - "prior_entry_sha256", - "entry_sha256" + "registry_entry_sha256", "kind", "object_schema_version", "object_path", + "object_sha256", "size_bytes", "object_media_type", + "semantic_identity_sha256", "subject_sha256" ], "properties": { - "sequence": { - "type": "integer", - "minimum": 1 - }, - "kind": { - "enum": [ - "evidence-summary", - "attestation-bundle", - "evidence-manifest", - "acceptance-run-record" - ] - }, - "url": { - "type": "string", - "maxLength": 2048, - "pattern": "^https://" - }, - "sha256": { - "type": "string", - "pattern": "^sha256:[0-9a-f]{64}$" - }, - "size_bytes": { - "type": "integer", - "minimum": 1 - }, - "recorded_at": { - "type": "string", - "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3}Z$" - }, - "prior_entry_sha256": { - "oneOf": [ - { - "type": "null" - }, - { - "type": "string", - "pattern": "^sha256:[0-9a-f]{64}$" - } - ] - }, - "entry_sha256": { - "type": "string", - "pattern": "^sha256:[0-9a-f]{64}$" - } + "registry_entry_sha256": {"$ref": "#/$defs/digest"}, + "kind": {"type": "string", "pattern": "^[a-z][a-z0-9-]{1,95}$"}, + "object_schema_version": {"type": "string", "minLength": 1, "maxLength": 160}, + "object_path": {"type": "string", "pattern": "^production-evidence/objects/sha256/[0-9a-f]{2}/[0-9a-f]{64}\\.[a-z][a-z0-9-]{1,95}\\.json$"}, + "object_sha256": {"$ref": "#/$defs/digest"}, + "size_bytes": {"type": "integer", "minimum": 1}, + "object_media_type": {"type": "string", "minLength": 1, "maxLength": 200}, + "semantic_identity_sha256": {"$ref": "#/$defs/digest"}, + "subject_sha256": {"oneOf": [{"$ref": "#/$defs/digest"}, {"type": "null"}]} } } } + }, + "$defs": { + "digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "signer_registry_pointer": { + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "object_path", "object_sha256", "registry_identity_sha256", "registry_revision"], + "properties": { + "schema_version": {"const": "openadapt.qualification-signer-registry-pointer/v1"}, + "object_path": {"type": "string", "pattern": "^production-evidence/signer-registries/sha256/[0-9a-f]{2}/[0-9a-f]{64}\\.qualification-signer-registry\\.json$"}, + "object_sha256": {"$ref": "#/$defs/digest"}, + "registry_identity_sha256": {"$ref": "#/$defs/digest"}, + "registry_revision": {"type": "integer", "minimum": 1} + } + } } } diff --git a/schemas/production-cloud-deploy-authorization.schema.json b/schemas/production-cloud-deploy-authorization.schema.json new file mode 100644 index 0000000..cbedb9d --- /dev/null +++ b/schemas/production-cloud-deploy-authorization.schema.json @@ -0,0 +1,56 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "production-cloud-deploy-authorization.schema.json", + "title": "OpenAdapt production Cloud deploy authorization v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", "authorization_sha256", "deployment_intent_sha256", + "profile_repository", "profile_repository_id", "profile_commit", + "profile_workflow", "profile_run_id", "profile_run_attempt", + "cloud_source_commitment_sha256", "expected_live_attestation_sha256", + "provider_idempotency_key", "audience", "signer_registry_sha256", + "revocation_state_sha256", "issued_at", "not_before", "expires_at", "issuer" + ], + "properties": { + "schema_version": {"const": "openadapt.production-cloud-deploy-authorization/v1"}, + "authorization_sha256": {"$ref": "#/$defs/digest"}, + "deployment_intent_sha256": {"$ref": "#/$defs/digest"}, + "profile_repository": {"type": "string", "pattern": "^OpenAdaptAI/[A-Za-z0-9._-]+$"}, + "profile_repository_id": {"$ref": "#/$defs/decimal_id"}, + "profile_commit": {"$ref": "#/$defs/commit"}, + "profile_workflow": {"type": "string", "pattern": "^\\.github/workflows/[A-Za-z0-9._-]+\\.ya?ml$"}, + "profile_run_id": {"$ref": "#/$defs/decimal_id"}, + "profile_run_attempt": {"type": "integer", "const": 1}, + "cloud_source_commitment_sha256": {"$ref": "#/$defs/digest"}, + "expected_live_attestation_sha256": {"$ref": "#/$defs/digest"}, + "provider_idempotency_key": {"type": "string", "pattern": "^cloud-deploy:[0-9a-f]{64}$"}, + "audience": {"const": "openadapt-public-cloud-source-proof"}, + "signer_registry_sha256": {"$ref": "#/$defs/digest"}, + "revocation_state_sha256": {"$ref": "#/$defs/digest"}, + "issued_at": {"$ref": "#/$defs/timestamp"}, + "not_before": {"$ref": "#/$defs/timestamp"}, + "expires_at": {"$ref": "#/$defs/timestamp"}, + "issuer": {"$ref": "#/$defs/issuer"} + }, + "$defs": { + "digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "decimal_id": {"type": "string", "pattern": "^[1-9][0-9]*$"}, + "commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "timestamp": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"}, + "issuer": { + "type": "object", + "additionalProperties": false, + "required": ["repository", "repository_id", "repository_owner_id", "workflow", "ref", "source_commit", "environment"], + "properties": { + "repository": {"const": "OpenAdaptAI/openadapt-ops"}, + "repository_id": {"const": "1172011294"}, + "repository_owner_id": {"const": "132681217"}, + "workflow": {"const": ".github/workflows/production-cloud-deploy-authorization.yml"}, + "ref": {"const": "refs/heads/main"}, + "source_commit": {"$ref": "#/$defs/commit"}, + "environment": {"const": "production-cloud-deploy-authorization"} + } + } + } +} diff --git a/schemas/production-cloud-deployment-result.schema.json b/schemas/production-cloud-deployment-result.schema.json new file mode 100644 index 0000000..1b0b754 --- /dev/null +++ b/schemas/production-cloud-deployment-result.schema.json @@ -0,0 +1,205 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "production-cloud-deployment-result.schema.json", + "title": "OpenAdapt production Cloud deployment handoff v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", "handoff_id_sha256", "authorization_reference", + "authorization_bundle_reference", "authorization_sha256", + "cloud_source_commitment_sha256", "profile_commit", "profile_run_id", + "profile_run_attempt", "source_proof_request", "source_proof_request_sha256", + "source_proof_response", "source_proof_response_sha256", + "reviewed_public_values_sha256", + "expected_live_attestation_sha256", "live_attestation_sha256", + "provider_idempotency_key", "audience", "signer_registry_sha256", + "revocation_state_sha256", "verdict", "issued_at", "not_before", + "expires_at", "issuer" + ], + "properties": { + "schema_version": {"const": "openadapt.production-cloud-deployment-result/v1"}, + "handoff_id_sha256": {"$ref": "#/$defs/digest"}, + "authorization_reference": {"$ref": "#/$defs/authorization_reference"}, + "authorization_bundle_reference": {"$ref": "#/$defs/authorization_bundle_reference"}, + "authorization_sha256": {"$ref": "#/$defs/digest"}, + "cloud_source_commitment_sha256": {"$ref": "#/$defs/digest"}, + "profile_commit": {"$ref": "#/$defs/commit"}, + "profile_run_id": {"$ref": "#/$defs/decimal_id"}, + "profile_run_attempt": {"const": "1"}, + "source_proof_request": {"$ref": "#/$defs/source_proof_request"}, + "source_proof_request_sha256": {"$ref": "#/$defs/digest"}, + "source_proof_response": {"$ref": "#/$defs/source_proof_response"}, + "source_proof_response_sha256": {"$ref": "#/$defs/digest"}, + "reviewed_public_values_sha256": {"$ref": "#/$defs/digest"}, + "expected_live_attestation_sha256": {"$ref": "#/$defs/digest"}, + "live_attestation_sha256": {"$ref": "#/$defs/digest"}, + "provider_idempotency_key": {"type": "string", "pattern": "^cloud-deploy:[0-9a-f]{64}$"}, + "audience": {"const": "openadapt-private-cloud-production-deploy"}, + "signer_registry_sha256": {"$ref": "#/$defs/digest"}, + "revocation_state_sha256": {"$ref": "#/$defs/digest"}, + "verdict": {"const": "accepted"}, + "issued_at": {"$ref": "#/$defs/timestamp"}, + "not_before": {"$ref": "#/$defs/timestamp"}, + "expires_at": {"$ref": "#/$defs/timestamp"}, + "issuer": {"$ref": "#/$defs/issuer"} + }, + "$defs": { + "digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "raw_digest": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, + "decimal_id": {"type": "string", "pattern": "^[1-9][0-9]*$"}, + "commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "timestamp": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"}, + "source_proof_request": { + "type": "object", + "additionalProperties": false, + "required": [ + "authorization_sha256", "cloud_source_commit", + "expected_live_attestation_sha256", "expires_at", "issued_at", "nonce", + "production_origin", "profile_commit", "profile_environment", + "profile_repository", "profile_workflow_ref", "run_attempt", "run_id", + "schema_version", "site_id", "source_names" + ], + "properties": { + "schema_version": {"const": "openadapt.production-secret-source-proof-request/v2"}, + "authorization_sha256": {"$ref": "#/$defs/raw_digest"}, + "cloud_source_commit": {"$ref": "#/$defs/commit"}, + "expected_live_attestation_sha256": {"$ref": "#/$defs/raw_digest"}, + "profile_commit": {"$ref": "#/$defs/commit"}, + "production_origin": {"const": "https://app.openadapt.ai"}, + "profile_environment": {"const": "production-cloud-deploy"}, + "profile_repository": {"const": "OpenAdaptAI/.github"}, + "profile_workflow_ref": {"const": "OpenAdaptAI/.github/.github/workflows/production-cloud-deploy.yml@refs/heads/main"}, + "run_id": {"$ref": "#/$defs/decimal_id"}, + "run_attempt": {"const": "1"}, + "site_id": {"const": "ccf1a48a-d934-48ea-a9a8-36cbddf27cb5"}, + "nonce": {"type": "string", "pattern": "^[A-Za-z0-9_-]{43}$"}, + "source_names": { + "const": [ + "BUSINESS_DECISION_ANSWER_HMAC_KEY", + "BUSINESS_DECISION_ANSWER_HMAC_KEY_ID", + "BUSINESS_DECISION_CLOUD_POLICY_HMAC_KEYS_JSON", + "BUSINESS_DECISION_PRINCIPAL_ALIAS_HMAC_KEY", + "BUSINESS_DECISION_QUALIFICATION_HMAC_KEYS_JSON", + "BUSINESS_DECISION_ROLE_MAPPING_HMAC_KEY", + "BUSINESS_DECISION_RUNNER_RECEIPT_HMAC_KEYS_JSON", + "BUSINESS_DECISION_SESSION_BINDING_HMAC_KEY", + "BUSINESS_DECISION_TASK_HMAC_KEYS_JSON", + "EXECUTE_ACCEPTANCE_CAMPAIGN_PROVISIONER_TOKEN", + "EXECUTE_ACCEPTANCE_OBSERVER_BEARER_TOKEN", + "EXECUTE_ACCEPTANCE_OBSERVER_SIGNING_PRIVATE_KEY", + "EXECUTE_ACCEPTANCE_TARGET_SIGNING_SECRET" + ] + }, + "issued_at": {"$ref": "#/$defs/timestamp"}, + "expires_at": {"$ref": "#/$defs/timestamp"} + } + }, + "source_proof_response": { + "type": "object", + "additionalProperties": false, + "required": [ + "authorization_sha256", "live_attestation_sha256", "live_deployment", + "oidc_identity_sha256", "production_origin", "proofs", "public_values", + "request_sha256", "schema_version", "site_id" + ], + "properties": { + "schema_version": {"const": "openadapt.production-secret-source-proof-response/v2"}, + "request_sha256": {"$ref": "#/$defs/raw_digest"}, + "authorization_sha256": {"$ref": "#/$defs/raw_digest"}, + "live_attestation_sha256": {"$ref": "#/$defs/raw_digest"}, + "live_deployment": {"$ref": "#/$defs/live_deployment"}, + "oidc_identity_sha256": {"$ref": "#/$defs/raw_digest"}, + "production_origin": {"const": "https://app.openadapt.ai"}, + "site_id": {"const": "ccf1a48a-d934-48ea-a9a8-36cbddf27cb5"}, + "proofs": { + "type": "object", + "additionalProperties": false, + "required": [ + "BUSINESS_DECISION_ANSWER_HMAC_KEY", + "BUSINESS_DECISION_CLOUD_POLICY_HMAC_KEYS_JSON", + "BUSINESS_DECISION_PRINCIPAL_ALIAS_HMAC_KEY", + "BUSINESS_DECISION_QUALIFICATION_HMAC_KEYS_JSON", + "BUSINESS_DECISION_ROLE_MAPPING_HMAC_KEY", + "BUSINESS_DECISION_RUNNER_RECEIPT_HMAC_KEYS_JSON", + "BUSINESS_DECISION_SESSION_BINDING_HMAC_KEY", + "BUSINESS_DECISION_TASK_HMAC_KEYS_JSON", + "EXECUTE_ACCEPTANCE_CAMPAIGN_PROVISIONER_TOKEN", + "EXECUTE_ACCEPTANCE_OBSERVER_BEARER_TOKEN", + "EXECUTE_ACCEPTANCE_OBSERVER_SIGNING_PRIVATE_KEY", + "EXECUTE_ACCEPTANCE_TARGET_SIGNING_SECRET" + ], + "properties": { + "BUSINESS_DECISION_ANSWER_HMAC_KEY": {"$ref": "#/$defs/raw_digest"}, + "BUSINESS_DECISION_CLOUD_POLICY_HMAC_KEYS_JSON": {"$ref": "#/$defs/raw_digest"}, + "BUSINESS_DECISION_PRINCIPAL_ALIAS_HMAC_KEY": {"$ref": "#/$defs/raw_digest"}, + "BUSINESS_DECISION_QUALIFICATION_HMAC_KEYS_JSON": {"$ref": "#/$defs/raw_digest"}, + "BUSINESS_DECISION_ROLE_MAPPING_HMAC_KEY": {"$ref": "#/$defs/raw_digest"}, + "BUSINESS_DECISION_RUNNER_RECEIPT_HMAC_KEYS_JSON": {"$ref": "#/$defs/raw_digest"}, + "BUSINESS_DECISION_SESSION_BINDING_HMAC_KEY": {"$ref": "#/$defs/raw_digest"}, + "BUSINESS_DECISION_TASK_HMAC_KEYS_JSON": {"$ref": "#/$defs/raw_digest"}, + "EXECUTE_ACCEPTANCE_CAMPAIGN_PROVISIONER_TOKEN": {"$ref": "#/$defs/raw_digest"}, + "EXECUTE_ACCEPTANCE_OBSERVER_BEARER_TOKEN": {"$ref": "#/$defs/raw_digest"}, + "EXECUTE_ACCEPTANCE_OBSERVER_SIGNING_PRIVATE_KEY": {"$ref": "#/$defs/raw_digest"}, + "EXECUTE_ACCEPTANCE_TARGET_SIGNING_SECRET": {"$ref": "#/$defs/raw_digest"} + } + }, + "public_values": { + "type": "object", + "additionalProperties": false, + "required": ["BUSINESS_DECISION_ANSWER_HMAC_KEY_ID"], + "properties": { + "BUSINESS_DECISION_ANSWER_HMAC_KEY_ID": { + "type": "string", + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$" + } + } + } + } + }, + "live_deployment": { + "type": "object", + "additionalProperties": false, + "required": ["build_id", "source_commit"], + "properties": { + "build_id": {"$ref": "#/$defs/digest"}, + "source_commit": {"$ref": "#/$defs/commit"} + } + }, + "authorization_reference": { + "allOf": [ + {"$ref": "production-evidence-object-reference.schema.json"}, + {"properties": { + "kind": {"const": "production-cloud-deploy-authorization"}, + "object_schema_version": {"const": "openadapt.production-cloud-deploy-authorization/v1"}, + "object_media_type": {"const": "application/vnd.openadapt.production-cloud-deploy-authorization+json;version=1"}, + "subject_sha256": {"type": "null"} + }} + ] + }, + "authorization_bundle_reference": { + "allOf": [ + {"$ref": "production-evidence-object-reference.schema.json"}, + {"properties": { + "kind": {"const": "production-cloud-deploy-authorization-sigstore-bundle"}, + "object_schema_version": {"const": "application/vnd.dev.sigstore.bundle.v0.3+json"}, + "object_media_type": {"const": "application/vnd.dev.sigstore.bundle.v0.3+json"}, + "subject_sha256": {"$ref": "#/$defs/digest"} + }} + ] + }, + "issuer": { + "type": "object", + "additionalProperties": false, + "required": ["repository", "repository_id", "repository_owner_id", "workflow", "ref", "source_commit", "environment"], + "properties": { + "repository": {"const": "OpenAdaptAI/.github"}, + "repository_id": {"const": "858454062"}, + "repository_owner_id": {"const": "132681217"}, + "workflow": {"const": ".github/workflows/production-cloud-deploy.yml"}, + "ref": {"const": "refs/heads/main"}, + "source_commit": {"$ref": "#/$defs/commit"}, + "environment": {"const": "production-cloud-deploy"} + } + } + } +} diff --git a/schemas/production-current-default.schema.json b/schemas/production-current-default.schema.json new file mode 100644 index 0000000..67a37e6 --- /dev/null +++ b/schemas/production-current-default.schema.json @@ -0,0 +1,76 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "production-current-default.schema.json", + "title": "OpenAdapt production current default v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "default_set_revision", + "previous_default_set_sha256", + "targets", + "issued_at", + "not_before", + "expires_at", + "issuer" + ], + "properties": { + "schema_version": {"const": "openadapt.production-current-default/v1"}, + "default_set_revision": {"type": "integer", "minimum": 1}, + "previous_default_set_sha256": {"oneOf": [{"$ref": "#/$defs/digest"}, {"type": "null"}]}, + "targets": { + "type": "array", + "minItems": 7, + "maxItems": 7, + "uniqueItems": true, + "items": {"$ref": "#/$defs/default_target"} + }, + "issued_at": {"$ref": "#/$defs/timestamp"}, + "not_before": {"$ref": "#/$defs/timestamp"}, + "expires_at": {"$ref": "#/$defs/timestamp"}, + "issuer": {"$ref": "#/$defs/issuer"} + }, + "$defs": { + "target": {"enum": ["agent", "capture", "cloud", "desktop", "docs", "flow", "openadapt"]}, + "digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "decimal_id": {"type": "string", "pattern": "^[1-9][0-9]*$"}, + "timestamp": {"type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"}, + "default_target": { + "type": "object", + "additionalProperties": false, + "required": [ + "target", "release_sha256", "artifact_inventory_sha256", "version", "tag", + "deployment_id", "deployment_sha256", "qualification_release_sha256", "default_identity_sha256" + ], + "properties": { + "target": {"$ref": "#/$defs/target"}, + "release_sha256": {"$ref": "#/$defs/digest"}, + "artifact_inventory_sha256": {"$ref": "#/$defs/digest"}, + "version": {"oneOf": [{"type": "string", "minLength": 1, "maxLength": 80}, {"type": "null"}]}, + "tag": {"oneOf": [{"type": "string", "minLength": 1, "maxLength": 120}, {"type": "null"}]}, + "deployment_id": {"oneOf": [{"$ref": "#/$defs/decimal_id"}, {"type": "null"}]}, + "deployment_sha256": {"oneOf": [{"$ref": "#/$defs/digest"}, {"type": "null"}]}, + "qualification_release_sha256": {"$ref": "#/$defs/digest"}, + "default_identity_sha256": {"$ref": "#/$defs/digest"} + } + }, + "issuer": { + "type": "object", + "additionalProperties": false, + "required": [ + "repository", "repository_id", "repository_owner_id", "workflow", + "ref", "source_commit", "environment" + ], + "properties": { + "repository": {"const": "OpenAdaptAI/openadapt-ops"}, + "repository_id": {"const": "1172011294"}, + "repository_owner_id": {"const": "132681217"}, + "workflow": {"const": ".github/workflows/production-current-default.yml"}, + "ref": {"const": "refs/heads/main"}, + "source_commit": {"$ref": "#/$defs/commit"}, + "environment": {"const": "production-current-default"} + } + } + } +} diff --git a/schemas/production-evidence-object-reference.schema.json b/schemas/production-evidence-object-reference.schema.json new file mode 100644 index 0000000..54806b0 --- /dev/null +++ b/schemas/production-evidence-object-reference.schema.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "production-evidence-object-reference.schema.json", + "title": "OpenAdapt production evidence object reference v2", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "repository", + "repository_id", + "repository_owner_id", + "registry_source_commit", + "registry_revision", + "registry_head_sha256", + "registry_entry_sha256", + "kind", + "object_schema_version", + "object_path", + "object_sha256", + "size_bytes", + "object_media_type", + "semantic_identity_sha256", + "subject_sha256" + ], + "properties": { + "schema_version": {"const": "openadapt.production-evidence-object-reference/v2"}, + "repository": {"const": "OpenAdaptAI/.github"}, + "repository_id": {"const": "858454062"}, + "repository_owner_id": {"const": "132681217"}, + "registry_source_commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "registry_revision": {"type": "integer", "minimum": 1}, + "registry_head_sha256": {"$ref": "#/$defs/digest"}, + "registry_entry_sha256": {"$ref": "#/$defs/digest"}, + "kind": {"type": "string", "pattern": "^[a-z][a-z0-9-]{1,95}$"}, + "object_schema_version": {"type": "string", "minLength": 1, "maxLength": 160}, + "object_path": { + "type": "string", + "pattern": "^production-evidence/objects/sha256/[0-9a-f]{2}/[0-9a-f]{64}\\.[a-z][a-z0-9-]{1,95}\\.json$" + }, + "object_sha256": {"$ref": "#/$defs/digest"}, + "size_bytes": {"type": "integer", "minimum": 1}, + "object_media_type": {"type": "string", "minLength": 1, "maxLength": 200}, + "semantic_identity_sha256": {"$ref": "#/$defs/digest"}, + "subject_sha256": { + "oneOf": [ + {"$ref": "#/$defs/digest"}, + {"type": "null"} + ] + } + }, + "$defs": { + "digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"} + } +} diff --git a/schemas/production-evidence-verification-policy.schema.json b/schemas/production-evidence-verification-policy.schema.json new file mode 100644 index 0000000..7259d7e --- /dev/null +++ b/schemas/production-evidence-verification-policy.schema.json @@ -0,0 +1,225 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "production-evidence-verification-policy.schema.json", + "title": "OpenAdapt production evidence verification policy v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "repository", + "repository_id", + "repository_owner_id", + "reference_schema_version", + "transport", + "signer_registry_schema_version", + "embedded_signatures", + "message_signature", + "sigstore", + "public_trust_dsse" + ], + "properties": { + "schema_version": {"const": "openadapt.production-evidence-verification-policy/v1"}, + "repository": {"const": "OpenAdaptAI/.github"}, + "repository_id": {"const": "858454062"}, + "repository_owner_id": {"const": "132681217"}, + "reference_schema_version": {"const": "openadapt.production-evidence-object-reference/v2"}, + "transport": {"const": "exact-commit-raw-github"}, + "signer_registry_schema_version": {"const": "openadapt.qualification-signer-registry/v2"}, + "embedded_signatures": { + "type": "array", + "minItems": 3, + "maxItems": 3, + "uniqueItems": true, + "items": {"$ref": "#/$defs/embedded_signature"} + }, + "message_signature": {"$ref": "#/$defs/message_signature"}, + "sigstore": {"$ref": "#/$defs/sigstore"}, + "public_trust_dsse": {"$ref": "#/$defs/public_trust_dsse"} + }, + "$defs": { + "digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "archive": { + "type": "object", + "additionalProperties": false, + "required": ["os", "arch", "url", "sha256"], + "properties": { + "os": {"enum": ["linux", "macos"]}, + "arch": {"enum": ["amd64", "arm64"]}, + "url": {"type": "string", "pattern": "^https://github\\.com/cli/cli/releases/download/v[0-9]+\\.[0-9]+\\.[0-9]+/gh_"}, + "sha256": {"$ref": "#/$defs/digest"} + } + }, + "certificate_identity": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "bundle_profile", "issuer_repository", "certificate_identity"], + "properties": { + "kind": {"type": "string", "pattern": "^[a-z][a-z0-9-]*$"}, + "bundle_profile": {"enum": ["github-attestation", "sigstore-message-signature"]}, + "issuer_repository": {"type": "string", "pattern": "^OpenAdaptAI/[A-Za-z0-9._-]+$"}, + "certificate_identity": {"type": "string", "pattern": "^https://github\\.com/OpenAdaptAI/[A-Za-z0-9._-]+/\\.github/workflows/[A-Za-z0-9._-]+\\.yml@refs/heads/main$"} + } + }, + "pinned_document": { + "type": "object", + "additionalProperties": false, + "required": ["repository", "commit", "path", "sha256", "size_bytes"], + "properties": { + "repository": {"const": "sigstore/root-signing"}, + "commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "path": {"enum": ["targets/trusted_root.json", "targets/signing_config.json"]}, + "sha256": {"$ref": "#/$defs/digest"}, + "size_bytes": {"type": "integer", "minimum": 1} + } + }, + "embedded_signature": { + "type": "object", + "additionalProperties": false, + "required": [ + "kind", "algorithm", "profile", "statement_schema_version", + "signature_domain", "projection", "encoding", "key_source" + ], + "properties": { + "kind": { + "enum": [ + "qualification-authority-state-receipt", + "qualification-evidence-decision-receipt", + "qualification-revocation-state-receipt" + ] + }, + "algorithm": {"const": "ed25519"}, + "profile": {"const": "ed25519-canonical-signing-statement"}, + "statement_schema_version": { + "const": "openadapt.qualification-evidence-signing-statement/v1" + }, + "signature_domain": { + "enum": [ + "OpenAdapt qualification authority state receipt v2\u0000", + "OpenAdapt qualification evidence decision receipt v1\u0000", + "OpenAdapt qualification revocation state receipt v1\u0000" + ] + }, + "projection": { + "const": "canonical-object-minus-signature-and-signing-statement" + }, + "encoding": {"const": "base64-rfc4648-padded"}, + "key_source": {"const": "active-qualification-signer-registry-v2"} + } + }, + "message_signature": { + "type": "object", + "additionalProperties": false, + "required": [ + "profile", "tool", "version", "runner", "architecture", "installer_action", "url", + "sha256", "size_bytes", "trusted_root", "signing_config" + ], + "properties": { + "profile": {"const": "sigstore-message-signature"}, + "tool": {"const": "cosign"}, + "version": {"const": "3.1.3"}, + "runner": {"const": "ubuntu-24.04"}, + "architecture": {"const": "linux-amd64"}, + "installer_action": {"const": "sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6"}, + "url": {"const": "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/sigstore/cosign/releases/download/v3.1.3/cosign-linux-amd64"}, + "sha256": {"$ref": "#/$defs/digest"}, + "size_bytes": {"type": "integer", "minimum": 1}, + "trusted_root": {"$ref": "#/$defs/pinned_document"}, + "signing_config": {"$ref": "#/$defs/pinned_document"} + } + }, + "public_trust_dsse": { + "type": "object", + "additionalProperties": false, + "required": [ + "profile", "bundle_media_type", "statement_schema_version", + "statement_media_type", "signature_encoding", "key_origin", + "aws_account_id", "aws_region", "kms_key_spec", + "kms_signing_algorithm", "kms_message_type", "role_arn", + "signing_workflow", "signing_environment", "key_source", + "offline_verification_required", "alias_allowed", "activation_state", + "allowed_kinds" + ], + "properties": { + "profile": {"const": "aws-kms-p256-dsse-v1"}, + "bundle_media_type": {"const": "application/vnd.dev.sigstore.bundle.v0.3+json"}, + "statement_schema_version": {"const": "openadapt.production-public-trust-signing-statement/v1"}, + "statement_media_type": {"const": "application/vnd.openadapt.production-public-trust-signing-statement+json;version=1"}, + "signature_encoding": {"const": "asn1-der-low-s-base64-rfc4648-padded"}, + "key_origin": {"const": "aws-kms"}, + "aws_account_id": {"const": "992382684924"}, + "aws_region": {"const": "us-east-1"}, + "kms_key_spec": {"const": "ECC_NIST_P256"}, + "kms_signing_algorithm": {"const": "ECDSA_SHA_256"}, + "kms_message_type": {"const": "DIGEST"}, + "role_arn": {"const": "arn:aws:iam::992382684924:role/openadapt-public-trust-signer"}, + "signing_workflow": {"const": "/OpenAdaptAI/.github/.github/workflows/sign-production-evidence.yml@refs/heads/main"}, + "signing_environment": {"const": "public-trust-signing"}, + "key_source": {"const": "active-qualification-signer-registry-v2"}, + "offline_verification_required": {"const": true}, + "alias_allowed": {"const": false}, + "activation_state": {"const": "inactive-until-all-consumers-pin-exact-verifier"}, + "allowed_kinds": { + "type": "array", + "minItems": 17, + "maxItems": 17, + "uniqueItems": true, + "items": { + "enum": [ + "production-acceptance-manifest", + "production-acceptance-summary", + "production-cloud-deploy-authorization", + "production-cloud-deployment-result", + "production-current-default", + "production-deployment-observation", + "production-lifecycle-checkpoint", + "qualification-admission", + "qualification-authority-state-receipt", + "qualification-campaign-permit", + "qualification-campaign-permit-policy", + "qualification-campaign-permit-receipt", + "qualification-campaign-permit-request", + "qualification-evidence-decision-receipt", + "qualification-release", + "qualification-revocation-state-receipt", + "support-release-admission" + ] + } + } + } + }, + "sigstore": { + "type": "object", + "additionalProperties": false, + "required": [ + "tool", + "version", + "cert_oidc_issuer", + "deny_self_hosted_runners", + "allow_public_good", + "archives", + "certificate_identities" + ], + "properties": { + "tool": {"const": "gh"}, + "version": {"type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"}, + "cert_oidc_issuer": {"const": "https://token.actions.githubusercontent.com"}, + "deny_self_hosted_runners": {"const": true}, + "allow_public_good": {"const": false}, + "archives": { + "type": "array", + "minItems": 4, + "maxItems": 4, + "uniqueItems": true, + "items": {"$ref": "#/$defs/archive"} + }, + "certificate_identities": { + "type": "array", + "minItems": 10, + "maxItems": 10, + "uniqueItems": true, + "items": {"$ref": "#/$defs/certificate_identity"} + } + } + } + } +} diff --git a/schemas/production-lifecycle-checkpoint.schema.json b/schemas/production-lifecycle-checkpoint.schema.json new file mode 100644 index 0000000..22990ee --- /dev/null +++ b/schemas/production-lifecycle-checkpoint.schema.json @@ -0,0 +1,267 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "production-lifecycle-checkpoint.schema.json", + "title": "OpenAdapt production lifecycle checkpoint v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "checkpoint_id_sha256", + "checkpoint_revision", + "previous_checkpoint_sha256", + "registry_source_commit", + "registry_revision", + "registry_head_sha256", + "signer_registry", + "lifecycle_policy_sha256", + "current_default_reference", + "current_default_bundle_reference", + "lifecycle_projection", + "lifecycle_projection_sha256", + "release_admissions", + "release_admission_set_sha256", + "workflow_admissions", + "workflow_admission_set_sha256", + "authority_state_reference", + "authority_state_bundle_reference", + "authority_state_sha256", + "revocation_state_reference", + "revocation_state_bundle_reference", + "revocation_state_sha256", + "generated_at", + "not_before", + "expires_at", + "issuer" + ], + "properties": { + "schema_version": {"const": "openadapt.production-lifecycle-checkpoint/v1"}, + "checkpoint_id_sha256": {"$ref": "#/$defs/digest"}, + "checkpoint_revision": {"type": "integer", "minimum": 1}, + "previous_checkpoint_sha256": {"oneOf": [{"$ref": "#/$defs/digest"}, {"type": "null"}]}, + "registry_source_commit": {"$ref": "#/$defs/commit"}, + "registry_revision": {"type": "integer", "minimum": 1}, + "registry_head_sha256": {"$ref": "#/$defs/digest"}, + "signer_registry": {"$ref": "#/$defs/signer_registry_pointer"}, + "lifecycle_policy_sha256": {"$ref": "#/$defs/digest"}, + "current_default_reference": {"$ref": "#/$defs/current_default_reference"}, + "current_default_bundle_reference": {"$ref": "#/$defs/current_default_bundle_reference"}, + "lifecycle_projection": {"$ref": "#/$defs/lifecycle_projection"}, + "lifecycle_projection_sha256": {"$ref": "#/$defs/digest"}, + "release_admissions": { + "type": "array", + "minItems": 7, + "maxItems": 7, + "uniqueItems": true, + "items": {"$ref": "#/$defs/release_admission_pair"} + }, + "release_admission_set_sha256": {"$ref": "#/$defs/digest"}, + "workflow_admissions": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": {"$ref": "#/$defs/workflow_admission_pair"} + }, + "workflow_admission_set_sha256": {"$ref": "#/$defs/digest"}, + "authority_state_reference": {"$ref": "#/$defs/authority_state_reference"}, + "authority_state_bundle_reference": {"$ref": "#/$defs/authority_state_bundle_reference"}, + "authority_state_sha256": {"$ref": "#/$defs/digest"}, + "revocation_state_reference": {"$ref": "#/$defs/revocation_state_reference"}, + "revocation_state_bundle_reference": {"$ref": "#/$defs/revocation_state_bundle_reference"}, + "revocation_state_sha256": {"$ref": "#/$defs/digest"}, + "generated_at": {"$ref": "#/$defs/timestamp"}, + "not_before": {"$ref": "#/$defs/timestamp"}, + "expires_at": {"$ref": "#/$defs/timestamp"}, + "issuer": {"$ref": "#/$defs/issuer"} + }, + "$defs": { + "target": {"enum": ["agent", "capture", "cloud", "desktop", "docs", "flow", "openadapt"]}, + "digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "timestamp": {"type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"}, + "signer_registry_pointer": { + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "object_path", "object_sha256", "registry_identity_sha256", "registry_revision"], + "properties": { + "schema_version": {"const": "openadapt.qualification-signer-registry-pointer/v1"}, + "object_path": {"type": "string", "pattern": "^production-evidence/signer-registries/sha256/[0-9a-f]{2}/[0-9a-f]{64}\\.qualification-signer-registry\\.json$"}, + "object_sha256": {"$ref": "#/$defs/digest"}, + "registry_identity_sha256": {"$ref": "#/$defs/digest"}, + "registry_revision": {"type": "integer", "minimum": 1} + } + }, + "regular_reference": { + "allOf": [ + {"$ref": "production-evidence-object-reference.schema.json"}, + {"properties": {"subject_sha256": {"type": "null"}}} + ] + }, + "bundle_reference": { + "allOf": [ + {"$ref": "production-evidence-object-reference.schema.json"}, + {"properties": { + "object_schema_version": {"const": "application/vnd.dev.sigstore.bundle.v0.3+json"}, + "object_media_type": {"const": "application/vnd.dev.sigstore.bundle.v0.3+json"}, + "subject_sha256": {"$ref": "#/$defs/digest"} + }} + ] + }, + "current_default_reference": { + "allOf": [ + {"$ref": "#/$defs/regular_reference"}, + {"properties": { + "kind": {"const": "production-current-default"}, + "object_schema_version": {"const": "openadapt.production-current-default/v1"}, + "object_media_type": {"const": "application/vnd.openadapt.production-current-default+json;version=1"} + }} + ] + }, + "current_default_bundle_reference": { + "allOf": [ + {"$ref": "#/$defs/bundle_reference"}, + {"properties": {"kind": {"const": "production-current-default-sigstore-bundle"}}} + ] + }, + "release_reference": { + "allOf": [ + {"$ref": "#/$defs/regular_reference"}, + {"properties": { + "kind": {"const": "qualification-release"}, + "object_schema_version": {"const": "openadapt.qualification-release/v1"}, + "object_media_type": {"const": "application/vnd.openadapt.qualification-release+json;version=1"} + }} + ] + }, + "release_bundle_reference": { + "allOf": [ + {"$ref": "#/$defs/bundle_reference"}, + {"properties": {"kind": {"const": "qualification-release-sigstore-bundle"}}} + ] + }, + "workflow_reference": { + "allOf": [ + {"$ref": "#/$defs/regular_reference"}, + {"properties": { + "kind": {"const": "qualification-admission"}, + "object_schema_version": {"const": "openadapt.qualification-admission/v3"}, + "object_media_type": {"const": "application/vnd.openadapt.qualification-admission+json;version=3"} + }} + ] + }, + "workflow_bundle_reference": { + "allOf": [ + {"$ref": "#/$defs/bundle_reference"}, + {"properties": {"kind": {"const": "qualification-admission-sigstore-bundle"}}} + ] + }, + "authority_state_reference": { + "allOf": [ + {"$ref": "#/$defs/regular_reference"}, + {"properties": { + "kind": {"const": "qualification-authority-state-receipt"}, + "object_schema_version": {"const": "openadapt.qualification-authority-state-receipt/v2"}, + "object_media_type": {"const": "application/vnd.openadapt.qualification-authority-state-receipt+json;version=2"} + }} + ] + }, + "authority_state_bundle_reference": { + "allOf": [ + {"$ref": "#/$defs/bundle_reference"}, + {"properties": {"kind": {"const": "qualification-authority-state-receipt-sigstore-bundle"}}} + ] + }, + "revocation_state_reference": { + "allOf": [ + {"$ref": "#/$defs/regular_reference"}, + {"properties": { + "kind": {"const": "qualification-revocation-state-receipt"}, + "object_schema_version": {"const": "openadapt.qualification-revocation-state-receipt/v1"}, + "object_media_type": {"const": "application/vnd.openadapt.qualification-revocation-state-receipt+json;version=1"} + }} + ] + }, + "revocation_state_bundle_reference": { + "allOf": [ + {"$ref": "#/$defs/bundle_reference"}, + {"properties": {"kind": {"const": "qualification-revocation-state-receipt-sigstore-bundle"}}} + ] + }, + "release_admission_pair": { + "type": "object", + "additionalProperties": false, + "required": ["target", "admission_reference", "admission_bundle_reference"], + "properties": { + "target": {"$ref": "#/$defs/target"}, + "admission_reference": {"$ref": "#/$defs/release_reference"}, + "admission_bundle_reference": {"$ref": "#/$defs/release_bundle_reference"} + } + }, + "workflow_admission_pair": { + "type": "object", + "additionalProperties": false, + "required": ["admission_reference", "admission_bundle_reference"], + "properties": { + "admission_reference": {"$ref": "#/$defs/workflow_reference"}, + "admission_bundle_reference": {"$ref": "#/$defs/workflow_bundle_reference"} + } + }, + "lifecycle_projection_target": { + "type": "object", + "additionalProperties": false, + "required": [ + "target", "state", "claim_scope", "release_sha256", "artifact_inventory_sha256", + "release_admission_object_sha256", "default_identity_sha256", "not_before", "expires_at", "reason_codes" + ], + "properties": { + "target": {"$ref": "#/$defs/target"}, + "state": {"enum": ["Production", "not actively admitted"]}, + "claim_scope": {"type": "string", "pattern": "^production_[a-z]+$"}, + "release_sha256": {"$ref": "#/$defs/digest"}, + "artifact_inventory_sha256": {"$ref": "#/$defs/digest"}, + "release_admission_object_sha256": {"$ref": "#/$defs/digest"}, + "default_identity_sha256": {"$ref": "#/$defs/digest"}, + "not_before": {"$ref": "#/$defs/timestamp"}, + "expires_at": {"$ref": "#/$defs/timestamp"}, + "reason_codes": {"type": "array", "uniqueItems": true, "items": {"type": "string", "pattern": "^[a-z][a-z0-9_]*$"}} + } + }, + "lifecycle_projection": { + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "overall_state", "targets"], + "properties": { + "schema_version": {"const": "openadapt.production-lifecycle-projection/v2"}, + "overall_state": {"enum": ["Production", "not actively admitted"]}, + "targets": { + "type": "array", + "minItems": 7, + "maxItems": 7, + "uniqueItems": true, + "items": {"$ref": "#/$defs/lifecycle_projection_target"} + } + } + }, + "issuer": { + "type": "object", + "additionalProperties": false, + "required": [ + "repository", "repository_id", "repository_owner_id", "workflow", + "ref", "source_commit", "environment", "policy_repository", + "policy_repository_id", "policy_source_commit", "policy_path" + ], + "properties": { + "repository": {"const": "OpenAdaptAI/openadapt-ops"}, + "repository_id": {"const": "1172011294"}, + "repository_owner_id": {"const": "132681217"}, + "workflow": {"const": ".github/workflows/production-lifecycle-checkpoint.yml"}, + "ref": {"const": "refs/heads/main"}, + "source_commit": {"$ref": "#/$defs/commit"}, + "environment": {"const": "production-lifecycle-checkpoint"}, + "policy_repository": {"const": "OpenAdaptAI/.github"}, + "policy_repository_id": {"const": "858454062"}, + "policy_source_commit": {"$ref": "#/$defs/commit"}, + "policy_path": {"const": "production-lifecycle-policy.json"} + } + } + } +} diff --git a/schemas/production-lifecycle-evidence-manifest.schema.json b/schemas/production-lifecycle-evidence-manifest.schema.json index d0fa2c3..304856d 100644 --- a/schemas/production-lifecycle-evidence-manifest.schema.json +++ b/schemas/production-lifecycle-evidence-manifest.schema.json @@ -1,190 +1,128 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "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/OpenAdaptAI/.github/blob/main/schemas/production-lifecycle-evidence-manifest.schema.json", - "title": "OpenAdapt Production acceptance evidence manifest", + "$id": "production-lifecycle-evidence-manifest.schema.json", + "title": "OpenAdapt production acceptance manifest v2", "type": "object", "additionalProperties": false, "required": [ "schema_version", "target", - "claim_scope", "verdict", + "claim_scope", "acceptance_policy_sha256", "lifecycle_policy_sha256", - "target_release_sha256", - "target_artifact_inventory_sha256", - "evidence_identity_sha256", - "source_evidence", - "qualification", - "failure_taxonomy_counts", - "reliability", - "retention" + "release_identity", + "release", + "release_sha256", + "artifact_inventory", + "artifact_inventory_sha256", + "publication_staging", + "publication_staging_sha256", + "qualification_evidence_decision_receipt_reference", + "qualification_evidence_decision_receipt_bundle_reference", + "qualification_admission_reference", + "qualification_admission_bundle_reference", + "campaign_summary", + "authority_state_sha256", + "revocation_state_sha256", + "signer_registry_sha256", + "issued_at", + "not_before", + "expires_at", + "issuer" ], "properties": { - "schema_version": {"const": "openadapt.production-acceptance/v1"}, - "target": {"type": "string", "pattern": "^[a-z][a-z0-9-]{1,63}$"}, - "claim_scope": {"type": "string", "pattern": "^[a-z][a-z0-9_]{2,127}$"}, + "schema_version": {"const": "openadapt.production-acceptance/v2"}, + "target": {"enum": ["agent", "capture", "cloud", "desktop", "docs", "flow", "openadapt"]}, "verdict": {"const": "accepted"}, + "claim_scope": {"type": "string", "pattern": "^production_[a-z]+$"}, "acceptance_policy_sha256": {"$ref": "#/$defs/digest"}, "lifecycle_policy_sha256": {"$ref": "#/$defs/digest"}, - "target_release_sha256": {"$ref": "#/$defs/digest"}, - "target_artifact_inventory_sha256": {"$ref": "#/$defs/digest"}, - "evidence_identity_sha256": {"$ref": "#/$defs/digest"}, - "source_evidence": { - "type": "object", - "additionalProperties": false, - "required": [ - "source_result_sha256", - "certificate_sha256", - "campaign_sha256", - "qualification_admission_sha256", - "attestation_sha256", - "attestation_bundle_sha256" - ], - "properties": { - "source_result_sha256": {"$ref": "#/$defs/digest"}, - "certificate_sha256": {"$ref": "#/$defs/digest"}, - "campaign_sha256": {"$ref": "#/$defs/digest"}, - "qualification_admission_sha256": {"$ref": "#/$defs/digest"}, - "attestation_sha256": {"$ref": "#/$defs/digest"}, - "attestation_bundle_sha256": {"$ref": "#/$defs/digest"} - } - }, - "qualification": { - "type": "object", - "additionalProperties": false, - "required": [ - "campaign_contract_sha256", - "campaign_outcomes_sha256", - "oracle_contract_sha256", - "task_count", - "condition_count", - "required_trial_count", - "observed_trial_count", - "minimum_trials_per_condition", - "excluded_trial_count", - "task_condition_inventory_sha256" - ], - "properties": { - "campaign_contract_sha256": {"$ref": "#/$defs/digest"}, - "campaign_outcomes_sha256": {"$ref": "#/$defs/digest"}, - "oracle_contract_sha256": {"$ref": "#/$defs/digest"}, - "task_count": {"type": "integer", "minimum": 1}, - "condition_count": {"type": "integer", "minimum": 1}, - "required_trial_count": {"type": "integer", "minimum": 3}, - "observed_trial_count": {"type": "integer", "minimum": 3}, - "minimum_trials_per_condition": {"type": "integer", "minimum": 3}, - "excluded_trial_count": {"const": 0}, - "task_condition_inventory_sha256": {"$ref": "#/$defs/digest"} - } - }, - "failure_taxonomy_counts": {"$ref": "#/$defs/failureTaxonomy"}, - "reliability": {"$ref": "#/$defs/reliability"}, - "retention": {"$ref": "#/$defs/retention"} + "release_identity": {"$ref": "qualification-release.schema.json#/$defs/release_identity"}, + "release": {"$ref": "qualification-release.schema.json#/$defs/release_candidate"}, + "release_sha256": {"$ref": "#/$defs/digest"}, + "artifact_inventory": {"$ref": "production-release-artifact-inventory.schema.json"}, + "artifact_inventory_sha256": {"$ref": "#/$defs/digest"}, + "publication_staging": {"$ref": "qualification-release.schema.json#/$defs/publication_staging"}, + "publication_staging_sha256": {"$ref": "#/$defs/digest"}, + "qualification_evidence_decision_receipt_reference": {"$ref": "#/$defs/receipt_reference"}, + "qualification_evidence_decision_receipt_bundle_reference": {"$ref": "#/$defs/receipt_bundle_reference"}, + "qualification_admission_reference": {"$ref": "#/$defs/admission_reference"}, + "qualification_admission_bundle_reference": {"$ref": "#/$defs/admission_bundle_reference"}, + "campaign_summary": {"$ref": "qualification-admission.schema.json#/$defs/campaign_summary"}, + "authority_state_sha256": {"$ref": "#/$defs/digest"}, + "revocation_state_sha256": {"$ref": "#/$defs/digest"}, + "signer_registry_sha256": {"$ref": "#/$defs/digest"}, + "issued_at": {"$ref": "#/$defs/timestamp"}, + "not_before": {"$ref": "#/$defs/timestamp"}, + "expires_at": {"$ref": "#/$defs/timestamp"}, + "issuer": {"$ref": "#/$defs/issuer"} }, "$defs": { "digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, - "millisecondTimestamp": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3}Z$"}, - "count": {"type": "integer", "minimum": 0}, - "failureTaxonomy": { + "timestamp": {"type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"}, + "commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "issuer": { "type": "object", "additionalProperties": false, - "required": [ - "collateral_effect", - "duplicate_effect", - "healthy_path_model_call", - "operator_intervention", - "over_halt", - "platform_failure", - "safe_halt", - "silent_incorrect_success", - "uncertain_delivery", - "verified", - "wrong_record" - ], + "required": ["repository", "repository_id", "repository_owner_id", "workflow", "ref", "source_commit", "environment"], "properties": { - "collateral_effect": {"$ref": "#/$defs/count"}, - "duplicate_effect": {"$ref": "#/$defs/count"}, - "healthy_path_model_call": {"$ref": "#/$defs/count"}, - "operator_intervention": {"$ref": "#/$defs/count"}, - "over_halt": {"$ref": "#/$defs/count"}, - "platform_failure": {"$ref": "#/$defs/count"}, - "safe_halt": {"$ref": "#/$defs/count"}, - "silent_incorrect_success": {"$ref": "#/$defs/count"}, - "uncertain_delivery": {"$ref": "#/$defs/count"}, - "verified": {"$ref": "#/$defs/count"}, - "wrong_record": {"$ref": "#/$defs/count"} + "repository": {"const": "OpenAdaptAI/openadapt-evals"}, + "repository_id": {"const": "1135998197"}, + "repository_owner_id": {"const": "132681217"}, + "workflow": {"const": ".github/workflows/issue-production-acceptance.yml"}, + "ref": {"const": "refs/heads/main"}, + "source_commit": {"$ref": "#/$defs/commit"}, + "environment": {"const": "production-acceptance"} } }, - "reliability": { - "type": "object", - "additionalProperties": false, - "required": [ - "silent_incorrect_success_count", - "over_halt_count", - "wrong_record_count", - "duplicate_effect_count", - "collateral_effect_count", - "operator_intervention_count", - "uncertain_delivery_count", - "model_call_count" - ], - "properties": { - "silent_incorrect_success_count": {"$ref": "#/$defs/count"}, - "over_halt_count": {"$ref": "#/$defs/count"}, - "wrong_record_count": {"$ref": "#/$defs/count"}, - "duplicate_effect_count": {"$ref": "#/$defs/count"}, - "collateral_effect_count": {"$ref": "#/$defs/count"}, - "operator_intervention_count": {"$ref": "#/$defs/count"}, - "uncertain_delivery_count": {"$ref": "#/$defs/count"}, - "model_call_count": {"$ref": "#/$defs/count"} - } + "regular_reference": { + "allOf": [ + {"$ref": "production-evidence-object-reference.schema.json"}, + {"properties": {"subject_sha256": {"type": "null"}}} + ] }, - "retention": { - "type": "object", - "additionalProperties": false, - "required": [ - "receipt_id", - "ciphertext_sha256", - "candidate_sha256", - "private_envelope_sha256", - "store_attestation_sha256", - "storage_identity_sha256", - "object_version_sha256", - "private_locator_version_sha256", - "kms_key_identity_sha256", - "uploader_identity_sha256", - "retention_mode", - "retention_until", - "retained_at", - "upload_verified", - "head_verified", - "object_lock_verified", - "private_locator_recorded", - "acceptance_verified_at", - "provenance_attestation" - ], - "properties": { - "receipt_id": {"type": "string", "pattern": "^retention:[0-9a-f]{32}$"}, - "ciphertext_sha256": {"$ref": "#/$defs/digest"}, - "candidate_sha256": {"$ref": "#/$defs/digest"}, - "private_envelope_sha256": {"$ref": "#/$defs/digest"}, - "store_attestation_sha256": {"$ref": "#/$defs/digest"}, - "storage_identity_sha256": {"$ref": "#/$defs/digest"}, - "object_version_sha256": {"$ref": "#/$defs/digest"}, - "private_locator_version_sha256": {"$ref": "#/$defs/digest"}, - "kms_key_identity_sha256": {"$ref": "#/$defs/digest"}, - "uploader_identity_sha256": {"$ref": "#/$defs/digest"}, - "retention_mode": {"const": "COMPLIANCE"}, - "retention_until": {"$ref": "#/$defs/millisecondTimestamp"}, - "retained_at": {"$ref": "#/$defs/millisecondTimestamp"}, - "upload_verified": {"const": true}, - "head_verified": {"const": true}, - "object_lock_verified": {"const": true}, - "private_locator_recorded": {"const": true}, - "acceptance_verified_at": {"$ref": "#/$defs/millisecondTimestamp"}, - "provenance_attestation": {"const": "github-artifact-attestation-v4"} - } + "bundle_reference": { + "allOf": [ + {"$ref": "production-evidence-object-reference.schema.json"}, + {"properties": { + "object_schema_version": {"const": "application/vnd.dev.sigstore.bundle.v0.3+json"}, + "object_media_type": {"const": "application/vnd.dev.sigstore.bundle.v0.3+json"}, + "subject_sha256": {"$ref": "#/$defs/digest"} + }} + ] + }, + "receipt_reference": { + "allOf": [ + {"$ref": "#/$defs/regular_reference"}, + {"properties": { + "kind": {"const": "qualification-evidence-decision-receipt"}, + "object_schema_version": {"const": "openadapt.qualification-evidence-decision-receipt/v1"}, + "object_media_type": {"const": "application/vnd.openadapt.qualification-evidence-decision-receipt+json;version=1"} + }} + ] + }, + "receipt_bundle_reference": { + "allOf": [ + {"$ref": "#/$defs/bundle_reference"}, + {"properties": {"kind": {"const": "qualification-evidence-decision-receipt-sigstore-bundle"}}} + ] + }, + "admission_reference": { + "allOf": [ + {"$ref": "#/$defs/regular_reference"}, + {"properties": { + "kind": {"const": "qualification-admission"}, + "object_schema_version": {"const": "openadapt.qualification-admission/v3"}, + "object_media_type": {"const": "application/vnd.openadapt.qualification-admission+json;version=3"} + }} + ] + }, + "admission_bundle_reference": { + "allOf": [ + {"$ref": "#/$defs/bundle_reference"}, + {"properties": {"kind": {"const": "qualification-admission-sigstore-bundle"}}} + ] } } } diff --git a/schemas/production-lifecycle-evidence-summary.schema.json b/schemas/production-lifecycle-evidence-summary.schema.json index a0980c7..f8f00b4 100644 --- a/schemas/production-lifecycle-evidence-summary.schema.json +++ b/schemas/production-lifecycle-evidence-summary.schema.json @@ -1,8 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "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/OpenAdaptAI/.github/blob/main/schemas/production-lifecycle-evidence-summary.schema.json", - "title": "OpenAdapt remote-safe Production lifecycle evidence summary", - "description": "An attested public projection. It binds a private acceptance certificate by digest and signer provenance only.", + "$id": "production-lifecycle-evidence-summary.schema.json", + "title": "OpenAdapt production acceptance summary v2", "type": "object", "additionalProperties": false, "required": [ @@ -15,77 +14,133 @@ "release_identity", "release_sha256", "artifact_inventory_sha256", + "publication_staging", + "publication_staging_sha256", "evidence_identity_sha256", - "private_certificate_binding", - "evidence_manifest", + "qualification_evidence_decision_receipt_reference", + "qualification_evidence_decision_receipt_bundle_reference", + "qualification_admission_reference", + "qualification_admission_bundle_reference", + "production_acceptance_manifest_reference", + "production_acceptance_manifest_bundle_reference", + "campaign_summary", + "authority_state_sha256", + "revocation_state_sha256", + "signer_registry_sha256", "issued_at", + "not_before", "expires_at", - "revoked_at" + "issuer" ], "properties": { - "schema_version": {"const": "openadapt.production-lifecycle-evidence-summary/v1"}, - "target": {"type": "string", "pattern": "^[a-z][a-z0-9-]{1,63}$"}, + "schema_version": {"const": "openadapt.production-lifecycle-evidence-summary/v2"}, + "target": {"enum": ["agent", "capture", "cloud", "desktop", "docs", "flow", "openadapt"]}, "verdict": {"const": "accepted"}, - "claim_scope": {"type": "string", "pattern": "^[a-z][a-z0-9_]{2,127}$"}, + "claim_scope": {"type": "string", "pattern": "^production_[a-z]+$"}, "acceptance_policy_sha256": {"$ref": "#/$defs/digest"}, "lifecycle_policy_sha256": {"$ref": "#/$defs/digest"}, - "release_identity": { - "type": "object", - "additionalProperties": false, - "required": ["schema_version", "channel", "sequence", "previous_admission_sha256"], - "properties": { - "schema_version": {"const": "openadapt.monotonic-production-release/v1"}, - "channel": {"const": "production"}, - "sequence": {"type": "integer", "minimum": 1}, - "previous_admission_sha256": { - "oneOf": [{"$ref": "#/$defs/digest"}, {"type": "null"}] - } - } - }, + "release_identity": {"$ref": "qualification-release.schema.json#/$defs/release_identity"}, "release_sha256": {"$ref": "#/$defs/digest"}, "artifact_inventory_sha256": {"$ref": "#/$defs/digest"}, + "publication_staging": {"$ref": "qualification-release.schema.json#/$defs/publication_staging"}, + "publication_staging_sha256": {"$ref": "#/$defs/digest"}, "evidence_identity_sha256": {"$ref": "#/$defs/digest"}, - "private_certificate_binding": { + "qualification_evidence_decision_receipt_reference": {"$ref": "#/$defs/receipt_reference"}, + "qualification_evidence_decision_receipt_bundle_reference": {"$ref": "#/$defs/receipt_bundle_reference"}, + "qualification_admission_reference": {"$ref": "#/$defs/admission_reference"}, + "qualification_admission_bundle_reference": {"$ref": "#/$defs/admission_bundle_reference"}, + "production_acceptance_manifest_reference": {"$ref": "#/$defs/manifest_reference"}, + "production_acceptance_manifest_bundle_reference": {"$ref": "#/$defs/manifest_bundle_reference"}, + "campaign_summary": {"$ref": "qualification-admission.schema.json#/$defs/campaign_summary"}, + "authority_state_sha256": {"$ref": "#/$defs/digest"}, + "revocation_state_sha256": {"$ref": "#/$defs/digest"}, + "signer_registry_sha256": {"$ref": "#/$defs/digest"}, + "issued_at": {"$ref": "#/$defs/timestamp"}, + "not_before": {"$ref": "#/$defs/timestamp"}, + "expires_at": {"$ref": "#/$defs/timestamp"}, + "issuer": {"$ref": "#/$defs/issuer"} + }, + "$defs": { + "digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "timestamp": {"type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"}, + "commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "issuer": { "type": "object", "additionalProperties": false, - "required": [ - "schema_version", - "sha256", - "signer_provenance_sha256", - "evidence_identity_sha256", - "target", - "target_release_sha256", - "target_artifact_inventory_sha256" - ], + "required": ["repository", "repository_id", "repository_owner_id", "workflow", "ref", "source_commit", "environment"], "properties": { - "schema_version": {"const": "openadapt.execute-live-acceptance-record/v2"}, - "sha256": {"$ref": "#/$defs/digest"}, - "signer_provenance_sha256": { - "description": "SHA-256 over the domain-separated canonical normalized verified Cloud provenance plus the admission signer-registry digest and revision.", - "$ref": "#/$defs/digest" - }, - "evidence_identity_sha256": {"$ref": "#/$defs/digest"}, - "target": {"type": "string", "pattern": "^[a-z][a-z0-9-]{1,63}$"}, - "target_release_sha256": {"$ref": "#/$defs/digest"}, - "target_artifact_inventory_sha256": {"$ref": "#/$defs/digest"} + "repository": {"const": "OpenAdaptAI/openadapt-evals"}, + "repository_id": {"const": "1135998197"}, + "repository_owner_id": {"const": "132681217"}, + "workflow": {"const": ".github/workflows/issue-production-acceptance.yml"}, + "ref": {"const": "refs/heads/main"}, + "source_commit": {"$ref": "#/$defs/commit"}, + "environment": {"const": "production-acceptance"} } }, - "evidence_manifest": { - "type": "object", - "additionalProperties": false, - "required": ["schema_version", "url", "sha256"], - "properties": { - "schema_version": {"const": "openadapt.production-acceptance/v1"}, - "url": {"type": "string", "format": "uri", "pattern": "^https://"}, - "sha256": {"$ref": "#/$defs/digest"} - } + "regular_reference": { + "allOf": [ + {"$ref": "production-evidence-object-reference.schema.json"}, + {"properties": {"subject_sha256": {"type": "null"}}} + ] }, - "issued_at": {"$ref": "#/$defs/timestamp"}, - "expires_at": {"$ref": "#/$defs/timestamp"}, - "revoked_at": {"oneOf": [{"type": "null"}, {"$ref": "#/$defs/timestamp"}]} - }, - "$defs": { - "digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, - "timestamp": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"} + "bundle_reference": { + "allOf": [ + {"$ref": "production-evidence-object-reference.schema.json"}, + {"properties": { + "object_schema_version": {"const": "application/vnd.dev.sigstore.bundle.v0.3+json"}, + "object_media_type": {"const": "application/vnd.dev.sigstore.bundle.v0.3+json"}, + "subject_sha256": {"$ref": "#/$defs/digest"} + }} + ] + }, + "receipt_reference": { + "allOf": [ + {"$ref": "#/$defs/regular_reference"}, + {"properties": { + "kind": {"const": "qualification-evidence-decision-receipt"}, + "object_schema_version": {"const": "openadapt.qualification-evidence-decision-receipt/v1"}, + "object_media_type": {"const": "application/vnd.openadapt.qualification-evidence-decision-receipt+json;version=1"} + }} + ] + }, + "receipt_bundle_reference": { + "allOf": [ + {"$ref": "#/$defs/bundle_reference"}, + {"properties": {"kind": {"const": "qualification-evidence-decision-receipt-sigstore-bundle"}}} + ] + }, + "admission_reference": { + "allOf": [ + {"$ref": "#/$defs/regular_reference"}, + {"properties": { + "kind": {"const": "qualification-admission"}, + "object_schema_version": {"const": "openadapt.qualification-admission/v3"}, + "object_media_type": {"const": "application/vnd.openadapt.qualification-admission+json;version=3"} + }} + ] + }, + "admission_bundle_reference": { + "allOf": [ + {"$ref": "#/$defs/bundle_reference"}, + {"properties": {"kind": {"const": "qualification-admission-sigstore-bundle"}}} + ] + }, + "manifest_reference": { + "allOf": [ + {"$ref": "#/$defs/regular_reference"}, + {"properties": { + "kind": {"const": "production-acceptance-manifest"}, + "object_schema_version": {"const": "openadapt.production-acceptance/v2"}, + "object_media_type": {"const": "application/vnd.openadapt.production-acceptance+json;version=2"} + }} + ] + }, + "manifest_bundle_reference": { + "allOf": [ + {"$ref": "#/$defs/bundle_reference"}, + {"properties": {"kind": {"const": "production-acceptance-manifest-sigstore-bundle"}}} + ] + } } } diff --git a/schemas/production-lifecycle-feed-update.schema.json b/schemas/production-lifecycle-feed-update.schema.json new file mode 100644 index 0000000..ca63f2b --- /dev/null +++ b/schemas/production-lifecycle-feed-update.schema.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "production-lifecycle-feed-update.schema.json", + "title": "OpenAdapt production lifecycle feed update v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "event_type", + "source_repository", + "source_repository_id", + "source_ref", + "source_commit", + "target_repository", + "target_repository_id", + "target_ref", + "expected_old_commit", + "new_commit", + "feed_path", + "feed_sha256", + "checkpoint_sha256", + "registry_head_sha256", + "expires_at", + "idempotency_key" + ], + "properties": { + "schema_version": {"const": "openadapt.production-lifecycle-feed-update/v1"}, + "event_type": {"const": "production_lifecycle_feed_updated"}, + "source_repository": {"const": "OpenAdaptAI/openadapt-ops"}, + "source_repository_id": {"const": "1172011294"}, + "source_ref": {"const": "refs/heads/main"}, + "source_commit": {"$ref": "#/$defs/commit"}, + "target_repository": {"const": "OpenAdaptAI/.github"}, + "target_repository_id": {"const": "858454062"}, + "target_ref": {"const": "refs/heads/production-lifecycle-feed"}, + "expected_old_commit": {"oneOf": [{"$ref": "#/$defs/commit"}, {"type": "null"}]}, + "new_commit": {"$ref": "#/$defs/commit"}, + "feed_path": {"const": "production-lifecycle-feed.json"}, + "feed_sha256": {"$ref": "#/$defs/digest"}, + "checkpoint_sha256": {"$ref": "#/$defs/digest"}, + "registry_head_sha256": {"$ref": "#/$defs/digest"}, + "expires_at": {"$ref": "#/$defs/timestamp"}, + "idempotency_key": {"type": "string", "pattern": "^lifecycle-feed-update:[0-9a-f]{64}$"} + }, + "$defs": { + "digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "timestamp": {"type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"} + } +} diff --git a/schemas/production-lifecycle-feed.schema.json b/schemas/production-lifecycle-feed.schema.json new file mode 100644 index 0000000..8e6e325 --- /dev/null +++ b/schemas/production-lifecycle-feed.schema.json @@ -0,0 +1,91 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "production-lifecycle-feed.schema.json", + "title": "OpenAdapt production lifecycle feed v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "repository", + "repository_id", + "repository_owner_id", + "ref", + "feed_revision", + "generated_at", + "expires_at", + "registry_source_commit", + "registry_revision", + "registry_head_sha256", + "signer_registry", + "checkpoints" + ], + "properties": { + "schema_version": {"const": "openadapt.production-lifecycle-feed/v1"}, + "repository": {"const": "OpenAdaptAI/.github"}, + "repository_id": {"const": "858454062"}, + "repository_owner_id": {"const": "132681217"}, + "ref": {"const": "refs/heads/production-lifecycle-feed"}, + "feed_revision": {"type": "integer", "minimum": 1}, + "generated_at": {"$ref": "#/$defs/timestamp"}, + "expires_at": {"$ref": "#/$defs/timestamp"}, + "registry_source_commit": {"$ref": "#/$defs/commit"}, + "registry_revision": {"type": "integer", "minimum": 1}, + "registry_head_sha256": {"$ref": "#/$defs/digest"}, + "signer_registry": {"$ref": "#/$defs/signer_registry_pointer"}, + "checkpoints": { + "type": "array", + "minItems": 1, + "maxItems": 2, + "uniqueItems": true, + "items": {"$ref": "#/$defs/checkpoint_pair"} + } + }, + "$defs": { + "digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "timestamp": {"type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"}, + "signer_registry_pointer": { + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "object_path", "object_sha256", "registry_identity_sha256", "registry_revision"], + "properties": { + "schema_version": {"const": "openadapt.qualification-signer-registry-pointer/v1"}, + "object_path": {"type": "string", "pattern": "^production-evidence/signer-registries/sha256/[0-9a-f]{2}/[0-9a-f]{64}\\.qualification-signer-registry\\.json$"}, + "object_sha256": {"$ref": "#/$defs/digest"}, + "registry_identity_sha256": {"$ref": "#/$defs/digest"}, + "registry_revision": {"type": "integer", "minimum": 1} + } + }, + "checkpoint_reference": { + "allOf": [ + {"$ref": "production-evidence-object-reference.schema.json"}, + {"properties": { + "kind": {"const": "production-lifecycle-checkpoint"}, + "object_schema_version": {"const": "openadapt.production-lifecycle-checkpoint/v1"}, + "object_media_type": {"const": "application/vnd.openadapt.production-lifecycle-checkpoint+json;version=1"}, + "subject_sha256": {"type": "null"} + }} + ] + }, + "checkpoint_bundle_reference": { + "allOf": [ + {"$ref": "production-evidence-object-reference.schema.json"}, + {"properties": { + "kind": {"const": "production-lifecycle-checkpoint-sigstore-bundle"}, + "object_schema_version": {"const": "application/vnd.dev.sigstore.bundle.v0.3+json"}, + "object_media_type": {"const": "application/vnd.dev.sigstore.bundle.v0.3+json"}, + "subject_sha256": {"$ref": "#/$defs/digest"} + }} + ] + }, + "checkpoint_pair": { + "type": "object", + "additionalProperties": false, + "required": ["checkpoint_reference", "checkpoint_bundle_reference"], + "properties": { + "checkpoint_reference": {"$ref": "#/$defs/checkpoint_reference"}, + "checkpoint_bundle_reference": {"$ref": "#/$defs/checkpoint_bundle_reference"} + } + } + } +} diff --git a/schemas/production-lifecycle-policy.schema.json b/schemas/production-lifecycle-policy.schema.json index 9f199ff..9fd9aae 100644 --- a/schemas/production-lifecycle-policy.schema.json +++ b/schemas/production-lifecycle-policy.schema.json @@ -1,96 +1,75 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "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/OpenAdaptAI/.github/blob/main/schemas/production-lifecycle-policy.schema.json", - "title": "OpenAdapt Production lifecycle policy", + "$id": "production-lifecycle-policy.schema.json", + "title": "OpenAdapt production lifecycle policy v2", "type": "object", "additionalProperties": false, "required": [ "$schema", "schema_version", "revision", - "maximum_admission_days", - "summary_authority", + "maximum_release_admission_days", + "maximum_workflow_admission_days", + "object_reference_schema_version", + "release_admission_schema_version", + "workflow_admission_schema_version", + "lifecycle_checkpoint_schema_version", + "lifecycle_feed_schema_version", + "lifecycle_feed_ref", "targets" ], "properties": { "$schema": {"const": "schemas/production-lifecycle-policy.schema.json"}, - "schema_version": {"const": "openadapt.production-lifecycle-policy/v1"}, - "revision": {"type": "integer", "minimum": 1}, - "maximum_admission_days": {"type": "integer", "minimum": 1, "maximum": 30}, - "summary_authority": { + "schema_version": {"const": "openadapt.production-lifecycle-policy/v2"}, + "revision": {"type": "integer", "minimum": 2}, + "maximum_release_admission_days": {"const": 30}, + "maximum_workflow_admission_days": {"const": 7}, + "object_reference_schema_version": {"const": "openadapt.production-evidence-object-reference/v2"}, + "release_admission_schema_version": {"const": "openadapt.qualification-release/v1"}, + "workflow_admission_schema_version": {"const": "openadapt.qualification-admission/v3"}, + "lifecycle_checkpoint_schema_version": {"const": "openadapt.production-lifecycle-checkpoint/v1"}, + "lifecycle_feed_schema_version": {"const": "openadapt.production-lifecycle-feed/v1"}, + "lifecycle_feed_ref": {"const": "refs/heads/production-lifecycle-feed"}, + "targets": { + "type": "array", + "minItems": 7, + "maxItems": 7, + "uniqueItems": true, + "items": {"$ref": "#/$defs/target"} + } + }, + "$defs": { + "target": { "type": "object", "additionalProperties": false, "required": [ - "repository", - "workflow", - "source_ref", - "oidc_issuer", - "certificate_identity", - "summary_schema_version", - "private_certificate_schema_version", - "evidence_manifest_schema_version", - "acceptance_policy_sha256", - "release_identity_schema_version", - "production_channel", - "signer_provenance_digest_domain" + "id", + "display_name", + "source_repository", + "source_repository_id", + "release_kind", + "claim_scope", + "required_artifact_kinds", + "package_index_project" ], "properties": { - "repository": {"type": "string", "pattern": "^OpenAdaptAI/[A-Za-z0-9._-]+$"}, - "workflow": {"type": "string", "pattern": "^\\.github/workflows/[A-Za-z0-9._-]+\\.ya?ml$"}, - "source_ref": {"const": "refs/heads/main"}, - "oidc_issuer": {"const": "https://token.actions.githubusercontent.com"}, - "certificate_identity": {"type": "string", "format": "uri"}, - "summary_schema_version": {"const": "openadapt.production-lifecycle-evidence-summary/v1"}, - "private_certificate_schema_version": {"const": "openadapt.execute-live-acceptance-record/v2"}, - "evidence_manifest_schema_version": {"const": "openadapt.production-acceptance/v1"}, - "acceptance_policy_sha256": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, - "release_identity_schema_version": {"const": "openadapt.monotonic-production-release/v1"}, - "production_channel": {"const": "production"}, - "signer_provenance_digest_domain": {"const": "OpenAdapt production certificate signer provenance v1\u0000"} - } - }, - "targets": { - "type": "array", - "minItems": 7, - "maxItems": 7, - "items": { - "type": "object", - "additionalProperties": false, - "required": [ - "id", - "display_name", - "lifecycle_scope", - "lifecycle_subject", - "source_repository", - "release_kind", - "required_claim_scope", - "required_artifact_kinds", - "package_index_project", - "artifact_authority_by_kind" - ], - "properties": { - "id": {"type": "string", "pattern": "^[a-z][a-z0-9-]{1,63}$"}, - "display_name": {"type": "string", "minLength": 1}, - "lifecycle_scope": {"enum": ["repository", "public_surface"]}, - "lifecycle_subject": {"type": "string", "minLength": 1}, - "source_repository": {"type": "string", "pattern": "^OpenAdaptAI/[A-Za-z0-9._-]+$"}, - "release_kind": {"enum": ["public_package", "private_deployment", "public_deployment"]}, - "required_claim_scope": {"type": "string", "pattern": "^[a-z][a-z0-9_]{2,127}$"}, - "package_index_project": { - "oneOf": [ - {"type": "string", "pattern": "^[a-z0-9][a-z0-9._-]+$"}, - {"type": "null"} - ] - }, - "artifact_authority_by_kind": { - "type": "object", - "additionalProperties": {"enum": ["pypi", "github_release", "managed_evidence"]} - }, - "required_artifact_kinds": { - "type": "array", - "uniqueItems": true, - "items": {"type": "string", "minLength": 1} - } + "id": {"enum": ["agent", "capture", "cloud", "desktop", "docs", "flow", "openadapt"]}, + "display_name": {"type": "string", "minLength": 1, "maxLength": 80}, + "source_repository": {"type": "string", "pattern": "^OpenAdaptAI/[A-Za-z0-9._-]+$"}, + "source_repository_id": {"type": "string", "pattern": "^[1-9][0-9]*$"}, + "release_kind": {"enum": ["package", "deployment", "hybrid"]}, + "claim_scope": {"type": "string", "pattern": "^production_[a-z]+$"}, + "required_artifact_kinds": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": {"type": "string", "pattern": "^[a-z][a-z0-9-]*$"} + }, + "package_index_project": { + "oneOf": [ + {"type": "string", "pattern": "^[a-z][a-z0-9-]*$"}, + {"type": "null"} + ] } } } diff --git a/schemas/production-public-trust-dsse-bundle.schema.json b/schemas/production-public-trust-dsse-bundle.schema.json new file mode 100644 index 0000000..f1fdd6e --- /dev/null +++ b/schemas/production-public-trust-dsse-bundle.schema.json @@ -0,0 +1,49 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "production-public-trust-dsse-bundle.schema.json", + "title": "OpenAdapt production public-trust raw Sigstore bundle profile", + "type": "object", + "additionalProperties": false, + "required": ["mediaType", "verificationMaterial", "dsseEnvelope"], + "properties": { + "mediaType": {"const": "application/vnd.dev.sigstore.bundle.v0.3+json"}, + "verificationMaterial": { + "type": "object", + "additionalProperties": false, + "required": ["publicKey"], + "properties": { + "publicKey": { + "type": "object", + "additionalProperties": false, + "required": ["hint"], + "properties": { + "hint": {"type": "string", "pattern": "^oa-public-trust-p256-[0-9a-f]{16}$"} + } + } + } + }, + "dsseEnvelope": { + "type": "object", + "additionalProperties": false, + "required": ["payload", "payloadType", "signatures"], + "properties": { + "payload": {"type": "string", "pattern": "^[A-Za-z0-9+/]+={0,2}$"}, + "payloadType": {"const": "application/vnd.openadapt.production-public-trust-signing-statement+json;version=1"}, + "signatures": { + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["keyid", "sig"], + "properties": { + "keyid": {"type": "string", "pattern": "^oa-public-trust-p256-[0-9a-f]{16}$"}, + "sig": {"type": "string", "pattern": "^[A-Za-z0-9+/]+={0,2}$"} + } + } + } + } + } + } +} diff --git a/schemas/production-public-trust-signing-statement.schema.json b/schemas/production-public-trust-signing-statement.schema.json new file mode 100644 index 0000000..a461249 --- /dev/null +++ b/schemas/production-public-trust-signing-statement.schema.json @@ -0,0 +1,78 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "production-public-trust-signing-statement.schema.json", + "title": "OpenAdapt production public-trust signing statement v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", "object_kind", "object_schema_version", "object_media_type", + "object_sha256", "object_size_bytes", "semantic_identity_sha256", "source_issuer", + "signer_registry_sha256", "authority_state_sha256", "revocation_state_sha256", + "issued_at", "not_before", "expires_at", "request_id_sha256", + "signing_authority", "key_id", "signature_profile" + ], + "properties": { + "schema_version": {"const": "openadapt.production-public-trust-signing-statement/v1"}, + "object_kind": {"type": "string", "pattern": "^[a-z][a-z0-9-]{1,95}$"}, + "object_schema_version": {"type": "string", "minLength": 1, "maxLength": 160}, + "object_media_type": {"type": "string", "minLength": 1, "maxLength": 200}, + "object_sha256": {"$ref": "#/$defs/digest"}, + "object_size_bytes": {"type": "integer", "minimum": 1}, + "semantic_identity_sha256": {"$ref": "#/$defs/digest"}, + "source_issuer": {"$ref": "#/$defs/issuer"}, + "signer_registry_sha256": {"$ref": "#/$defs/digest"}, + "authority_state_sha256": {"$ref": "#/$defs/digest"}, + "revocation_state_sha256": {"$ref": "#/$defs/digest"}, + "issued_at": {"$ref": "#/$defs/timestamp"}, + "not_before": {"$ref": "#/$defs/timestamp"}, + "expires_at": {"$ref": "#/$defs/timestamp"}, + "request_id_sha256": {"$ref": "#/$defs/digest"}, + "signing_authority": {"$ref": "#/$defs/signing_authority"}, + "key_id": {"type": "string", "pattern": "^oa-public-trust-p256-[0-9a-f]{16}$"}, + "signature_profile": {"const": "aws-kms-p256-dsse-v1"} + }, + "$defs": { + "digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "timestamp": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"}, + "issuer": { + "type": "object", + "additionalProperties": false, + "required": ["repository", "repository_id", "repository_owner_id", "workflow", "ref", "source_commit", "environment"], + "properties": { + "repository": {"type": "string", "pattern": "^OpenAdaptAI/[A-Za-z0-9._-]+$"}, + "repository_id": {"type": "string", "pattern": "^[1-9][0-9]*$"}, + "repository_owner_id": {"const": "132681217"}, + "workflow": {"type": "string", "pattern": "^\\.github/workflows/[A-Za-z0-9._-]+\\.yml$"}, + "ref": {"const": "refs/heads/main"}, + "source_commit": {"$ref": "#/$defs/commit"}, + "environment": {"type": "string", "pattern": "^[a-z][a-z0-9-]{2,63}$"} + } + }, + "signing_authority": { + "type": "object", + "additionalProperties": false, + "required": [ + "repository", "repository_id", "repository_owner_id", "workflow", "ref", + "source_commit", "environment", "aws_account_id", "aws_region", "kms_key_arn", + "kms_key_spec", "kms_signing_algorithm", "kms_message_type", "role_arn" + ], + "properties": { + "repository": {"const": "OpenAdaptAI/.github"}, + "repository_id": {"const": "858454062"}, + "repository_owner_id": {"const": "132681217"}, + "workflow": {"const": ".github/workflows/sign-production-evidence.yml"}, + "ref": {"const": "refs/heads/main"}, + "source_commit": {"$ref": "#/$defs/commit"}, + "environment": {"type": "string", "pattern": "^[a-z][a-z0-9-]{2,63}$"}, + "aws_account_id": {"const": "992382684924"}, + "aws_region": {"const": "us-east-1"}, + "kms_key_arn": {"type": "string", "pattern": "^arn:aws:kms:us-east-1:992382684924:key/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"}, + "kms_key_spec": {"const": "ECC_NIST_P256"}, + "kms_signing_algorithm": {"const": "ECDSA_SHA_256"}, + "kms_message_type": {"const": "DIGEST"}, + "role_arn": {"const": "arn:aws:iam::992382684924:role/openadapt-public-trust-signer"} + } + } + } +} diff --git a/schemas/production-publication-recovery-authorization.schema.json b/schemas/production-publication-recovery-authorization.schema.json new file mode 100644 index 0000000..d1ff89f --- /dev/null +++ b/schemas/production-publication-recovery-authorization.schema.json @@ -0,0 +1,113 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "production-publication-recovery-authorization.schema.json", + "title": "OpenAdapt production publication recovery authorization v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", "authorization_id_sha256", + "qualification_release_reference", "qualification_release_object_sha256", + "release_sha256", "artifact_inventory_sha256", "publication_staging_sha256", + "repository", "repository_id", "target", "tag", "tag_ref", + "tag_object_id", "target_commit", "draft_release_id", "requested_effect", + "run_id", "run_attempt", "dispatcher_actor_id", "environment", + "release_app", "idempotency_key", "issued_at", "not_before", + "expires_at", "issuer" + ], + "properties": { + "schema_version": {"const": "openadapt.production-publication-recovery-authorization/v1"}, + "authorization_id_sha256": {"$ref": "#/$defs/digest"}, + "qualification_release_reference": {"$ref": "#/$defs/release_reference"}, + "qualification_release_object_sha256": {"$ref": "#/$defs/digest"}, + "release_sha256": {"$ref": "#/$defs/digest"}, + "artifact_inventory_sha256": {"$ref": "#/$defs/digest"}, + "publication_staging_sha256": {"$ref": "#/$defs/digest"}, + "repository": {"type": "string", "pattern": "^OpenAdaptAI/[A-Za-z0-9._-]+$"}, + "repository_id": {"$ref": "#/$defs/decimal_id"}, + "target": {"enum": ["agent", "capture", "desktop", "flow", "openadapt"]}, + "tag": {"type": "string", "pattern": "^v(0|[1-9][0-9]{0,9})\\.(0|[1-9][0-9]{0,9})\\.(0|[1-9][0-9]{0,9})(-[0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$"}, + "tag_ref": {"type": "string", "pattern": "^refs/tags/v(0|[1-9][0-9]{0,9})\\.(0|[1-9][0-9]{0,9})\\.(0|[1-9][0-9]{0,9})(-[0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$"}, + "tag_object_id": {"$ref": "#/$defs/commit"}, + "target_commit": {"$ref": "#/$defs/commit"}, + "draft_release_id": {"$ref": "#/$defs/decimal_id"}, + "requested_effect": { + "enum": [ + "stage-draft-assets", + "publish-pypi", + "publish-github-release", + "publish-mcp-registry" + ] + }, + "run_id": {"$ref": "#/$defs/decimal_id"}, + "run_attempt": {"const": "1"}, + "dispatcher_actor_id": {"const": "774615"}, + "environment": {"enum": ["release-identity", "pypi", "mcp-registry"]}, + "release_app": {"$ref": "#/$defs/release_app"}, + "idempotency_key": {"type": "string", "pattern": "^publication-recovery:[0-9a-f]{64}$"}, + "issued_at": {"$ref": "#/$defs/timestamp"}, + "not_before": {"$ref": "#/$defs/timestamp"}, + "expires_at": {"$ref": "#/$defs/timestamp"}, + "issuer": {"$ref": "#/$defs/issuer"} + }, + "allOf": [ + { + "if": {"properties": {"requested_effect": {"enum": ["stage-draft-assets", "publish-github-release"]}}}, + "then": {"properties": {"environment": {"const": "release-identity"}}} + }, + { + "if": {"properties": {"requested_effect": {"const": "publish-pypi"}}}, + "then": {"properties": {"environment": {"const": "pypi"}}} + }, + { + "if": {"properties": {"requested_effect": {"const": "publish-mcp-registry"}}}, + "then": { + "properties": { + "environment": {"const": "mcp-registry"}, + "target": {"const": "agent"} + } + } + } + ], + "$defs": { + "digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "decimal_id": {"type": "string", "pattern": "^[1-9][0-9]*$"}, + "commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "timestamp": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"}, + "release_reference": { + "allOf": [ + {"$ref": "production-evidence-object-reference.schema.json"}, + {"properties": { + "kind": {"const": "qualification-release"}, + "object_schema_version": {"const": "openadapt.qualification-release/v1"}, + "object_media_type": {"const": "application/vnd.openadapt.qualification-release+json;version=1"}, + "subject_sha256": {"type": "null"} + }} + ] + }, + "release_app": { + "type": "object", + "additionalProperties": false, + "required": ["app_id", "installation_id", "bot_user_id", "bot_login"], + "properties": { + "app_id": {"const": "4730708"}, + "installation_id": {"const": "156835568"}, + "bot_user_id": {"const": "321543906"}, + "bot_login": {"const": "openadapt-release[bot]"} + } + }, + "issuer": { + "type": "object", + "additionalProperties": false, + "required": ["repository", "repository_id", "repository_owner_id", "workflow", "ref", "source_commit", "environment"], + "properties": { + "repository": {"const": "OpenAdaptAI/.github"}, + "repository_id": {"const": "858454062"}, + "repository_owner_id": {"const": "132681217"}, + "workflow": {"const": ".github/workflows/issue-production-publication-recovery.yml"}, + "ref": {"const": "refs/heads/main"}, + "source_commit": {"$ref": "#/$defs/commit"}, + "environment": {"enum": ["mcp-registry", "pypi", "release-identity"]} + } + } + } +} diff --git a/schemas/production-release-artifact-inventory.schema.json b/schemas/production-release-artifact-inventory.schema.json new file mode 100644 index 0000000..01d2494 --- /dev/null +++ b/schemas/production-release-artifact-inventory.schema.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "production-release-artifact-inventory.schema.json", + "title": "OpenAdapt production release artifact inventory v1", + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "target", "claim_scope", "artifacts"], + "properties": { + "schema_version": {"const": "openadapt.production-release-artifact-inventory/v1"}, + "target": {"$ref": "#/$defs/target"}, + "claim_scope": {"type": "string", "pattern": "^production_[a-z]+$"}, + "artifacts": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": {"$ref": "#/$defs/artifact"} + } + }, + "$defs": { + "target": {"enum": ["agent", "capture", "cloud", "desktop", "docs", "flow", "openadapt"]}, + "digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "artifact": { + "type": "object", + "additionalProperties": false, + "required": ["name", "kind", "sha256", "size_bytes", "media_type", "publish_destinations"], + "properties": { + "name": {"type": "string", "pattern": "^[^/\\\\]+$", "minLength": 1, "maxLength": 255}, + "kind": {"type": "string", "pattern": "^[a-z][a-z0-9-]*$"}, + "sha256": {"$ref": "#/$defs/digest"}, + "size_bytes": {"type": "integer", "minimum": 1}, + "media_type": {"type": "string", "pattern": "^[^/]+/[^/]+$", "maxLength": 200}, + "publish_destinations": { + "type": "array", + "minItems": 1, + "maxItems": 3, + "uniqueItems": true, + "items": {"enum": ["deployment", "github-release", "pypi"]} + } + } + } + } +} diff --git a/schemas/production-release-authority-policy.schema.json b/schemas/production-release-authority-policy.schema.json new file mode 100644 index 0000000..177aea3 --- /dev/null +++ b/schemas/production-release-authority-policy.schema.json @@ -0,0 +1,71 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "production-release-authority-policy.schema.json", + "title": "OpenAdapt production release authority policy v1", + "type": "object", + "additionalProperties": false, + "required": [ + "$schema", "schema_version", "release_app", "repository_selection", + "permissions", "repositories", "tag_rulesets", "support_release_authorities" + ], + "properties": { + "$schema": {"const": "schemas/production-release-authority-policy.schema.json"}, + "schema_version": {"const": "openadapt.production-release-authority-policy/v1"}, + "release_app": { + "type": "object", + "additionalProperties": false, + "required": ["app_id", "app_slug", "installation_id", "bot_user_id", "bot_login"], + "properties": { + "app_id": {"const": "4730708"}, + "app_slug": {"const": "openadapt-release"}, + "installation_id": {"const": "156835568"}, + "bot_user_id": {"const": "321543906"}, + "bot_login": {"const": "openadapt-release[bot]"} + } + }, + "repository_selection": {"const": "selected"}, + "permissions": { + "type": "object", + "additionalProperties": false, + "required": ["administration", "contents", "metadata"], + "properties": { + "administration": {"const": "read"}, + "contents": {"const": "write"}, + "metadata": {"const": "read"} + } + }, + "repositories": { + "const": [ + {"target":"agent","repository":"OpenAdaptAI/openadapt-agent","repository_id":"1136136670"}, + {"target":"capture","repository":"OpenAdaptAI/openadapt-capture","repository_id":"1115283835"}, + {"target":"cloud","repository":"OpenAdaptAI/openadapt-cloud","repository_id":"1300570990"}, + {"target":"desktop","repository":"OpenAdaptAI/openadapt-desktop","repository_id":"1171291730"}, + {"target":"docs","repository":"OpenAdaptAI/openadapt-ops","repository_id":"1172011294"}, + {"target":"flow","repository":"OpenAdaptAI/openadapt-flow","repository_id":"1291376938"}, + {"target":"openadapt","repository":"OpenAdaptAI/OpenAdapt","repository_id":"627024850"} + ] + }, + "tag_rulesets": { + "type": "object", + "additionalProperties": false, + "required": ["creation_authority", "immutability"], + "properties": { + "creation_authority": {"const": "OpenAdapt policy: release tag creation"}, + "immutability": {"const": "OpenAdapt policy: immutable release tags"} + } + }, + "support_release_authorities": { + "const": [ + { + "id": "desktop-managed-ffmpeg-runtime", + "lifecycle": "Support", + "repository": "OpenAdaptAI/openadapt-desktop", + "repository_id": "1171291730", + "tag_pattern": "refs/tags/ffmpeg-runtime-v*", + "creation_ruleset": "OpenAdapt policy: FFmpeg runtime tag creation", + "immutability_ruleset": "OpenAdapt policy: immutable FFmpeg runtime tags" + } + ] + } + } +} diff --git a/schemas/qualification-admission.schema.json b/schemas/qualification-admission.schema.json new file mode 100644 index 0000000..79805df --- /dev/null +++ b/schemas/qualification-admission.schema.json @@ -0,0 +1,178 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "qualification-admission.schema.json", + "title": "OpenAdapt qualification admission v3", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "admission_id_sha256", + "organization_id_sha256", + "workflow_id_sha256", + "workflow_version_id_sha256", + "bundle_version", + "bundle_sha256", + "admitted_runtime_sha256", + "application_contract_sha256", + "environment_contract_sha256", + "input_contract_sha256", + "action_contract_sha256", + "identity_contract_sha256", + "effect_contract_sha256", + "policy_contract_sha256", + "evidence_authority_sha256", + "campaign_artifact_sha256", + "campaign_permit_sha256", + "decision_receipt_reference", + "decision_receipt_bundle_reference", + "signer_registry_sha256", + "revocation_state_sha256", + "entity_class", + "campaign_summary", + "local_identity_opening", + "verdict", + "issued_at", + "not_before", + "expires_at", + "issuer" + ], + "properties": { + "schema_version": {"const": "openadapt.qualification-admission/v3"}, + "admission_id_sha256": {"$ref": "#/$defs/digest"}, + "organization_id_sha256": {"$ref": "#/$defs/digest"}, + "workflow_id_sha256": {"$ref": "#/$defs/digest"}, + "workflow_version_id_sha256": {"$ref": "#/$defs/digest"}, + "bundle_version": { + "type": "string", + "minLength": 5, + "maxLength": 64, + "pattern": "^(0|[1-9][0-9]{0,9})\\.(0|[1-9][0-9]{0,9})\\.(0|[1-9][0-9]{0,9})(-[0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$" + }, + "bundle_sha256": {"$ref": "#/$defs/digest"}, + "admitted_runtime_sha256": {"$ref": "#/$defs/digest"}, + "application_contract_sha256": {"$ref": "#/$defs/digest"}, + "environment_contract_sha256": {"$ref": "#/$defs/digest"}, + "input_contract_sha256": {"$ref": "#/$defs/digest"}, + "action_contract_sha256": {"$ref": "#/$defs/digest"}, + "identity_contract_sha256": {"$ref": "#/$defs/digest"}, + "effect_contract_sha256": {"$ref": "#/$defs/digest"}, + "policy_contract_sha256": {"$ref": "#/$defs/digest"}, + "evidence_authority_sha256": {"$ref": "#/$defs/digest"}, + "campaign_artifact_sha256": {"$ref": "#/$defs/digest"}, + "campaign_permit_sha256": {"$ref": "#/$defs/digest"}, + "decision_receipt_reference": {"$ref": "#/$defs/decision_receipt_reference"}, + "decision_receipt_bundle_reference": {"$ref": "#/$defs/decision_receipt_bundle_reference"}, + "signer_registry_sha256": {"$ref": "#/$defs/digest"}, + "revocation_state_sha256": {"$ref": "#/$defs/digest"}, + "entity_class": {"type": "string", "pattern": "^[a-z][a-z0-9 -]{0,63}$"}, + "campaign_summary": {"$ref": "#/$defs/campaign_summary"}, + "local_identity_opening": {"$ref": "#/$defs/local_identity_opening"}, + "verdict": {"const": "accepted"}, + "issued_at": {"$ref": "#/$defs/timestamp"}, + "not_before": {"$ref": "#/$defs/timestamp"}, + "expires_at": {"$ref": "#/$defs/timestamp"}, + "issuer": {"$ref": "#/$defs/issuer"} + }, + "$defs": { + "digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "timestamp": {"type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"}, + "decision_receipt_reference": { + "allOf": [ + {"$ref": "production-evidence-object-reference.schema.json"}, + {"properties": { + "kind": {"const": "qualification-evidence-decision-receipt"}, + "object_schema_version": {"const": "openadapt.qualification-evidence-decision-receipt/v1"}, + "object_media_type": {"const": "application/vnd.openadapt.qualification-evidence-decision-receipt+json;version=1"}, + "subject_sha256": {"type": "null"} + }} + ] + }, + "decision_receipt_bundle_reference": { + "allOf": [ + {"$ref": "production-evidence-object-reference.schema.json"}, + {"properties": { + "kind": {"const": "qualification-evidence-decision-receipt-sigstore-bundle"}, + "object_schema_version": {"const": "application/vnd.dev.sigstore.bundle.v0.3+json"}, + "object_media_type": {"const": "application/vnd.dev.sigstore.bundle.v0.3+json"}, + "subject_sha256": {"$ref": "#/$defs/digest"} + }} + ] + }, + "campaign_counts": { + "type": "object", + "additionalProperties": false, + "required": [ + "task_condition_cell_count", "minimum_trials_per_cell", "observed_trial_count", + "silent_incorrect_success_count", "over_halt_count", "unsafe_effect_count", + "blind_retry_count", "replay_dispatch_count", "model_call_count", + "unplanned_intervention_count", "reconciliation_required_count", + "authenticated_bound_decision_count", "live_target_revalidation_count", + "policy_approved_repair_count", "approved_repair_count", + "retained_repair_evidence_count", "unverified_direct_action_count" + ], + "properties": { + "task_condition_cell_count": {"type": "integer", "minimum": 1}, + "minimum_trials_per_cell": {"type": "integer", "minimum": 3}, + "observed_trial_count": {"type": "integer", "minimum": 3}, + "silent_incorrect_success_count": {"type": "integer", "minimum": 0}, + "over_halt_count": {"type": "integer", "minimum": 0}, + "unsafe_effect_count": {"type": "integer", "minimum": 0}, + "blind_retry_count": {"type": "integer", "minimum": 0}, + "replay_dispatch_count": {"type": "integer", "minimum": 0}, + "model_call_count": {"type": "integer", "minimum": 0}, + "unplanned_intervention_count": {"type": "integer", "minimum": 0}, + "reconciliation_required_count": {"type": "integer", "minimum": 0}, + "authenticated_bound_decision_count": {"type": "integer", "minimum": 0}, + "live_target_revalidation_count": {"type": "integer", "minimum": 0}, + "policy_approved_repair_count": {"type": "integer", "minimum": 0}, + "approved_repair_count": {"type": "integer", "minimum": 0}, + "retained_repair_evidence_count": {"type": "integer", "minimum": 0}, + "unverified_direct_action_count": {"type": "integer", "minimum": 0} + } + }, + "campaign_summary": { + "type": "object", + "additionalProperties": false, + "required": ["healthy", "safe_halt", "idempotency_replay", "uncertain_delivery", "declared_attended", "governed_repair"], + "properties": { + "healthy": {"$ref": "#/$defs/campaign_counts"}, + "safe_halt": {"$ref": "#/$defs/campaign_counts"}, + "idempotency_replay": {"$ref": "#/$defs/campaign_counts"}, + "uncertain_delivery": {"$ref": "#/$defs/campaign_counts"}, + "declared_attended": {"$ref": "#/$defs/campaign_counts"}, + "governed_repair": {"$ref": "#/$defs/campaign_counts"} + } + }, + "local_identity_opening": { + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", "algorithm", "required", "customer_controlled_secret_required", + "exact_contract_match_required", "revalidation_before_actuation", "maximum_age_seconds" + ], + "properties": { + "schema_version": {"const": "openadapt.qualification-local-identity-opening/v1"}, + "algorithm": {"const": "hmac-sha256"}, + "required": {"const": true}, + "customer_controlled_secret_required": {"const": true}, + "exact_contract_match_required": {"const": true}, + "revalidation_before_actuation": {"const": true}, + "maximum_age_seconds": {"const": 60} + } + }, + "issuer": { + "type": "object", + "additionalProperties": false, + "required": ["repository", "repository_id", "repository_owner_id", "workflow", "ref", "source_commit", "environment"], + "properties": { + "repository": {"const": "OpenAdaptAI/.github"}, + "repository_id": {"const": "858454062"}, + "repository_owner_id": {"const": "132681217"}, + "workflow": {"const": ".github/workflows/issue-qualification-admission.yml"}, + "ref": {"const": "refs/heads/main"}, + "source_commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "environment": {"const": "qualification-admission"} + } + } + } +} diff --git a/schemas/qualification-authority-state-receipt.schema.json b/schemas/qualification-authority-state-receipt.schema.json new file mode 100644 index 0000000..51caabf --- /dev/null +++ b/schemas/qualification-authority-state-receipt.schema.json @@ -0,0 +1,62 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "qualification-authority-state-receipt.schema.json", + "title": "OpenAdapt qualification authority state receipt v2", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", "authority_state_sha256", "status", + "signer_registry_sha256", "signer_registry_identity_sha256", + "signer_registry_revision", "evidence_authority_sha256", "observed_at", + "not_before", "expires_at", "issuer_key_id", "algorithm", + "signing_statement", "signature", "issuer" + ], + "properties": { + "schema_version": {"const": "openadapt.qualification-authority-state-receipt/v2"}, + "authority_state_sha256": {"$ref": "#/$defs/digest"}, + "status": {"const": "active"}, + "signer_registry_sha256": {"$ref": "#/$defs/digest"}, + "signer_registry_identity_sha256": {"$ref": "#/$defs/digest"}, + "signer_registry_revision": {"type": "integer", "minimum": 1}, + "evidence_authority_sha256": {"$ref": "#/$defs/digest"}, + "observed_at": {"$ref": "#/$defs/timestamp"}, + "not_before": {"$ref": "#/$defs/timestamp"}, + "expires_at": {"$ref": "#/$defs/timestamp"}, + "issuer_key_id": {"type": "string", "pattern": "^qa-ed25519-[0-9a-f]{16}$"}, + "algorithm": {"const": "ed25519"}, + "signing_statement": { + "allOf": [ + {"$ref": "qualification-evidence-signing-statement.schema.json"}, + {"properties": { + "object_schema_version": { + "const": "openadapt.qualification-authority-state-receipt/v2" + }, + "signature_domain": { + "const": "OpenAdapt qualification authority state receipt v2\u0000" + } + }} + ] + }, + "signature": {"type": "string", "pattern": "^[A-Za-z0-9+/]{86}==$"}, + "issuer": {"$ref": "#/$defs/issuer"} + }, + "$defs": { + "digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "timestamp": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"}, + "issuer": { + "type": "object", + "additionalProperties": false, + "required": ["repository", "repository_id", "repository_owner_id", "workflow", "ref", "source_commit", "environment"], + "properties": { + "repository": {"const": "OpenAdaptAI/openadapt-ops"}, + "repository_id": {"const": "1172011294"}, + "repository_owner_id": {"const": "132681217"}, + "workflow": {"const": ".github/workflows/qualification-authority-state.yml"}, + "ref": {"const": "refs/heads/main"}, + "source_commit": {"$ref": "#/$defs/commit"}, + "environment": {"const": "qualification-authority-state"} + } + } + } +} diff --git a/schemas/qualification-evidence-decision-receipt.schema.json b/schemas/qualification-evidence-decision-receipt.schema.json new file mode 100644 index 0000000..5c0dd16 --- /dev/null +++ b/schemas/qualification-evidence-decision-receipt.schema.json @@ -0,0 +1,200 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "qualification-evidence-decision-receipt.schema.json", + "title": "OpenAdapt qualification evidence decision receipt v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "decision_identity_sha256", + "decision_revision", + "decision_commitment_sha256", + "evidence_manifest_sha256", + "evidence_manifest_readback_sha256", + "organization_id_sha256", + "workflow_id_sha256", + "workflow_version_id_sha256", + "bundle_version", + "bundle_sha256", + "admitted_runtime_sha256", + "application_contract_sha256", + "environment_contract_sha256", + "input_contract_sha256", + "action_contract_sha256", + "identity_contract_sha256", + "effect_contract_sha256", + "policy_contract_sha256", + "evidence_authority_contract_sha256", + "campaign_permit_sha256", + "signer_registry_sha256", + "revocation_state_sha256", + "entity_class", + "campaign_summary", + "verdict", + "issued_at", + "not_before", + "expires_at", + "issuer_key_id", + "algorithm", + "signing_statement", + "signature", + "campaign_artifact_sha256", + "issuer" + ], + "properties": { + "schema_version": {"const": "openadapt.qualification-evidence-decision-receipt/v1"}, + "decision_identity_sha256": {"$ref": "#/$defs/digest"}, + "decision_revision": {"type": "integer", "minimum": 1}, + "decision_commitment_sha256": {"$ref": "#/$defs/digest"}, + "evidence_manifest_sha256": {"$ref": "#/$defs/digest"}, + "evidence_manifest_readback_sha256": {"$ref": "#/$defs/digest"}, + "organization_id_sha256": {"$ref": "#/$defs/digest"}, + "workflow_id_sha256": {"$ref": "#/$defs/digest"}, + "workflow_version_id_sha256": {"$ref": "#/$defs/digest"}, + "bundle_version": { + "type": "string", + "minLength": 5, + "maxLength": 64, + "pattern": "^(0|[1-9][0-9]{0,9})\\.(0|[1-9][0-9]{0,9})\\.(0|[1-9][0-9]{0,9})(-[0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$" + }, + "bundle_sha256": {"$ref": "#/$defs/digest"}, + "admitted_runtime_sha256": {"$ref": "#/$defs/digest"}, + "application_contract_sha256": {"$ref": "#/$defs/digest"}, + "environment_contract_sha256": {"$ref": "#/$defs/digest"}, + "input_contract_sha256": {"$ref": "#/$defs/digest"}, + "action_contract_sha256": {"$ref": "#/$defs/digest"}, + "identity_contract_sha256": {"$ref": "#/$defs/digest"}, + "effect_contract_sha256": {"$ref": "#/$defs/digest"}, + "policy_contract_sha256": {"$ref": "#/$defs/digest"}, + "evidence_authority_contract_sha256": {"$ref": "#/$defs/digest"}, + "campaign_permit_sha256": {"$ref": "#/$defs/digest"}, + "signer_registry_sha256": {"$ref": "#/$defs/digest"}, + "revocation_state_sha256": {"$ref": "#/$defs/digest"}, + "entity_class": {"type": "string", "pattern": "^[a-z][a-z0-9 -]{0,63}$"}, + "campaign_summary": {"$ref": "#/$defs/campaign_summary"}, + "verdict": {"const": "ADMIT"}, + "issued_at": {"$ref": "#/$defs/timestamp"}, + "not_before": {"$ref": "#/$defs/timestamp"}, + "expires_at": {"$ref": "#/$defs/timestamp"}, + "issuer_key_id": {"type": "string", "pattern": "^qa-ed25519-[0-9a-f]{16}$"}, + "algorithm": {"const": "ed25519"}, + "signing_statement": {"$ref": "#/$defs/signing_statement"}, + "signature": {"type": "string", "pattern": "^[A-Za-z0-9+/]{86}==$"}, + "campaign_artifact_sha256": {"$ref": "#/$defs/digest"}, + "issuer": {"$ref": "#/$defs/issuer"} + }, + "$defs": { + "digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "timestamp": {"type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"}, + "signing_statement": { + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", "object_schema_version", "signature_domain", + "unsigned_object_sha256", "unsigned_size_bytes", "commitment_scheme" + ], + "properties": { + "schema_version": { + "const": "openadapt.qualification-evidence-signing-statement/v1" + }, + "object_schema_version": { + "const": "openadapt.qualification-evidence-decision-receipt/v1" + }, + "signature_domain": { + "const": "OpenAdapt qualification evidence decision receipt v1\u0000" + }, + "unsigned_object_sha256": {"$ref": "#/$defs/digest"}, + "unsigned_size_bytes": {"type": "integer", "minimum": 1}, + "commitment_scheme": {"const": "sha256-canonical-json-lf"} + } + }, + "issuer": { + "type": "object", + "additionalProperties": false, + "required": ["repository", "repository_id", "repository_owner_id", "workflow", "ref", "source_commit", "environment"], + "properties": { + "repository": {"const": "OpenAdaptAI/openadapt-internal"}, + "repository_id": {"const": "1170060695"}, + "repository_owner_id": {"const": "132681217"}, + "workflow": {"const": ".github/workflows/issue-private-qualification-evidence-decision.yml"}, + "ref": {"const": "refs/heads/main"}, + "source_commit": {"$ref": "#/$defs/commit"}, + "environment": {"const": "private-qualification-evidence-decision"} + } + }, + "campaign_counts": { + "type": "object", + "additionalProperties": false, + "required": [ + "task_condition_cell_count", + "minimum_trials_per_cell", + "observed_trial_count", + "silent_incorrect_success_count", + "over_halt_count", + "unsafe_effect_count", + "blind_retry_count", + "replay_dispatch_count", + "model_call_count", + "unplanned_intervention_count", + "reconciliation_required_count", + "authenticated_bound_decision_count", + "live_target_revalidation_count", + "policy_approved_repair_count", + "approved_repair_count", + "retained_repair_evidence_count", + "unverified_direct_action_count" + ], + "properties": { + "task_condition_cell_count": {"type": "integer", "minimum": 1}, + "minimum_trials_per_cell": {"type": "integer", "minimum": 3}, + "observed_trial_count": {"type": "integer", "minimum": 3}, + "silent_incorrect_success_count": {"type": "integer", "minimum": 0}, + "over_halt_count": {"type": "integer", "minimum": 0}, + "unsafe_effect_count": {"type": "integer", "minimum": 0}, + "blind_retry_count": {"type": "integer", "minimum": 0}, + "replay_dispatch_count": {"type": "integer", "minimum": 0}, + "model_call_count": {"type": "integer", "minimum": 0}, + "unplanned_intervention_count": {"type": "integer", "minimum": 0}, + "reconciliation_required_count": {"type": "integer", "minimum": 0}, + "authenticated_bound_decision_count": {"type": "integer", "minimum": 0}, + "live_target_revalidation_count": {"type": "integer", "minimum": 0}, + "policy_approved_repair_count": {"type": "integer", "minimum": 0}, + "approved_repair_count": {"type": "integer", "minimum": 0}, + "retained_repair_evidence_count": {"type": "integer", "minimum": 0}, + "unverified_direct_action_count": {"type": "integer", "minimum": 0} + } + }, + "campaign_classes": { + "type": "object", + "additionalProperties": false, + "required": ["healthy", "safe_halt", "idempotency_replay", "uncertain_delivery", "declared_attended", "governed_repair"], + "properties": { + "healthy": {"$ref": "#/$defs/campaign_counts"}, + "safe_halt": {"$ref": "#/$defs/campaign_counts"}, + "idempotency_replay": {"$ref": "#/$defs/campaign_counts"}, + "uncertain_delivery": {"$ref": "#/$defs/campaign_counts"}, + "declared_attended": {"$ref": "#/$defs/campaign_counts"}, + "governed_repair": {"$ref": "#/$defs/campaign_counts"} + } + }, + "campaign_summary": { + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "minimum_trials_per_task_condition", + "task_count", + "classes" + ], + "properties": { + "schema_version": { + "const": "openadapt.qualification-evidence-decision-campaign-summary/v1" + }, + "minimum_trials_per_task_condition": {"const": 3}, + "task_count": {"type": "integer", "minimum": 1}, + "classes": {"$ref": "#/$defs/campaign_classes"} + } + } + } +} diff --git a/schemas/qualification-evidence-signing-statement.schema.json b/schemas/qualification-evidence-signing-statement.schema.json new file mode 100644 index 0000000..8cfae6b --- /dev/null +++ b/schemas/qualification-evidence-signing-statement.schema.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "qualification-evidence-signing-statement.schema.json", + "title": "OpenAdapt qualification evidence signing statement v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "object_schema_version", + "signature_domain", + "unsigned_object_sha256", + "unsigned_size_bytes", + "commitment_scheme" + ], + "properties": { + "schema_version": { + "const": "openadapt.qualification-evidence-signing-statement/v1" + }, + "object_schema_version": { + "type": "string", + "pattern": "^openadapt\\.[a-z0-9.-]+/v[1-9][0-9]*$" + }, + "signature_domain": { + "type": "string", + "pattern": "^OpenAdapt .+\\u0000$" + }, + "unsigned_object_sha256": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$" + }, + "unsigned_size_bytes": {"type": "integer", "minimum": 1}, + "commitment_scheme": {"const": "sha256-canonical-json-lf"} + } +} diff --git a/schemas/qualification-release.schema.json b/schemas/qualification-release.schema.json new file mode 100644 index 0000000..387a369 --- /dev/null +++ b/schemas/qualification-release.schema.json @@ -0,0 +1,327 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "qualification-release.schema.json", + "title": "OpenAdapt qualification release v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "admission_id_sha256", + "target", + "verdict", + "claim_scope", + "release_identity", + "release", + "release_sha256", + "artifact_inventory_sha256", + "publication_staging", + "publication_staging_sha256", + "production_acceptance_summary_reference", + "production_acceptance_summary_bundle_reference", + "authority_state_sha256", + "revocation_state_sha256", + "signer_registry_sha256", + "publication_policy_sha256", + "issued_at", + "not_before", + "expires_at", + "issuer" + ], + "properties": { + "schema_version": {"const": "openadapt.qualification-release/v1"}, + "admission_id_sha256": {"$ref": "#/$defs/digest"}, + "target": {"$ref": "#/$defs/target"}, + "verdict": {"const": "accepted"}, + "claim_scope": {"type": "string", "pattern": "^production_[a-z]+$"}, + "release_identity": {"$ref": "#/$defs/release_identity"}, + "release": {"$ref": "#/$defs/release_candidate"}, + "release_sha256": {"$ref": "#/$defs/digest"}, + "artifact_inventory_sha256": {"$ref": "#/$defs/digest"}, + "publication_staging": {"$ref": "#/$defs/publication_staging"}, + "publication_staging_sha256": {"$ref": "#/$defs/digest"}, + "production_acceptance_summary_reference": {"$ref": "#/$defs/summary_reference"}, + "production_acceptance_summary_bundle_reference": {"$ref": "#/$defs/summary_bundle_reference"}, + "authority_state_sha256": {"$ref": "#/$defs/digest"}, + "revocation_state_sha256": {"$ref": "#/$defs/digest"}, + "signer_registry_sha256": {"$ref": "#/$defs/digest"}, + "publication_policy_sha256": {"$ref": "#/$defs/digest"}, + "issued_at": {"$ref": "#/$defs/timestamp"}, + "not_before": {"$ref": "#/$defs/timestamp"}, + "expires_at": {"$ref": "#/$defs/timestamp"}, + "issuer": {"$ref": "#/$defs/issuer"} + }, + "$defs": { + "target": {"enum": ["agent", "capture", "cloud", "desktop", "docs", "flow", "openadapt"]}, + "digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "decimal_id": {"type": "string", "pattern": "^[1-9][0-9]*$"}, + "timestamp": {"type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"}, + "release_identity": { + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "channel", "sequence", "previous_admission_sha256"], + "properties": { + "schema_version": {"const": "openadapt.monotonic-production-release/v1"}, + "channel": {"const": "production"}, + "sequence": {"type": "integer", "minimum": 1}, + "previous_admission_sha256": { + "oneOf": [ + {"$ref": "#/$defs/digest"}, + {"type": "null"} + ] + } + } + }, + "publish_destinations": { + "type": "array", + "minItems": 1, + "maxItems": 3, + "uniqueItems": true, + "items": {"enum": ["deployment", "github-release", "pypi"]} + }, + "artifact": { + "type": "object", + "additionalProperties": false, + "required": ["name", "kind", "sha256", "size_bytes", "media_type", "publish_destinations"], + "properties": { + "name": {"type": "string", "pattern": "^[^/\\\\]+$", "minLength": 1, "maxLength": 255}, + "kind": {"type": "string", "pattern": "^[a-z][a-z0-9-]*$"}, + "sha256": {"$ref": "#/$defs/digest"}, + "size_bytes": {"type": "integer", "minimum": 1}, + "media_type": {"type": "string", "pattern": "^[^/]+/[^/]+$", "maxLength": 200}, + "publish_destinations": {"$ref": "#/$defs/publish_destinations"} + } + }, + "release_candidate": { + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", "kind", "source_repository", "source_repository_id", + "source_commit", "version", "tag", "deployment_id", "deployment_sha256", "artifacts" + ], + "properties": { + "schema_version": {"const": "openadapt.production-release-candidate/v1"}, + "kind": {"enum": ["package", "deployment", "hybrid"]}, + "source_repository": {"type": "string", "pattern": "^OpenAdaptAI/[A-Za-z0-9._-]+$"}, + "source_repository_id": {"$ref": "#/$defs/decimal_id"}, + "source_commit": {"$ref": "#/$defs/commit"}, + "version": {"oneOf": [{"type": "string", "minLength": 1, "maxLength": 80}, {"type": "null"}]}, + "tag": {"oneOf": [{"type": "string", "minLength": 1, "maxLength": 120}, {"type": "null"}]}, + "deployment_id": {"oneOf": [{"$ref": "#/$defs/decimal_id"}, {"type": "null"}]}, + "deployment_sha256": {"oneOf": [{"$ref": "#/$defs/digest"}, {"type": "null"}]}, + "artifacts": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": {"$ref": "#/$defs/artifact"} + } + }, + "oneOf": [ + { + "properties": { + "kind": {"const": "package"}, + "version": {"type": "string", "minLength": 1, "maxLength": 80}, + "tag": {"type": "string", "minLength": 1, "maxLength": 120}, + "deployment_id": {"type": "null"}, + "deployment_sha256": {"type": "null"} + } + }, + { + "properties": { + "kind": {"const": "deployment"}, + "version": {"type": "null"}, + "tag": {"type": "null"}, + "deployment_id": {"$ref": "#/$defs/decimal_id"}, + "deployment_sha256": {"$ref": "#/$defs/digest"} + } + }, + { + "properties": { + "kind": {"const": "hybrid"}, + "version": {"type": "string", "minLength": 1, "maxLength": 80}, + "tag": {"type": "string", "minLength": 1, "maxLength": 120}, + "deployment_id": {"$ref": "#/$defs/decimal_id"}, + "deployment_sha256": {"$ref": "#/$defs/digest"} + } + } + ] + }, + "staged_asset": { + "type": "object", + "additionalProperties": false, + "required": [ + "asset_id", "name", "kind", "sha256", "size_bytes", "media_type", + "publish_destinations", "uploader_id", "uploader_login" + ], + "properties": { + "asset_id": {"$ref": "#/$defs/decimal_id"}, + "name": {"type": "string", "pattern": "^[^/\\\\]+$", "minLength": 1, "maxLength": 255}, + "kind": {"type": "string", "pattern": "^[a-z][a-z0-9-]*$"}, + "sha256": {"$ref": "#/$defs/digest"}, + "size_bytes": {"type": "integer", "minimum": 1}, + "media_type": {"type": "string", "pattern": "^[^/]+/[^/]+$", "maxLength": 200}, + "publish_destinations": {"$ref": "#/$defs/publish_destinations"}, + "uploader_id": {"const": "321543906"}, + "uploader_login": {"const": "openadapt-release[bot]"} + } + }, + "tag_conditions": { + "type": "object", + "additionalProperties": false, + "required": ["ref_name"], + "properties": { + "ref_name": { + "type": "object", + "additionalProperties": false, + "required": ["include", "exclude"], + "properties": { + "include": {"const": ["refs/tags/v*"]}, + "exclude": {"const": []} + } + } + } + }, + "tag_ruleset": { + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", "role", "repository", "repository_id", "ruleset_id", + "name", "target", "enforcement", "bypass_actors", "conditions", "rules" + ], + "properties": { + "schema_version": {"const": "openadapt.production-release-tag-ruleset/v1"}, + "role": {"enum": ["creation_authority", "immutability"]}, + "repository": {"type": "string", "pattern": "^OpenAdaptAI/[A-Za-z0-9._-]+$"}, + "repository_id": {"$ref": "#/$defs/decimal_id"}, + "ruleset_id": {"$ref": "#/$defs/decimal_id"}, + "name": {"type": "string", "minLength": 1, "maxLength": 100}, + "target": {"const": "tag"}, + "enforcement": {"const": "active"}, + "bypass_actors": {"type": "array"}, + "conditions": {"$ref": "#/$defs/tag_conditions"}, + "rules": {"type": "array"} + } + }, + "creation_ruleset": { + "allOf": [ + {"$ref": "#/$defs/tag_ruleset"}, + {"properties": { + "role": {"const": "creation_authority"}, + "name": {"const": "OpenAdapt policy: release tag creation"}, + "bypass_actors": {"const": [{"actor_id": "4730708", "actor_type": "Integration", "bypass_mode": "always"}]}, + "rules": {"const": [{"type": "creation"}]} + }} + ] + }, + "immutability_ruleset": { + "allOf": [ + {"$ref": "#/$defs/tag_ruleset"}, + {"properties": { + "role": {"const": "immutability"}, + "name": {"const": "OpenAdapt policy: immutable release tags"}, + "bypass_actors": {"const": []}, + "rules": {"const": [ + {"type": "deletion"}, + {"type": "non_fast_forward"}, + {"type": "update", "parameters": {"update_allows_fetch_and_merge": false}} + ]} + }} + ] + }, + "publication_staging": { + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", "repository", "repository_id", "draft_release_id", "tag", + "target_commitish", "draft", "prerelease", "release_app_id", + "release_app_installation_id", "release_app_bot_user_id", "release_author_login", + "assets", "immutable_releases", "immutable_releases_sha256", + "tag_rulesets", "tag_rulesets_sha256", "tag_ref_state", + "tag_ref_state_sha256", "observed_at" + ], + "properties": { + "schema_version": {"const": "openadapt.production-release-staging-evidence/v1"}, + "repository": {"type": "string", "pattern": "^OpenAdaptAI/[A-Za-z0-9._-]+$"}, + "repository_id": {"$ref": "#/$defs/decimal_id"}, + "draft_release_id": {"$ref": "#/$defs/decimal_id"}, + "tag": {"type": "string", "minLength": 1, "maxLength": 120}, + "target_commitish": {"$ref": "#/$defs/commit"}, + "draft": {"const": true}, + "prerelease": {"const": false}, + "release_app_id": {"const": "4730708"}, + "release_app_installation_id": {"const": "156835568"}, + "release_app_bot_user_id": {"const": "321543906"}, + "release_author_login": {"const": "openadapt-release[bot]"}, + "assets": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"$ref": "#/$defs/staged_asset"}}, + "immutable_releases": { + "type": "object", + "additionalProperties": false, + "required": ["enabled", "enforced_by_owner"], + "properties": { + "enabled": {"const": true}, + "enforced_by_owner": {"type": "boolean"} + } + }, + "immutable_releases_sha256": {"$ref": "#/$defs/digest"}, + "tag_rulesets": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "prefixItems": [ + {"$ref": "#/$defs/creation_ruleset"}, + {"$ref": "#/$defs/immutability_ruleset"} + ], + "items": false + }, + "tag_rulesets_sha256": {"$ref": "#/$defs/digest"}, + "tag_ref_state": { + "type": "object", + "additionalProperties": false, + "required": ["ref", "exists"], + "properties": { + "ref": {"type": "string", "pattern": "^refs/tags/v[^/]+$"}, + "exists": {"const": false} + } + }, + "tag_ref_state_sha256": {"$ref": "#/$defs/digest"}, + "observed_at": {"$ref": "#/$defs/timestamp"} + } + }, + "summary_reference": { + "allOf": [ + {"$ref": "production-evidence-object-reference.schema.json"}, + {"properties": { + "kind": {"const": "production-acceptance-summary"}, + "object_schema_version": {"const": "openadapt.production-lifecycle-evidence-summary/v2"}, + "object_media_type": {"const": "application/vnd.openadapt.production-lifecycle-evidence-summary+json;version=2"}, + "subject_sha256": {"type": "null"} + }} + ] + }, + "summary_bundle_reference": { + "allOf": [ + {"$ref": "production-evidence-object-reference.schema.json"}, + {"properties": { + "kind": {"const": "production-acceptance-summary-sigstore-bundle"}, + "object_schema_version": {"const": "application/vnd.dev.sigstore.bundle.v0.3+json"}, + "object_media_type": {"const": "application/vnd.dev.sigstore.bundle.v0.3+json"}, + "subject_sha256": {"$ref": "#/$defs/digest"} + }} + ] + }, + "issuer": { + "type": "object", + "additionalProperties": false, + "required": ["repository", "repository_id", "repository_owner_id", "workflow", "ref", "source_commit", "environment"], + "properties": { + "repository": {"const": "OpenAdaptAI/.github"}, + "repository_id": {"const": "858454062"}, + "repository_owner_id": {"const": "132681217"}, + "workflow": {"const": ".github/workflows/issue-production-release-admission.yml"}, + "ref": {"const": "refs/heads/main"}, + "source_commit": {"$ref": "#/$defs/commit"}, + "environment": {"const": "production-release-admission"} + } + } + } +} diff --git a/schemas/qualification-revocation-state-receipt.schema.json b/schemas/qualification-revocation-state-receipt.schema.json new file mode 100644 index 0000000..ffcbe3e --- /dev/null +++ b/schemas/qualification-revocation-state-receipt.schema.json @@ -0,0 +1,73 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "qualification-revocation-state-receipt.schema.json", + "title": "OpenAdapt qualification revocation state receipt v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", "revocation_state_sha256", "previous_revocation_state_sha256", + "revision", "status", "authority_state_sha256", "signer_registry_sha256", + "revocations", "observed_at", "not_before", "expires_at", "issuer_key_id", + "algorithm", "signing_statement", "signature", "issuer" + ], + "properties": { + "schema_version": {"const": "openadapt.qualification-revocation-state-receipt/v1"}, + "revocation_state_sha256": {"$ref": "#/$defs/digest"}, + "previous_revocation_state_sha256": {"oneOf": [{"$ref": "#/$defs/digest"}, {"type": "null"}]}, + "revision": {"type": "integer", "minimum": 1}, + "status": {"const": "current"}, + "authority_state_sha256": {"$ref": "#/$defs/digest"}, + "signer_registry_sha256": {"$ref": "#/$defs/digest"}, + "revocations": {"type": "array", "uniqueItems": true, "items": {"$ref": "#/$defs/revocation"}}, + "observed_at": {"$ref": "#/$defs/timestamp"}, + "not_before": {"$ref": "#/$defs/timestamp"}, + "expires_at": {"$ref": "#/$defs/timestamp"}, + "issuer_key_id": {"type": "string", "pattern": "^qa-ed25519-[0-9a-f]{16}$"}, + "algorithm": {"const": "ed25519"}, + "signing_statement": { + "allOf": [ + {"$ref": "qualification-evidence-signing-statement.schema.json"}, + {"properties": { + "object_schema_version": { + "const": "openadapt.qualification-revocation-state-receipt/v1" + }, + "signature_domain": { + "const": "OpenAdapt qualification revocation state receipt v1\u0000" + } + }} + ] + }, + "signature": {"type": "string", "pattern": "^[A-Za-z0-9+/]{86}==$"}, + "issuer": {"$ref": "#/$defs/issuer"} + }, + "$defs": { + "digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "timestamp": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"}, + "revocation": { + "type": "object", + "additionalProperties": false, + "required": ["subject_kind", "subject_id", "revoked_at", "reason_code"], + "properties": { + "subject_kind": {"type": "string", "pattern": "^[a-z][a-z0-9-]*$"}, + "subject_id": {"type": "string", "minLength": 1, "maxLength": 256}, + "revoked_at": {"$ref": "#/$defs/timestamp"}, + "reason_code": {"type": "string", "pattern": "^[a-z][a-z0-9_]*$"} + } + }, + "issuer": { + "type": "object", + "additionalProperties": false, + "required": ["repository", "repository_id", "repository_owner_id", "workflow", "ref", "source_commit", "environment"], + "properties": { + "repository": {"const": "OpenAdaptAI/openadapt-ops"}, + "repository_id": {"const": "1172011294"}, + "repository_owner_id": {"const": "132681217"}, + "workflow": {"const": ".github/workflows/qualification-revocation-state.yml"}, + "ref": {"const": "refs/heads/main"}, + "source_commit": {"$ref": "#/$defs/commit"}, + "environment": {"const": "qualification-revocation-state"} + } + } + } +} diff --git a/schemas/qualification-signer-registry.schema.json b/schemas/qualification-signer-registry.schema.json new file mode 100644 index 0000000..ec31819 --- /dev/null +++ b/schemas/qualification-signer-registry.schema.json @@ -0,0 +1,154 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "qualification-signer-registry.schema.json", + "title": "OpenAdapt qualification signer registry v2", + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "revision", "generated_at", "expires_at", "signers"], + "properties": { + "schema_version": {"const": "openadapt.qualification-signer-registry/v2"}, + "revision": {"type": "integer", "minimum": 1}, + "generated_at": {"$ref": "#/$defs/timestamp"}, + "expires_at": {"$ref": "#/$defs/timestamp"}, + "signers": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": {"$ref": "#/$defs/signer"} + } + }, + "$defs": { + "digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "timestamp": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$" + }, + "signer": { + "oneOf": [ + {"$ref": "#/$defs/ed25519_signer"}, + {"$ref": "#/$defs/public_trust_p256_signer"} + ] + }, + "ed25519_signer": { + "type": "object", + "additionalProperties": false, + "required": [ + "algorithm", "key_id", "public_key", "public_key_spki_der_base64", + "public_key_sha256", + "statement_schema_versions", "allowed_usages", "allowed_workflows", + "allowed_ref_prefixes", "status", "revoked_at" + ], + "properties": { + "algorithm": {"const": "ed25519"}, + "key_id": {"type": "string", "pattern": "^qa-ed25519-[0-9a-f]{16}$"}, + "public_key": {"type": "string", "pattern": "^[A-Za-z0-9_-]{43}$"}, + "public_key_spki_der_base64": { + "type": "string", + "pattern": "^[A-Za-z0-9+/]{59}=$" + }, + "public_key_sha256": {"$ref": "#/$defs/digest"}, + "statement_schema_versions": { + "const": ["openadapt.qualification-evidence-signing-statement/v1"] + }, + "allowed_usages": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "enum": [ + "private-qualification-evidence-decision", + "private-qualification-evidence-decision-final-manifest", + "private-qualification-evidence-decision-request", + "private-qualification-evidence-decision-storage-seal", + "private-qualification-evidence-recovery-receipt", + "private-qualification-evidence-manifest", + "qualification-authority-state-receipt", + "qualification-campaign-permit", + "qualification-evidence-decision-receipt", + "qualification-revocation-state-receipt", + "qualification-source-evidence-manifest", + "qualification-trial-receipt" + ] + } + }, + "allowed_workflows": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^https://github\\.com/OpenAdaptAI/[A-Za-z0-9_.-]+/\\.github/workflows/[A-Za-z0-9_.-]+\\.ya?ml@refs/heads/[A-Za-z0-9._/-]+$" + } + }, + "allowed_ref_prefixes": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": {"type": "string", "pattern": "^refs/"} + }, + "status": {"enum": ["active", "revoked"]}, + "revoked_at": { + "oneOf": [{"$ref": "#/$defs/timestamp"}, {"type": "null"}] + } + } + }, + "public_trust_p256_signer": { + "type": "object", + "additionalProperties": false, + "required": [ + "algorithm", "key_id", "key_origin", "kms_key_arn", "public_key", + "public_key_spki_der_base64", "public_key_sha256", "signature_encoding", + "statement_schema_versions", "allowed_usages", "allowed_kinds", + "allowed_workflows", "allowed_ref_prefixes", "allowed_environments", + "status", "revoked_at" + ], + "properties": { + "algorithm": {"const": "ecdsa-p256-sha256"}, + "key_id": {"type": "string", "pattern": "^oa-public-trust-p256-[0-9a-f]{16}$"}, + "key_origin": {"const": "aws-kms"}, + "kms_key_arn": {"type": "string", "pattern": "^arn:aws:kms:us-east-1:992382684924:key/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"}, + "public_key": {"type": "string", "pattern": "^[A-Za-z0-9_-]{87}$"}, + "public_key_spki_der_base64": {"type": "string", "pattern": "^[A-Za-z0-9+/]{122}==$"}, + "public_key_sha256": {"$ref": "#/$defs/digest"}, + "signature_encoding": {"const": "asn1-der-low-s-base64-rfc4648-padded"}, + "statement_schema_versions": {"const": ["openadapt.production-public-trust-signing-statement/v1"]}, + "allowed_usages": {"const": ["production-public-evidence"]}, + "allowed_kinds": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "enum": [ + "production-acceptance-manifest", + "production-acceptance-summary", + "production-cloud-deploy-authorization", + "production-cloud-deployment-result", + "production-current-default", + "production-deployment-observation", + "production-lifecycle-checkpoint", + "qualification-admission", + "qualification-authority-state-receipt", + "qualification-campaign-permit", + "qualification-campaign-permit-policy", + "qualification-campaign-permit-receipt", + "qualification-campaign-permit-request", + "qualification-evidence-decision-receipt", + "qualification-release", + "qualification-revocation-state-receipt", + "support-release-admission" + ] + } + }, + "allowed_workflows": { + "const": ["/OpenAdaptAI/.github/.github/workflows/sign-production-evidence.yml@refs/heads/main"] + }, + "allowed_ref_prefixes": {"const": ["refs/heads/main"]}, + "allowed_environments": {"const": ["public-trust-signing"]}, + "status": {"enum": ["active", "revoked"]}, + "revoked_at": { + "oneOf": [{"$ref": "#/$defs/timestamp"}, {"type": "null"}] + } + } + } + } +} diff --git a/schemas/qualification-trial-receipt-authority-policy.schema.json b/schemas/qualification-trial-receipt-authority-policy.schema.json new file mode 100644 index 0000000..721774e --- /dev/null +++ b/schemas/qualification-trial-receipt-authority-policy.schema.json @@ -0,0 +1,93 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "qualification-trial-receipt-authority-policy.schema.json", + "title": "OpenAdapt qualification trial receipt authority policy v1", + "type": "object", + "additionalProperties": false, + "required": [ + "$schema", "schema_version", "repository", "repository_id", + "repository_owner_id", "workflow", "ref", "trigger", "input", "signing", + "authorities" + ], + "properties": { + "$schema": {"const": "schemas/qualification-trial-receipt-authority-policy.schema.json"}, + "schema_version": {"const": "openadapt.qualification-trial-receipt-authority-policy/v1"}, + "repository": {"const": "OpenAdaptAI/openadapt-evals"}, + "repository_id": {"const": "1135998197"}, + "repository_owner_id": {"const": "132681217"}, + "workflow": {"const": ".github/workflows/issue-qualification-trial-receipts.yml"}, + "ref": {"const": "refs/heads/main"}, + "trigger": {"const": "workflow_dispatch"}, + "input": { + "type": "object", + "additionalProperties": false, + "required": ["name", "schema_version", "pattern", "atomically_consumed"], + "properties": { + "name": {"const": "campaign_request_handle"}, + "schema_version": {"const": "openadapt.opaque-one-use-campaign-request-handle/v1"}, + "pattern": {"const": "^qcrh_[A-Za-z0-9_-]{43}$"}, + "atomically_consumed": {"const": true} + } + }, + "signing": { + "type": "object", + "additionalProperties": false, + "required": [ + "algorithm", "signer_registry_usage", "distinct_key_per_receipt_type", + "outer_sigstore_bundle" + ], + "properties": { + "algorithm": {"const": "ed25519"}, + "signer_registry_usage": {"const": "qualification-trial-receipt"}, + "distinct_key_per_receipt_type": {"const": true}, + "outer_sigstore_bundle": {"const": false} + } + }, + "authorities": { + "type": "array", + "minItems": 8, + "maxItems": 8, + "prefixItems": [ + {"$ref": "#/$defs/runner"}, + {"$ref": "#/$defs/observer"}, + {"$ref": "#/$defs/delivery"}, + {"$ref": "#/$defs/decision"}, + {"$ref": "#/$defs/policy"}, + {"$ref": "#/$defs/repair"}, + {"$ref": "#/$defs/cleanup"}, + {"$ref": "#/$defs/cleanup_absence"} + ], + "items": false + } + }, + "$defs": { + "base": { + "type": "object", + "additionalProperties": false, + "required": ["receipt_type", "environment", "runner_labels"], + "properties": { + "receipt_type": {"enum": ["runner", "observer", "delivery", "decision", "policy", "repair", "cleanup", "cleanup_absence"]}, + "environment": {"type": "string", "pattern": "^qualification-trial-[a-z-]+$"}, + "runner_labels": { + "type": "array", + "minItems": 3, + "maxItems": 3, + "prefixItems": [ + {"const": "self-hosted"}, + {"const": "evidence-authority"}, + {"type": "string", "pattern": "^qualification-trial-[a-z-]+$"} + ], + "items": false + } + } + }, + "runner": {"allOf": [{"$ref": "#/$defs/base"}, {"properties": {"receipt_type": {"const": "runner"}, "environment": {"const": "qualification-trial-runner"}, "runner_labels": {"const": ["self-hosted", "evidence-authority", "qualification-trial-runner"]}}}]}, + "observer": {"allOf": [{"$ref": "#/$defs/base"}, {"properties": {"receipt_type": {"const": "observer"}, "environment": {"const": "qualification-trial-observer"}, "runner_labels": {"const": ["self-hosted", "evidence-authority", "qualification-trial-observer"]}}}]}, + "delivery": {"allOf": [{"$ref": "#/$defs/base"}, {"properties": {"receipt_type": {"const": "delivery"}, "environment": {"const": "qualification-trial-delivery"}, "runner_labels": {"const": ["self-hosted", "evidence-authority", "qualification-trial-delivery"]}}}]}, + "decision": {"allOf": [{"$ref": "#/$defs/base"}, {"properties": {"receipt_type": {"const": "decision"}, "environment": {"const": "qualification-trial-decision"}, "runner_labels": {"const": ["self-hosted", "evidence-authority", "qualification-trial-decision"]}}}]}, + "policy": {"allOf": [{"$ref": "#/$defs/base"}, {"properties": {"receipt_type": {"const": "policy"}, "environment": {"const": "qualification-trial-policy"}, "runner_labels": {"const": ["self-hosted", "evidence-authority", "qualification-trial-policy"]}}}]}, + "repair": {"allOf": [{"$ref": "#/$defs/base"}, {"properties": {"receipt_type": {"const": "repair"}, "environment": {"const": "qualification-trial-repair"}, "runner_labels": {"const": ["self-hosted", "evidence-authority", "qualification-trial-repair"]}}}]}, + "cleanup": {"allOf": [{"$ref": "#/$defs/base"}, {"properties": {"receipt_type": {"const": "cleanup"}, "environment": {"const": "qualification-trial-cleanup"}, "runner_labels": {"const": ["self-hosted", "evidence-authority", "qualification-trial-cleanup"]}}}]}, + "cleanup_absence": {"allOf": [{"$ref": "#/$defs/base"}, {"properties": {"receipt_type": {"const": "cleanup_absence"}, "environment": {"const": "qualification-trial-cleanup-absence"}, "runner_labels": {"const": ["self-hosted", "evidence-authority", "qualification-trial-cleanup-absence"]}}}]} + } +} diff --git a/schemas/qualification-worker-admission-verifier-result.schema.json b/schemas/qualification-worker-admission-verifier-result.schema.json new file mode 100644 index 0000000..9bd3d16 --- /dev/null +++ b/schemas/qualification-worker-admission-verifier-result.schema.json @@ -0,0 +1,57 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "qualification-worker-admission-verifier-result.schema.json", + "title": "OpenAdapt qualification worker admission verifier result v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", "verdict", "admission_object_sha256", + "provider_identity_sha256", "worker_identity_sha256", + "live_provider_observation_sha256", "admitted_runtime_sha256", + "worker_image_sha256", "baseline_sha256", "host_identity_sha256", + "tls_identity_sha256", "egress_policy_sha256", "campaign_permit_sha256", + "capability_handle_sha256", "authority_state_sha256", + "revocation_state_sha256", "signer_registry_sha256", "not_before", + "expires_at", "verified_at", "verifier" + ], + "properties": { + "schema_version": {"const": "openadapt.qualification-worker-admission-verifier-result/v1"}, + "verdict": {"const": "accepted"}, + "admission_object_sha256": {"$ref": "#/$defs/digest"}, + "provider_identity_sha256": {"$ref": "#/$defs/digest"}, + "worker_identity_sha256": {"$ref": "#/$defs/digest"}, + "live_provider_observation_sha256": {"$ref": "#/$defs/digest"}, + "admitted_runtime_sha256": {"$ref": "#/$defs/digest"}, + "worker_image_sha256": {"$ref": "#/$defs/digest"}, + "baseline_sha256": {"$ref": "#/$defs/digest"}, + "host_identity_sha256": {"$ref": "#/$defs/digest"}, + "tls_identity_sha256": {"$ref": "#/$defs/digest"}, + "egress_policy_sha256": {"$ref": "#/$defs/digest"}, + "campaign_permit_sha256": {"$ref": "#/$defs/digest"}, + "capability_handle_sha256": {"$ref": "#/$defs/digest"}, + "authority_state_sha256": {"$ref": "#/$defs/digest"}, + "revocation_state_sha256": {"$ref": "#/$defs/digest"}, + "signer_registry_sha256": {"$ref": "#/$defs/digest"}, + "not_before": {"$ref": "#/$defs/timestamp"}, + "expires_at": {"$ref": "#/$defs/timestamp"}, + "verified_at": {"$ref": "#/$defs/timestamp"}, + "verifier": { + "type": "object", + "additionalProperties": false, + "required": ["repository", "repository_id", "repository_owner_id", "workflow", "source_commit", "runner_environment"], + "properties": { + "repository": {"const": "OpenAdaptAI/.github"}, + "repository_id": {"const": "858454062"}, + "repository_owner_id": {"const": "132681217"}, + "workflow": {"const": ".github/workflows/verify-qualification-worker-admission.yml"}, + "source_commit": {"$ref": "#/$defs/commit"}, + "runner_environment": {"const": "github-hosted"} + } + } + }, + "$defs": { + "digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "timestamp": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"} + } +} diff --git a/schemas/qualification-worker-admission.schema.json b/schemas/qualification-worker-admission.schema.json new file mode 100644 index 0000000..0180b31 --- /dev/null +++ b/schemas/qualification-worker-admission.schema.json @@ -0,0 +1,57 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "qualification-worker-admission.schema.json", + "title": "OpenAdapt qualification worker admission v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", "admission_id_sha256", "provider_identity_sha256", + "worker_identity_sha256", "live_provider_observation_sha256", + "admitted_runtime_sha256", "worker_image_sha256", "baseline_sha256", + "host_identity_sha256", "tls_identity_sha256", "egress_policy_sha256", + "campaign_permit_sha256", "capability_handle_sha256", + "authority_state_sha256", "revocation_state_sha256", + "signer_registry_sha256", "issued_at", "not_before", "expires_at", "issuer" + ], + "properties": { + "schema_version": {"const": "openadapt.qualification-worker-admission/v1"}, + "admission_id_sha256": {"$ref": "#/$defs/digest"}, + "provider_identity_sha256": {"$ref": "#/$defs/digest"}, + "worker_identity_sha256": {"$ref": "#/$defs/digest"}, + "live_provider_observation_sha256": {"$ref": "#/$defs/digest"}, + "admitted_runtime_sha256": {"$ref": "#/$defs/digest"}, + "worker_image_sha256": {"$ref": "#/$defs/digest"}, + "baseline_sha256": {"$ref": "#/$defs/digest"}, + "host_identity_sha256": {"$ref": "#/$defs/digest"}, + "tls_identity_sha256": {"$ref": "#/$defs/digest"}, + "egress_policy_sha256": {"$ref": "#/$defs/digest"}, + "campaign_permit_sha256": {"$ref": "#/$defs/digest"}, + "capability_handle_sha256": {"$ref": "#/$defs/digest"}, + "authority_state_sha256": {"$ref": "#/$defs/digest"}, + "revocation_state_sha256": {"$ref": "#/$defs/digest"}, + "signer_registry_sha256": {"$ref": "#/$defs/digest"}, + "issued_at": {"$ref": "#/$defs/timestamp"}, + "not_before": {"$ref": "#/$defs/timestamp"}, + "expires_at": {"$ref": "#/$defs/timestamp"}, + "issuer": {"$ref": "#/$defs/issuer"} + }, + "$defs": { + "digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "timestamp": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"}, + "issuer": { + "type": "object", + "additionalProperties": false, + "required": ["repository", "repository_id", "repository_owner_id", "workflow", "ref", "source_commit", "environment"], + "properties": { + "repository": {"const": "OpenAdaptAI/.github"}, + "repository_id": {"const": "858454062"}, + "repository_owner_id": {"const": "132681217"}, + "workflow": {"const": ".github/workflows/issue-qualification-worker-admission.yml"}, + "ref": {"const": "refs/heads/main"}, + "source_commit": {"$ref": "#/$defs/commit"}, + "environment": {"const": "qualification-worker-admission"} + } + } + } +} diff --git a/schemas/qualification-worker-dispatch.schema.json b/schemas/qualification-worker-dispatch.schema.json new file mode 100644 index 0000000..8f2104a --- /dev/null +++ b/schemas/qualification-worker-dispatch.schema.json @@ -0,0 +1,58 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "qualification-worker-dispatch.schema.json", + "title": "OpenAdapt qualification worker dispatch v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", "dispatch_id_sha256", "worker_admission_sha256", + "provider_identity_sha256", "worker_identity_sha256", + "live_provider_observation_sha256", "admitted_runtime_sha256", "run_id", + "run_attempt", "start_id_sha256", "task_id_sha256", + "task_condition_sha256", "campaign_artifact_sha256", "process_lease_sha256", + "capability_handle_sha256", "idempotency_key", "issued_at", "not_before", + "expires_at", "issuer" + ], + "properties": { + "schema_version": {"const": "openadapt.qualification-worker-dispatch/v1"}, + "dispatch_id_sha256": {"$ref": "#/$defs/digest"}, + "worker_admission_sha256": {"$ref": "#/$defs/digest"}, + "provider_identity_sha256": {"$ref": "#/$defs/digest"}, + "worker_identity_sha256": {"$ref": "#/$defs/digest"}, + "live_provider_observation_sha256": {"$ref": "#/$defs/digest"}, + "admitted_runtime_sha256": {"$ref": "#/$defs/digest"}, + "run_id": {"$ref": "#/$defs/decimal_id"}, + "run_attempt": {"const": "1"}, + "start_id_sha256": {"$ref": "#/$defs/digest"}, + "task_id_sha256": {"$ref": "#/$defs/digest"}, + "task_condition_sha256": {"$ref": "#/$defs/digest"}, + "campaign_artifact_sha256": {"$ref": "#/$defs/digest"}, + "process_lease_sha256": {"$ref": "#/$defs/digest"}, + "capability_handle_sha256": {"$ref": "#/$defs/digest"}, + "idempotency_key": {"type": "string", "pattern": "^qualification-worker-dispatch:[0-9a-f]{64}$"}, + "issued_at": {"$ref": "#/$defs/timestamp"}, + "not_before": {"$ref": "#/$defs/timestamp"}, + "expires_at": {"$ref": "#/$defs/timestamp"}, + "issuer": {"$ref": "#/$defs/issuer"} + }, + "$defs": { + "digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "decimal_id": {"type": "string", "pattern": "^[1-9][0-9]*$"}, + "commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "timestamp": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"}, + "issuer": { + "type": "object", + "additionalProperties": false, + "required": ["repository", "repository_id", "repository_owner_id", "workflow", "ref", "source_commit", "environment"], + "properties": { + "repository": {"const": "OpenAdaptAI/.github"}, + "repository_id": {"const": "858454062"}, + "repository_owner_id": {"const": "132681217"}, + "workflow": {"const": ".github/workflows/issue-qualification-worker-dispatch.yml"}, + "ref": {"const": "refs/heads/main"}, + "source_commit": {"$ref": "#/$defs/commit"}, + "environment": {"const": "qualification-worker-dispatch"} + } + } + } +} diff --git a/schemas/qualification-worker-task-condition.schema.json b/schemas/qualification-worker-task-condition.schema.json new file mode 100644 index 0000000..d21f900 --- /dev/null +++ b/schemas/qualification-worker-task-condition.schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "qualification-worker-task-condition.schema.json", + "title": "OpenAdapt qualification worker task condition v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "task_id_sha256", + "condition_source_sha256", + "condition_ordinal", + "task_condition_sha256" + ], + "properties": { + "schema_version": { + "const": "openadapt.qualification-worker-task-condition/v1" + }, + "task_id_sha256": {"$ref": "#/$defs/digest"}, + "condition_source_sha256": {"$ref": "#/$defs/digest"}, + "condition_ordinal": {"type": "integer", "minimum": 1}, + "task_condition_sha256": {"$ref": "#/$defs/digest"} + }, + "$defs": { + "digest": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$" + } + } +} diff --git a/schemas/qualification-worker-task-selector.schema.json b/schemas/qualification-worker-task-selector.schema.json new file mode 100644 index 0000000..bd72a3a --- /dev/null +++ b/schemas/qualification-worker-task-selector.schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "qualification-worker-task-selector.schema.json", + "title": "OpenAdapt qualification worker task selector v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "campaign_artifact_sha256", + "task_source_sha256", + "task_ordinal", + "task_id_sha256" + ], + "properties": { + "schema_version": { + "const": "openadapt.qualification-worker-task-selector/v1" + }, + "campaign_artifact_sha256": {"$ref": "#/$defs/digest"}, + "task_source_sha256": {"$ref": "#/$defs/digest"}, + "task_ordinal": {"type": "integer", "minimum": 1}, + "task_id_sha256": {"$ref": "#/$defs/digest"} + }, + "$defs": { + "digest": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$" + } + } +} diff --git a/schemas/qualification-worker-terminal-receipt.schema.json b/schemas/qualification-worker-terminal-receipt.schema.json new file mode 100644 index 0000000..79553a6 --- /dev/null +++ b/schemas/qualification-worker-terminal-receipt.schema.json @@ -0,0 +1,177 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "qualification-worker-terminal-receipt.schema.json", + "title": "OpenAdapt qualification worker terminal receipt v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", "receipt_id_sha256", "worker_admission_sha256", + "dispatch_id_sha256", "provider_identity_sha256", "worker_identity_sha256", + "live_provider_observation_sha256", "admitted_runtime_sha256", + "run_id", "run_attempt", "start_id_sha256", "task_id_sha256", + "task_condition_sha256", "capability_handle_sha256", "launch_attempt", + "launch_attempt_sha256", "process", + "oracle_sha256", "result_sha256", "log_sha256", "burned_identities_sha256", + "burn_ledger_revision", "burn_receipt_sha256", "burned_at", + "ledger_readback_sha256", "effect_started", "delivery_state", + "terminal_state", "exit_code", "uncertainty_sha256", "quarantine", + "completed_at", "issuer" + ], + "properties": { + "schema_version": {"const": "openadapt.qualification-worker-terminal-receipt/v1"}, + "receipt_id_sha256": {"$ref": "#/$defs/digest"}, + "worker_admission_sha256": {"$ref": "#/$defs/digest"}, + "dispatch_id_sha256": {"$ref": "#/$defs/digest"}, + "provider_identity_sha256": {"$ref": "#/$defs/digest"}, + "worker_identity_sha256": {"$ref": "#/$defs/digest"}, + "live_provider_observation_sha256": {"$ref": "#/$defs/digest"}, + "admitted_runtime_sha256": {"$ref": "#/$defs/digest"}, + "run_id": {"$ref": "#/$defs/decimal_id"}, + "run_attempt": {"const": "1"}, + "start_id_sha256": {"$ref": "#/$defs/digest"}, + "task_id_sha256": {"$ref": "#/$defs/digest"}, + "task_condition_sha256": {"$ref": "#/$defs/digest"}, + "capability_handle_sha256": {"$ref": "#/$defs/digest"}, + "launch_attempt": { + "type": "object", + "additionalProperties": false, + "required": ["attempted_at", "host_identity_sha256", "executable_sha256", "capability_handle_sha256", "evidence_sha256", "child_created", "failure_classification"], + "properties": { + "attempted_at": {"$ref": "#/$defs/timestamp"}, + "host_identity_sha256": {"$ref": "#/$defs/digest"}, + "executable_sha256": {"$ref": "#/$defs/digest"}, + "capability_handle_sha256": {"$ref": "#/$defs/digest"}, + "evidence_sha256": {"$ref": "#/$defs/digest"}, + "child_created": {"type": "boolean"}, + "failure_classification": { + "oneOf": [ + {"enum": ["PROCESS_START_REFUSED", "PROCESS_START_FAILED"]}, + {"type": "null"} + ] + } + } + }, + "launch_attempt_sha256": {"$ref": "#/$defs/digest"}, + "process": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["pid", "process_group_id", "process_start_ticks", "launched_at", "executable_sha256", "process_start_identity_sha256"], + "properties": { + "pid": {"type": "integer", "minimum": 1}, + "process_group_id": {"type": "integer", "minimum": 1}, + "process_start_ticks": {"type": "string", "pattern": "^[1-9][0-9]*$"}, + "launched_at": {"$ref": "#/$defs/timestamp"}, + "executable_sha256": {"$ref": "#/$defs/digest"}, + "process_start_identity_sha256": {"$ref": "#/$defs/digest"} + } + }, + {"type": "null"} + ] + }, + "oracle_sha256": {"$ref": "#/$defs/digest"}, + "result_sha256": {"$ref": "#/$defs/digest"}, + "log_sha256": {"$ref": "#/$defs/digest"}, + "burned_identities_sha256": {"$ref": "#/$defs/digest"}, + "burn_ledger_revision": {"type": "integer", "minimum": 1}, + "burn_receipt_sha256": {"$ref": "#/$defs/digest"}, + "burned_at": {"$ref": "#/$defs/timestamp"}, + "ledger_readback_sha256": {"$ref": "#/$defs/digest"}, + "effect_started": {"type": "boolean"}, + "delivery_state": {"enum": ["not_started", "verified", "uncertain"]}, + "terminal_state": {"enum": ["VERIFIED", "SAFE_HALT", "RECONCILIATION_REQUIRED", "QUARANTINED", "PRELAUNCH_QUARANTINED"]}, + "exit_code": {"type": ["integer", "null"]}, + "uncertainty_sha256": {"oneOf": [{"$ref": "#/$defs/digest"}, {"type": "null"}]}, + "quarantine": { + "type": "object", + "additionalProperties": false, + "required": ["active", "reason_code", "evidence_sha256"], + "properties": { + "active": {"type": "boolean"}, + "reason_code": {"type": ["string", "null"], "pattern": "^[A-Z][A-Z0-9_]{2,63}$"}, + "evidence_sha256": {"oneOf": [{"$ref": "#/$defs/digest"}, {"type": "null"}]} + } + }, + "completed_at": {"$ref": "#/$defs/timestamp"}, + "issuer": {"$ref": "#/$defs/issuer"} + }, + "allOf": [ + { + "if": {"properties": {"process": {"type": "null"}}}, + "then": { + "properties": { + "effect_started": {"const": false}, + "delivery_state": {"const": "not_started"}, + "terminal_state": {"const": "PRELAUNCH_QUARANTINED"}, + "uncertainty_sha256": {"type": "null"}, + "launch_attempt": { + "properties": { + "child_created": {"const": false}, + "failure_classification": {"enum": ["PROCESS_START_REFUSED", "PROCESS_START_FAILED"]} + } + }, + "quarantine": { + "properties": { + "active": {"const": true}, + "reason_code": {"type": "string", "pattern": "^[A-Z][A-Z0-9_]{2,63}$"}, + "evidence_sha256": {"$ref": "#/$defs/digest"} + } + } + } + } + }, + { + "if": {"properties": {"terminal_state": {"const": "PRELAUNCH_QUARANTINED"}}}, + "then": {"properties": {"process": {"type": "null"}}} + }, + { + "if": {"properties": {"terminal_state": {"not": {"const": "PRELAUNCH_QUARANTINED"}}}}, + "then": { + "properties": { + "process": {"type": "object"}, + "launch_attempt": {"properties": {"child_created": {"const": true}}} + } + } + }, + { + "if": {"properties": {"delivery_state": {"const": "uncertain"}}}, + "then": { + "properties": { + "effect_started": {"const": true}, + "terminal_state": {"enum": ["RECONCILIATION_REQUIRED", "QUARANTINED"]}, + "uncertainty_sha256": {"$ref": "#/$defs/digest"}, + "quarantine": {"properties": {"active": {"const": true}}} + } + } + }, + { + "if": {"properties": {"terminal_state": {"const": "VERIFIED"}}}, + "then": {"properties": {"delivery_state": {"const": "verified"}, "uncertainty_sha256": {"type": "null"}}} + }, + { + "if": {"properties": {"terminal_state": {"const": "SAFE_HALT"}}}, + "then": {"properties": {"effect_started": {"const": false}, "delivery_state": {"const": "not_started"}, "uncertainty_sha256": {"type": "null"}}} + } + ], + "$defs": { + "digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "decimal_id": {"type": "string", "pattern": "^[1-9][0-9]*$"}, + "commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "timestamp": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"}, + "issuer": { + "type": "object", + "additionalProperties": false, + "required": ["repository", "repository_id", "repository_owner_id", "workflow", "ref", "source_commit", "environment"], + "properties": { + "repository": {"const": "OpenAdaptAI/.github"}, + "repository_id": {"const": "858454062"}, + "repository_owner_id": {"const": "132681217"}, + "workflow": {"const": ".github/workflows/issue-qualification-worker-terminal-receipt.yml"}, + "ref": {"const": "refs/heads/main"}, + "source_commit": {"$ref": "#/$defs/commit"}, + "environment": {"const": "qualification-worker-terminal-receipt"} + } + } + } +} diff --git a/schemas/support-release-admission.schema.json b/schemas/support-release-admission.schema.json new file mode 100644 index 0000000..8763bae --- /dev/null +++ b/schemas/support-release-admission.schema.json @@ -0,0 +1,68 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "support-release-admission.schema.json", + "title": "OpenAdapt Support release admission v1", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", "admission_id_sha256", "lifecycle_state", + "support_target", "verdict", "claim_scope", "release_identity", + "release", "release_sha256", "artifact_inventory_sha256", + "publication_staging", "publication_staging_sha256", + "authority_state_sha256", "revocation_state_sha256", + "signer_registry_sha256", "support_policy_sha256", "issued_at", + "not_before", "expires_at", "issuer" + ], + "properties": { + "schema_version": {"const": "openadapt.support-release-admission/v1"}, + "admission_id_sha256": {"$ref": "#/$defs/digest"}, + "lifecycle_state": {"const": "Support"}, + "support_target": {"const": "openadapt-tray"}, + "verdict": {"const": "accepted"}, + "claim_scope": {"const": "support_openadapt_tray"}, + "release_identity": {"$ref": "#/$defs/release_identity"}, + "release": {"$ref": "qualification-release.schema.json#/$defs/release_candidate"}, + "release_sha256": {"$ref": "#/$defs/digest"}, + "artifact_inventory_sha256": {"$ref": "#/$defs/digest"}, + "publication_staging": {"$ref": "qualification-release.schema.json#/$defs/publication_staging"}, + "publication_staging_sha256": {"$ref": "#/$defs/digest"}, + "authority_state_sha256": {"$ref": "#/$defs/digest"}, + "revocation_state_sha256": {"$ref": "#/$defs/digest"}, + "signer_registry_sha256": {"$ref": "#/$defs/digest"}, + "support_policy_sha256": {"$ref": "#/$defs/digest"}, + "issued_at": {"$ref": "#/$defs/timestamp"}, + "not_before": {"$ref": "#/$defs/timestamp"}, + "expires_at": {"$ref": "#/$defs/timestamp"}, + "issuer": {"$ref": "#/$defs/issuer"} + }, + "$defs": { + "digest": {"type": "string", "pattern": "^sha256:[0-9a-f]{64}$"}, + "commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "timestamp": {"type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"}, + "release_identity": { + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "channel", "sequence", "previous_admission_sha256"], + "properties": { + "schema_version": {"const": "openadapt.monotonic-support-release/v1"}, + "channel": {"const": "support"}, + "sequence": {"type": "integer", "minimum": 1}, + "previous_admission_sha256": {"oneOf": [{"$ref": "#/$defs/digest"}, {"type": "null"}]} + } + }, + "issuer": { + "type": "object", + "additionalProperties": false, + "required": ["repository", "repository_id", "repository_owner_id", "workflow", "ref", "source_commit", "environment"], + "properties": { + "repository": {"const": "OpenAdaptAI/.github"}, + "repository_id": {"const": "858454062"}, + "repository_owner_id": {"const": "132681217"}, + "workflow": {"const": ".github/workflows/issue-support-release-admission.yml"}, + "ref": {"const": "refs/heads/main"}, + "source_commit": {"$ref": "#/$defs/commit"}, + "environment": {"const": "support-release-admission"} + } + } + } +} diff --git a/schemas/support-release-policy.schema.json b/schemas/support-release-policy.schema.json new file mode 100644 index 0000000..2c8098b --- /dev/null +++ b/schemas/support-release-policy.schema.json @@ -0,0 +1,87 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "support-release-policy.schema.json", + "title": "OpenAdapt Support release policy v1", + "type": "object", + "additionalProperties": false, + "required": [ + "$schema", "schema_version", "lifecycle_state", "production_projection", + "maximum_admission_days", "release_authority", "targets" + ], + "properties": { + "$schema": {"const": "schemas/support-release-policy.schema.json"}, + "schema_version": {"const": "openadapt.support-release-policy/v1"}, + "lifecycle_state": {"const": "Support"}, + "production_projection": {"const": false}, + "maximum_admission_days": {"const": 7}, + "release_authority": { + "type": "object", + "additionalProperties": false, + "required": [ + "app_id", "app_slug", "installation_id", "bot_user_id", "bot_login", + "admission_environment", "effect_environment", + "tag_creation_ruleset", "tag_immutability_ruleset" + ], + "properties": { + "app_id": {"const": "4730708"}, + "app_slug": {"const": "openadapt-release"}, + "installation_id": {"const": "156835568"}, + "bot_user_id": {"const": "321543906"}, + "bot_login": {"const": "openadapt-release[bot]"}, + "admission_environment": {"const": "support-release-admission"}, + "effect_environment": {"const": "release-identity"}, + "tag_creation_ruleset": {"const": "OpenAdapt policy: release tag creation"}, + "tag_immutability_ruleset": {"const": "OpenAdapt policy: immutable release tags"} + } + }, + "targets": { + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", "repository", "repository_id", "claim_scope", "release_kind", + "tag_pattern", "artifacts" + ], + "properties": { + "id": {"const": "openadapt-tray"}, + "repository": {"const": "OpenAdaptAI/openadapt-tray"}, + "repository_id": {"const": "1136122737"}, + "claim_scope": {"const": "support_openadapt_tray"}, + "release_kind": {"const": "package"}, + "tag_pattern": {"const": "refs/tags/v*"}, + "artifacts": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "prefixItems": [ + { + "type": "object", + "additionalProperties": false, + "required": ["kind", "media_type", "publish_destinations"], + "properties": { + "kind": {"const": "python-sdist"}, + "media_type": {"const": "application/gzip"}, + "publish_destinations": {"const": ["github-release", "pypi"]} + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["kind", "media_type", "publish_destinations"], + "properties": { + "kind": {"const": "python-wheel"}, + "media_type": {"const": "application/zip"}, + "publish_destinations": {"const": ["github-release", "pypi"]} + } + } + ], + "items": false + } + } + } + } + } +} diff --git a/scripts/make_evidence_reference.py b/scripts/make_evidence_reference.py new file mode 100644 index 0000000..e233819 --- /dev/null +++ b/scripts/make_evidence_reference.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Emit an exact-commit v2 reference for one registered entry.""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + +import validate_evidence_registry as registry + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--registry", default=str(registry.REGISTRY_PATH)) + parser.add_argument("--registry-source-commit", required=True) + parser.add_argument("--object-sha256", required=True) + args = parser.parse_args(argv) + try: + value = json.loads(Path(args.registry).read_text(encoding="utf-8")) + entries = registry.validate_registry(value) + matches = [item for item in entries if item["object_sha256"] == args.object_sha256] + if len(matches) != 1: + raise registry.EvidenceRegistryError("object digest does not select one entry") + reference = { + "schema_version": registry.REFERENCE_SCHEMA, + "repository": registry.REPOSITORY, + "repository_id": registry.REPOSITORY_ID, + "repository_owner_id": registry.REPOSITORY_OWNER_ID, + "registry_source_commit": args.registry_source_commit, + "registry_revision": value["revision"], + "registry_head_sha256": value["registry_head_sha256"], + **matches[0], + } + registry.validate_reference(reference) + print(json.dumps(reference, sort_keys=True, separators=(",", ":"))) + except (OSError, ValueError, json.JSONDecodeError) as exc: + print(f"REFUSED: {exc}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/production_trust.py b/scripts/production_trust.py new file mode 100644 index 0000000..9104eb1 --- /dev/null +++ b/scripts/production_trust.py @@ -0,0 +1,3047 @@ +#!/usr/bin/env python3 +"""Closed validators and digest rules for OpenAdapt Production trust objects.""" + +from __future__ import annotations + +import hashlib +import json +import re +import subprocess +import tempfile +from collections.abc import Mapping, Sequence +from datetime import datetime, timedelta, timezone +from pathlib import Path +from typing import Any + +import validate_evidence_registry as evidence + +ROOT = Path(__file__).resolve().parents[1] +SUPPORT_POLICY_PATH = ROOT / "support-release-policy.json" + +DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$") +RAW_DIGEST = re.compile(r"^[0-9a-f]{64}$") +HEX40 = re.compile(r"^[0-9a-f]{40}$") +TIMESTAMP = re.compile( + r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$" +) +DECIMAL_ID = re.compile(r"^[1-9][0-9]*$") +ENTITY_CLASS = re.compile(r"^[a-z][a-z0-9 -]{0,63}$") +BUNDLE_VERSION = re.compile( + r"^(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})\." + r"(0|[1-9][0-9]{0,9})(-[0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$" +) +RELEASE_TAG = re.compile( + r"^v(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})\." + r"(0|[1-9][0-9]{0,9})(-[0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$" +) + +TARGETS = ("agent", "capture", "cloud", "desktop", "docs", "flow", "openadapt") +TARGET_CONTRACTS = { + "agent": { + "claim_scope": "production_agent", + "repository": "OpenAdaptAI/openadapt-agent", + "repository_id": "1136136670", + "release_kind": "package", + "artifacts": { + "python-sdist": ("application/gzip", ("github-release", "pypi")), + "python-wheel": ("application/zip", ("github-release", "pypi")), + }, + }, + "capture": { + "claim_scope": "production_capture", + "repository": "OpenAdaptAI/openadapt-capture", + "repository_id": "1115283835", + "release_kind": "package", + "artifacts": { + "chrome-extension-zip": ("application/zip", ("github-release",)), + "python-sdist": ("application/gzip", ("github-release", "pypi")), + "python-wheel": ("application/zip", ("github-release", "pypi")), + "spdx-sbom": ("application/spdx+json", ("github-release",)), + }, + }, + "cloud": { + "claim_scope": "production_cloud", + "repository": "OpenAdaptAI/openadapt-cloud", + "repository_id": "1300570990", + "release_kind": "deployment", + "artifacts": { + "deployment-manifest": ( + "application/vnd.openadapt.production-deployment-manifest+json;version=1", + ("deployment",), + ), + }, + }, + "desktop": { + "claim_scope": "production_desktop", + "repository": "OpenAdaptAI/openadapt-desktop", + "repository_id": "1171291730", + "release_kind": "package", + "artifacts": { + "cyclonedx-sbom": ( + "application/vnd.cyclonedx+json", + ("github-release",), + ), + "linux-appimage": ("application/vnd.appimage", ("github-release",)), + "linux-deb": ( + "application/vnd.debian.binary-package", + ("github-release",), + ), + "macos-dmg-arm64": ( + "application/x-apple-diskimage", + ("github-release",), + ), + "macos-dmg-x86-64": ( + "application/x-apple-diskimage", + ("github-release",), + ), + "python-sdist": ("application/gzip", ("github-release", "pypi")), + "python-wheel": ("application/zip", ("github-release", "pypi")), + "release-checksums": ("text/plain", ("github-release",)), + "verification-metadata-linux-x86-64": ( + "application/vnd.openadapt.desktop-platform-verification+json;version=1", + ("github-release",), + ), + "verification-metadata-macos-arm64": ( + "application/vnd.openadapt.desktop-platform-verification+json;version=1", + ("github-release",), + ), + "verification-metadata-macos-x86-64": ( + "application/vnd.openadapt.desktop-platform-verification+json;version=1", + ("github-release",), + ), + "verification-metadata-windows-x86-64": ( + "application/vnd.openadapt.desktop-platform-verification+json;version=1", + ("github-release",), + ), + "windows-msi": ("application/x-msi", ("github-release",)), + "windows-nsis": ( + "application/vnd.microsoft.portable-executable", + ("github-release",), + ), + }, + }, + "docs": { + "claim_scope": "production_docs", + "repository": "OpenAdaptAI/openadapt-ops", + "repository_id": "1172011294", + "release_kind": "deployment", + "artifacts": { + "deployment-manifest": ( + "application/vnd.openadapt.production-deployment-manifest+json;version=1", + ("deployment",), + ), + "site-archive": ("application/gzip", ("deployment",)), + }, + }, + "flow": { + "claim_scope": "production_flow", + "repository": "OpenAdaptAI/openadapt-flow", + "repository_id": "1291376938", + "release_kind": "package", + "artifacts": { + "python-sdist": ("application/gzip", ("github-release", "pypi")), + "python-wheel": ("application/zip", ("github-release", "pypi")), + }, + }, + "openadapt": { + "claim_scope": "production_openadapt", + "repository": "OpenAdaptAI/OpenAdapt", + "repository_id": "627024850", + "release_kind": "package", + "artifacts": { + "python-sdist": ("application/gzip", ("github-release", "pypi")), + "python-wheel": ("application/zip", ("github-release", "pypi")), + }, + }, +} +CAMPAIGN_CLASSES = ( + "healthy", + "safe_halt", + "idempotency_replay", + "uncertain_delivery", + "declared_attended", + "governed_repair", +) +CAMPAIGN_COUNT_FIELDS = { + "task_condition_cell_count", + "minimum_trials_per_cell", + "observed_trial_count", + "silent_incorrect_success_count", + "over_halt_count", + "unsafe_effect_count", + "blind_retry_count", + "replay_dispatch_count", + "model_call_count", + "unplanned_intervention_count", + "reconciliation_required_count", + "authenticated_bound_decision_count", + "live_target_revalidation_count", + "policy_approved_repair_count", + "approved_repair_count", + "retained_repair_evidence_count", + "unverified_direct_action_count", +} + +ARTIFACT_INVENTORY_DOMAIN = b"OpenAdapt production release artifact inventory v1\0" +RELEASE_DOMAIN = b"OpenAdapt production release candidate v1\0" +RELEASE_ADMISSION_DOMAIN = b"OpenAdapt qualification release admission v1\0" +STAGING_DOMAIN = b"OpenAdapt production release staging evidence v1\0" +TAG_RULESETS_DOMAIN = b"OpenAdapt production release tag rulesets v1\0" +IMMUTABLE_RELEASES_DOMAIN = b"OpenAdapt production immutable releases response v1\0" +TAG_REF_STATE_DOMAIN = b"OpenAdapt production release tag ref state v1\0" +PUBLICATION_RECOVERY_AUTHORIZATION_DOMAIN = ( + b"OpenAdapt production publication recovery authorization v1\0" +) +PUBLICATION_RECOVERY_IDEMPOTENCY_DOMAIN = ( + b"OpenAdapt production publication recovery idempotency v1\0" +) +ADMISSION_DOMAIN = b"OpenAdapt qualification admission v3\0" +CHECKPOINT_DOMAIN = b"OpenAdapt production lifecycle checkpoint v1\0" +PROJECTION_DOMAIN = b"OpenAdapt production lifecycle projection v2\0" +RELEASE_SET_DOMAIN = b"OpenAdapt production release admission set v1\0" +WORKFLOW_SET_DOMAIN = b"OpenAdapt production workflow admission set v1\0" +AUTHORITY_STATE_IDENTITY_DOMAIN = ( + b"OpenAdapt qualification authority state identity v2\0" +) +REVOCATION_STATE_IDENTITY_DOMAIN = ( + b"OpenAdapt qualification revocation state identity v1\0" +) +PRODUCTION_EVIDENCE_IDENTITY_DOMAIN = ( + b"OpenAdapt production acceptance evidence identity v2\0" +) +DECISION_RECEIPT_SIGNATURE_DOMAIN = ( + b"OpenAdapt qualification evidence decision receipt v1\0" +) +AUTHORITY_STATE_SIGNATURE_DOMAIN = ( + b"OpenAdapt qualification authority state receipt v2\0" +) +REVOCATION_STATE_SIGNATURE_DOMAIN = ( + b"OpenAdapt qualification revocation state receipt v1\0" +) +SUPPORT_RELEASE_DOMAIN = b"OpenAdapt Support release candidate v1\0" +SUPPORT_ARTIFACT_INVENTORY_DOMAIN = ( + b"OpenAdapt Support release artifact inventory v1\0" +) +SUPPORT_RELEASE_ADMISSION_DOMAIN = b"OpenAdapt Support release admission v1\0" +SUPPORT_POLICY_DOMAIN = b"OpenAdapt Support release policy v1\0" +CLOUD_AUTHORIZATION_DOMAIN = b"OpenAdapt production Cloud deploy authorization v1\0" +CLOUD_HANDOFF_DOMAIN = b"OpenAdapt production Cloud deployment handoff v1\0" +CLOUD_SOURCE_PROOF_REQUEST_DOMAIN = ( + b"OpenAdapt production secret source proof request v2\0" +) +CLOUD_SOURCE_COMMIT_DOMAIN = b"OpenAdapt Cloud source commit commitment v1\0" +CLOUD_PROVIDER_IDEMPOTENCY_DOMAIN = ( + b"OpenAdapt production Cloud provider idempotency v1\0" +) +CLOUD_PUBLIC_VALUES_DOMAIN = b"OpenAdapt production Cloud public values v1\0" +CLOUD_LIVE_SOURCE_NAMES = ( + "BUSINESS_DECISION_ANSWER_HMAC_KEY", + "BUSINESS_DECISION_ANSWER_HMAC_KEY_ID", + "BUSINESS_DECISION_CLOUD_POLICY_HMAC_KEYS_JSON", + "BUSINESS_DECISION_PRINCIPAL_ALIAS_HMAC_KEY", + "BUSINESS_DECISION_QUALIFICATION_HMAC_KEYS_JSON", + "BUSINESS_DECISION_ROLE_MAPPING_HMAC_KEY", + "BUSINESS_DECISION_RUNNER_RECEIPT_HMAC_KEYS_JSON", + "BUSINESS_DECISION_SESSION_BINDING_HMAC_KEY", + "BUSINESS_DECISION_TASK_HMAC_KEYS_JSON", + "EXECUTE_ACCEPTANCE_CAMPAIGN_PROVISIONER_TOKEN", + "EXECUTE_ACCEPTANCE_OBSERVER_BEARER_TOKEN", + "EXECUTE_ACCEPTANCE_OBSERVER_SIGNING_PRIVATE_KEY", + "EXECUTE_ACCEPTANCE_TARGET_SIGNING_SECRET", +) +CLOUD_PUBLIC_SOURCE_NAME = "BUSINESS_DECISION_ANSWER_HMAC_KEY_ID" +CLOUD_SECRET_SOURCE_NAMES = tuple( + name for name in CLOUD_LIVE_SOURCE_NAMES if name != CLOUD_PUBLIC_SOURCE_NAME +) +FEED_UPDATE_IDEMPOTENCY_DOMAIN = ( + b"OpenAdapt production lifecycle feed update idempotency v1\0" +) + + +class TrustError(ValueError): + """A Production trust object is invalid.""" + + +def canonical(value: Any) -> bytes: + return json.dumps( + value, sort_keys=True, separators=(",", ":"), ensure_ascii=False + ).encode("utf-8") + + +def digest_bytes(domain: bytes, value: Any) -> str: + return "sha256:" + hashlib.sha256(domain + canonical(value)).hexdigest() + + +def closed(value: Any, fields: set[str], label: str) -> dict[str, Any]: + if not isinstance(value, dict) or set(value) != fields: + actual = sorted(value) if isinstance(value, dict) else type(value).__name__ + raise TrustError(f"{label} must contain exactly {sorted(fields)}; got {actual}") + return value + + +def require_digest(value: Any, label: str) -> str: + if not isinstance(value, str) or DIGEST.fullmatch(value) is None: + raise TrustError(f"{label} must be a lowercase sha256 digest") + return value + + +def require_raw_digest(value: Any, label: str) -> str: + if not isinstance(value, str) or RAW_DIGEST.fullmatch(value) is None: + raise TrustError(f"{label} must be lowercase raw sha256 hex") + return value + + +def require_decimal_id(value: Any, label: str) -> str: + if not isinstance(value, str) or DECIMAL_ID.fullmatch(value) is None: + raise TrustError(f"{label} must be a decimal string") + return value + + +def require_timestamp(value: Any, label: str) -> datetime: + if not isinstance(value, str) or TIMESTAMP.fullmatch(value) is None: + raise TrustError(f"{label} must be an exact UTC timestamp") + try: + return datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ").replace( + tzinfo=timezone.utc + ) + except ValueError as exc: + raise TrustError(f"{label} is not a calendar timestamp") from exc + + +def require_positive_int(value: Any, label: str, minimum: int = 1) -> int: + if not isinstance(value, int) or isinstance(value, bool) or value < minimum: + raise TrustError(f"{label} must be an integer of at least {minimum}") + return value + + +def validate_window( + value: Mapping[str, Any], *, maximum: timedelta, now: datetime | None = None +) -> None: + issued = require_timestamp(value["issued_at"], "issued_at") + not_before = require_timestamp(value["not_before"], "not_before") + expires = require_timestamp(value["expires_at"], "expires_at") + if not not_before <= issued < expires <= not_before + maximum: + raise TrustError("validity window is invalid") + if now is not None and not not_before <= now < expires: + raise TrustError("object is not active at the requested time") + + +def validate_staging_observation_age( + staging: Mapping[str, Any], admission: Mapping[str, Any] +) -> None: + observed = require_timestamp(staging["observed_at"], "staging observed_at") + issued = require_timestamp(admission["issued_at"], "admission issued_at") + if not observed <= issued <= observed + timedelta(hours=1): + raise TrustError("publication staging observation is stale or postdates admission") + + +def validate_reference_pair( + regular: Any, bundle: Any, *, kind: str +) -> tuple[dict[str, Any], dict[str, Any]]: + try: + regular_ref = evidence.validate_reference(regular) + bundle_ref = evidence.validate_reference(bundle) + except evidence.EvidenceRegistryError as exc: + raise TrustError(str(exc)) from exc + if regular_ref["kind"] != kind: + raise TrustError(f"regular reference kind must be {kind}") + if bundle_ref["kind"] != f"{kind}-sigstore-bundle": + raise TrustError(f"bundle reference kind must be {kind}-sigstore-bundle") + if ( + bundle_ref["registry_source_commit"] != regular_ref["registry_source_commit"] + or bundle_ref["registry_revision"] != regular_ref["registry_revision"] + or bundle_ref["registry_head_sha256"] != regular_ref["registry_head_sha256"] + or bundle_ref["subject_sha256"] != regular_ref["object_sha256"] + ): + raise TrustError("reference pair does not share a registry or bind its subject") + return regular_ref, bundle_ref + + +def validate_campaign_summary(value: Any) -> dict[str, Any]: + summary = closed(value, set(CAMPAIGN_CLASSES), "campaign summary") + for campaign_class in CAMPAIGN_CLASSES: + counts = closed( + summary[campaign_class], CAMPAIGN_COUNT_FIELDS, f"{campaign_class} counts" + ) + for key, count in counts.items(): + require_positive_int( + count, + f"{campaign_class} {key}", + minimum=3 if key == "minimum_trials_per_cell" else 0, + ) + cells = counts["task_condition_cell_count"] + trials = counts["observed_trial_count"] + if cells < 1 or trials < cells * counts["minimum_trials_per_cell"]: + raise TrustError(f"{campaign_class} does not have three trials per cell") + if counts["unsafe_effect_count"] or counts["silent_incorrect_success_count"] or counts["blind_retry_count"]: + raise TrustError(f"{campaign_class} contains a forbidden failure") + for campaign_class in ("healthy", "idempotency_replay"): + counts = summary[campaign_class] + if counts["model_call_count"] or counts["unplanned_intervention_count"]: + raise TrustError(f"{campaign_class} is not a zero-model healthy path") + uncertain = summary["uncertain_delivery"] + if uncertain["observed_trial_count"] < 3: + raise TrustError("uncertain delivery requires at least three fault trials") + if uncertain["reconciliation_required_count"] != uncertain["observed_trial_count"]: + raise TrustError("every uncertain-delivery trial must require reconciliation") + if uncertain["replay_dispatch_count"]: + raise TrustError("uncertain delivery cannot replay dispatch") + attended = summary["declared_attended"] + if ( + attended["authenticated_bound_decision_count"] != attended["observed_trial_count"] + or attended["live_target_revalidation_count"] != attended["observed_trial_count"] + ): + raise TrustError("attended trials require a bound decision and revalidation") + repair = summary["governed_repair"] + for field in ( + "policy_approved_repair_count", + "approved_repair_count", + "retained_repair_evidence_count", + "live_target_revalidation_count", + ): + if repair[field] != repair["observed_trial_count"]: + raise TrustError("governed repair evidence is incomplete") + if repair["unverified_direct_action_count"]: + raise TrustError("governed repair contains an unverified direct action") + return summary + + +def validate_artifacts( + value: Any, *, target: str | None = None +) -> list[dict[str, Any]]: + if not isinstance(value, list) or not value: + raise TrustError("artifact inventory must be a non-empty list") + result: list[dict[str, Any]] = [] + names: set[str] = set() + folded: set[str] = set() + for index, artifact_value in enumerate(value): + artifact = closed( + artifact_value, + {"name", "kind", "sha256", "size_bytes", "media_type", "publish_destinations"}, + f"artifact {index}", + ) + name = artifact["name"] + if ( + not isinstance(name, str) + or not name + or name != Path(name).name + or "/" in name + or "\\" in name + ): + raise TrustError(f"artifact {index} name is unsafe") + if name in names or name.casefold() in folded: + raise TrustError("artifact names must be unique under case folding") + names.add(name) + folded.add(name.casefold()) + if not isinstance(artifact["kind"], str) or not artifact["kind"]: + raise TrustError(f"artifact {index} kind is invalid") + require_digest(artifact["sha256"], f"artifact {index} digest") + require_positive_int(artifact["size_bytes"], f"artifact {index} size") + if not isinstance(artifact["media_type"], str) or "/" not in artifact["media_type"]: + raise TrustError(f"artifact {index} media type is invalid") + destinations = artifact["publish_destinations"] + if ( + not isinstance(destinations, list) + or not destinations + or destinations != sorted(set(destinations)) + or any(item not in {"deployment", "github-release", "pypi"} for item in destinations) + ): + raise TrustError(f"artifact {index} publish destinations are invalid") + result.append(artifact) + if result != sorted(result, key=lambda item: (item["kind"], item["name"], item["sha256"])): + raise TrustError("artifacts must be sorted by kind, name, and digest") + if target is not None: + contract = TARGET_CONTRACTS.get(target) + if contract is None: + raise TrustError("artifact target is invalid") + profile = contract["artifacts"] + if [item["kind"] for item in result] != sorted(profile): + raise TrustError(f"{target} artifact kinds differ from the target profile") + for artifact in result: + media_type, destinations = profile[artifact["kind"]] + if ( + artifact["media_type"] != media_type + or artifact["publish_destinations"] != list(destinations) + ): + raise TrustError( + f"{target} {artifact['kind']} media type or destinations differ" + ) + return result + + +def validate_artifact_inventory(value: Any) -> dict[str, Any]: + inventory = closed( + value, {"schema_version", "target", "claim_scope", "artifacts"}, "artifact inventory" + ) + if inventory["schema_version"] != "openadapt.production-release-artifact-inventory/v1": + raise TrustError("artifact inventory schema is not supported") + if inventory["target"] not in TARGETS: + raise TrustError("artifact inventory target is invalid") + if not isinstance(inventory["claim_scope"], str) or not inventory["claim_scope"]: + raise TrustError("artifact inventory claim scope is invalid") + validate_artifacts(inventory["artifacts"], target=inventory["target"]) + return inventory + + +def artifact_inventory_digest(value: Mapping[str, Any]) -> str: + return digest_bytes( + ARTIFACT_INVENTORY_DOMAIN, + { + "target": value["target"], + "claim_scope": value["claim_scope"], + "artifacts": value["artifacts"], + }, + ) + + +def validate_tag_rulesets(value: Any, *, repository: str, repository_id: str) -> list[dict[str, Any]]: + if not isinstance(value, list) or len(value) != 2: + raise TrustError("tag rulesets must contain creation and immutability rulesets") + roles = ["creation_authority", "immutability"] + for index, ruleset_value in enumerate(value): + ruleset = closed( + ruleset_value, + { + "schema_version", "role", "repository", "repository_id", "ruleset_id", + "name", "target", "enforcement", "bypass_actors", "conditions", "rules", + }, + f"tag ruleset {index}", + ) + if ruleset["schema_version"] != "openadapt.production-release-tag-ruleset/v1": + raise TrustError("tag ruleset schema is not supported") + if ruleset["role"] != roles[index] or ruleset["repository"] != repository or ruleset["repository_id"] != repository_id: + raise TrustError("tag ruleset identity differs") + require_decimal_id(ruleset["ruleset_id"], "tag ruleset id") + if ruleset["target"] != "tag" or ruleset["enforcement"] != "active": + raise TrustError("tag ruleset is not active for tags") + expected_name = ( + "OpenAdapt policy: release tag creation" + if index == 0 + else "OpenAdapt policy: immutable release tags" + ) + if ruleset["name"] != expected_name: + raise TrustError("tag ruleset name differs from policy") + actors = ruleset["bypass_actors"] + expected_actors = [] if index else [{"actor_id": "4730708", "actor_type": "Integration", "bypass_mode": "always"}] + if actors != expected_actors: + raise TrustError("tag ruleset bypass authority differs") + conditions = closed(ruleset["conditions"], {"ref_name"}, "tag ruleset conditions") + ref_name = closed(conditions["ref_name"], {"include", "exclude"}, "tag ref conditions") + for key in ("include", "exclude"): + items = ref_name[key] + if not isinstance(items, list) or items != sorted(set(items)) or any(not isinstance(item, str) or not item for item in items): + raise TrustError("tag ref patterns must be sorted unique strings") + if ref_name != {"include": ["refs/tags/v*"], "exclude": []}: + raise TrustError("tag ruleset must match exactly refs/tags/v*") + rules = ruleset["rules"] + expected_rules = ( + [{"type": "creation"}] + if index == 0 + else [ + {"type": "deletion"}, + {"type": "non_fast_forward"}, + { + "type": "update", + "parameters": {"update_allows_fetch_and_merge": False}, + }, + ] + ) + if rules != expected_rules: + raise TrustError("tag ruleset rules differ from immutable policy") + return value + + +def validate_staging(value: Any) -> dict[str, Any]: + staging = closed( + value, + { + "schema_version", "repository", "repository_id", "draft_release_id", "tag", + "target_commitish", "draft", "prerelease", "release_app_id", + "release_app_installation_id", "release_app_bot_user_id", + "release_author_login", "assets", "immutable_releases", + "immutable_releases_sha256", "tag_rulesets", + "tag_rulesets_sha256", "tag_ref_state", "tag_ref_state_sha256", + "observed_at", + }, + "publication staging", + ) + if staging["schema_version"] != "openadapt.production-release-staging-evidence/v1": + raise TrustError("publication staging schema is not supported") + for field in ("repository_id", "draft_release_id", "release_app_id", "release_app_installation_id", "release_app_bot_user_id"): + require_decimal_id(staging[field], f"publication staging {field}") + if ( + staging["draft"] is not True + or staging["prerelease"] is not False + or staging["release_app_id"] != "4730708" + or staging["release_app_installation_id"] != "156835568" + or staging["release_app_bot_user_id"] != "321543906" + or staging["release_author_login"] != "openadapt-release[bot]" + ): + raise TrustError("publication staging authority or state differs") + immutable_releases = closed( + staging["immutable_releases"], + {"enabled", "enforced_by_owner"}, + "immutable releases response", + ) + if ( + immutable_releases["enabled"] is not True + or not isinstance(immutable_releases["enforced_by_owner"], bool) + ): + raise TrustError("immutable releases are not enabled") + if staging["immutable_releases_sha256"] != digest_bytes( + IMMUTABLE_RELEASES_DOMAIN, immutable_releases + ): + raise TrustError("immutable releases response digest is invalid") + tag_ref_state = closed( + staging["tag_ref_state"], {"ref", "exists"}, "tag ref state" + ) + if tag_ref_state != {"ref": f"refs/tags/{staging['tag']}", "exists": False}: + raise TrustError("release tag must not exist when the draft is staged") + if staging["tag_ref_state_sha256"] != digest_bytes( + TAG_REF_STATE_DOMAIN, tag_ref_state + ): + raise TrustError("tag ref state digest is invalid") + if not isinstance(staging["target_commitish"], str) or HEX40.fullmatch(staging["target_commitish"]) is None: + raise TrustError("publication staging target commit must be exact") + if not isinstance(staging["tag"], str) or not staging["tag"]: + raise TrustError("publication staging tag is invalid") + assets = staging["assets"] + if not isinstance(assets, list) or not assets: + raise TrustError("publication staging assets are required") + names: set[str] = set() + ids: set[str] = set() + for index, asset_value in enumerate(assets): + asset = closed( + asset_value, + { + "asset_id", "name", "kind", "sha256", "size_bytes", "media_type", + "publish_destinations", "uploader_id", "uploader_login", + }, + f"staged asset {index}", + ) + require_decimal_id(asset["asset_id"], f"staged asset {index} id") + require_digest(asset["sha256"], f"staged asset {index} digest") + require_positive_int(asset["size_bytes"], f"staged asset {index} size") + if not isinstance(asset["media_type"], str) or "/" not in asset["media_type"]: + raise TrustError("staged asset media type is invalid") + destinations = asset["publish_destinations"] + if not isinstance(destinations, list) or not destinations or destinations != sorted(set(destinations)) or any(item not in {"deployment", "github-release", "pypi"} for item in destinations): + raise TrustError("staged asset publish destinations are invalid") + if asset["uploader_id"] != "321543906" or asset["uploader_login"] != "openadapt-release[bot]": + raise TrustError("staged asset uploader is not the release App") + if asset["name"] in names or asset["asset_id"] in ids: + raise TrustError("staged asset names and ids must be unique") + names.add(asset["name"]) + ids.add(asset["asset_id"]) + if assets != sorted(assets, key=lambda item: (item["name"], item["asset_id"])): + raise TrustError("staged assets must be sorted") + validate_tag_rulesets(staging["tag_rulesets"], repository=staging["repository"], repository_id=staging["repository_id"]) + if staging["tag_rulesets_sha256"] != digest_bytes(TAG_RULESETS_DOMAIN, staging["tag_rulesets"]): + raise TrustError("tag rulesets digest is invalid") + require_timestamp(staging["observed_at"], "publication staging observed_at") + return staging + + +def staging_digest(value: Mapping[str, Any]) -> str: + return digest_bytes(STAGING_DOMAIN, value) + + +def validate_release(value: Any, *, now: datetime | None = None) -> dict[str, Any]: + release_admission = closed( + value, + { + "schema_version", "admission_id_sha256", "target", "verdict", "claim_scope", + "release_identity", "release", "release_sha256", "artifact_inventory_sha256", + "publication_staging", "publication_staging_sha256", + "production_acceptance_summary_reference", + "production_acceptance_summary_bundle_reference", "authority_state_sha256", + "revocation_state_sha256", "signer_registry_sha256", + "publication_policy_sha256", "issued_at", "not_before", "expires_at", "issuer", + }, + "qualification release", + ) + if release_admission["schema_version"] != "openadapt.qualification-release/v1" or release_admission["verdict"] != "accepted": + raise TrustError("qualification release schema or verdict is invalid") + if release_admission["target"] not in TARGETS: + raise TrustError("qualification release target is invalid") + target_contract = TARGET_CONTRACTS[release_admission["target"]] + if release_admission["claim_scope"] != target_contract["claim_scope"]: + raise TrustError("qualification release claim scope differs from target policy") + identity = closed(release_admission["release_identity"], {"schema_version", "channel", "sequence", "previous_admission_sha256"}, "release identity") + if identity["schema_version"] != "openadapt.monotonic-production-release/v1" or identity["channel"] != "production": + raise TrustError("release identity schema or channel is invalid") + require_positive_int(identity["sequence"], "release sequence") + if identity["previous_admission_sha256"] is not None: + require_digest(identity["previous_admission_sha256"], "previous release admission") + release = closed(release_admission["release"], {"schema_version", "kind", "source_repository", "source_repository_id", "source_commit", "version", "tag", "deployment_id", "deployment_sha256", "artifacts"}, "release candidate") + if release["schema_version"] != "openadapt.production-release-candidate/v1" or release["kind"] not in {"package", "deployment", "hybrid"}: + raise TrustError("release candidate schema or kind is invalid") + if ( + release["kind"] != target_contract["release_kind"] + or release["source_repository"] != target_contract["repository"] + or release["source_repository_id"] != target_contract["repository_id"] + ): + raise TrustError("release candidate differs from the target policy") + require_decimal_id(release["source_repository_id"], "release source repository id") + if not isinstance(release["source_commit"], str) or HEX40.fullmatch(release["source_commit"]) is None: + raise TrustError("release source commit must be exact") + if release["kind"] == "package": + if ( + not isinstance(release["version"], str) + or not release["version"] + or not isinstance(release["tag"], str) + or not release["tag"] + or release["deployment_id"] is not None + or release["deployment_sha256"] is not None + ): + raise TrustError("package release identity fields are invalid") + elif release["kind"] == "deployment": + if ( + release["version"] is not None + or release["tag"] is not None + or require_decimal_id(release["deployment_id"], "deployment id") is None + ): + raise TrustError("deployment release identity fields are invalid") + require_digest(release["deployment_sha256"], "deployment digest") + else: + if ( + not isinstance(release["version"], str) + or not release["version"] + or not isinstance(release["tag"], str) + or not release["tag"] + ): + raise TrustError("hybrid package identity fields are invalid") + require_decimal_id(release["deployment_id"], "deployment id") + require_digest(release["deployment_sha256"], "deployment digest") + artifacts = validate_artifacts( + release["artifacts"], target=release_admission["target"] + ) + if release_admission["target"] == "desktop": + if any( + token in item["name"].casefold() + for item in artifacts + for token in ("beta", "candidate", "adhoc", "unsigned") + ): + raise TrustError("Desktop release filenames contain a non-production label") + version = release["version"] + expected_metadata_names = { + "verification-metadata-linux-x86-64": ( + f"OpenAdapt-Desktop-v{version}-linux-x86_64-verification.json" + ), + "verification-metadata-macos-arm64": ( + f"OpenAdapt-Desktop-v{version}-macos-arm64-verification.json" + ), + "verification-metadata-macos-x86-64": ( + f"OpenAdapt-Desktop-v{version}-macos-x86_64-verification.json" + ), + "verification-metadata-windows-x86-64": ( + f"OpenAdapt-Desktop-v{version}-windows-x86_64-verification.json" + ), + } + for artifact in artifacts: + expected_name = expected_metadata_names.get(artifact["kind"]) + if expected_name is not None and artifact["name"] != expected_name: + raise TrustError("Desktop verification metadata filename differs") + inventory = {"target": release_admission["target"], "claim_scope": release_admission["claim_scope"], "artifacts": artifacts} + if release_admission["artifact_inventory_sha256"] != digest_bytes(ARTIFACT_INVENTORY_DOMAIN, inventory): + raise TrustError("release artifact inventory digest is invalid") + if release_admission["release_sha256"] != digest_bytes(RELEASE_DOMAIN, {"target": release_admission["target"], "claim_scope": release_admission["claim_scope"], "release": release}): + raise TrustError("release candidate digest is invalid") + staging = validate_staging(release_admission["publication_staging"]) + if release_admission["publication_staging_sha256"] != staging_digest(staging): + raise TrustError("publication staging digest is invalid") + if staging["repository"] != release["source_repository"] or staging["repository_id"] != release["source_repository_id"] or staging["target_commitish"] != release["source_commit"] or staging["tag"] != release["tag"]: + raise TrustError("publication staging differs from the release candidate") + bound_fields = ("name", "kind", "sha256", "size_bytes", "media_type", "publish_destinations") + staged = [tuple(item[field] if field != "publish_destinations" else tuple(item[field]) for field in bound_fields) for item in staging["assets"]] + admitted = [tuple(item[field] if field != "publish_destinations" else tuple(item[field]) for field in bound_fields) for item in artifacts] + if sorted(staged) != sorted(admitted): + raise TrustError("publication staging assets differ from the admitted inventory") + validate_reference_pair(release_admission["production_acceptance_summary_reference"], release_admission["production_acceptance_summary_bundle_reference"], kind="production-acceptance-summary") + for field in ("authority_state_sha256", "revocation_state_sha256", "signer_registry_sha256", "publication_policy_sha256"): + require_digest(release_admission[field], field) + issuer = closed(release_admission["issuer"], {"repository", "repository_id", "repository_owner_id", "workflow", "ref", "source_commit", "environment"}, "release issuer") + if issuer != { + "repository": "OpenAdaptAI/.github", "repository_id": "858454062", "repository_owner_id": "132681217", + "workflow": ".github/workflows/issue-production-release-admission.yml", "ref": "refs/heads/main", + "source_commit": issuer["source_commit"], "environment": "production-release-admission", + } or not isinstance(issuer["source_commit"], str) or HEX40.fullmatch(issuer["source_commit"]) is None: + raise TrustError("release issuer identity differs") + validate_window(release_admission, maximum=timedelta(days=30), now=now) + validate_staging_observation_age(staging, release_admission) + projection = dict(release_admission) + admission_id = projection.pop("admission_id_sha256") + if admission_id != digest_bytes(RELEASE_ADMISSION_DOMAIN, projection): + raise TrustError("qualification release admission id is invalid") + return release_admission + + +def verify_local_artifacts(root: Path, artifacts: Sequence[Mapping[str, Any]]) -> None: + if not root.is_dir(): + raise TrustError("artifact root is not a directory") + actual_files = sorted(path.name for path in root.iterdir() if path.is_file()) + expected_files = sorted(item["name"] for item in artifacts) + if actual_files != expected_files: + raise TrustError("candidate artifact contains missing or extra files") + if any(path.is_symlink() for path in root.rglob("*")): + raise TrustError("candidate artifact cannot contain symlinks") + if any(path.is_file() and path.parent != root for path in root.rglob("*")): + raise TrustError("candidate artifact files must be at the artifact root") + for artifact in artifacts: + path = root / artifact["name"] + raw = path.read_bytes() + if len(raw) != artifact["size_bytes"] or "sha256:" + hashlib.sha256(raw).hexdigest() != artifact["sha256"]: + raise TrustError(f"candidate artifact bytes differ: {artifact['name']}") + + +def validate_support_release( + value: Any, *, now: datetime | None = None +) -> dict[str, Any]: + fields = { + "schema_version", "admission_id_sha256", "lifecycle_state", + "support_target", "verdict", "claim_scope", "release_identity", + "release", "release_sha256", "artifact_inventory_sha256", + "publication_staging", "publication_staging_sha256", + "authority_state_sha256", "revocation_state_sha256", + "signer_registry_sha256", "support_policy_sha256", "issued_at", + "not_before", "expires_at", "issuer", + } + admission = closed(value, fields, "Support release admission") + if ( + admission["schema_version"] + != "openadapt.support-release-admission/v1" + or admission["lifecycle_state"] != "Support" + or admission["support_target"] != "openadapt-tray" + or admission["verdict"] != "accepted" + or admission["claim_scope"] != "support_openadapt_tray" + ): + raise TrustError("Support release admission identity differs") + for field in fields: + if field.endswith("_sha256"): + require_digest(admission[field], field) + try: + support_policy = json.loads(SUPPORT_POLICY_PATH.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise TrustError("Support release policy is unavailable") from exc + expected_support_policy = { + "$schema": "schemas/support-release-policy.schema.json", + "schema_version": "openadapt.support-release-policy/v1", + "lifecycle_state": "Support", + "production_projection": False, + "maximum_admission_days": 7, + "release_authority": { + "app_id": "4730708", + "app_slug": "openadapt-release", + "installation_id": "156835568", + "bot_user_id": "321543906", + "bot_login": "openadapt-release[bot]", + "admission_environment": "support-release-admission", + "effect_environment": "release-identity", + "tag_creation_ruleset": "OpenAdapt policy: release tag creation", + "tag_immutability_ruleset": "OpenAdapt policy: immutable release tags", + }, + "targets": [ + { + "id": "openadapt-tray", + "repository": "OpenAdaptAI/openadapt-tray", + "repository_id": "1136122737", + "claim_scope": "support_openadapt_tray", + "release_kind": "package", + "tag_pattern": "refs/tags/v*", + "artifacts": [ + { + "kind": "python-sdist", + "media_type": "application/gzip", + "publish_destinations": ["github-release", "pypi"], + }, + { + "kind": "python-wheel", + "media_type": "application/zip", + "publish_destinations": ["github-release", "pypi"], + }, + ], + } + ], + } + if support_policy != expected_support_policy or admission[ + "support_policy_sha256" + ] != digest_bytes(SUPPORT_POLICY_DOMAIN, support_policy): + raise TrustError("Support release policy binding differs") + identity = closed( + admission["release_identity"], + {"schema_version", "channel", "sequence", "previous_admission_sha256"}, + "Support release identity", + ) + if ( + identity["schema_version"] != "openadapt.monotonic-support-release/v1" + or identity["channel"] != "support" + ): + raise TrustError("Support release identity schema differs") + sequence = require_positive_int(identity["sequence"], "Support release sequence") + if sequence == 1: + if identity["previous_admission_sha256"] is not None: + raise TrustError("first Support release cannot have a previous admission") + else: + require_digest( + identity["previous_admission_sha256"], "previous Support release admission" + ) + release = closed( + admission["release"], + { + "schema_version", "kind", "source_repository", + "source_repository_id", "source_commit", "version", "tag", + "deployment_id", "deployment_sha256", "artifacts", + }, + "Support release candidate", + ) + if ( + release["schema_version"] != "openadapt.production-release-candidate/v1" + or release["kind"] != "package" + or release["source_repository"] != "OpenAdaptAI/openadapt-tray" + or release["source_repository_id"] != "1136122737" + or not isinstance(release["source_commit"], str) + or HEX40.fullmatch(release["source_commit"]) is None + or not isinstance(release["version"], str) + or not release["version"] + or release["tag"] != f"v{release['version']}" + or release["deployment_id"] is not None + or release["deployment_sha256"] is not None + ): + raise TrustError("Support release candidate differs from openadapt-tray") + artifacts = validate_artifacts(release["artifacts"]) + if [item["kind"] for item in artifacts] != ["python-sdist", "python-wheel"]: + raise TrustError("openadapt-tray Support release must contain wheel and sdist") + expected_profiles = { + "python-sdist": ("application/gzip", ["github-release", "pypi"]), + "python-wheel": ("application/zip", ["github-release", "pypi"]), + } + for artifact in artifacts: + media_type, destinations = expected_profiles[artifact["kind"]] + if ( + artifact["media_type"] != media_type + or artifact["publish_destinations"] != destinations + ): + raise TrustError("openadapt-tray Support artifact profile differs") + inventory = { + "support_target": admission["support_target"], + "claim_scope": admission["claim_scope"], + "artifacts": artifacts, + } + if admission["artifact_inventory_sha256"] != digest_bytes( + SUPPORT_ARTIFACT_INVENTORY_DOMAIN, inventory + ): + raise TrustError("Support artifact inventory digest is invalid") + if admission["release_sha256"] != digest_bytes( + SUPPORT_RELEASE_DOMAIN, + { + "support_target": admission["support_target"], + "claim_scope": admission["claim_scope"], + "release": release, + }, + ): + raise TrustError("Support release candidate digest is invalid") + staging = validate_staging(admission["publication_staging"]) + if ( + admission["publication_staging_sha256"] != staging_digest(staging) + or staging["repository"] != release["source_repository"] + or staging["repository_id"] != release["source_repository_id"] + or staging["target_commitish"] != release["source_commit"] + or staging["tag"] != release["tag"] + ): + raise TrustError("Support publication staging differs from the release") + bound_fields = ( + "name", "kind", "sha256", "size_bytes", "media_type", + "publish_destinations", + ) + staged = sorted( + tuple( + item[field] if field != "publish_destinations" else tuple(item[field]) + for field in bound_fields + ) + for item in staging["assets"] + ) + admitted = sorted( + tuple( + item[field] if field != "publish_destinations" else tuple(item[field]) + for field in bound_fields + ) + for item in artifacts + ) + if staged != admitted: + raise TrustError("Support staged assets differ from the admitted inventory") + issuer = closed( + admission["issuer"], + { + "repository", "repository_id", "repository_owner_id", "workflow", + "ref", "source_commit", "environment", + }, + "Support release issuer", + ) + if ( + issuer["repository"] != "OpenAdaptAI/.github" + or issuer["repository_id"] != "858454062" + or issuer["repository_owner_id"] != "132681217" + or issuer["workflow"] + != ".github/workflows/issue-support-release-admission.yml" + or issuer["ref"] != "refs/heads/main" + or issuer["environment"] != "support-release-admission" + or not isinstance(issuer["source_commit"], str) + or HEX40.fullmatch(issuer["source_commit"]) is None + ): + raise TrustError("Support release issuer differs") + validate_window(admission, maximum=timedelta(days=7), now=now) + validate_staging_observation_age(staging, admission) + projection = dict(admission) + identity_digest = projection.pop("admission_id_sha256") + if identity_digest != digest_bytes(SUPPORT_RELEASE_ADMISSION_DOMAIN, projection): + raise TrustError("Support release admission id is invalid") + return admission + + +def validate_publication_recovery_authorization( + value: Any, *, now: datetime | None = None +) -> dict[str, Any]: + fields = { + "schema_version", "authorization_id_sha256", + "qualification_release_reference", "qualification_release_object_sha256", + "release_sha256", "artifact_inventory_sha256", + "publication_staging_sha256", "repository", "repository_id", "target", + "tag", "tag_ref", "tag_object_id", "target_commit", "draft_release_id", + "requested_effect", "run_id", "run_attempt", "dispatcher_actor_id", + "environment", "release_app", "idempotency_key", "issued_at", + "not_before", "expires_at", "issuer", + } + authorization = closed( + value, fields, "production publication recovery authorization" + ) + if ( + authorization["schema_version"] + != "openadapt.production-publication-recovery-authorization/v1" + or authorization["run_attempt"] != "1" + or authorization["dispatcher_actor_id"] != "774615" + ): + raise TrustError("publication recovery authorization identity differs") + target = authorization["target"] + contract = TARGET_CONTRACTS.get(target) + if contract is None or contract["release_kind"] != "package": + raise TrustError("publication recovery applies only to package targets") + if ( + authorization["repository"] != contract["repository"] + or authorization["repository_id"] != contract["repository_id"] + ): + raise TrustError("publication recovery target repository differs") + for field in ( + "authorization_id_sha256", "qualification_release_object_sha256", + "release_sha256", "artifact_inventory_sha256", + "publication_staging_sha256", + ): + require_digest(authorization[field], field) + reference = evidence.validate_reference( + authorization["qualification_release_reference"] + ) + if ( + reference["kind"] != "qualification-release" + or reference["object_schema_version"] + != "openadapt.qualification-release/v1" + or reference["object_media_type"] + != "application/vnd.openadapt.qualification-release+json;version=1" + or reference["subject_sha256"] is not None + or reference["object_sha256"] + != authorization["qualification_release_object_sha256"] + ): + raise TrustError("publication recovery release reference differs") + if ( + not isinstance(authorization["tag"], str) + or RELEASE_TAG.fullmatch(authorization["tag"]) is None + or authorization["tag_ref"] != f"refs/tags/{authorization['tag']}" + or not isinstance(authorization["tag_object_id"], str) + or HEX40.fullmatch(authorization["tag_object_id"]) is None + or not isinstance(authorization["target_commit"], str) + or HEX40.fullmatch(authorization["target_commit"]) is None + ): + raise TrustError("publication recovery immutable tag binding differs") + require_decimal_id(authorization["draft_release_id"], "draft release id") + require_decimal_id(authorization["run_id"], "recovery run id") + effect = authorization["requested_effect"] + effect_environments = { + "stage-draft-assets": "release-identity", + "publish-pypi": "pypi", + "publish-github-release": "release-identity", + "publish-mcp-registry": "mcp-registry", + } + if ( + effect not in effect_environments + or authorization["environment"] != effect_environments[effect] + or (effect == "publish-mcp-registry" and target != "agent") + ): + raise TrustError("publication recovery effect or environment differs") + release_app = closed( + authorization["release_app"], + {"app_id", "installation_id", "bot_user_id", "bot_login"}, + "publication recovery release App", + ) + if release_app != { + "app_id": "4730708", + "installation_id": "156835568", + "bot_user_id": "321543906", + "bot_login": "openadapt-release[bot]", + }: + raise TrustError("publication recovery release App differs") + idempotency_projection = { + field: authorization[field] + for field in ( + "qualification_release_object_sha256", + "release_sha256", + "artifact_inventory_sha256", + "publication_staging_sha256", + "repository", + "repository_id", + "target", + "tag", + "tag_ref", + "tag_object_id", + "target_commit", + "draft_release_id", + "requested_effect", + "run_id", + "run_attempt", + "dispatcher_actor_id", + "environment", + "release_app", + ) + } + expected_idempotency = "publication-recovery:" + hashlib.sha256( + PUBLICATION_RECOVERY_IDEMPOTENCY_DOMAIN + + canonical(idempotency_projection) + ).hexdigest() + if authorization["idempotency_key"] != expected_idempotency: + raise TrustError("publication recovery idempotency key differs") + issuer = closed( + authorization["issuer"], + { + "repository", "repository_id", "repository_owner_id", "workflow", + "ref", "source_commit", "environment", + }, + "publication recovery issuer", + ) + if ( + issuer["repository"] != "OpenAdaptAI/.github" + or issuer["repository_id"] != "858454062" + or issuer["repository_owner_id"] != "132681217" + or issuer["workflow"] + != ".github/workflows/issue-production-publication-recovery.yml" + or issuer["ref"] != "refs/heads/main" + or issuer["environment"] != authorization["environment"] + or not isinstance(issuer["source_commit"], str) + or HEX40.fullmatch(issuer["source_commit"]) is None + ): + raise TrustError("publication recovery issuer differs") + validate_window(authorization, maximum=timedelta(minutes=5), now=now) + projection = dict(authorization) + identity = projection.pop("authorization_id_sha256") + if identity != digest_bytes( + PUBLICATION_RECOVERY_AUTHORIZATION_DOMAIN, projection + ): + raise TrustError("publication recovery authorization id differs") + return authorization + + +def validate_publication_recovery_replay( + previous_value: Any, + current_value: Any, + *, + now: datetime | None = None, +) -> dict[str, Any]: + """Refuse expired effects and any changed one-use recovery claim.""" + + previous = validate_publication_recovery_authorization(previous_value) + current = validate_publication_recovery_authorization(current_value) + if canonical(previous) == canonical(current): + return validate_publication_recovery_authorization( + current, now=now or datetime.now(timezone.utc) + ) + previous_consumption = ( + previous["repository_id"], + previous["tag_ref"], + previous["requested_effect"], + ) + current_consumption = ( + current["repository_id"], + current["tag_ref"], + current["requested_effect"], + ) + if ( + previous["idempotency_key"] == current["idempotency_key"] + or previous_consumption == current_consumption + ): + raise TrustError("publication recovery one-use claim conflicts") + return validate_publication_recovery_authorization( + current, now=now or datetime.now(timezone.utc) + ) + + +def _validate_receipt_structure( + value: Any, *, now: datetime | None = None +) -> dict[str, Any]: + fields = { + "schema_version", "decision_identity_sha256", "decision_revision", + "decision_commitment_sha256", "evidence_manifest_sha256", + "evidence_manifest_readback_sha256", + "campaign_artifact_sha256", "organization_id_sha256", "workflow_id_sha256", + "workflow_version_id_sha256", "bundle_version", "bundle_sha256", + "admitted_runtime_sha256", "application_contract_sha256", + "environment_contract_sha256", "input_contract_sha256", "action_contract_sha256", + "identity_contract_sha256", "effect_contract_sha256", "policy_contract_sha256", + "evidence_authority_contract_sha256", "campaign_permit_sha256", "signer_registry_sha256", + "revocation_state_sha256", "entity_class", "campaign_summary", "verdict", + "issued_at", "not_before", "expires_at", "issuer_key_id", "algorithm", + "signing_statement", "signature", + "issuer", + } + receipt = closed(value, fields, "qualification evidence decision receipt") + if receipt["schema_version"] != "openadapt.qualification-evidence-decision-receipt/v1" or receipt["verdict"] != "ADMIT": + raise TrustError("decision receipt schema or verdict is invalid") + digest_fields = fields - { + "schema_version", "decision_revision", "bundle_version", "entity_class", + "campaign_summary", "verdict", + "issued_at", "not_before", "expires_at", "issuer_key_id", "algorithm", + "signing_statement", "signature", + "issuer", + } + for field in digest_fields: + require_digest(receipt[field], field) + if len( + { + receipt["decision_commitment_sha256"], + receipt["evidence_manifest_sha256"], + receipt["evidence_manifest_readback_sha256"], + receipt["campaign_artifact_sha256"], + } + ) != 4: + raise TrustError("decision, final manifest, and campaign commitments must be distinct") + require_positive_int(receipt["decision_revision"], "decision revision") + if ( + not isinstance(receipt["bundle_version"], str) + or len(receipt["bundle_version"]) > 64 + or BUNDLE_VERSION.fullmatch(receipt["bundle_version"]) is None + ): + raise TrustError("bundle version is not canonical") + if not isinstance(receipt["entity_class"], str) or ENTITY_CLASS.fullmatch(receipt["entity_class"]) is None: + raise TrustError("entity class is not remote-safe") + campaign_summary = closed( + receipt["campaign_summary"], + { + "schema_version", "minimum_trials_per_task_condition", + "task_count", "classes", + }, + "decision receipt campaign summary", + ) + if ( + campaign_summary["schema_version"] + != "openadapt.qualification-evidence-decision-campaign-summary/v1" + or campaign_summary["minimum_trials_per_task_condition"] != 3 + ): + raise TrustError("decision receipt campaign summary identity differs") + require_positive_int(campaign_summary["task_count"], "campaign task count") + validate_campaign_summary(campaign_summary["classes"]) + if any( + counts["task_condition_cell_count"] < campaign_summary["task_count"] + for counts in campaign_summary["classes"].values() + ): + raise TrustError("decision receipt campaign summary omits a task/class cell") + if receipt["algorithm"] != "ed25519" or not isinstance(receipt["issuer_key_id"], str) or evidence.KEY_ID.fullmatch(receipt["issuer_key_id"]) is None: + raise TrustError("decision receipt signer is invalid") + signature = receipt["signature"] + if not isinstance(signature, str) or re.fullmatch(r"[A-Za-z0-9+/]{86}==", signature) is None: + raise TrustError("decision receipt signature must be canonical padded base64") + try: + raw_signature = evidence.base64.b64decode(signature, validate=True) + except Exception as exc: + raise TrustError("decision receipt signature is invalid") from exc + if len(raw_signature) != 64: + raise TrustError("decision receipt signature must contain 64 bytes") + validate_signing_statement( + receipt, + object_schema_version="openadapt.qualification-evidence-decision-receipt/v1", + signature_domain=DECISION_RECEIPT_SIGNATURE_DOMAIN, + ) + issuer = closed( + receipt["issuer"], + {"repository", "repository_id", "repository_owner_id", "workflow", "ref", "source_commit", "environment"}, + "decision receipt issuer", + ) + if ( + issuer["repository"] != "OpenAdaptAI/openadapt-internal" + or issuer["repository_id"] != "1170060695" + or issuer["repository_owner_id"] != "132681217" + or issuer["workflow"] + != ".github/workflows/issue-private-qualification-evidence-decision.yml" + or issuer["ref"] != "refs/heads/main" + or issuer["environment"] != "private-qualification-evidence-decision" + or not isinstance(issuer["source_commit"], str) + or HEX40.fullmatch(issuer["source_commit"]) is None + ): + raise TrustError("decision receipt issuer differs") + validate_window(receipt, maximum=timedelta(days=7), now=now) + return receipt + + +def validate_receipt( + value: Any, + *, + signer_registry: Any, + now: datetime | None = None, +) -> dict[str, Any]: + """Validate the receipt bytes and its active registered Ed25519 signature.""" + + receipt = _validate_receipt_structure(value, now=now) + try: + registry = evidence.validate_signer_registry(signer_registry) + except evidence.EvidenceRegistryError as exc: + raise TrustError(str(exc)) from exc + if receipt["signer_registry_sha256"] != evidence.signer_registry_identity_digest( + registry + ): + raise TrustError("decision receipt signer registry identity differs") + verify_embedded_signature( + receipt, + signer_registry=registry, + object_schema_version=( + "openadapt.qualification-evidence-decision-receipt/v1" + ), + signature_domain=DECISION_RECEIPT_SIGNATURE_DOMAIN, + usage="qualification-evidence-decision-receipt", + now=now, + ) + return receipt + + +def signing_statement( + value: Mapping[str, Any], + *, + object_schema_version: str, + signature_domain: bytes, +) -> dict[str, Any]: + unsigned = dict(value) + unsigned.pop("signature", None) + unsigned.pop("signing_statement", None) + unsigned_bytes = canonical(unsigned) + b"\n" + return { + "schema_version": "openadapt.qualification-evidence-signing-statement/v1", + "object_schema_version": object_schema_version, + "signature_domain": signature_domain.decode("utf-8"), + "unsigned_object_sha256": ( + "sha256:" + hashlib.sha256(unsigned_bytes).hexdigest() + ), + "unsigned_size_bytes": len(unsigned_bytes), + "commitment_scheme": "sha256-canonical-json-lf", + } + + +def validate_signing_statement( + value: Mapping[str, Any], + *, + object_schema_version: str, + signature_domain: bytes, +) -> dict[str, Any]: + statement = closed( + value.get("signing_statement"), + { + "schema_version", "object_schema_version", "signature_domain", + "unsigned_object_sha256", "unsigned_size_bytes", "commitment_scheme", + }, + "qualification evidence signing statement", + ) + expected = signing_statement( + value, + object_schema_version=object_schema_version, + signature_domain=signature_domain, + ) + if statement != expected: + raise TrustError("qualification evidence signing statement differs") + return statement + + +def verify_embedded_signature( + value: Mapping[str, Any], + *, + signer_registry: Any, + object_schema_version: str, + signature_domain: bytes, + usage: str, + now: datetime | None = None, +) -> None: + try: + registry = evidence.validate_signer_registry(signer_registry) + except evidence.EvidenceRegistryError as exc: + raise TrustError(str(exc)) from exc + requested_time = now or datetime.now(timezone.utc) + generated = evidence._timestamp(registry["generated_at"], "generated_at") + expires = evidence._timestamp(registry["expires_at"], "expires_at") + if not generated <= requested_time < expires: + raise TrustError("signer registry is not active") + key_id = value.get("issuer_key_id") + matches = [item for item in registry["signers"] if item["key_id"] == key_id] + if len(matches) != 1 or matches[0]["status"] != "active": + raise TrustError("embedded signature key is not an active registered signer") + signer = matches[0] + if usage not in signer["allowed_usages"]: + raise TrustError("embedded signature usage is not allowed for the key") + issuer = value.get("issuer") + if not isinstance(issuer, dict): + raise TrustError("signed object has no issuer identity") + workflow_identity = ( + f"/{issuer['repository']}/{issuer['workflow']}" + f"@{issuer['ref']}" + ) + if workflow_identity not in signer["allowed_workflows"]: + raise TrustError("embedded signature workflow is not allowed for the key") + ref = issuer["ref"] + if not any( + ref == prefix or ref.startswith(prefix.rstrip("/") + "/") + for prefix in signer["allowed_ref_prefixes"] + ): + raise TrustError("embedded signature ref is not allowed for the key") + if signer["statement_schema_versions"] != [ + "openadapt.qualification-evidence-signing-statement/v1" + ]: + raise TrustError("embedded signature statement schema is not allowed") + statement = validate_signing_statement( + value, + object_schema_version=object_schema_version, + signature_domain=signature_domain, + ) + statement_bytes = canonical(statement) + b"\n" + try: + public_key = evidence.base64.urlsafe_b64decode( + signer["public_key"] + "=" * (-len(signer["public_key"]) % 4) + ) + signature = evidence.base64.b64decode(value["signature"], validate=True) + except Exception as exc: + raise TrustError("embedded signature bytes are invalid") from exc + spki = bytes.fromhex("302a300506032b6570032100") + public_key + with tempfile.TemporaryDirectory() as temporary: + directory = Path(temporary) + key_path = directory / "key.der" + statement_path = directory / "statement.json" + signature_path = directory / "signature.bin" + key_path.write_bytes(spki) + statement_path.write_bytes(statement_bytes) + signature_path.write_bytes(signature) + result = subprocess.run( + [ + "openssl", "pkeyutl", "-verify", "-pubin", "-keyform", "DER", + "-inkey", str(key_path), "-rawin", "-in", str(statement_path), + "-sigfile", str(signature_path), + ], + capture_output=True, + text=True, + check=False, + ) + if result.returncode: + raise TrustError("embedded Ed25519 signing statement verification failed") + + +def _validate_embedded_signature( + value: Mapping[str, Any], + label: str, + *, + object_schema_version: str, + signature_domain: bytes, +) -> None: + if ( + value["algorithm"] != "ed25519" + or not isinstance(value["issuer_key_id"], str) + or evidence.KEY_ID.fullmatch(value["issuer_key_id"]) is None + ): + raise TrustError(f"{label} signer is invalid") + signature = value["signature"] + if ( + not isinstance(signature, str) + or re.fullmatch(r"[A-Za-z0-9+/]{86}==", signature) is None + ): + raise TrustError(f"{label} signature must be canonical padded base64") + try: + raw = evidence.base64.b64decode(signature, validate=True) + except Exception as exc: + raise TrustError(f"{label} signature is invalid") from exc + if len(raw) != 64: + raise TrustError(f"{label} signature must contain 64 bytes") + validate_signing_statement( + value, + object_schema_version=object_schema_version, + signature_domain=signature_domain, + ) + + +def validate_authority_state( + value: Any, *, now: datetime | None = None +) -> dict[str, Any]: + fields = { + "schema_version", "authority_state_sha256", "status", + "signer_registry_sha256", "signer_registry_identity_sha256", + "signer_registry_revision", "evidence_authority_sha256", "observed_at", + "not_before", "expires_at", "issuer_key_id", "algorithm", + "signing_statement", "signature", "issuer", + } + state = closed(value, fields, "qualification authority state receipt") + if ( + state["schema_version"] + != "openadapt.qualification-authority-state-receipt/v2" + or state["status"] != "active" + ): + raise TrustError("qualification authority state is not active") + for field in ( + "signer_registry_sha256", "signer_registry_identity_sha256", + "evidence_authority_sha256", + ): + require_digest(state[field], field) + require_positive_int(state["signer_registry_revision"], "signer registry revision") + projection = dict(state) + identity = projection.pop("authority_state_sha256") + projection.pop("signature") + projection.pop("signing_statement") + if identity != digest_bytes(AUTHORITY_STATE_IDENTITY_DOMAIN, projection): + raise TrustError("qualification authority state identity is invalid") + _validate_embedded_signature( + state, + "qualification authority state", + object_schema_version="openadapt.qualification-authority-state-receipt/v2", + signature_domain=AUTHORITY_STATE_SIGNATURE_DOMAIN, + ) + issuer = closed( + state["issuer"], + {"repository", "repository_id", "repository_owner_id", "workflow", "ref", "source_commit", "environment"}, + "authority state issuer", + ) + if ( + issuer["repository"] != "OpenAdaptAI/openadapt-ops" + or issuer["repository_id"] != "1172011294" + or issuer["repository_owner_id"] != "132681217" + or issuer["workflow"] != ".github/workflows/qualification-authority-state.yml" + or issuer["ref"] != "refs/heads/main" + or issuer["environment"] != "qualification-authority-state" + or not isinstance(issuer["source_commit"], str) + or HEX40.fullmatch(issuer["source_commit"]) is None + ): + raise TrustError("authority state issuer differs") + validate_window( + { + "issued_at": state["observed_at"], + "not_before": state["not_before"], + "expires_at": state["expires_at"], + }, + maximum=timedelta(days=7), + now=now, + ) + return state + + +def validate_revocation_state( + value: Any, *, now: datetime | None = None +) -> dict[str, Any]: + fields = { + "schema_version", "revocation_state_sha256", + "previous_revocation_state_sha256", "revision", "status", + "authority_state_sha256", "signer_registry_sha256", "revocations", + "observed_at", "not_before", "expires_at", "issuer_key_id", "algorithm", + "signing_statement", "signature", "issuer", + } + state = closed(value, fields, "qualification revocation state receipt") + if ( + state["schema_version"] + != "openadapt.qualification-revocation-state-receipt/v1" + or state["status"] != "current" + ): + raise TrustError("qualification revocation state is not current") + for field in ("authority_state_sha256", "signer_registry_sha256"): + require_digest(state[field], field) + if state["previous_revocation_state_sha256"] is not None: + require_digest( + state["previous_revocation_state_sha256"], "previous revocation state" + ) + require_positive_int(state["revision"], "revocation state revision") + revocations = state["revocations"] + if not isinstance(revocations, list): + raise TrustError("revocations must be an array") + identities: list[tuple[str, str]] = [] + for index, item_value in enumerate(revocations): + item = closed( + item_value, + {"subject_kind", "subject_id", "revoked_at", "reason_code"}, + f"revocation {index}", + ) + if not all( + isinstance(item[field], str) and item[field] + for field in ("subject_kind", "subject_id", "reason_code") + ): + raise TrustError("revocation identity or reason is invalid") + if ( + item["subject_kind"] == "qualification-campaign-permit" + and DIGEST.fullmatch(item["subject_id"]) is None + ): + raise TrustError("campaign permit revocation subject must be opaque") + require_timestamp(item["revoked_at"], "revoked_at") + identities.append((item["subject_kind"], item["subject_id"])) + if identities != sorted(set(identities)): + raise TrustError("revocations must be sorted and unique") + projection = dict(state) + identity = projection.pop("revocation_state_sha256") + projection.pop("signature") + projection.pop("signing_statement") + if identity != digest_bytes(REVOCATION_STATE_IDENTITY_DOMAIN, projection): + raise TrustError("qualification revocation state identity is invalid") + _validate_embedded_signature( + state, + "qualification revocation state", + object_schema_version="openadapt.qualification-revocation-state-receipt/v1", + signature_domain=REVOCATION_STATE_SIGNATURE_DOMAIN, + ) + issuer = closed( + state["issuer"], + {"repository", "repository_id", "repository_owner_id", "workflow", "ref", "source_commit", "environment"}, + "revocation state issuer", + ) + if ( + issuer["repository"] != "OpenAdaptAI/openadapt-ops" + or issuer["repository_id"] != "1172011294" + or issuer["repository_owner_id"] != "132681217" + or issuer["workflow"] != ".github/workflows/qualification-revocation-state.yml" + or issuer["ref"] != "refs/heads/main" + or issuer["environment"] != "qualification-revocation-state" + or not isinstance(issuer["source_commit"], str) + or HEX40.fullmatch(issuer["source_commit"]) is None + ): + raise TrustError("revocation state issuer differs") + validate_window( + { + "issued_at": state["observed_at"], + "not_before": state["not_before"], + "expires_at": state["expires_at"], + }, + maximum=timedelta(days=7), + now=now, + ) + return state + + +def validate_admission_current_state( + admission_value: Any, + *, + admission_reference: Any, + authority_state: Any, + revocation_state: Any, + signer_registry: Any, + now: datetime | None = None, +) -> dict[str, Any]: + """Bind one release/workflow admission to the exact active trust state.""" + + verification_time = now or datetime.now(timezone.utc) + schema = admission_value.get("schema_version") if isinstance(admission_value, dict) else None + if schema == "openadapt.qualification-release/v1": + admission = validate_release(admission_value, now=verification_time) + expected_kind = "qualification-release" + elif schema == "openadapt.support-release-admission/v1": + admission = validate_support_release(admission_value, now=verification_time) + expected_kind = "support-release-admission" + elif schema == "openadapt.qualification-admission/v3": + admission = validate_qualification_admission( + admission_value, now=verification_time + ) + expected_kind = "qualification-admission" + else: + raise TrustError("admission schema is not current-state aware") + reference = evidence.validate_reference(admission_reference) + if ( + reference["kind"] != expected_kind + or reference["subject_sha256"] is not None + or reference["object_sha256"] + != "sha256:" + hashlib.sha256(canonical(admission) + b"\n").hexdigest() + or reference["semantic_identity_sha256"] + != evidence.semantic_identity_digest( + kind=expected_kind, + object_schema_version=reference["object_schema_version"], + object_value=admission, + object_sha256=reference["object_sha256"], + ) + ): + raise TrustError("admission reference differs from the signed object") + try: + registry = evidence.validate_signer_registry(signer_registry) + except evidence.EvidenceRegistryError as exc: + raise TrustError(str(exc)) from exc + authority = validate_authority_state(authority_state, now=verification_time) + revocation = validate_revocation_state(revocation_state, now=verification_time) + registry_identity = evidence.signer_registry_identity_digest(registry) + registry_raw_sha = "sha256:" + hashlib.sha256( + evidence.canonical(registry) + b"\n" + ).hexdigest() + if ( + authority["signer_registry_sha256"] != registry_raw_sha + or authority["signer_registry_identity_sha256"] != registry_identity + or authority["signer_registry_revision"] != registry["revision"] + or revocation["signer_registry_sha256"] != registry_identity + or revocation["authority_state_sha256"] + != authority["authority_state_sha256"] + or admission["signer_registry_sha256"] != registry_identity + or admission["revocation_state_sha256"] + != revocation["revocation_state_sha256"] + ): + raise TrustError("admission current trust state differs") + if "authority_state_sha256" in admission and ( + admission["authority_state_sha256"] != authority["authority_state_sha256"] + ): + raise TrustError("admission current authority state differs") + verify_embedded_signature( + authority, + signer_registry=registry, + object_schema_version="openadapt.qualification-authority-state-receipt/v2", + signature_domain=AUTHORITY_STATE_SIGNATURE_DOMAIN, + usage="qualification-authority-state-receipt", + now=verification_time, + ) + verify_embedded_signature( + revocation, + signer_registry=registry, + object_schema_version="openadapt.qualification-revocation-state-receipt/v1", + signature_domain=REVOCATION_STATE_SIGNATURE_DOMAIN, + usage="qualification-revocation-state-receipt", + now=verification_time, + ) + revoked = { + (item["subject_kind"], item["subject_id"]) + for item in revocation["revocations"] + } + if (reference["kind"], reference["semantic_identity_sha256"]) in revoked: + raise TrustError("admission is revoked") + for signer in registry["signers"]: + if signer["status"] == "active" and ( + "qualification-signer-key", signer["public_key_sha256"] + ) in revoked: + raise TrustError("an active signer key is revoked") + return admission + + +def validate_local_identity_opening(value: Any) -> dict[str, Any]: + opening = closed( + value, + { + "schema_version", "algorithm", "required", "customer_controlled_secret_required", + "exact_contract_match_required", "revalidation_before_actuation", + "maximum_age_seconds", + }, + "local identity opening", + ) + if opening != { + "schema_version": "openadapt.qualification-local-identity-opening/v1", + "algorithm": "hmac-sha256", + "required": True, + "customer_controlled_secret_required": True, + "exact_contract_match_required": True, + "revalidation_before_actuation": True, + "maximum_age_seconds": 60, + }: + raise TrustError("local customer-bound identity opening contract differs") + return opening + + +def validate_qualification_admission( + value: Any, *, now: datetime | None = None +) -> dict[str, Any]: + fields = { + "schema_version", "admission_id_sha256", "organization_id_sha256", + "workflow_id_sha256", "workflow_version_id_sha256", "bundle_version", + "bundle_sha256", "admitted_runtime_sha256", "application_contract_sha256", + "environment_contract_sha256", "input_contract_sha256", "action_contract_sha256", + "identity_contract_sha256", "effect_contract_sha256", "policy_contract_sha256", + "evidence_authority_sha256", "campaign_artifact_sha256", "campaign_permit_sha256", + "decision_receipt_reference", "decision_receipt_bundle_reference", + "signer_registry_sha256", "revocation_state_sha256", "entity_class", + "campaign_summary", "local_identity_opening", "verdict", "issued_at", + "not_before", "expires_at", "issuer", + } + admission = closed(value, fields, "qualification admission") + if admission["schema_version"] != "openadapt.qualification-admission/v3" or admission["verdict"] != "accepted": + raise TrustError("qualification admission schema or verdict is invalid") + for field in fields: + if field.endswith("_sha256"): + require_digest(admission[field], field) + if ( + not isinstance(admission["bundle_version"], str) + or len(admission["bundle_version"]) > 64 + or BUNDLE_VERSION.fullmatch(admission["bundle_version"]) is None + ): + raise TrustError("qualification admission bundle version is not canonical") + if not isinstance(admission["entity_class"], str) or ENTITY_CLASS.fullmatch(admission["entity_class"]) is None: + raise TrustError("qualification admission entity class is not remote-safe") + validate_campaign_summary(admission["campaign_summary"]) + validate_local_identity_opening(admission["local_identity_opening"]) + receipt_ref, _ = validate_reference_pair( + admission["decision_receipt_reference"], + admission["decision_receipt_bundle_reference"], + kind="qualification-evidence-decision-receipt", + ) + if admission["campaign_artifact_sha256"] == receipt_ref["object_sha256"]: + raise TrustError("campaign commitment cannot alias the receipt object") + issuer = closed( + admission["issuer"], + {"repository", "repository_id", "repository_owner_id", "workflow", "ref", "source_commit", "environment"}, + "qualification admission issuer", + ) + if ( + issuer["repository"] != "OpenAdaptAI/.github" + or issuer["repository_id"] != "858454062" + or issuer["repository_owner_id"] != "132681217" + or issuer["workflow"] != ".github/workflows/issue-qualification-admission.yml" + or issuer["ref"] != "refs/heads/main" + or issuer["environment"] != "qualification-admission" + or not isinstance(issuer["source_commit"], str) + or HEX40.fullmatch(issuer["source_commit"]) is None + ): + raise TrustError("qualification admission issuer differs") + validate_window(admission, maximum=timedelta(days=7), now=now) + projection = dict(admission) + admission_id = projection.pop("admission_id_sha256") + if admission_id != digest_bytes(ADMISSION_DOMAIN, projection): + raise TrustError("qualification admission id is invalid") + return admission + + +def _validate_release_identity(value: Any) -> dict[str, Any]: + identity = closed( + value, + { + "schema_version", "channel", "sequence", + "previous_admission_sha256", + }, + "release identity", + ) + if ( + identity["schema_version"] + != "openadapt.monotonic-production-release/v1" + or identity["channel"] != "production" + ): + raise TrustError("release identity schema or channel is invalid") + sequence = require_positive_int(identity["sequence"], "release sequence") + previous = identity["previous_admission_sha256"] + if sequence == 1: + if previous is not None: + raise TrustError("the first release identity previous admission must be null") + else: + require_digest(previous, "previous release admission") + return identity + + +def _validate_acceptance_issuer(value: Any) -> dict[str, Any]: + issuer = closed( + value, + { + "repository", "repository_id", "repository_owner_id", "workflow", + "ref", "source_commit", "environment", + }, + "production acceptance issuer", + ) + if ( + issuer["repository"] != "OpenAdaptAI/openadapt-evals" + or issuer["repository_id"] != "1135998197" + or issuer["repository_owner_id"] != "132681217" + or issuer["workflow"] + != ".github/workflows/issue-production-acceptance.yml" + or issuer["ref"] != "refs/heads/main" + or issuer["environment"] != "production-acceptance" + or not isinstance(issuer["source_commit"], str) + or HEX40.fullmatch(issuer["source_commit"]) is None + ): + raise TrustError("production acceptance issuer differs") + return issuer + + +def _validate_receipt_admission_binding( + receipt_value: Any, + admission_value: Any, + *, + signer_registry: Any, + now: datetime | None = None, +) -> tuple[dict[str, Any], dict[str, Any]]: + receipt = validate_receipt( + receipt_value, signer_registry=signer_registry, now=now + ) + admission = validate_qualification_admission(admission_value, now=now) + validate_reference_pair( + admission["decision_receipt_reference"], + admission["decision_receipt_bundle_reference"], + kind="qualification-evidence-decision-receipt", + ) + bindings = { + "organization_id_sha256": "organization_id_sha256", + "workflow_id_sha256": "workflow_id_sha256", + "workflow_version_id_sha256": "workflow_version_id_sha256", + "bundle_version": "bundle_version", + "bundle_sha256": "bundle_sha256", + "admitted_runtime_sha256": "admitted_runtime_sha256", + "application_contract_sha256": "application_contract_sha256", + "environment_contract_sha256": "environment_contract_sha256", + "input_contract_sha256": "input_contract_sha256", + "action_contract_sha256": "action_contract_sha256", + "identity_contract_sha256": "identity_contract_sha256", + "effect_contract_sha256": "effect_contract_sha256", + "policy_contract_sha256": "policy_contract_sha256", + "evidence_authority_sha256": "evidence_authority_contract_sha256", + "campaign_artifact_sha256": "campaign_artifact_sha256", + "campaign_permit_sha256": "campaign_permit_sha256", + "signer_registry_sha256": "signer_registry_sha256", + "revocation_state_sha256": "revocation_state_sha256", + "entity_class": "entity_class", + } + for admission_field, receipt_field in bindings.items(): + if admission[admission_field] != receipt[receipt_field]: + raise TrustError( + f"qualification admission {admission_field} differs from the receipt" + ) + if admission["campaign_summary"] != receipt["campaign_summary"]["classes"]: + raise TrustError("qualification admission campaign summary differs from the receipt") + receipt_not_before = require_timestamp(receipt["not_before"], "receipt not_before") + receipt_issued = require_timestamp(receipt["issued_at"], "receipt issued_at") + receipt_expires = require_timestamp(receipt["expires_at"], "receipt expires_at") + admission_not_before = require_timestamp( + admission["not_before"], "qualification admission not_before" + ) + admission_issued = require_timestamp( + admission["issued_at"], "qualification admission issued_at" + ) + admission_expires = require_timestamp( + admission["expires_at"], "qualification admission expires_at" + ) + if not ( + receipt_not_before <= receipt_issued <= admission_issued + and admission_not_before >= receipt_not_before + and admission_expires <= receipt_expires + ): + raise TrustError("qualification admission validity exceeds the receipt") + return receipt, admission + + +def validate_acceptance_manifest( + value: Any, + *, + receipt: Any, + qualification_admission: Any, + receipt_signer_registry: Any, + now: datetime | None = None, +) -> dict[str, Any]: + fields = { + "schema_version", "target", "verdict", "claim_scope", + "acceptance_policy_sha256", "lifecycle_policy_sha256", + "release_identity", "release", "release_sha256", "artifact_inventory", + "artifact_inventory_sha256", "publication_staging", + "publication_staging_sha256", + "qualification_evidence_decision_receipt_reference", + "qualification_evidence_decision_receipt_bundle_reference", + "qualification_admission_reference", "qualification_admission_bundle_reference", + "campaign_summary", "authority_state_sha256", "revocation_state_sha256", + "signer_registry_sha256", "issued_at", "not_before", "expires_at", "issuer", + } + manifest = closed(value, fields, "production acceptance manifest") + if ( + manifest["schema_version"] != "openadapt.production-acceptance/v2" + or manifest["verdict"] != "accepted" + or manifest["target"] not in TARGETS + or manifest["claim_scope"] + != TARGET_CONTRACTS[manifest["target"]]["claim_scope"] + ): + raise TrustError("production acceptance manifest identity differs") + for field in fields: + if field.endswith("_sha256"): + require_digest(manifest[field], field) + _validate_release_identity(manifest["release_identity"]) + _validate_acceptance_issuer(manifest["issuer"]) + release = closed( + manifest["release"], + { + "schema_version", "kind", "source_repository", "source_repository_id", + "source_commit", "version", "tag", "deployment_id", + "deployment_sha256", "artifacts", + }, + "manifest release candidate", + ) + contract = TARGET_CONTRACTS[manifest["target"]] + if ( + release["schema_version"] != "openadapt.production-release-candidate/v1" + or release["kind"] != contract["release_kind"] + or release["source_repository"] != contract["repository"] + or release["source_repository_id"] != contract["repository_id"] + or not isinstance(release["source_commit"], str) + or HEX40.fullmatch(release["source_commit"]) is None + ): + raise TrustError("manifest release candidate differs from the target policy") + artifacts = validate_artifacts(release["artifacts"], target=manifest["target"]) + if manifest["release_sha256"] != digest_bytes( + RELEASE_DOMAIN, + { + "target": manifest["target"], + "claim_scope": manifest["claim_scope"], + "release": release, + }, + ): + raise TrustError("manifest release candidate digest is invalid") + inventory = validate_artifact_inventory(manifest["artifact_inventory"]) + if ( + inventory["target"] != manifest["target"] + or inventory["claim_scope"] != manifest["claim_scope"] + or inventory["artifacts"] != artifacts + or manifest["artifact_inventory_sha256"] + != artifact_inventory_digest(inventory) + ): + raise TrustError("manifest artifact inventory differs from the release") + staging = validate_staging(manifest["publication_staging"]) + if ( + manifest["publication_staging_sha256"] != staging_digest(staging) + or staging["repository"] != release["source_repository"] + or staging["repository_id"] != release["source_repository_id"] + or staging["target_commitish"] != release["source_commit"] + or (release["tag"] is not None and staging["tag"] != release["tag"]) + ): + raise TrustError("manifest publication staging differs from the release") + bound_fields = ( + "name", "kind", "sha256", "size_bytes", "media_type", + "publish_destinations", + ) + staged_projection = sorted( + tuple( + item[field] if field != "publish_destinations" else tuple(item[field]) + for field in bound_fields + ) + for item in staging["assets"] + ) + admitted_projection = sorted( + tuple( + item[field] if field != "publish_destinations" else tuple(item[field]) + for field in bound_fields + ) + for item in artifacts + ) + if staged_projection != admitted_projection: + raise TrustError("manifest staged assets differ from the release inventory") + validate_reference_pair( + manifest["qualification_evidence_decision_receipt_reference"], + manifest["qualification_evidence_decision_receipt_bundle_reference"], + kind="qualification-evidence-decision-receipt", + ) + validate_reference_pair( + manifest["qualification_admission_reference"], + manifest["qualification_admission_bundle_reference"], + kind="qualification-admission", + ) + bound_receipt, bound_admission = _validate_receipt_admission_binding( + receipt, + qualification_admission, + signer_registry=receipt_signer_registry, + now=now, + ) + if ( + manifest["qualification_evidence_decision_receipt_reference"] + != bound_admission["decision_receipt_reference"] + or manifest["qualification_evidence_decision_receipt_bundle_reference"] + != bound_admission["decision_receipt_bundle_reference"] + or manifest["campaign_summary"] + != bound_receipt["campaign_summary"]["classes"] + or manifest["signer_registry_sha256"] + != bound_receipt["signer_registry_sha256"] + or manifest["revocation_state_sha256"] + != bound_receipt["revocation_state_sha256"] + ): + raise TrustError("production acceptance manifest evidence bindings differ") + validate_campaign_summary(manifest["campaign_summary"]) + validate_window(manifest, maximum=timedelta(days=7), now=now) + issued = require_timestamp(manifest["issued_at"], "manifest issued_at") + not_before = require_timestamp(manifest["not_before"], "manifest not_before") + expires = require_timestamp(manifest["expires_at"], "manifest expires_at") + if require_timestamp( + manifest["publication_staging"]["observed_at"], "staging observed_at" + ) > issued: + raise TrustError("publication staging was observed after manifest issuance") + for child, label in ((bound_receipt, "receipt"), (bound_admission, "admission")): + if ( + not_before < require_timestamp(child["not_before"], f"{label} not_before") + or issued < require_timestamp(child["issued_at"], f"{label} issued_at") + or expires > require_timestamp(child["expires_at"], f"{label} expires_at") + ): + raise TrustError(f"manifest validity exceeds the {label}") + return manifest + + +def acceptance_summary_identity(value: Mapping[str, Any]) -> str: + projection = { + field: value[field] + for field in ( + "target", "claim_scope", "release_identity", "release_sha256", + "artifact_inventory_sha256", "publication_staging_sha256", + "qualification_evidence_decision_receipt_reference", + "qualification_admission_reference", + "production_acceptance_manifest_reference", "campaign_summary", + "authority_state_sha256", "revocation_state_sha256", + "signer_registry_sha256", "issuer", + ) + } + return digest_bytes(PRODUCTION_EVIDENCE_IDENTITY_DOMAIN, projection) + + +def validate_acceptance_summary( + value: Any, + *, + manifest: Any, + receipt: Any, + qualification_admission: Any, + receipt_signer_registry: Any, + now: datetime | None = None, +) -> dict[str, Any]: + fields = { + "schema_version", "target", "verdict", "claim_scope", + "acceptance_policy_sha256", "lifecycle_policy_sha256", + "release_identity", "release_sha256", "artifact_inventory_sha256", + "publication_staging", "publication_staging_sha256", + "evidence_identity_sha256", + "qualification_evidence_decision_receipt_reference", + "qualification_evidence_decision_receipt_bundle_reference", + "qualification_admission_reference", "qualification_admission_bundle_reference", + "production_acceptance_manifest_reference", + "production_acceptance_manifest_bundle_reference", "campaign_summary", + "authority_state_sha256", "revocation_state_sha256", "signer_registry_sha256", + "issued_at", "not_before", "expires_at", "issuer", + } + summary = closed(value, fields, "production acceptance summary") + if ( + summary["schema_version"] + != "openadapt.production-lifecycle-evidence-summary/v2" + or summary["verdict"] != "accepted" + or summary["target"] not in TARGETS + or summary["claim_scope"] + != TARGET_CONTRACTS[summary["target"]]["claim_scope"] + ): + raise TrustError("production acceptance summary identity differs") + for field in fields: + if field.endswith("_sha256"): + require_digest(summary[field], field) + _validate_release_identity(summary["release_identity"]) + _validate_acceptance_issuer(summary["issuer"]) + validate_reference_pair( + summary["qualification_evidence_decision_receipt_reference"], + summary["qualification_evidence_decision_receipt_bundle_reference"], + kind="qualification-evidence-decision-receipt", + ) + validate_reference_pair( + summary["qualification_admission_reference"], + summary["qualification_admission_bundle_reference"], + kind="qualification-admission", + ) + validate_reference_pair( + summary["production_acceptance_manifest_reference"], + summary["production_acceptance_manifest_bundle_reference"], + kind="production-acceptance-manifest", + ) + bound_manifest = validate_acceptance_manifest( + manifest, + receipt=receipt, + qualification_admission=qualification_admission, + receipt_signer_registry=receipt_signer_registry, + now=now, + ) + for field in ( + "target", "claim_scope", "acceptance_policy_sha256", + "lifecycle_policy_sha256", "release_identity", "release_sha256", + "artifact_inventory_sha256", "publication_staging", + "publication_staging_sha256", + "qualification_evidence_decision_receipt_reference", + "qualification_evidence_decision_receipt_bundle_reference", + "qualification_admission_reference", "qualification_admission_bundle_reference", + "campaign_summary", "authority_state_sha256", "revocation_state_sha256", + "signer_registry_sha256", "issuer", + ): + if summary[field] != bound_manifest[field]: + raise TrustError(f"production acceptance summary {field} differs from the manifest") + validate_window(summary, maximum=timedelta(days=7), now=now) + summary_not_before = require_timestamp(summary["not_before"], "summary not_before") + summary_issued = require_timestamp(summary["issued_at"], "summary issued_at") + summary_expires = require_timestamp(summary["expires_at"], "summary expires_at") + if ( + summary_not_before + < require_timestamp(bound_manifest["not_before"], "manifest not_before") + or summary_issued + < require_timestamp(bound_manifest["issued_at"], "manifest issued_at") + or summary_expires + > require_timestamp(bound_manifest["expires_at"], "manifest expires_at") + ): + raise TrustError("summary validity exceeds the manifest") + if summary["evidence_identity_sha256"] != acceptance_summary_identity(summary): + raise TrustError("production acceptance evidence identity differs") + return summary + + +def validate_release_evidence_chain( + release_value: Any, + *, + summary: Any, + manifest: Any, + receipt: Any, + qualification_admission: Any, + receipt_signer_registry: Any, + now: datetime | None = None, +) -> dict[str, Any]: + release = validate_release(release_value, now=now) + bound_summary = validate_acceptance_summary( + summary, + manifest=manifest, + receipt=receipt, + qualification_admission=qualification_admission, + receipt_signer_registry=receipt_signer_registry, + now=now, + ) + bound_manifest = dict(manifest) + for field in ( + "target", "claim_scope", "release_identity", "release_sha256", + "artifact_inventory_sha256", "publication_staging", + "publication_staging_sha256", "authority_state_sha256", + "revocation_state_sha256", "signer_registry_sha256", + ): + if release[field] != bound_summary[field]: + raise TrustError(f"release admission {field} differs from its evidence chain") + if release["release"] != bound_manifest["release"]: + raise TrustError("release admission candidate differs from its manifest") + if ( + bound_manifest["artifact_inventory"]["target"] != release["target"] + or bound_manifest["artifact_inventory"]["claim_scope"] + != release["claim_scope"] + or bound_manifest["artifact_inventory"]["artifacts"] + != release["release"]["artifacts"] + ): + raise TrustError("release admission inventory differs from its manifest") + if release["publication_policy_sha256"] != bound_summary["acceptance_policy_sha256"]: + raise TrustError("release publication policy differs from acceptance policy") + release_not_before = require_timestamp(release["not_before"], "release not_before") + release_issued = require_timestamp(release["issued_at"], "release issued_at") + release_expires = require_timestamp(release["expires_at"], "release expires_at") + if ( + release_not_before + < require_timestamp(bound_summary["not_before"], "summary not_before") + or release_issued + < require_timestamp(bound_summary["issued_at"], "summary issued_at") + or release_expires + > require_timestamp(bound_summary["expires_at"], "summary expires_at") + ): + raise TrustError("release validity exceeds its acceptance summary") + return release + + +def validate_cloud_deploy_authorization( + value: Any, *, now: datetime | None = None +) -> dict[str, Any]: + fields = { + "schema_version", "authorization_sha256", "deployment_intent_sha256", + "profile_repository", "profile_repository_id", "profile_commit", + "profile_workflow", "profile_run_id", "profile_run_attempt", + "cloud_source_commitment_sha256", "expected_live_attestation_sha256", + "provider_idempotency_key", "audience", "signer_registry_sha256", + "revocation_state_sha256", "issued_at", "not_before", "expires_at", "issuer", + } + authorization = closed(value, fields, "production Cloud deploy authorization") + if ( + authorization["schema_version"] + != "openadapt.production-cloud-deploy-authorization/v1" + or authorization["audience"] != "openadapt-public-cloud-source-proof" + or authorization["profile_run_attempt"] != 1 + ): + raise TrustError("production Cloud deploy authorization identity differs") + for field in fields: + if field.endswith("_sha256"): + require_digest(authorization[field], field) + for field in ("profile_repository_id", "profile_run_id"): + require_decimal_id(authorization[field], field) + if HEX40.fullmatch(authorization["profile_commit"]) is None: + raise TrustError("Cloud profile commit must be exact") + if ( + not isinstance(authorization["provider_idempotency_key"], str) + or re.fullmatch(r"cloud-deploy:[0-9a-f]{64}", authorization["provider_idempotency_key"]) + is None + ): + raise TrustError("Cloud provider idempotency key is invalid") + issuer = closed( + authorization["issuer"], + {"repository", "repository_id", "repository_owner_id", "workflow", "ref", "source_commit", "environment"}, + "Cloud authorization issuer", + ) + if ( + issuer["repository"] != "OpenAdaptAI/openadapt-ops" + or issuer["repository_id"] != "1172011294" + or issuer["repository_owner_id"] != "132681217" + or issuer["workflow"] + != ".github/workflows/production-cloud-deploy-authorization.yml" + or issuer["ref"] != "refs/heads/main" + or issuer["environment"] != "production-cloud-deploy-authorization" + or not isinstance(issuer["source_commit"], str) + or HEX40.fullmatch(issuer["source_commit"]) is None + ): + raise TrustError("Cloud authorization issuer differs") + validate_window(authorization, maximum=timedelta(minutes=5), now=now) + projection = dict(authorization) + identity = projection.pop("authorization_sha256") + if identity != digest_bytes(CLOUD_AUTHORIZATION_DOMAIN, projection): + raise TrustError("Cloud deploy authorization identity is invalid") + return authorization + + +def validate_cloud_deployment_handoff( + value: Any, *, now: datetime | None = None +) -> dict[str, Any]: + fields = { + "schema_version", "handoff_id_sha256", "authorization_reference", + "authorization_bundle_reference", "authorization_sha256", + "cloud_source_commitment_sha256", "profile_commit", "profile_run_id", + "profile_run_attempt", "source_proof_request", "source_proof_request_sha256", + "source_proof_response", "source_proof_response_sha256", + "reviewed_public_values_sha256", + "expected_live_attestation_sha256", "live_attestation_sha256", + "provider_idempotency_key", "audience", "signer_registry_sha256", + "revocation_state_sha256", "verdict", "issued_at", "not_before", + "expires_at", "issuer", + } + handoff = closed(value, fields, "production Cloud deployment handoff") + if ( + handoff["schema_version"] + != "openadapt.production-cloud-deployment-result/v1" + or handoff["audience"] != "openadapt-private-cloud-production-deploy" + or handoff["verdict"] != "accepted" + or handoff["profile_run_attempt"] != "1" + ): + raise TrustError("production Cloud deployment handoff identity differs") + for field in fields: + if field.endswith("_sha256"): + require_digest(handoff[field], field) + require_decimal_id(handoff["profile_run_id"], "profile run id") + if HEX40.fullmatch(handoff["profile_commit"]) is None: + raise TrustError("Cloud handoff profile commit must be exact") + validate_reference_pair( + handoff["authorization_reference"], + handoff["authorization_bundle_reference"], + kind="production-cloud-deploy-authorization", + ) + request_fields = { + "schema_version", "authorization_sha256", "cloud_source_commit", + "expected_live_attestation_sha256", "expires_at", "issued_at", "nonce", + "production_origin", "profile_commit", "profile_environment", + "profile_repository", "profile_workflow_ref", "run_attempt", "run_id", + "site_id", "source_names", + } + request = closed(handoff["source_proof_request"], request_fields, "Cloud source proof request") + if ( + request["schema_version"] + != "openadapt.production-secret-source-proof-request/v2" + or request["run_attempt"] != "1" + or HEX40.fullmatch(request["cloud_source_commit"]) is None + or request["production_origin"] != "https://app.openadapt.ai" + or request["site_id"] != "ccf1a48a-d934-48ea-a9a8-36cbddf27cb5" + or request["profile_repository"] != "OpenAdaptAI/.github" + or request["profile_environment"] != "production-cloud-deploy" + or request["profile_workflow_ref"] + != ( + "OpenAdaptAI/.github/.github/workflows/" + "production-cloud-deploy.yml@refs/heads/main" + ) + ): + raise TrustError("Cloud source proof request identity differs") + for field in ("authorization_sha256", "expected_live_attestation_sha256"): + require_raw_digest(request[field], f"Cloud request {field}") + nonce = request["nonce"] + if not isinstance(nonce, str) or "=" in nonce: + raise TrustError("Cloud source proof nonce is not canonical base64url") + try: + nonce_bytes = evidence.base64.urlsafe_b64decode( + nonce + "=" * (-len(nonce) % 4) + ) + except Exception as exc: + raise TrustError("Cloud source proof nonce is not canonical base64url") from exc + if ( + len(nonce_bytes) != 32 + or evidence.base64.urlsafe_b64encode(nonce_bytes).decode().rstrip("=") + != nonce + ): + raise TrustError("Cloud source proof nonce must encode exactly 32 bytes") + source_names = request["source_names"] + if ( + not isinstance(source_names, list) + or source_names != list(CLOUD_LIVE_SOURCE_NAMES) + ): + raise TrustError("Cloud source names differ from the frozen live source set") + if handoff["source_proof_request_sha256"] != digest_bytes( + CLOUD_SOURCE_PROOF_REQUEST_DOMAIN, request + ): + raise TrustError("Cloud source proof request digest differs") + expected_idempotency = "cloud-deploy:" + hashlib.sha256( + CLOUD_PROVIDER_IDEMPOTENCY_DOMAIN + + canonical( + { + "authorization_sha256": handoff["authorization_sha256"], + "source_proof_request_sha256": handoff[ + "source_proof_request_sha256" + ], + "profile_run_id": handoff["profile_run_id"], + "profile_run_attempt": handoff["profile_run_attempt"], + } + ) + ).hexdigest() + if handoff["provider_idempotency_key"] != expected_idempotency: + raise TrustError("Cloud provider idempotency key differs") + commitment = "sha256:" + hashlib.sha256( + CLOUD_SOURCE_COMMIT_DOMAIN + request["cloud_source_commit"].encode("ascii") + ).hexdigest() + if handoff["cloud_source_commitment_sha256"] != commitment: + raise TrustError("Cloud source commit commitment differs") + response_fields = { + "schema_version", "request_sha256", "authorization_sha256", + "live_attestation_sha256", "live_deployment", "oidc_identity_sha256", + "production_origin", "proofs", "public_values", "site_id", + } + response = closed(handoff["source_proof_response"], response_fields, "Cloud source proof response") + if response["schema_version"] != "openadapt.production-secret-source-proof-response/v2": + raise TrustError("Cloud source proof response schema differs") + for field in response_fields: + if field.endswith("_sha256"): + require_raw_digest(response[field], field) + live_deployment = closed( + response["live_deployment"], + {"build_id", "source_commit"}, + "Cloud live deployment", + ) + if ( + not isinstance(live_deployment["build_id"], str) + or DIGEST.fullmatch(live_deployment["build_id"]) is None + or not isinstance(live_deployment["source_commit"], str) + or HEX40.fullmatch(live_deployment["source_commit"]) is None + ): + raise TrustError("Cloud live deployment identity differs") + proofs = response["proofs"] + if not isinstance(proofs, dict) or sorted(proofs) != list(CLOUD_SECRET_SOURCE_NAMES): + raise TrustError("Cloud source proof set differs from the request") + for digest in proofs.values(): + require_raw_digest(digest, "Cloud HMAC proof") + public_values = response["public_values"] + if ( + not isinstance(public_values, dict) + or set(public_values) != {CLOUD_PUBLIC_SOURCE_NAME} + or not isinstance(public_values[CLOUD_PUBLIC_SOURCE_NAME], str) + or not public_values[CLOUD_PUBLIC_SOURCE_NAME] + ): + raise TrustError("reviewed public values differ") + if sorted([*proofs, *public_values]) != list(CLOUD_LIVE_SOURCE_NAMES): + raise TrustError("Cloud source proof partition differs from the request") + if handoff["reviewed_public_values_sha256"] != digest_bytes( + CLOUD_PUBLIC_VALUES_DOMAIN, public_values + ): + raise TrustError("reviewed public values digest differs") + if handoff["source_proof_response_sha256"] != "sha256:" + hashlib.sha256(canonical(response)).hexdigest(): + raise TrustError("Cloud source proof response digest differs") + equalities = { + "authorization_sha256": handoff["authorization_sha256"].removeprefix( + "sha256:" + ), + "profile_commit": handoff["profile_commit"], + "run_id": handoff["profile_run_id"], + "run_attempt": handoff["profile_run_attempt"], + "expected_live_attestation_sha256": handoff[ + "expected_live_attestation_sha256" + ].removeprefix("sha256:"), + "issued_at": handoff["issued_at"], + "expires_at": handoff["expires_at"], + } + if any(request[field] != expected for field, expected in equalities.items()): + raise TrustError("Cloud source proof request differs from the handoff") + if ( + "sha256:" + response["request_sha256"] + != handoff["source_proof_request_sha256"] + or "sha256:" + response["authorization_sha256"] + != handoff["authorization_sha256"] + or "sha256:" + request["authorization_sha256"] + != handoff["authorization_sha256"] + or "sha256:" + request["expected_live_attestation_sha256"] + != handoff["expected_live_attestation_sha256"] + or "sha256:" + response["live_attestation_sha256"] + != handoff["live_attestation_sha256"] + or response["production_origin"] != request["production_origin"] + or response["site_id"] != request["site_id"] + or live_deployment["source_commit"] != request["cloud_source_commit"] + ): + raise TrustError("Cloud source proof response differs from the handoff") + issuer = closed( + handoff["issuer"], + {"repository", "repository_id", "repository_owner_id", "workflow", "ref", "source_commit", "environment"}, + "Cloud handoff issuer", + ) + if ( + issuer["repository"] != "OpenAdaptAI/.github" + or issuer["repository_id"] != "858454062" + or issuer["repository_owner_id"] != "132681217" + or issuer["workflow"] != ".github/workflows/production-cloud-deploy.yml" + or issuer["ref"] != "refs/heads/main" + or issuer["environment"] != "production-cloud-deploy" + or not isinstance(issuer["source_commit"], str) + or HEX40.fullmatch(issuer["source_commit"]) is None + ): + raise TrustError("Cloud handoff issuer differs") + validate_window(handoff, maximum=timedelta(minutes=5), now=now) + projection = dict(handoff) + identity = projection.pop("handoff_id_sha256") + if identity != digest_bytes(CLOUD_HANDOFF_DOMAIN, projection): + raise TrustError("Cloud deployment handoff identity differs") + return handoff + + +def validate_current_default(value: Any, *, now: datetime | None = None) -> dict[str, Any]: + current = closed( + value, + { + "schema_version", "default_set_revision", "previous_default_set_sha256", + "targets", "issued_at", "not_before", "expires_at", "issuer", + }, + "production current default", + ) + if current["schema_version"] != "openadapt.production-current-default/v1": + raise TrustError("current default schema is not supported") + require_positive_int(current["default_set_revision"], "default set revision") + if current["previous_default_set_sha256"] is not None: + require_digest(current["previous_default_set_sha256"], "previous default set") + targets = current["targets"] + if not isinstance(targets, list) or [item.get("target") for item in targets if isinstance(item, dict)] != list(TARGETS): + raise TrustError("current default must contain all seven targets in order") + for index, target_value in enumerate(targets): + target = closed( + target_value, + { + "target", "release_sha256", "artifact_inventory_sha256", "version", "tag", + "deployment_id", "deployment_sha256", "qualification_release_sha256", + "default_identity_sha256", + }, + f"current default target {index}", + ) + for field in ("release_sha256", "artifact_inventory_sha256", "qualification_release_sha256", "default_identity_sha256"): + require_digest(target[field], field) + if target["deployment_sha256"] is not None: + require_digest(target["deployment_sha256"], "deployment digest") + if target["deployment_id"] is not None: + require_decimal_id(target["deployment_id"], "deployment id") + issuer = closed( + current["issuer"], + { + "repository", "repository_id", "repository_owner_id", "workflow", + "ref", "source_commit", "environment", + }, + "current default issuer", + ) + if ( + issuer["repository"] != "OpenAdaptAI/openadapt-ops" + or issuer["repository_id"] != "1172011294" + or issuer["repository_owner_id"] != "132681217" + or issuer["workflow"] + != ".github/workflows/production-current-default.yml" + or issuer["ref"] != "refs/heads/main" + or issuer["environment"] != "production-current-default" + or not isinstance(issuer["source_commit"], str) + or HEX40.fullmatch(issuer["source_commit"]) is None + ): + raise TrustError("current default issuer differs") + validate_window(current, maximum=timedelta(days=7), now=now) + return current + + +def validate_projection(value: Any) -> dict[str, Any]: + projection = closed(value, {"schema_version", "overall_state", "targets"}, "lifecycle projection") + if projection["schema_version"] != "openadapt.production-lifecycle-projection/v2" or projection["overall_state"] not in {"Production", "not actively admitted"}: + raise TrustError("lifecycle projection schema or state is invalid") + targets = projection["targets"] + if not isinstance(targets, list) or [item.get("target") for item in targets if isinstance(item, dict)] != list(TARGETS): + raise TrustError("lifecycle projection must contain all seven targets in order") + all_production = True + for index, target_value in enumerate(targets): + target = closed( + target_value, + { + "target", "state", "claim_scope", "release_sha256", + "artifact_inventory_sha256", "release_admission_object_sha256", + "default_identity_sha256", "not_before", "expires_at", "reason_codes", + }, + f"lifecycle projection target {index}", + ) + if target["state"] not in {"Production", "not actively admitted"}: + raise TrustError("target lifecycle state is invalid") + all_production = all_production and target["state"] == "Production" + for field in ("release_sha256", "artifact_inventory_sha256", "release_admission_object_sha256", "default_identity_sha256"): + require_digest(target[field], field) + require_timestamp(target["not_before"], "target not_before") + require_timestamp(target["expires_at"], "target expires_at") + reasons = target["reason_codes"] + if not isinstance(reasons, list) or reasons != sorted(set(reasons)) or any(not isinstance(reason, str) or not reason for reason in reasons): + raise TrustError("reason codes must be sorted unique strings") + if target["state"] == "Production" and reasons: + raise TrustError("Production target cannot have reason codes") + if (projection["overall_state"] == "Production") != all_production: + raise TrustError("overall lifecycle state differs from target states") + return projection + + +def _validate_reference_pair_array(value: Any, *, release: bool) -> list[dict[str, Any]]: + if not isinstance(value, list) or not value: + raise TrustError("admission reference set is empty") + for index, item_value in enumerate(value): + fields = {"target", "admission_reference", "admission_bundle_reference"} if release else {"admission_reference", "admission_bundle_reference"} + item = closed(item_value, fields, f"admission reference pair {index}") + validate_reference_pair( + item["admission_reference"], item["admission_bundle_reference"], + kind="qualification-release" if release else "qualification-admission", + ) + if release: + if [item["target"] for item in value] != list(TARGETS): + raise TrustError("release admission set must contain all seven targets in order") + else: + digests = [item["admission_reference"]["object_sha256"] for item in value] + if digests != sorted(set(digests)): + raise TrustError("workflow admission set must be sorted and unique") + return value + + +def validate_checkpoint(value: Any, *, now: datetime | None = None) -> dict[str, Any]: + fields = { + "schema_version", "checkpoint_id_sha256", "checkpoint_revision", + "previous_checkpoint_sha256", "registry_source_commit", "registry_revision", + "registry_head_sha256", "signer_registry", "lifecycle_policy_sha256", + "current_default_reference", "current_default_bundle_reference", + "lifecycle_projection", "lifecycle_projection_sha256", "release_admissions", + "release_admission_set_sha256", "workflow_admissions", + "workflow_admission_set_sha256", "authority_state_reference", + "authority_state_bundle_reference", "authority_state_sha256", + "revocation_state_reference", "revocation_state_bundle_reference", + "revocation_state_sha256", "generated_at", "not_before", "expires_at", "issuer", + } + checkpoint = closed(value, fields, "production lifecycle checkpoint") + if checkpoint["schema_version"] != "openadapt.production-lifecycle-checkpoint/v1": + raise TrustError("lifecycle checkpoint schema is not supported") + require_positive_int(checkpoint["checkpoint_revision"], "checkpoint revision") + if checkpoint["previous_checkpoint_sha256"] is not None: + require_digest(checkpoint["previous_checkpoint_sha256"], "previous checkpoint") + if not isinstance(checkpoint["registry_source_commit"], str) or HEX40.fullmatch(checkpoint["registry_source_commit"]) is None: + raise TrustError("checkpoint registry commit must be exact") + require_positive_int(checkpoint["registry_revision"], "checkpoint registry revision") + require_digest(checkpoint["registry_head_sha256"], "checkpoint registry head") + if not isinstance(checkpoint["signer_registry"], dict): + raise TrustError("checkpoint signer registry pointer is required") + projection = validate_projection(checkpoint["lifecycle_projection"]) + if checkpoint["lifecycle_projection_sha256"] != digest_bytes(PROJECTION_DOMAIN, projection): + raise TrustError("lifecycle projection digest is invalid") + release_set = _validate_reference_pair_array(checkpoint["release_admissions"], release=True) + if checkpoint["release_admission_set_sha256"] != digest_bytes(RELEASE_SET_DOMAIN, release_set): + raise TrustError("release admission set digest is invalid") + workflow_set = _validate_reference_pair_array(checkpoint["workflow_admissions"], release=False) + if checkpoint["workflow_admission_set_sha256"] != digest_bytes(WORKFLOW_SET_DOMAIN, workflow_set): + raise TrustError("workflow admission set digest is invalid") + validate_reference_pair(checkpoint["current_default_reference"], checkpoint["current_default_bundle_reference"], kind="production-current-default") + authority, _ = validate_reference_pair(checkpoint["authority_state_reference"], checkpoint["authority_state_bundle_reference"], kind="qualification-authority-state-receipt") + revocation, _ = validate_reference_pair(checkpoint["revocation_state_reference"], checkpoint["revocation_state_bundle_reference"], kind="qualification-revocation-state-receipt") + if checkpoint["authority_state_sha256"] != authority["object_sha256"] or checkpoint["revocation_state_sha256"] != revocation["object_sha256"]: + raise TrustError("checkpoint state digests differ from their references") + require_digest(checkpoint["lifecycle_policy_sha256"], "lifecycle policy digest") + require_timestamp(checkpoint["generated_at"], "checkpoint generated_at") + issuer = closed( + checkpoint["issuer"], + { + "repository", "repository_id", "repository_owner_id", "workflow", + "ref", "source_commit", "environment", "policy_repository", + "policy_repository_id", "policy_source_commit", "policy_path", + }, + "checkpoint issuer", + ) + for field in ("source_commit", "policy_source_commit"): + if not isinstance(issuer[field], str) or HEX40.fullmatch(issuer[field]) is None: + raise TrustError("checkpoint issuer commit must be exact") + if ( + issuer["repository"] != "OpenAdaptAI/openadapt-ops" + or issuer["repository_id"] != "1172011294" + or issuer["repository_owner_id"] != "132681217" + or issuer["workflow"] + != ".github/workflows/production-lifecycle-checkpoint.yml" + or issuer["ref"] != "refs/heads/main" + or issuer["environment"] != "production-lifecycle-checkpoint" + or issuer["policy_repository"] != "OpenAdaptAI/.github" + or issuer["policy_repository_id"] != "858454062" + or issuer["policy_source_commit"] != checkpoint["registry_source_commit"] + or issuer["policy_path"] != "production-lifecycle-policy.json" + ): + raise TrustError("checkpoint lifecycle policy source differs") + generated = require_timestamp(checkpoint["generated_at"], "checkpoint generated_at") + not_before = require_timestamp(checkpoint["not_before"], "checkpoint not_before") + expires = require_timestamp(checkpoint["expires_at"], "checkpoint expires_at") + if not generated <= not_before < expires <= generated + timedelta(days=7): + raise TrustError("checkpoint scheduled validity window is invalid") + if now is not None and not not_before <= now < expires: + raise TrustError("checkpoint is not active at the requested time") + projection_without_id = dict(checkpoint) + checkpoint_id = projection_without_id.pop("checkpoint_id_sha256") + if checkpoint_id != digest_bytes(CHECKPOINT_DOMAIN, projection_without_id): + raise TrustError("checkpoint id is invalid") + return checkpoint + + +def validate_checkpoint_expiry_containment( + checkpoint_value: Any, + *, + signer_registry: Any, + current_default: Any, + release_admissions: Sequence[Any], + workflow_admissions: Sequence[Any], + authority_state: Any, + revocation_state: Any, + acceptance_summaries: Sequence[Any] = (), + acceptance_manifests: Sequence[Any] = (), + decision_receipts: Sequence[Any] = (), + now: datetime | None = None, +) -> dict[str, Any]: + """Require the retained checkpoint window to fit every verified child. + + ``now`` selects an active checkpoint for a runtime read. Issuance callers + omit it so that they can construct a checkpoint before its scheduled + ``not_before``. A child must already exist when the checkpoint is issued, + and its complete validity window must contain the checkpoint window. + """ + + checkpoint = validate_checkpoint(checkpoint_value, now=now) + try: + registry = evidence.validate_signer_registry(signer_registry) + except evidence.EvidenceRegistryError as exc: + raise TrustError(str(exc)) from exc + current = validate_current_default(current_default) + releases = [validate_release(item) for item in release_admissions] + workflows = [ + validate_qualification_admission(item) + for item in workflow_admissions + ] + authority = validate_authority_state(authority_state) + revocation = validate_revocation_state(revocation_state) + summaries = list(acceptance_summaries) + manifests = list(acceptance_manifests) + receipts = [_validate_receipt_structure(item) for item in decision_receipts] + if len(releases) != len(checkpoint["release_admissions"]): + raise TrustError("checkpoint release object count differs") + if len(workflows) != len(checkpoint["workflow_admissions"]): + raise TrustError("checkpoint workflow object count differs") + + checkpoint_not_before = require_timestamp( + checkpoint["not_before"], "checkpoint not_before" + ) + checkpoint_expires = require_timestamp( + checkpoint["expires_at"], "checkpoint expires_at" + ) + checkpoint_generated = require_timestamp( + checkpoint["generated_at"], "checkpoint generated_at" + ) + windows = [ + ( + evidence._timestamp(registry["generated_at"], "generated_at"), + evidence._timestamp(registry["expires_at"], "expires_at"), + evidence._timestamp(registry["generated_at"], "generated_at"), + "signer registry", + ), + ( + require_timestamp(current["not_before"], "current default not_before"), + require_timestamp(current["expires_at"], "current default expires_at"), + require_timestamp(current["issued_at"], "current default issued_at"), + "current default", + ), + ( + require_timestamp(authority["not_before"], "authority not_before"), + require_timestamp(authority["expires_at"], "authority expires_at"), + require_timestamp(authority["observed_at"], "authority observed_at"), + "authority state", + ), + ( + require_timestamp(revocation["not_before"], "revocation not_before"), + require_timestamp(revocation["expires_at"], "revocation expires_at"), + require_timestamp(revocation["observed_at"], "revocation observed_at"), + "revocation state", + ), + ] + windows.extend( + ( + require_timestamp(item["not_before"], "release not_before"), + require_timestamp(item["expires_at"], "release expires_at"), + require_timestamp(item["issued_at"], "release issued_at"), + f"{item['target']} release", + ) + for item in releases + ) + windows.extend( + ( + require_timestamp(item["not_before"], "workflow not_before"), + require_timestamp(item["expires_at"], "workflow expires_at"), + require_timestamp(item["issued_at"], "workflow issued_at"), + "workflow admission", + ) + for item in workflows + ) + for items, label in ( + (summaries, "acceptance summary"), + (manifests, "acceptance manifest"), + (receipts, "qualification decision receipt"), + ): + windows.extend( + ( + require_timestamp(item["not_before"], f"{label} not_before"), + require_timestamp(item["expires_at"], f"{label} expires_at"), + require_timestamp(item["issued_at"], f"{label} issued_at"), + label, + ) + for item in items + ) + for child_not_before, child_expires, child_issued, label in windows: + if child_issued > checkpoint_generated: + raise TrustError(f"{label} was issued after the checkpoint") + if checkpoint_not_before < child_not_before or checkpoint_expires > child_expires: + raise TrustError(f"checkpoint validity exceeds {label} validity") + return checkpoint + + +def validate_feed(value: Any, *, now: datetime | None = None) -> dict[str, Any]: + feed = closed( + value, + { + "schema_version", "repository", "repository_id", "repository_owner_id", "ref", + "feed_revision", "generated_at", "expires_at", "registry_source_commit", + "registry_revision", "registry_head_sha256", "signer_registry", "checkpoints", + }, + "production lifecycle feed", + ) + if feed["schema_version"] != "openadapt.production-lifecycle-feed/v1" or feed["repository"] != "OpenAdaptAI/.github" or feed["repository_id"] != "858454062" or feed["repository_owner_id"] != "132681217" or feed["ref"] != "refs/heads/production-lifecycle-feed": + raise TrustError("lifecycle feed identity differs") + require_positive_int(feed["feed_revision"], "feed revision") + if not isinstance(feed["registry_source_commit"], str) or HEX40.fullmatch(feed["registry_source_commit"]) is None: + raise TrustError("feed registry commit must be exact") + require_positive_int(feed["registry_revision"], "feed registry revision") + require_digest(feed["registry_head_sha256"], "feed registry head") + if not isinstance(feed["signer_registry"], dict): + raise TrustError("feed signer registry pointer is required") + checkpoints = feed["checkpoints"] + if not isinstance(checkpoints, list) or len(checkpoints) not in {1, 2}: + raise TrustError("feed must select one or two checkpoints") + for index, pair_value in enumerate(checkpoints): + pair = closed(pair_value, {"checkpoint_reference", "checkpoint_bundle_reference"}, f"feed checkpoint {index}") + validate_reference_pair(pair["checkpoint_reference"], pair["checkpoint_bundle_reference"], kind="production-lifecycle-checkpoint") + generated = require_timestamp(feed["generated_at"], "feed generated_at") + expires = require_timestamp(feed["expires_at"], "feed expires_at") + if not generated < expires <= generated + timedelta(days=7): + raise TrustError("feed lifetime is invalid") + if now is not None and not generated <= now < expires: + raise TrustError("feed is not current") + return feed + + +def validate_feed_expiry_containment( + feed_value: Any, + *, + checkpoints: Sequence[Any], + signer_registry: Any, + now: datetime | None = None, +) -> dict[str, Any]: + """Validate one active checkpoint or one exact future rotation. + + Two-checkpoint feeds are newest-first. The older checkpoint is active at + feed issuance. The newer checkpoint starts at the exact instant that the + older checkpoint expires. Signed validity intervals are half-open and do + not use clock-skew grace. + """ + + feed = validate_feed(feed_value, now=now) + try: + registry = evidence.validate_signer_registry(signer_registry) + except evidence.EvidenceRegistryError as exc: + raise TrustError(str(exc)) from exc + pointer = evidence._validate_signer_pointer(feed["signer_registry"]) + assert pointer is not None + registry_raw = evidence.canonical(registry) + b"\n" + if ( + pointer["object_sha256"] + != "sha256:" + hashlib.sha256(registry_raw).hexdigest() + or pointer["registry_identity_sha256"] + != evidence.signer_registry_identity_digest(registry) + or pointer["registry_revision"] != registry["revision"] + ): + raise TrustError("feed signer registry pointer differs from the registry") + if len(checkpoints) != len(feed["checkpoints"]): + raise TrustError("feed checkpoint object count differs") + # A two-checkpoint feed intentionally contains a checkpoint whose + # not-before is in the future. Validate structure here and select the + # active checkpoint against the feed/runtime time below. + resolved = [validate_checkpoint(item) for item in checkpoints] + revisions = [item["checkpoint_revision"] for item in resolved] + if revisions != sorted(set(revisions), reverse=True): + raise TrustError("feed checkpoints must be ordered by decreasing revision") + if len(resolved) == 2: + older_reference = feed["checkpoints"][1]["checkpoint_reference"] + if ( + revisions[0] != revisions[1] + 1 + or resolved[0]["previous_checkpoint_sha256"] + != older_reference["object_sha256"] + ): + raise TrustError("feed checkpoints are not one exact raw hash-chain step") + + generated = require_timestamp(feed["generated_at"], "feed generated_at") + expires = require_timestamp(feed["expires_at"], "feed expires_at") + registry_generated = evidence._timestamp( + registry["generated_at"], "signer registry generated_at" + ) + registry_expires = evidence._timestamp( + registry["expires_at"], "signer registry expires_at" + ) + if generated < registry_generated or expires > registry_expires: + raise TrustError("feed validity exceeds signer registry validity") + + checkpoint_windows: list[tuple[datetime, datetime]] = [] + for index, (pair, checkpoint) in enumerate( + zip(feed["checkpoints"], resolved, strict=True) + ): + reference = pair["checkpoint_reference"] + raw = canonical(checkpoint) + b"\n" + if reference["object_sha256"] != ( + "sha256:" + hashlib.sha256(raw).hexdigest() + ): + raise TrustError(f"feed checkpoint {index} bytes differ from its reference") + checkpoint_not_before = require_timestamp( + checkpoint["not_before"], "checkpoint not_before" + ) + checkpoint_expires = require_timestamp( + checkpoint["expires_at"], "checkpoint expires_at" + ) + checkpoint_generated = require_timestamp( + checkpoint["generated_at"], "checkpoint generated_at" + ) + if checkpoint_generated > generated: + raise TrustError(f"feed predates checkpoint {index}") + checkpoint_windows.append((checkpoint_not_before, checkpoint_expires)) + + if len(checkpoint_windows) == 1: + checkpoint_not_before, checkpoint_expires = checkpoint_windows[0] + if generated < checkpoint_not_before or expires > checkpoint_expires: + raise TrustError("feed validity exceeds checkpoint 0 validity") + else: + newer_not_before, newer_expires = checkpoint_windows[0] + older_not_before, older_expires = checkpoint_windows[1] + if older_expires != newer_not_before: + raise TrustError("feed checkpoint handoff has an overlap or gap") + if not older_not_before <= generated < older_expires: + raise TrustError("older checkpoint is not active at feed issuance") + if not newer_not_before < expires <= newer_expires: + raise TrustError("feed does not cross the exact checkpoint handoff") + + if now is not None: + active = [ + index + for index, (not_before, checkpoint_expires) in enumerate( + checkpoint_windows + ) + if not_before <= now < checkpoint_expires + ] + if len(active) != 1: + raise TrustError("feed does not select exactly one active checkpoint") + + current = resolved[0] + current_reference = feed["checkpoints"][0]["checkpoint_reference"] + if ( + feed["registry_source_commit"] != current["registry_source_commit"] + or feed["registry_revision"] != current["registry_revision"] + or feed["registry_head_sha256"] != current["registry_head_sha256"] + or feed["signer_registry"] != current["signer_registry"] + or current_reference["registry_source_commit"] + != feed["registry_source_commit"] + or current_reference["registry_revision"] != feed["registry_revision"] + or current_reference["registry_head_sha256"] + != feed["registry_head_sha256"] + ): + raise TrustError("feed current checkpoint or registry binding differs") + return feed + + +def validate_feed_transition(previous_value: Any, current_value: Any) -> dict[str, Any]: + """Reject feed revision, registry, and checkpoint rollback. + + The protected ref compare-and-swap binds the exact previous commit. This + function binds the state transition inside those two commits. + """ + + previous = validate_feed(previous_value) + current = validate_feed(current_value) + if current["feed_revision"] != previous["feed_revision"] + 1: + raise TrustError("feed revision is not one exact forward step") + if require_timestamp(current["generated_at"], "current feed generated_at") <= ( + require_timestamp(previous["generated_at"], "previous feed generated_at") + ): + raise TrustError("feed generation time did not advance") + if current["registry_revision"] < previous["registry_revision"]: + raise TrustError("feed signer registry revision rolled back") + if ( + current["registry_revision"] == previous["registry_revision"] + and current["signer_registry"] != previous["signer_registry"] + ): + raise TrustError("feed signer registry changed without a new revision") + + previous_reference = previous["checkpoints"][0]["checkpoint_reference"] + current_reference = current["checkpoints"][0]["checkpoint_reference"] + previous_revision = previous_reference["semantic_identity_sha256"] + current_revision = current_reference["semantic_identity_sha256"] + # The semantic identities need not be ordered. Exact equality means the + # same checkpoint is retained. Any replacement must retain the prior raw + # checkpoint in the two-entry rotation until its validity ends. + if current_reference["object_sha256"] == previous_reference["object_sha256"]: + if current_revision != previous_revision: + raise TrustError("retained checkpoint semantic identity changed") + else: + retained = [ + pair["checkpoint_reference"]["object_sha256"] + for pair in current["checkpoints"][1:] + ] + if retained != [previous_reference["object_sha256"]]: + raise TrustError("feed checkpoint transition skips or forks history") + return current + + +def validate_feed_update(value: Any) -> dict[str, Any]: + fields = { + "schema_version", "event_type", "source_repository", "source_repository_id", + "source_ref", "source_commit", "target_repository", "target_repository_id", + "target_ref", "expected_old_commit", "new_commit", "feed_path", "feed_sha256", + "checkpoint_sha256", "registry_head_sha256", "expires_at", "idempotency_key", + } + update = closed(value, fields, "lifecycle feed update") + constants = { + "schema_version": "openadapt.production-lifecycle-feed-update/v1", + "event_type": "production_lifecycle_feed_updated", + "source_repository": "OpenAdaptAI/openadapt-ops", + "source_repository_id": "1172011294", + "source_ref": "refs/heads/main", + "target_repository": "OpenAdaptAI/.github", + "target_repository_id": "858454062", + "target_ref": "refs/heads/production-lifecycle-feed", + "feed_path": "production-lifecycle-feed.json", + } + for field, expected in constants.items(): + if update[field] != expected: + raise TrustError(f"lifecycle feed update {field} differs") + for field in ("source_commit", "new_commit"): + if not isinstance(update[field], str) or HEX40.fullmatch(update[field]) is None: + raise TrustError(f"lifecycle feed update {field} must be exact") + if update["expected_old_commit"] is not None and (not isinstance(update["expected_old_commit"], str) or HEX40.fullmatch(update["expected_old_commit"]) is None): + raise TrustError("lifecycle feed expected old commit is invalid") + for field in ("feed_sha256", "checkpoint_sha256", "registry_head_sha256"): + require_digest(update[field], field) + require_timestamp(update["expires_at"], "feed update expires_at") + projection = dict(update) + idempotency_key = projection.pop("idempotency_key") + expected_key = "lifecycle-feed-update:" + hashlib.sha256( + FEED_UPDATE_IDEMPOTENCY_DOMAIN + canonical(projection) + ).hexdigest() + if idempotency_key != expected_key: + raise TrustError("lifecycle feed update idempotency key is invalid") + return update diff --git a/scripts/public_trust_kms.py b/scripts/public_trust_kms.py new file mode 100644 index 0000000..b2eb7bd --- /dev/null +++ b/scripts/public_trust_kms.py @@ -0,0 +1,533 @@ +"""Build and verify the offline OpenAdapt public-trust KMS DSSE profile.""" + +from __future__ import annotations + +import base64 +import binascii +import hashlib +import json +import re +from collections.abc import Mapping +from datetime import datetime, timezone +from typing import Any + +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature + +STATEMENT_SCHEMA = "openadapt.production-public-trust-signing-statement/v1" +STATEMENT_MEDIA_TYPE = ( + "application/vnd.openadapt.production-public-trust-signing-statement+json;version=1" +) +BUNDLE_MEDIA_TYPE = "application/vnd.dev.sigstore.bundle.v0.3+json" +SIGNATURE_PROFILE = "aws-kms-p256-dsse-v1" +KMS_ACCOUNT_ID = "992382684924" +KMS_REGION = "us-east-1" +KMS_ALGORITHM = "ECDSA_SHA_256" +KMS_MESSAGE_TYPE = "DIGEST" +KMS_KEY_SPEC = "ECC_NIST_P256" +KMS_ROLE_ARN = ( + "arn:aws:iam::992382684924:role/openadapt-public-trust-signer" +) + +SHA256 = re.compile(r"^sha256:[0-9a-f]{64}$") +HEX40 = re.compile(r"^[0-9a-f]{40}$") +KIND = re.compile(r"^[a-z][a-z0-9-]{1,95}$") +TIMESTAMP = re.compile( + r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$" +) +KMS_KEY_ARN = re.compile( + r"^arn:aws:kms:us-east-1:992382684924:key/" + r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" +) +KEY_ID = re.compile(r"^oa-public-trust-p256-[0-9a-f]{16}$") +P256_ORDER = int( + "FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", 16 +) + +STATEMENT_FIELDS = { + "schema_version", + "object_kind", + "object_schema_version", + "object_media_type", + "object_sha256", + "object_size_bytes", + "semantic_identity_sha256", + "source_issuer", + "signer_registry_sha256", + "authority_state_sha256", + "revocation_state_sha256", + "issued_at", + "not_before", + "expires_at", + "request_id_sha256", + "signing_authority", + "key_id", + "signature_profile", +} +ISSUER_FIELDS = { + "repository", + "repository_id", + "repository_owner_id", + "workflow", + "ref", + "source_commit", + "environment", +} +AUTHORITY_FIELDS = { + "repository", + "repository_id", + "repository_owner_id", + "workflow", + "ref", + "source_commit", + "environment", + "aws_account_id", + "aws_region", + "kms_key_arn", + "kms_key_spec", + "kms_signing_algorithm", + "kms_message_type", + "role_arn", +} +PUBLIC_SIGNER_FIELDS = { + "algorithm", + "key_id", + "key_origin", + "kms_key_arn", + "public_key", + "public_key_spki_der_base64", + "public_key_sha256", + "signature_encoding", + "statement_schema_versions", + "allowed_usages", + "allowed_kinds", + "allowed_workflows", + "allowed_ref_prefixes", + "allowed_environments", + "status", + "revoked_at", +} +PUBLIC_EVIDENCE_KINDS = { + "production-acceptance-manifest", + "production-acceptance-summary", + "production-cloud-deploy-authorization", + "production-cloud-deployment-result", + "production-current-default", + "production-deployment-observation", + "production-lifecycle-checkpoint", + "qualification-admission", + "qualification-authority-state-receipt", + "qualification-campaign-permit", + "qualification-campaign-permit-policy", + "qualification-campaign-permit-receipt", + "qualification-campaign-permit-request", + "qualification-evidence-decision-receipt", + "qualification-release", + "qualification-revocation-state-receipt", + "support-release-admission", +} +PUBLIC_SIGNING_WORKFLOW = ( + "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/OpenAdaptAI/.github/" + ".github/workflows/sign-production-evidence.yml@refs/heads/main" +) + + +class PublicTrustKmsError(ValueError): + """The public-trust signing statement, signer, or bundle is invalid.""" + + +def canonical(value: Any) -> bytes: + return json.dumps( + value, sort_keys=True, separators=(",", ":"), ensure_ascii=False + ).encode("utf-8") + + +def canonical_lf(value: Any) -> bytes: + return canonical(value) + b"\n" + + +def _closed(value: Any, fields: set[str], label: str) -> dict[str, Any]: + if not isinstance(value, dict) or set(value) != fields: + actual = sorted(value) if isinstance(value, dict) else type(value).__name__ + raise PublicTrustKmsError( + f"{label} must contain exactly {sorted(fields)}; got {actual}" + ) + return value + + +def _digest(value: Any, label: str) -> str: + if not isinstance(value, str) or SHA256.fullmatch(value) is None: + raise PublicTrustKmsError(f"{label} must be a lowercase sha256 digest") + return value + + +def _timestamp(value: Any, label: str) -> datetime: + if not isinstance(value, str) or TIMESTAMP.fullmatch(value) is None: + raise PublicTrustKmsError(f"{label} must be an exact UTC timestamp") + try: + return datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ").replace( + tzinfo=timezone.utc + ) + except ValueError as exc: + raise PublicTrustKmsError(f"{label} is not a calendar timestamp") from exc + + +def _canonical_base64(value: Any, label: str) -> bytes: + if not isinstance(value, str): + raise PublicTrustKmsError(f"{label} must be canonical padded base64") + try: + decoded = base64.b64decode(value, validate=True) + except (ValueError, binascii.Error) as exc: + raise PublicTrustKmsError(f"{label} is not valid base64") from exc + if base64.b64encode(decoded).decode("ascii") != value: + raise PublicTrustKmsError(f"{label} must be canonical padded base64") + return decoded + + +def _canonical_base64url(value: Any, label: str) -> bytes: + if not isinstance(value, str) or "=" in value: + raise PublicTrustKmsError(f"{label} must be canonical unpadded base64url") + try: + decoded = base64.urlsafe_b64decode(value + "=" * (-len(value) % 4)) + except (ValueError, binascii.Error) as exc: + raise PublicTrustKmsError(f"{label} is not valid base64url") from exc + if base64.urlsafe_b64encode(decoded).decode("ascii").rstrip("=") != value: + raise PublicTrustKmsError(f"{label} must be canonical unpadded base64url") + return decoded + + +def public_key_id(public_key: ec.EllipticCurvePublicKey) -> str: + spki = public_key.public_bytes( + serialization.Encoding.DER, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) + return "oa-public-trust-p256-" + hashlib.sha256(spki).hexdigest()[:16] + + +def validate_public_signer(value: Any) -> dict[str, Any]: + signer = _closed(value, PUBLIC_SIGNER_FIELDS, "public-trust signer") + if signer["algorithm"] != "ecdsa-p256-sha256": + raise PublicTrustKmsError("public-trust signer algorithm is not supported") + if signer["key_origin"] != "aws-kms": + raise PublicTrustKmsError("public-trust signer must use AWS KMS") + if ( + not isinstance(signer["kms_key_arn"], str) + or KMS_KEY_ARN.fullmatch(signer["kms_key_arn"]) is None + ): + raise PublicTrustKmsError("public-trust signer KMS key ARN is invalid") + key_bytes = _canonical_base64url(signer["public_key"], "public key") + if len(key_bytes) != 65 or key_bytes[:1] != b"\x04": + raise PublicTrustKmsError("public key must be an uncompressed P-256 point") + try: + public_key = ec.EllipticCurvePublicKey.from_encoded_point( + ec.SECP256R1(), key_bytes + ) + except ValueError as exc: + raise PublicTrustKmsError("public key is not a valid P-256 point") from exc + spki = public_key.public_bytes( + serialization.Encoding.DER, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) + if _canonical_base64( + signer["public_key_spki_der_base64"], "public key SPKI" + ) != spki: + raise PublicTrustKmsError("public key SPKI does not bind the public key") + if signer["public_key_sha256"] != "sha256:" + hashlib.sha256(spki).hexdigest(): + raise PublicTrustKmsError("public key fingerprint does not bind the public key") + expected_key_id = public_key_id(public_key) + if signer["key_id"] != expected_key_id: + raise PublicTrustKmsError("public-trust key id does not bind the public key") + if signer["signature_encoding"] != "asn1-der-low-s-base64-rfc4648-padded": + raise PublicTrustKmsError("public-trust signature encoding is not supported") + if signer["statement_schema_versions"] != [STATEMENT_SCHEMA]: + raise PublicTrustKmsError("public-trust statement schema is not supported") + if signer["allowed_usages"] != ["production-public-evidence"]: + raise PublicTrustKmsError("public-trust signer usage is not supported") + for field in ( + "allowed_kinds", + "allowed_workflows", + "allowed_ref_prefixes", + "allowed_environments", + ): + values = signer[field] + if ( + not isinstance(values, list) + or not values + or values != sorted(set(values)) + or not all(isinstance(item, str) and item for item in values) + ): + raise PublicTrustKmsError( + f"public-trust signer {field} must be sorted and unique" + ) + if any(kind not in PUBLIC_EVIDENCE_KINDS for kind in signer["allowed_kinds"]): + raise PublicTrustKmsError("public-trust signer object-kind allowlist is invalid") + if signer["allowed_workflows"] != [PUBLIC_SIGNING_WORKFLOW]: + raise PublicTrustKmsError("public-trust signer workflow allowlist is invalid") + if signer["allowed_ref_prefixes"] != ["refs/heads/main"]: + raise PublicTrustKmsError("public-trust signer ref allowlist is invalid") + if signer["allowed_environments"] != ["public-trust-signing"]: + raise PublicTrustKmsError("public-trust signer environment allowlist is invalid") + if signer["status"] not in {"active", "revoked"}: + raise PublicTrustKmsError("public-trust signer status is invalid") + if signer["status"] == "active" and signer["revoked_at"] is not None: + raise PublicTrustKmsError("an active public-trust signer cannot be revoked") + if signer["status"] == "revoked": + _timestamp(signer["revoked_at"], "public-trust signer revoked_at") + return signer + + +def validate_signing_statement( + value: Any, + *, + signer: Mapping[str, Any] | None = None, + now: datetime | None = None, +) -> dict[str, Any]: + statement = _closed(value, STATEMENT_FIELDS, "public-trust signing statement") + if statement["schema_version"] != STATEMENT_SCHEMA: + raise PublicTrustKmsError("public-trust signing statement schema is not supported") + if not isinstance(statement["object_kind"], str) or KIND.fullmatch(statement["object_kind"]) is None: + raise PublicTrustKmsError("public-trust object kind is invalid") + for field in ("object_schema_version", "object_media_type"): + if not isinstance(statement[field], str) or not statement[field]: + raise PublicTrustKmsError(f"public-trust {field} is invalid") + for field in ( + "object_sha256", + "semantic_identity_sha256", + "signer_registry_sha256", + "authority_state_sha256", + "revocation_state_sha256", + "request_id_sha256", + ): + _digest(statement[field], field) + if ( + not isinstance(statement["object_size_bytes"], int) + or isinstance(statement["object_size_bytes"], bool) + or statement["object_size_bytes"] < 1 + ): + raise PublicTrustKmsError("public-trust object size must be positive") + issuer = _closed(statement["source_issuer"], ISSUER_FIELDS, "source issuer") + for field in ("repository", "repository_id", "repository_owner_id", "workflow", "ref", "environment"): + if not isinstance(issuer[field], str) or not issuer[field]: + raise PublicTrustKmsError(f"source issuer {field} is invalid") + if not isinstance(issuer["source_commit"], str) or HEX40.fullmatch(issuer["source_commit"]) is None: + raise PublicTrustKmsError("source issuer commit is invalid") + authority = _closed( + statement["signing_authority"], AUTHORITY_FIELDS, "signing authority" + ) + expected_authority = { + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "workflow": ".github/workflows/sign-production-evidence.yml", + "ref": "refs/heads/main", + "aws_account_id": KMS_ACCOUNT_ID, + "aws_region": KMS_REGION, + "kms_key_spec": KMS_KEY_SPEC, + "kms_signing_algorithm": KMS_ALGORITHM, + "kms_message_type": KMS_MESSAGE_TYPE, + "role_arn": KMS_ROLE_ARN, + } + for field, expected in expected_authority.items(): + if authority[field] != expected: + raise PublicTrustKmsError(f"signing authority {field} is invalid") + if not isinstance(authority["source_commit"], str) or HEX40.fullmatch(authority["source_commit"]) is None: + raise PublicTrustKmsError("signing authority commit is invalid") + if not isinstance(authority["environment"], str) or not authority["environment"]: + raise PublicTrustKmsError("signing authority environment is invalid") + if not isinstance(authority["kms_key_arn"], str) or KMS_KEY_ARN.fullmatch(authority["kms_key_arn"]) is None: + raise PublicTrustKmsError("signing authority KMS key ARN is invalid") + if not isinstance(statement["key_id"], str) or KEY_ID.fullmatch(statement["key_id"]) is None: + raise PublicTrustKmsError("public-trust key id is invalid") + if statement["signature_profile"] != SIGNATURE_PROFILE: + raise PublicTrustKmsError("public-trust signature profile is invalid") + issued_at = _timestamp(statement["issued_at"], "statement issued_at") + not_before = _timestamp(statement["not_before"], "statement not_before") + expires_at = _timestamp(statement["expires_at"], "statement expires_at") + if not not_before <= issued_at < expires_at: + raise PublicTrustKmsError("public-trust statement validity is invalid") + if now is not None and not not_before <= now < expires_at: + raise PublicTrustKmsError("public-trust signing statement is not active") + if signer is not None: + validated_signer = validate_public_signer(signer) + if validated_signer["status"] != "active": + raise PublicTrustKmsError("public-trust signer is not active") + if statement["key_id"] != validated_signer["key_id"]: + raise PublicTrustKmsError("statement key id does not match the signer") + if authority["kms_key_arn"] != validated_signer["kms_key_arn"]: + raise PublicTrustKmsError("statement KMS key does not match the signer") + if statement["object_kind"] not in validated_signer["allowed_kinds"]: + raise PublicTrustKmsError("signer is not authorized for this object kind") + workflow_identity = ( + "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/OpenAdaptAI/.github/" + + authority["workflow"] + + "@" + + authority["ref"] + ) + if workflow_identity not in validated_signer["allowed_workflows"]: + raise PublicTrustKmsError("signer is not authorized for this workflow") + if not any(authority["ref"].startswith(prefix) for prefix in validated_signer["allowed_ref_prefixes"]): + raise PublicTrustKmsError("signer is not authorized for this ref") + if authority["environment"] not in validated_signer["allowed_environments"]: + raise PublicTrustKmsError("signer is not authorized for this environment") + return statement + + +def dsse_pae(payload_type: str, payload: bytes) -> bytes: + if not isinstance(payload_type, str) or not payload_type: + raise PublicTrustKmsError("DSSE payload type is invalid") + payload_type_bytes = payload_type.encode("utf-8") + return ( + b"DSSEv1 " + + str(len(payload_type_bytes)).encode("ascii") + + b" " + + payload_type_bytes + + b" " + + str(len(payload)).encode("ascii") + + b" " + + payload + ) + + +def kms_message_digest(statement: Mapping[str, Any]) -> bytes: + payload = canonical_lf(statement) + return hashlib.sha256(dsse_pae(STATEMENT_MEDIA_TYPE, payload)).digest() + + +def verify_bundle( + bundle_value: Any, + *, + expected_statement: Mapping[str, Any], + signer: Mapping[str, Any], + now: datetime | None = None, +) -> dict[str, Any]: + validated_signer = validate_public_signer(signer) + statement = validate_signing_statement( + expected_statement, signer=validated_signer, now=now + ) + bundle = _closed( + bundle_value, + {"mediaType", "verificationMaterial", "dsseEnvelope"}, + "public-trust DSSE bundle", + ) + if bundle["mediaType"] != BUNDLE_MEDIA_TYPE: + raise PublicTrustKmsError("public-trust bundle media type is invalid") + verification_material = _closed( + bundle["verificationMaterial"], {"publicKey"}, "verification material" + ) + public_key_hint = _closed( + verification_material["publicKey"], {"hint"}, "public key hint" + ) + if public_key_hint["hint"] != validated_signer["key_id"]: + raise PublicTrustKmsError("public key hint does not match the signer") + envelope = _closed( + bundle["dsseEnvelope"], + {"payload", "payloadType", "signatures"}, + "DSSE envelope", + ) + if envelope["payloadType"] != STATEMENT_MEDIA_TYPE: + raise PublicTrustKmsError("DSSE payload type is invalid") + payload = _canonical_base64(envelope["payload"], "DSSE payload") + expected_payload = canonical_lf(statement) + if payload != expected_payload: + raise PublicTrustKmsError("DSSE payload does not match the signing statement") + signatures = envelope["signatures"] + if not isinstance(signatures, list) or len(signatures) != 1: + raise PublicTrustKmsError("DSSE envelope must contain exactly one signature") + signature_entry = _closed( + signatures[0], {"keyid", "sig"}, "DSSE signature" + ) + if signature_entry["keyid"] != validated_signer["key_id"]: + raise PublicTrustKmsError("DSSE signature key id does not match the signer") + signature = _canonical_base64(signature_entry["sig"], "DSSE signature") + try: + _, signature_s = decode_dss_signature(signature) + except ValueError as exc: + raise PublicTrustKmsError("DSSE signature is not canonical ASN.1 DER") from exc + if signature_s > P256_ORDER // 2: + raise PublicTrustKmsError("DSSE signature must use canonical low-S form") + public_key_bytes = _canonical_base64url( + validated_signer["public_key"], "public key" + ) + public_key = ec.EllipticCurvePublicKey.from_encoded_point( + ec.SECP256R1(), public_key_bytes + ) + pae = dsse_pae(STATEMENT_MEDIA_TYPE, payload) + try: + public_key.verify(signature, pae, ec.ECDSA(hashes.SHA256())) + except InvalidSignature as exc: + raise PublicTrustKmsError("DSSE signature verification failed") from exc + return bundle + + +def statement_from_bundle(bundle_value: Any) -> dict[str, Any]: + bundle = _closed( + bundle_value, + {"mediaType", "verificationMaterial", "dsseEnvelope"}, + "public-trust DSSE bundle", + ) + envelope = _closed( + bundle["dsseEnvelope"], + {"payload", "payloadType", "signatures"}, + "DSSE envelope", + ) + if envelope["payloadType"] != STATEMENT_MEDIA_TYPE: + raise PublicTrustKmsError("DSSE payload type is invalid") + payload = _canonical_base64(envelope["payload"], "DSSE payload") + if payload[-1:] != b"\n": + raise PublicTrustKmsError("DSSE payload must end with exactly one LF") + try: + value = json.loads(payload) + except json.JSONDecodeError as exc: + raise PublicTrustKmsError("DSSE payload is not JSON") from exc + if payload != canonical_lf(value): + raise PublicTrustKmsError("DSSE payload is not canonical JSON plus LF") + return validate_signing_statement(value) + + +def validate_statement_object_binding( + statement_value: Any, + *, + object_raw: bytes, + object_value: Mapping[str, Any], + object_kind: str, + object_schema_version: str, + object_media_type: str, + semantic_identity_sha256: str, + expected_signer_registry_sha256: str, + expected_authority_state_sha256: str, + expected_revocation_state_sha256: str, +) -> dict[str, Any]: + statement = validate_signing_statement(statement_value) + if object_raw != canonical_lf(object_value): + raise PublicTrustKmsError("public-trust object must be canonical JSON plus LF") + bindings = { + "object_kind": object_kind, + "object_schema_version": object_schema_version, + "object_media_type": object_media_type, + "object_sha256": "sha256:" + hashlib.sha256(object_raw).hexdigest(), + "object_size_bytes": len(object_raw), + "semantic_identity_sha256": semantic_identity_sha256, + } + for field, expected in bindings.items(): + if statement[field] != expected: + raise PublicTrustKmsError(f"public-trust statement {field} binding differs") + source_issuer = object_value.get("issuer") + if not isinstance(source_issuer, dict): + raise PublicTrustKmsError("public-trust object has no signed issuer") + projected_issuer = {field: source_issuer[field] for field in ISSUER_FIELDS if field in source_issuer} + if set(projected_issuer) != ISSUER_FIELDS or statement["source_issuer"] != projected_issuer: + raise PublicTrustKmsError("public-trust statement source issuer binding differs") + expected_trust_state = { + "signer_registry_sha256": expected_signer_registry_sha256, + "authority_state_sha256": expected_authority_state_sha256, + "revocation_state_sha256": expected_revocation_state_sha256, + } + for field, expected in expected_trust_state.items(): + _digest(expected, f"expected {field}") + if statement[field] != expected: + raise PublicTrustKmsError(f"public-trust statement {field} binding differs") + if field in object_value and object_value[field] != expected: + raise PublicTrustKmsError(f"public-trust object {field} binding differs") + return statement diff --git a/scripts/public_trust_resolver.py b/scripts/public_trust_resolver.py new file mode 100644 index 0000000..dfbf589 --- /dev/null +++ b/scripts/public_trust_resolver.py @@ -0,0 +1,147 @@ +"""Resolve and verify one content-addressed KMS public-trust object pair.""" + +from __future__ import annotations + +import hashlib +import json +from datetime import datetime, timezone +from typing import Any + +import public_trust_kms as kms +import validate_evidence_registry as evidence + + +class PublicTrustResolutionError(ValueError): + """The reference pair, raw bytes, current state, or signature is invalid.""" + + +def _parse_canonical(raw: bytes, label: str) -> dict[str, Any]: + try: + value = json.loads(raw) + except json.JSONDecodeError as exc: + raise PublicTrustResolutionError(f"{label} is not JSON") from exc + if not isinstance(value, dict) or raw != evidence.canonical(value) + b"\n": + raise PublicTrustResolutionError( + f"{label} must be canonical JSON followed by one LF" + ) + return value + + +def _verify_raw_reference( + raw: bytes, reference_value: Any, *, label: str +) -> tuple[dict[str, Any], dict[str, Any]]: + try: + reference = evidence.validate_reference(reference_value) + except evidence.EvidenceRegistryError as exc: + raise PublicTrustResolutionError(f"{label} reference is invalid: {exc}") from exc + expected = "sha256:" + hashlib.sha256(raw).hexdigest() + if expected != reference["object_sha256"] or len(raw) != reference["size_bytes"]: + raise PublicTrustResolutionError(f"{label} bytes or size differ from the reference") + return reference, _parse_canonical(raw, label) + + +def verify_registered_public_trust_pair( + *, + object_raw: bytes, + object_reference: Any, + bundle_raw: bytes, + bundle_reference: Any, + signer_registry_raw: bytes, + expected_signer_registry_sha256: str, + expected_authority_state_sha256: str, + expected_revocation_state_sha256: str, + now: datetime | None = None, +) -> dict[str, Any]: + """Verify raw referenced object bytes and the adjacent offline KMS DSSE bundle.""" + + object_ref, object_value = _verify_raw_reference( + object_raw, object_reference, label="public-trust object" + ) + bundle_ref, bundle_value = _verify_raw_reference( + bundle_raw, bundle_reference, label="public-trust bundle" + ) + if ( + bundle_ref["kind"] != object_ref["kind"] + "-sigstore-bundle" + or bundle_ref["subject_sha256"] != object_ref["object_sha256"] + or object_ref["subject_sha256"] is not None + or any( + object_ref[field] != bundle_ref[field] + for field in ( + "repository", "repository_id", "repository_owner_id", + "registry_source_commit", "registry_revision", + "registry_head_sha256", + ) + ) + ): + raise PublicTrustResolutionError("public-trust reference pair is not adjacent and bound") + expected_object_identity = evidence.semantic_identity_digest( + kind=object_ref["kind"], + object_schema_version=object_ref["object_schema_version"], + object_value=object_value, + object_sha256=object_ref["object_sha256"], + ) + expected_bundle_identity = evidence.semantic_identity_digest( + kind=bundle_ref["kind"], + object_schema_version=bundle_ref["object_schema_version"], + object_value=bundle_ref["subject_sha256"], + object_sha256=bundle_ref["object_sha256"], + ) + if ( + object_ref["semantic_identity_sha256"] != expected_object_identity + or bundle_ref["semantic_identity_sha256"] != expected_bundle_identity + ): + raise PublicTrustResolutionError("public-trust semantic identity differs") + registry_value = _parse_canonical(signer_registry_raw, "signer registry") + try: + registry = evidence.validate_signer_registry(registry_value) + except evidence.EvidenceRegistryError as exc: + raise PublicTrustResolutionError(f"signer registry is invalid: {exc}") from exc + registry_identity = evidence.signer_registry_identity_digest(registry) + if registry_identity != expected_signer_registry_sha256: + raise PublicTrustResolutionError("signer registry identity is not current") + instant = now or datetime.now(timezone.utc) + generated_at = evidence._timestamp(registry["generated_at"], "registry generated_at") + expires_at = evidence._timestamp(registry["expires_at"], "registry expires_at") + if not generated_at <= instant < expires_at: + raise PublicTrustResolutionError("signer registry is not active") + try: + statement = kms.statement_from_bundle(bundle_value) + except kms.PublicTrustKmsError as exc: + raise PublicTrustResolutionError(f"public-trust bundle is invalid: {exc}") from exc + matches = [ + signer for signer in registry["signers"] + if signer.get("key_id") == statement["key_id"] + and signer.get("algorithm") == "ecdsa-p256-sha256" + ] + if len(matches) != 1: + raise PublicTrustResolutionError("public-trust bundle does not select one registry signer") + signer = matches[0] + try: + kms.validate_statement_object_binding( + statement, + object_raw=object_raw, + object_value=object_value, + object_kind=object_ref["kind"], + object_schema_version=object_ref["object_schema_version"], + object_media_type=object_ref["object_media_type"], + semantic_identity_sha256=object_ref["semantic_identity_sha256"], + expected_signer_registry_sha256=expected_signer_registry_sha256, + expected_authority_state_sha256=expected_authority_state_sha256, + expected_revocation_state_sha256=expected_revocation_state_sha256, + ) + kms.verify_bundle( + bundle_value, + expected_statement=statement, + signer=signer, + now=instant, + ) + except kms.PublicTrustKmsError as exc: + raise PublicTrustResolutionError(f"public-trust verification failed: {exc}") from exc + return { + "object": object_value, + "object_reference": object_ref, + "bundle_reference": bundle_ref, + "signing_statement": statement, + "signer_registry_sha256": registry_identity, + "verified_at": instant.strftime("%Y-%m-%dT%H:%M:%SZ"), + } diff --git a/scripts/qualification_worker_contract.py b/scripts/qualification_worker_contract.py new file mode 100644 index 0000000..d7fb51f --- /dev/null +++ b/scripts/qualification_worker_contract.py @@ -0,0 +1,432 @@ +"""Validate the remote-safe qualification worker terminal contract.""" + +from __future__ import annotations + +import hashlib +import json +import re +from datetime import datetime, timezone +from typing import Any + +PROCESS_START_DOMAIN = b"OpenAdapt qualification worker process start v1\0" +LAUNCH_ATTEMPT_DOMAIN = b"OpenAdapt qualification worker launch attempt v1\0" +TERMINAL_RECEIPT_DOMAIN = b"OpenAdapt qualification worker terminal receipt v1\0" +TERMINAL_SCHEMA = "openadapt.qualification-worker-terminal-receipt/v1" +DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$") +DECIMAL = re.compile(r"^[1-9][0-9]*$") +TIMESTAMP = re.compile( + r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$" +) +TERMINAL_FIELDS = { + "schema_version", + "receipt_id_sha256", + "worker_admission_sha256", + "dispatch_id_sha256", + "provider_identity_sha256", + "worker_identity_sha256", + "live_provider_observation_sha256", + "admitted_runtime_sha256", + "run_id", + "run_attempt", + "start_id_sha256", + "task_id_sha256", + "task_condition_sha256", + "capability_handle_sha256", + "launch_attempt", + "launch_attempt_sha256", + "process", + "oracle_sha256", + "result_sha256", + "log_sha256", + "burned_identities_sha256", + "burn_ledger_revision", + "burn_receipt_sha256", + "burned_at", + "ledger_readback_sha256", + "effect_started", + "delivery_state", + "terminal_state", + "exit_code", + "uncertainty_sha256", + "quarantine", + "completed_at", + "issuer", +} +PROCESS_FIELDS = { + "pid", + "process_group_id", + "process_start_ticks", + "launched_at", + "executable_sha256", + "process_start_identity_sha256", +} +LAUNCH_ATTEMPT_FIELDS = { + "attempted_at", + "host_identity_sha256", + "executable_sha256", + "capability_handle_sha256", + "evidence_sha256", + "child_created", + "failure_classification", +} +ISSUER_FIELDS = { + "repository", + "repository_id", + "repository_owner_id", + "workflow", + "ref", + "source_commit", + "environment", +} + + +class QualificationWorkerContractError(ValueError): + """The worker terminal receipt or replay is invalid.""" + + +def canonical(value: Any) -> bytes: + return json.dumps( + value, sort_keys=True, separators=(",", ":"), ensure_ascii=False + ).encode("utf-8") + + +def digest(domain: bytes, value: Any) -> str: + return "sha256:" + hashlib.sha256(domain + canonical(value)).hexdigest() + + +def _closed(value: Any, fields: set[str], label: str) -> dict[str, Any]: + if not isinstance(value, dict) or set(value) != fields: + actual = sorted(value) if isinstance(value, dict) else type(value).__name__ + raise QualificationWorkerContractError( + f"{label} must contain exactly {sorted(fields)}; got {actual}" + ) + return value + + +def _digest(value: Any, label: str) -> str: + if not isinstance(value, str) or DIGEST.fullmatch(value) is None: + raise QualificationWorkerContractError( + f"{label} must be a lowercase sha256 digest" + ) + return value + + +def _timestamp(value: Any, label: str) -> datetime: + if not isinstance(value, str) or TIMESTAMP.fullmatch(value) is None: + raise QualificationWorkerContractError(f"{label} is not an exact UTC time") + try: + return datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ").replace( + tzinfo=timezone.utc + ) + except ValueError as exc: + raise QualificationWorkerContractError( + f"{label} is not a calendar time" + ) from exc + + +def process_start_projection(receipt: dict[str, Any]) -> dict[str, Any]: + process = receipt["process"] + return { + "provider_identity_sha256": receipt["provider_identity_sha256"], + "worker_identity_sha256": receipt["worker_identity_sha256"], + "live_provider_observation_sha256": receipt[ + "live_provider_observation_sha256" + ], + "admitted_runtime_sha256": receipt["admitted_runtime_sha256"], + "run_id": receipt["run_id"], + "run_attempt": receipt["run_attempt"], + "start_id_sha256": receipt["start_id_sha256"], + "dispatch_id_sha256": receipt["dispatch_id_sha256"], + "pid": process["pid"], + "process_group_id": process["process_group_id"], + "process_start_ticks": process["process_start_ticks"], + "launched_at": process["launched_at"], + "executable_sha256": process["executable_sha256"], + } + + +def launch_attempt_projection(receipt: dict[str, Any]) -> dict[str, Any]: + return { + "worker_admission_sha256": receipt["worker_admission_sha256"], + "dispatch_id_sha256": receipt["dispatch_id_sha256"], + "provider_identity_sha256": receipt["provider_identity_sha256"], + "worker_identity_sha256": receipt["worker_identity_sha256"], + "live_provider_observation_sha256": receipt[ + "live_provider_observation_sha256" + ], + "admitted_runtime_sha256": receipt["admitted_runtime_sha256"], + "run_id": receipt["run_id"], + "run_attempt": receipt["run_attempt"], + "start_id_sha256": receipt["start_id_sha256"], + "capability_handle_sha256": receipt["capability_handle_sha256"], + "launch_attempt": receipt["launch_attempt"], + } + + +def terminal_receipt_projection(receipt: dict[str, Any]) -> dict[str, Any]: + projection = dict(receipt) + projection.pop("receipt_id_sha256", None) + return projection + + +def validate_terminal_receipt(value: Any) -> dict[str, Any]: + receipt = _closed(value, TERMINAL_FIELDS, "worker terminal receipt") + if receipt["schema_version"] != TERMINAL_SCHEMA: + raise QualificationWorkerContractError("terminal receipt schema is invalid") + if receipt["run_attempt"] != "1" or not isinstance(receipt["run_id"], str) or DECIMAL.fullmatch(receipt["run_id"]) is None: + raise QualificationWorkerContractError("terminal receipt run identity is invalid") + for field in ( + "receipt_id_sha256", + "worker_admission_sha256", + "dispatch_id_sha256", + "provider_identity_sha256", + "worker_identity_sha256", + "live_provider_observation_sha256", + "admitted_runtime_sha256", + "start_id_sha256", + "task_id_sha256", + "task_condition_sha256", + "capability_handle_sha256", + "launch_attempt_sha256", + "oracle_sha256", + "result_sha256", + "log_sha256", + "burned_identities_sha256", + "burn_receipt_sha256", + "ledger_readback_sha256", + ): + _digest(receipt[field], field) + launch_attempt = _closed( + receipt["launch_attempt"], LAUNCH_ATTEMPT_FIELDS, "worker launch attempt" + ) + attempted_at = _timestamp(launch_attempt["attempted_at"], "launch attempted_at") + for field in ( + "host_identity_sha256", + "executable_sha256", + "capability_handle_sha256", + "evidence_sha256", + ): + _digest(launch_attempt[field], f"launch attempt {field}") + if not isinstance(launch_attempt["child_created"], bool): + raise QualificationWorkerContractError( + "launch attempt child_created must be boolean" + ) + if launch_attempt["capability_handle_sha256"] != receipt["capability_handle_sha256"]: + raise QualificationWorkerContractError( + "launch attempt capability differs from the terminal receipt" + ) + if receipt["launch_attempt_sha256"] != digest( + LAUNCH_ATTEMPT_DOMAIN, launch_attempt_projection(receipt) + ): + raise QualificationWorkerContractError("worker launch attempt identity differs") + process_value = receipt["process"] + process: dict[str, Any] | None + if process_value is None: + process = None + else: + process = _closed(process_value, PROCESS_FIELDS, "worker process") + for field in ("pid", "process_group_id"): + if ( + not isinstance(process[field], int) + or isinstance(process[field], bool) + or process[field] < 1 + ): + raise QualificationWorkerContractError( + f"worker process {field} is invalid" + ) + if ( + not isinstance(process["process_start_ticks"], str) + or DECIMAL.fullmatch(process["process_start_ticks"]) is None + ): + raise QualificationWorkerContractError( + "worker process start ticks are invalid" + ) + _timestamp(process["launched_at"], "worker process launched_at") + _digest(process["executable_sha256"], "worker process executable") + _digest(process["process_start_identity_sha256"], "worker process identity") + if process["process_start_identity_sha256"] != digest( + PROCESS_START_DOMAIN, process_start_projection(receipt) + ): + raise QualificationWorkerContractError( + "worker process start identity differs" + ) + if not isinstance(receipt["burn_ledger_revision"], int) or isinstance(receipt["burn_ledger_revision"], bool) or receipt["burn_ledger_revision"] < 1: + raise QualificationWorkerContractError("burn ledger revision is invalid") + burned_at = _timestamp(receipt["burned_at"], "burned_at") + completed_at = _timestamp(receipt["completed_at"], "completed_at") + if process is not None: + launched_at = _timestamp(process["launched_at"], "launched_at") + if ( + launch_attempt["failure_classification"] is not None + or not launch_attempt["child_created"] + or launch_attempt["executable_sha256"] != process["executable_sha256"] + or not attempted_at <= launched_at + ): + raise QualificationWorkerContractError( + "completed process differs from its launch attempt" + ) + if not attempted_at <= burned_at <= launched_at <= completed_at: + raise QualificationWorkerContractError( + "terminal receipt time order is invalid" + ) + else: + if not attempted_at <= burned_at <= completed_at: + raise QualificationWorkerContractError( + "terminal receipt time order is invalid" + ) + if not isinstance(receipt["effect_started"], bool): + raise QualificationWorkerContractError("effect_started must be boolean") + if receipt["delivery_state"] not in {"not_started", "verified", "uncertain"}: + raise QualificationWorkerContractError("delivery state is invalid") + if receipt["terminal_state"] not in { + "VERIFIED", + "SAFE_HALT", + "RECONCILIATION_REQUIRED", + "QUARANTINED", + "PRELAUNCH_QUARANTINED", + }: + raise QualificationWorkerContractError("terminal state is invalid") + quarantine = _closed( + receipt["quarantine"], + {"active", "reason_code", "evidence_sha256"}, + "worker quarantine", + ) + if not isinstance(quarantine["active"], bool): + raise QualificationWorkerContractError("quarantine active must be boolean") + if process is None: + if ( + receipt["effect_started"] + or receipt["delivery_state"] != "not_started" + or receipt["terminal_state"] != "PRELAUNCH_QUARANTINED" + or receipt["uncertainty_sha256"] is not None + or not quarantine["active"] + or not isinstance(quarantine["reason_code"], str) + or launch_attempt["failure_classification"] + not in { + "PROCESS_START_REFUSED", + "PROCESS_START_FAILED", + } + or launch_attempt["child_created"] + or quarantine["reason_code"] + != launch_attempt["failure_classification"] + or quarantine["evidence_sha256"] + != launch_attempt["evidence_sha256"] + ): + raise QualificationWorkerContractError( + "a prelaunch terminal receipt must quarantine without an effect" + ) + _digest(quarantine["evidence_sha256"], "prelaunch quarantine evidence") + elif receipt["terminal_state"] == "PRELAUNCH_QUARANTINED": + raise QualificationWorkerContractError( + "PRELAUNCH_QUARANTINED cannot contain a process identity" + ) + if receipt["delivery_state"] == "uncertain": + _digest(receipt["uncertainty_sha256"], "uncertainty") + if ( + not receipt["effect_started"] + or receipt["terminal_state"] + not in {"RECONCILIATION_REQUIRED", "QUARANTINED"} + or not quarantine["active"] + ): + raise QualificationWorkerContractError( + "uncertain delivery must quarantine and require reconciliation" + ) + if receipt["terminal_state"] == "VERIFIED" and ( + receipt["delivery_state"] != "verified" + or receipt["uncertainty_sha256"] is not None + ): + raise QualificationWorkerContractError("VERIFIED terminal receipt is invalid") + if receipt["terminal_state"] == "SAFE_HALT" and ( + receipt["effect_started"] + or receipt["delivery_state"] != "not_started" + or receipt["uncertainty_sha256"] is not None + ): + raise QualificationWorkerContractError("SAFE_HALT terminal receipt is invalid") + issuer = _closed(receipt["issuer"], ISSUER_FIELDS, "terminal issuer") + expected_issuer = { + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "workflow": ".github/workflows/issue-qualification-worker-terminal-receipt.yml", + "ref": "refs/heads/main", + "environment": "qualification-worker-terminal-receipt", + } + for field, expected in expected_issuer.items(): + if issuer[field] != expected: + raise QualificationWorkerContractError("terminal issuer identity differs") + if not isinstance(issuer["source_commit"], str) or re.fullmatch(r"[0-9a-f]{40}", issuer["source_commit"]) is None: + raise QualificationWorkerContractError("terminal issuer commit is invalid") + if receipt["receipt_id_sha256"] != digest( + TERMINAL_RECEIPT_DOMAIN, terminal_receipt_projection(receipt) + ): + raise QualificationWorkerContractError("terminal receipt identity differs") + return receipt + + +def validate_terminal_replay(previous_value: Any, current_value: Any) -> dict[str, Any]: + previous = validate_terminal_receipt(previous_value) + current = validate_terminal_receipt(current_value) + if canonical(previous) == canonical(current): + return current + if previous["dispatch_id_sha256"] == current["dispatch_id_sha256"]: + raise QualificationWorkerContractError( + "one worker dispatch has conflicting terminal receipts" + ) + return current + + +def validate_terminal_bindings( + receipt_value: Any, + *, + admission: Any, + dispatch: Any, +) -> dict[str, Any]: + receipt = validate_terminal_receipt(receipt_value) + if not isinstance(admission, dict) or not isinstance(dispatch, dict): + raise QualificationWorkerContractError( + "worker admission and dispatch must be objects" + ) + admission_bindings = { + "worker_admission_sha256": admission.get("admission_id_sha256"), + "provider_identity_sha256": admission.get("provider_identity_sha256"), + "worker_identity_sha256": admission.get("worker_identity_sha256"), + "live_provider_observation_sha256": admission.get( + "live_provider_observation_sha256" + ), + "admitted_runtime_sha256": admission.get("admitted_runtime_sha256"), + "capability_handle_sha256": admission.get("capability_handle_sha256"), + "launch_attempt_host_identity_sha256": admission.get( + "host_identity_sha256" + ), + } + dispatch_bindings = { + "worker_admission_sha256": dispatch.get("worker_admission_sha256"), + "dispatch_id_sha256": dispatch.get("dispatch_id_sha256"), + "provider_identity_sha256": dispatch.get("provider_identity_sha256"), + "worker_identity_sha256": dispatch.get("worker_identity_sha256"), + "live_provider_observation_sha256": dispatch.get( + "live_provider_observation_sha256" + ), + "admitted_runtime_sha256": dispatch.get("admitted_runtime_sha256"), + "run_id": dispatch.get("run_id"), + "run_attempt": dispatch.get("run_attempt"), + "start_id_sha256": dispatch.get("start_id_sha256"), + "task_id_sha256": dispatch.get("task_id_sha256"), + "task_condition_sha256": dispatch.get("task_condition_sha256"), + "capability_handle_sha256": dispatch.get("capability_handle_sha256"), + } + for source, bindings in ( + ("admission", admission_bindings), + ("dispatch", dispatch_bindings), + ): + for field, expected in bindings.items(): + actual = ( + receipt["launch_attempt"]["host_identity_sha256"] + if field == "launch_attempt_host_identity_sha256" + else receipt[field] + ) + if actual != expected: + raise QualificationWorkerContractError( + f"terminal receipt {field} differs from the {source}" + ) + return receipt diff --git a/scripts/qualification_worker_task_contract.py b/scripts/qualification_worker_task_contract.py new file mode 100644 index 0000000..ba05955 --- /dev/null +++ b/scripts/qualification_worker_task_contract.py @@ -0,0 +1,168 @@ +"""Validate remote-safe qualification worker task identities.""" + +from __future__ import annotations + +import hashlib +import json +import re +from typing import Any + +TASK_SELECTOR_DOMAIN = b"OpenAdapt qualification worker task selector v1\0" +TASK_CONDITION_DOMAIN = b"OpenAdapt qualification worker task condition v1\0" +TASK_SELECTOR_SCHEMA = "openadapt.qualification-worker-task-selector/v1" +TASK_CONDITION_SCHEMA = "openadapt.qualification-worker-task-condition/v1" +DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$") +TASK_SELECTOR_FIELDS = { + "schema_version", + "campaign_artifact_sha256", + "task_source_sha256", + "task_ordinal", + "task_id_sha256", +} +TASK_CONDITION_FIELDS = { + "schema_version", + "task_id_sha256", + "condition_source_sha256", + "condition_ordinal", + "task_condition_sha256", +} + + +class QualificationWorkerTaskContractError(ValueError): + """A remote-safe task selector or condition identity is invalid.""" + + +def canonical(value: Any) -> bytes: + """Return the canonical compact sorted UTF-8 JSON representation.""" + + return json.dumps( + value, sort_keys=True, separators=(",", ":"), ensure_ascii=False + ).encode("utf-8") + + +def digest(domain: bytes, value: Any) -> str: + """Return a prefixed lowercase SHA-256 digest for a domain and object.""" + + return "sha256:" + hashlib.sha256(domain + canonical(value)).hexdigest() + + +def _closed(value: Any, fields: set[str], label: str) -> dict[str, Any]: + if not isinstance(value, dict) or set(value) != fields: + actual = sorted(value) if isinstance(value, dict) else type(value).__name__ + raise QualificationWorkerTaskContractError( + f"{label} must contain exactly {sorted(fields)}; got {actual}" + ) + return value + + +def _digest(value: Any, label: str) -> str: + if not isinstance(value, str) or DIGEST.fullmatch(value) is None: + raise QualificationWorkerTaskContractError( + f"{label} must be a lowercase sha256 digest" + ) + return value + + +def _positive_ordinal(value: Any, label: str) -> int: + if not isinstance(value, int) or isinstance(value, bool) or value < 1: + raise QualificationWorkerTaskContractError( + f"{label} must be a positive integer" + ) + return value + + +def task_selector_projection(selector: dict[str, Any]) -> dict[str, Any]: + """Return the exact projection covered by the task identity digest.""" + + return { + "schema_version": selector["schema_version"], + "campaign_artifact_sha256": selector["campaign_artifact_sha256"], + "task_source_sha256": selector["task_source_sha256"], + "task_ordinal": selector["task_ordinal"], + } + + +def task_condition_projection(condition: dict[str, Any]) -> dict[str, Any]: + """Return the exact projection covered by the task-condition digest.""" + + return { + "schema_version": condition["schema_version"], + "task_id_sha256": condition["task_id_sha256"], + "condition_source_sha256": condition["condition_source_sha256"], + "condition_ordinal": condition["condition_ordinal"], + } + + +def validate_task_selector(value: Any) -> dict[str, Any]: + """Validate one closed remote-safe task selector and its identity.""" + + selector = _closed(value, TASK_SELECTOR_FIELDS, "worker task selector") + if selector["schema_version"] != TASK_SELECTOR_SCHEMA: + raise QualificationWorkerTaskContractError( + "worker task selector schema is invalid" + ) + _digest(selector["campaign_artifact_sha256"], "campaign artifact identity") + _digest(selector["task_source_sha256"], "task source identity") + _positive_ordinal(selector["task_ordinal"], "task ordinal") + _digest(selector["task_id_sha256"], "task identity") + if selector["task_id_sha256"] != digest( + TASK_SELECTOR_DOMAIN, task_selector_projection(selector) + ): + raise QualificationWorkerTaskContractError("worker task identity differs") + return selector + + +def validate_task_condition(value: Any) -> dict[str, Any]: + """Validate one closed remote-safe task condition and its identity.""" + + condition = _closed(value, TASK_CONDITION_FIELDS, "worker task condition") + if condition["schema_version"] != TASK_CONDITION_SCHEMA: + raise QualificationWorkerTaskContractError( + "worker task condition schema is invalid" + ) + _digest(condition["task_id_sha256"], "task identity") + _digest(condition["condition_source_sha256"], "condition source identity") + _positive_ordinal(condition["condition_ordinal"], "condition ordinal") + _digest(condition["task_condition_sha256"], "task condition identity") + if condition["task_condition_sha256"] != digest( + TASK_CONDITION_DOMAIN, task_condition_projection(condition) + ): + raise QualificationWorkerTaskContractError( + "worker task condition identity differs" + ) + return condition + + +def validate_task_condition_binding( + selector_value: Any, condition_value: Any +) -> tuple[dict[str, Any], dict[str, Any]]: + """Require one condition to select the exact validated task identity.""" + + selector = validate_task_selector(selector_value) + condition = validate_task_condition(condition_value) + if condition["task_id_sha256"] != selector["task_id_sha256"]: + raise QualificationWorkerTaskContractError( + "worker task condition selects a different task identity" + ) + return selector, condition + + +def validate_task_contract( + selector_value: Any, + condition_value: Any, + *, + campaign_artifact_sha256: str, +) -> tuple[dict[str, Any], dict[str, Any]]: + """Bind a task and condition to the expected campaign artifact.""" + + expected_campaign = _digest( + campaign_artifact_sha256, "expected campaign artifact identity" + ) + selector, condition = validate_task_condition_binding( + selector_value, condition_value + ) + if selector["campaign_artifact_sha256"] != expected_campaign: + raise QualificationWorkerTaskContractError( + "worker task selector binds a different campaign artifact" + ) + return selector, condition diff --git a/scripts/stage_production_evidence.py b/scripts/stage_production_evidence.py new file mode 100644 index 0000000..9be3ba4 --- /dev/null +++ b/scripts/stage_production_evidence.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +"""Append one regular object and its raw Sigstore bundle to the v2 registry.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import shutil +import sys +from pathlib import Path + +import validate_evidence_registry as registry + + +def entry_for(raw: bytes, *, kind: str, subject: str | None) -> dict: + schema, media = registry.OBJECT_KIND_CONTRACTS[kind] + object_sha = "sha256:" + hashlib.sha256(raw).hexdigest() + digest_hex = object_sha.removeprefix("sha256:") + if kind.endswith("-sigstore-bundle"): + identity_value = subject + else: + value = json.loads(raw) + if not isinstance(value, dict) or value.get("schema_version") != schema: + raise registry.EvidenceRegistryError("regular object schema differs from kind") + if raw != registry.canonical(value) + b"\n": + raise registry.EvidenceRegistryError( + "regular object bytes must be canonical JSON followed by one LF" + ) + identity_value = value + entry = { + "kind": kind, + "object_schema_version": schema, + "object_path": ( + f"production-evidence/objects/sha256/{digest_hex[:2]}/" + f"{digest_hex}.{kind}.json" + ), + "object_sha256": object_sha, + "size_bytes": len(raw), + "object_media_type": media, + "semantic_identity_sha256": registry.semantic_identity_digest( + kind=kind, + object_schema_version=schema, + object_value=identity_value, + object_sha256=object_sha, + ), + "subject_sha256": subject, + } + entry["registry_entry_sha256"] = registry.entry_digest(entry) + return entry + + +def install_signer_registry(document: dict, source: Path, root: Path) -> None: + raw = source.read_bytes() + value = registry.validate_signer_registry(json.loads(raw)) + if raw != registry.canonical(value) + b"\n": + raise registry.EvidenceRegistryError( + "signer registry bytes must be canonical JSON followed by one LF" + ) + object_sha = "sha256:" + hashlib.sha256(raw).hexdigest() + digest_hex = object_sha.removeprefix("sha256:") + relative = ( + f"production-evidence/signer-registries/sha256/{digest_hex[:2]}/" + f"{digest_hex}.qualification-signer-registry.json" + ) + target = root / relative + target.parent.mkdir(parents=True, exist_ok=True) + if target.exists() and target.read_bytes() != raw: + raise registry.EvidenceRegistryError("signer registry content address collides") + target.write_bytes(raw) + pointer = { + "schema_version": registry.SIGNER_POINTER_SCHEMA, + "object_path": relative, + "object_sha256": object_sha, + "registry_identity_sha256": registry.signer_registry_identity_digest(value), + "registry_revision": value["revision"], + } + current = document["signer_registry"] + history = document["signer_registry_history"] + if current == pointer: + return + if current is None: + if history or pointer["registry_revision"] != 1: + raise registry.EvidenceRegistryError( + "the first signer registry must be revision 1" + ) + else: + if not history or history[-1] != current: + raise registry.EvidenceRegistryError( + "signer registry history does not end at the current pointer" + ) + if pointer["registry_revision"] != current["registry_revision"] + 1: + raise registry.EvidenceRegistryError( + "signer registry revision must increase by exactly one" + ) + history.append(pointer) + document["signer_registry"] = pointer + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--registry", default=str(registry.REGISTRY_PATH)) + parser.add_argument("--kind", required=True) + parser.add_argument("--object", required=True) + parser.add_argument("--bundle", required=True) + parser.add_argument("--signer-registry") + args = parser.parse_args(argv) + try: + if args.kind not in registry.REGULAR_KIND_CONTRACTS: + raise registry.EvidenceRegistryError("kind is not a regular registered kind") + registry_path = Path(args.registry).resolve() + root = registry_path.parent + document = json.loads(registry_path.read_text(encoding="utf-8")) + registry.validate_registry(document, root=root) + previous_head = document["registry_head_sha256"] + if args.signer_registry: + install_signer_registry(document, Path(args.signer_registry), root) + if document["signer_registry"] is None: + raise registry.EvidenceRegistryError("signer registry must be installed first") + regular_raw = Path(args.object).read_bytes() + bundle_raw = Path(args.bundle).read_bytes() + json.loads(bundle_raw) + regular = entry_for(regular_raw, kind=args.kind, subject=None) + bundle = entry_for( + bundle_raw, + kind=f"{args.kind}-sigstore-bundle", + subject=regular["object_sha256"], + ) + for index, item in enumerate(document["entries"]): + if ( + item["kind"] != regular["kind"] + or item["semantic_identity_sha256"] + != regular["semantic_identity_sha256"] + ): + continue + if index + 1 >= len(document["entries"]): + raise registry.EvidenceRegistryError( + "existing regular object has no adjacent bundle" + ) + existing_bundle = document["entries"][index + 1] + if ( + item["object_sha256"] != regular["object_sha256"] + or existing_bundle["kind"] != bundle["kind"] + or existing_bundle["object_sha256"] != bundle["object_sha256"] + or existing_bundle["subject_sha256"] + != regular["object_sha256"] + ): + raise registry.EvidenceRegistryError( + "semantic identity conflicts with an existing object pair" + ) + print( + json.dumps( + {"regular": item, "bundle": existing_bundle}, sort_keys=True + ) + ) + return 0 + existing = {item["object_sha256"] for item in document["entries"]} + if regular["object_sha256"] in existing or bundle["object_sha256"] in existing: + raise registry.EvidenceRegistryError("object is already registered") + for raw, item in ((regular_raw, regular), (bundle_raw, bundle)): + target = root / item["object_path"] + target.parent.mkdir(parents=True, exist_ok=True) + if target.exists() and target.read_bytes() != raw: + raise registry.EvidenceRegistryError("content-addressed object collides") + target.write_bytes(raw) + document["revision"] += 1 + document["previous_registry_head_sha256"] = previous_head + document["entries"].extend((regular, bundle)) + document["registry_head_sha256"] = registry.registry_head_digest(document) + registry_path.write_text(json.dumps(document, indent=2) + "\n", encoding="utf-8") + registry.validate_registry(document, root=root) + print(json.dumps({"regular": regular, "bundle": bundle}, sort_keys=True)) + except (OSError, ValueError, json.JSONDecodeError) as exc: + print(f"REFUSED: {exc}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/validate_evidence_registry.py b/scripts/validate_evidence_registry.py index 782e92b..3f18776 100644 --- a/scripts/validate_evidence_registry.py +++ b/scripts/validate_evidence_registry.py @@ -1,64 +1,191 @@ #!/usr/bin/env python3 -"""Content-addressed central evidence registry. +"""Validate the public, content-addressed production evidence registry. -Every piece of remote-safe acceptance evidence that a Production admission can -reference must be registered here first, bound by its exact SHA-256 digest. -The registry is append-only and hash-chained: each entry commits to the digest -of the previous entry, so any reordering, removal, or edit of history breaks -the chain and validation fails closed. - -The registry stores only public, privacy-safe references: an HTTPS URL, a -digest, a byte length, and recording metadata. It never stores report bodies, -frames, parameters, or any private certificate material. +The registry never accepts a caller-supplied transport URL. A consumer derives +the supported transport from the pinned repository identity, an exact +40-character registry commit, and the registered object path. """ from __future__ import annotations import argparse +import base64 +import binascii import hashlib +import itertools import json import re import sys -from pathlib import Path from collections.abc import Mapping +from datetime import datetime, timedelta, timezone +from pathlib import Path, PurePosixPath from typing import Any +from public_trust_kms import PublicTrustKmsError, validate_public_signer + ROOT = Path(__file__).resolve().parents[1] REGISTRY_PATH = ROOT / "evidence-registry.json" -REGISTRY_SCHEMA = "openadapt.production-evidence-registry/v1" +REGISTRY_SCHEMA = "openadapt.production-evidence-registry/v2" +REFERENCE_SCHEMA = "openadapt.production-evidence-object-reference/v2" +SIGNER_POINTER_SCHEMA = "openadapt.qualification-signer-registry-pointer/v1" +SIGNER_REGISTRY_SCHEMA = "openadapt.qualification-signer-registry/v2" ENTRY_DIGEST_DOMAIN = b"OpenAdapt production evidence registry entry v1\0" +HEAD_DIGEST_DOMAIN = b"OpenAdapt production evidence registry head v2\0" +SEMANTIC_IDENTITY_DOMAIN = b"OpenAdapt production evidence semantic identity v1\0" +BUNDLE_IDENTITY_DOMAIN = b"OpenAdapt production evidence Sigstore bundle identity v1\0" +SIGNER_REGISTRY_IDENTITY_DOMAIN = b"OpenAdapt qualification signer registry v2\0" +SIGNER_USAGES = { + "private-qualification-evidence-manifest", + "private-qualification-evidence-decision-request", + "qualification-campaign-permit", + "qualification-source-evidence-manifest", + "qualification-trial-receipt", + "private-qualification-evidence-decision", + "private-qualification-evidence-decision-storage-seal", + "private-qualification-evidence-decision-final-manifest", + "private-qualification-evidence-recovery-receipt", + "qualification-evidence-decision-receipt", + "qualification-authority-state-receipt", + "qualification-revocation-state-receipt", +} +RECOVERY_SIGNER_USAGE = "private-qualification-evidence-recovery-receipt" +DECISION_RECEIPT_IDENTITY_DOMAIN = ( + b"OpenAdapt qualification decision receipt series identity v1\0" +) +REPOSITORY = "OpenAdaptAI/.github" +REPOSITORY_ID = "858454062" +REPOSITORY_OWNER_ID = "132681217" +RAW_GITHUB_ORIGIN = "https://raw.githubusercontent.com" + SHA256 = re.compile(r"^sha256:[0-9a-f]{64}$") -MILLISECOND_UTC = re.compile( - r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$" +HEX40 = re.compile(r"^[0-9a-f]{40}$") +KIND = re.compile(r"^[a-z][a-z0-9-]{1,95}$") +TIMESTAMP = re.compile( + r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$" ) -ENTRY_KINDS = ( - "evidence-summary", - "attestation-bundle", - "evidence-manifest", - "acceptance-run-record", +KEY_ID = re.compile(r"^qa-ed25519-[0-9a-f]{16}$") +WORKFLOW_IDENTITY = re.compile( + r"^https://github\.com/OpenAdaptAI/[A-Za-z0-9_.-]+/\.github/workflows/" + r"[A-Za-z0-9_.-]+\.ya?ml@refs/heads/[A-Za-z0-9._/-]+$" ) +BUNDLE_MEDIA_TYPE = "application/vnd.dev.sigstore.bundle.v0.3+json" + +REGULAR_KIND_CONTRACTS: dict[str, tuple[str, str]] = { + "production-acceptance-manifest": ( + "openadapt.production-acceptance/v2", + "application/vnd.openadapt.production-acceptance+json;version=2", + ), + "production-acceptance-summary": ( + "openadapt.production-lifecycle-evidence-summary/v2", + "application/vnd.openadapt.production-lifecycle-evidence-summary+json;version=2", + ), + "production-cloud-deploy-authorization": ( + "openadapt.production-cloud-deploy-authorization/v1", + "application/vnd.openadapt.production-cloud-deploy-authorization+json;version=1", + ), + "production-cloud-deployment-result": ( + "openadapt.production-cloud-deployment-result/v1", + "application/vnd.openadapt.production-cloud-deployment-result+json;version=1", + ), + "production-current-default": ( + "openadapt.production-current-default/v1", + "application/vnd.openadapt.production-current-default+json;version=1", + ), + "production-deployment-observation": ( + "openadapt.production-deployment-observation/v1", + "application/vnd.openadapt.production-deployment-observation+json;version=1", + ), + "production-lifecycle-checkpoint": ( + "openadapt.production-lifecycle-checkpoint/v1", + "application/vnd.openadapt.production-lifecycle-checkpoint+json;version=1", + ), + "qualification-admission": ( + "openadapt.qualification-admission/v3", + "application/vnd.openadapt.qualification-admission+json;version=3", + ), + "qualification-authority-state-receipt": ( + "openadapt.qualification-authority-state-receipt/v2", + "application/vnd.openadapt.qualification-authority-state-receipt+json;version=2", + ), + "qualification-campaign-permit": ( + "openadapt.qualification-campaign-permit/v3", + "application/vnd.openadapt.qualification-campaign-permit+json;version=3", + ), + "qualification-campaign-permit-policy": ( + "openadapt.qualification-campaign-permit-policy/v3", + "application/vnd.openadapt.qualification-campaign-permit-policy+json;version=3", + ), + "qualification-campaign-permit-receipt": ( + "openadapt.qualification-campaign-permit-receipt/v3", + "application/vnd.openadapt.qualification-campaign-permit-receipt+json;version=3", + ), + "qualification-campaign-permit-request": ( + "openadapt.qualification-campaign-permit-request/v3", + "application/vnd.openadapt.qualification-campaign-permit-request+json;version=3", + ), + "qualification-evidence-decision-receipt": ( + "openadapt.qualification-evidence-decision-receipt/v1", + "application/vnd.openadapt.qualification-evidence-decision-receipt+json;version=1", + ), + "qualification-release": ( + "openadapt.qualification-release/v1", + "application/vnd.openadapt.qualification-release+json;version=1", + ), + "qualification-revocation-state-receipt": ( + "openadapt.qualification-revocation-state-receipt/v1", + "application/vnd.openadapt.qualification-revocation-state-receipt+json;version=1", + ), + "support-release-admission": ( + "openadapt.support-release-admission/v1", + "application/vnd.openadapt.support-release-admission+json;version=1", + ), +} +OBJECT_KIND_CONTRACTS: dict[str, tuple[str, str]] = dict(REGULAR_KIND_CONTRACTS) +for _kind in tuple(REGULAR_KIND_CONTRACTS): + OBJECT_KIND_CONTRACTS[f"{_kind}-sigstore-bundle"] = ( + BUNDLE_MEDIA_TYPE, + BUNDLE_MEDIA_TYPE, + ) + +ENTRY_FIELDS = { + "kind", + "object_media_type", + "object_path", + "object_schema_version", + "object_sha256", + "semantic_identity_sha256", + "size_bytes", + "subject_sha256", +} +REFERENCE_FIELDS = { + "schema_version", + "repository", + "repository_id", + "repository_owner_id", + "registry_source_commit", + "registry_revision", + "registry_head_sha256", + "registry_entry_sha256", + *ENTRY_FIELDS, +} class EvidenceRegistryError(ValueError): - """The evidence registry is invalid or does not bind the referenced object.""" + """The registry, object, or exact-commit reference is invalid.""" -def _canonical(value: Any) -> bytes: +def canonical(value: Any) -> bytes: return json.dumps( value, sort_keys=True, separators=(",", ":"), ensure_ascii=False ).encode("utf-8") def _closed(value: Any, keys: set[str], label: str) -> dict[str, Any]: - if not isinstance(value, dict): - raise EvidenceRegistryError(f"{label} must be an object") - unexpected = set(value) - keys - missing = keys - set(value) - if unexpected or missing: + if not isinstance(value, dict) or set(value) != keys: + actual = sorted(value) if isinstance(value, dict) else type(value).__name__ raise EvidenceRegistryError( - f"{label} has unexpected fields {sorted(unexpected)} " - f"and missing fields {sorted(missing)}" + f"{label} must contain exactly {sorted(keys)}; got {actual}" ) return value @@ -69,208 +196,653 @@ def _digest(value: Any, label: str) -> str: return value -def _timestamp(value: Any, label: str) -> str: - if not isinstance(value, str) or MILLISECOND_UTC.fullmatch(value) is None: - raise EvidenceRegistryError(f"{label} must be a millisecond UTC timestamp") +def _positive_integer(value: Any, label: str) -> int: + if not isinstance(value, int) or isinstance(value, bool) or value < 1: + raise EvidenceRegistryError(f"{label} must be a positive integer") return value -def _url(value: Any, label: str) -> str: - if not isinstance(value, str) or not value.startswith("https://"): - raise EvidenceRegistryError(f"{label} must be an HTTPS URL") - parsed = value.split("?", 1)[0].split("#", 1)[0] - if not parsed.startswith("https://") or len(value) > 2048: - raise EvidenceRegistryError(f"{label} is not a bounded HTTPS URL") - return value +def _timestamp(value: Any, label: str) -> datetime: + if not isinstance(value, str) or TIMESTAMP.fullmatch(value) is None: + raise EvidenceRegistryError(f"{label} must be an exact UTC timestamp") + try: + return datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ").replace( + tzinfo=timezone.utc + ) + except ValueError as exc: + raise EvidenceRegistryError(f"{label} is not a calendar timestamp") from exc + + +def semantic_identity_digest( + *, kind: str, object_schema_version: str, object_value: Any, object_sha256: str +) -> str: + if kind.endswith("-sigstore-bundle"): + payload = { + "kind": kind, + "object_sha256": object_sha256, + "subject_sha256": object_value, + } + domain = BUNDLE_IDENTITY_DOMAIN + elif kind == "qualification-evidence-decision-receipt": + if not isinstance(object_value, dict): + raise EvidenceRegistryError("decision receipt identity object is invalid") + decision_identity = object_value.get("decision_identity_sha256") + decision_revision = object_value.get("decision_revision") + _digest(decision_identity, "decision identity") + _positive_integer(decision_revision, "decision revision") + payload = { + "decision_identity_sha256": decision_identity, + "decision_revision": decision_revision, + } + domain = DECISION_RECEIPT_IDENTITY_DOMAIN + elif kind in { + "qualification-authority-state-receipt", + "qualification-revocation-state-receipt", + }: + if not isinstance(object_value, dict): + raise EvidenceRegistryError("qualification state identity object is invalid") + identity_field = ( + "authority_state_sha256" + if kind == "qualification-authority-state-receipt" + else "revocation_state_sha256" + ) + identity = object_value.get(identity_field) + _digest(identity, f"{kind} identity") + return identity + else: + payload = { + "kind": kind, + "object_schema_version": object_schema_version, + "object": object_value, + } + domain = SEMANTIC_IDENTITY_DOMAIN + return "sha256:" + hashlib.sha256(domain + canonical(payload)).hexdigest() + + +def signer_registry_identity_digest(value: Mapping[str, Any]) -> str: + return "sha256:" + hashlib.sha256( + SIGNER_REGISTRY_IDENTITY_DOMAIN + canonical(value) + ).hexdigest() + + +def validate_signer_registry(value: Any) -> dict[str, Any]: + registry = _closed( + value, + {"schema_version", "revision", "generated_at", "expires_at", "signers"}, + "signer registry", + ) + if registry["schema_version"] != SIGNER_REGISTRY_SCHEMA: + raise EvidenceRegistryError("signer registry schema is not supported") + _positive_integer(registry["revision"], "signer registry revision") + generated_at = _timestamp(registry["generated_at"], "signer registry generated_at") + expires_at = _timestamp(registry["expires_at"], "signer registry expires_at") + if not generated_at < expires_at <= generated_at + timedelta(days=7): + raise EvidenceRegistryError( + "signer registry lifetime must be positive and at most seven days" + ) + signers = registry["signers"] + if not isinstance(signers, list) or not signers: + raise EvidenceRegistryError("signer registry must contain at least one signer") + seen_ids: set[str] = set() + for index, signer_value in enumerate(signers): + label = f"signer registry signer {index}" + if not isinstance(signer_value, dict): + raise EvidenceRegistryError(f"{label} must be an object") + if signer_value.get("algorithm") == "ecdsa-p256-sha256": + try: + signer = validate_public_signer(signer_value) + except PublicTrustKmsError as exc: + raise EvidenceRegistryError(f"{label}: {exc}") from exc + key_id = signer["key_id"] + if key_id in seen_ids: + raise EvidenceRegistryError("signer registry key ids must be unique") + seen_ids.add(key_id) + if signer["status"] == "revoked": + revoked_at = _timestamp(signer["revoked_at"], f"{label} revoked_at") + if not generated_at <= revoked_at <= expires_at: + raise EvidenceRegistryError( + f"{label} revocation time is outside the registry lifetime" + ) + continue + signer = _closed( + signer_value, + { + "algorithm", + "key_id", + "public_key", + "public_key_spki_der_base64", + "public_key_sha256", + "statement_schema_versions", + "allowed_usages", + "allowed_workflows", + "allowed_ref_prefixes", + "status", + "revoked_at", + }, + label, + ) + if signer["algorithm"] != "ed25519": + raise EvidenceRegistryError(f"{label} algorithm must be ed25519") + key_id = signer["key_id"] + if not isinstance(key_id, str) or KEY_ID.fullmatch(key_id) is None: + raise EvidenceRegistryError(f"{label} key id is invalid") + if key_id in seen_ids: + raise EvidenceRegistryError("signer registry key ids must be unique") + seen_ids.add(key_id) + public_key = signer["public_key"] + if not isinstance(public_key, str) or "=" in public_key: + raise EvidenceRegistryError( + f"{label} public key must be unpadded base64url" + ) + try: + key_bytes = base64.urlsafe_b64decode( + public_key + "=" * (-len(public_key) % 4) + ) + except (ValueError, binascii.Error) as exc: + raise EvidenceRegistryError(f"{label} public key is invalid") from exc + canonical_key = base64.urlsafe_b64encode(key_bytes).decode().rstrip("=") + if len(key_bytes) != 32 or canonical_key != public_key: + raise EvidenceRegistryError( + f"{label} public key must encode 32 canonical bytes" + ) + expected_key_id = "qa-ed25519-" + hashlib.sha256(key_bytes).hexdigest()[:16] + if key_id != expected_key_id: + raise EvidenceRegistryError( + f"{label} key id does not bind the public key" + ) + spki = bytes.fromhex("302a300506032b6570032100") + key_bytes + expected_spki = base64.b64encode(spki).decode() + if signer["public_key_spki_der_base64"] != expected_spki: + raise EvidenceRegistryError( + f"{label} SPKI does not bind the public key" + ) + if signer["public_key_sha256"] != ( + "sha256:" + hashlib.sha256(spki).hexdigest() + ): + raise EvidenceRegistryError( + f"{label} public key fingerprint does not bind the public key" + ) + if signer["statement_schema_versions"] != [ + "openadapt.qualification-evidence-signing-statement/v1" + ]: + raise EvidenceRegistryError( + f"{label} signing statement schema is invalid" + ) + usages = signer["allowed_usages"] + if ( + not isinstance(usages, list) + or not usages + or usages != sorted(set(usages)) + or any(item not in SIGNER_USAGES for item in usages) + ): + raise EvidenceRegistryError(f"{label} usage allowlist is invalid") + if RECOVERY_SIGNER_USAGE in usages and usages != [RECOVERY_SIGNER_USAGE]: + raise EvidenceRegistryError( + f"{label} recovery usage must use a distinct signer" + ) + workflows = signer["allowed_workflows"] + if ( + not isinstance(workflows, list) + or not workflows + or workflows != sorted(set(workflows)) + or any( + not isinstance(item, str) + or WORKFLOW_IDENTITY.fullmatch(item) is None + for item in workflows + ) + ): + raise EvidenceRegistryError(f"{label} workflow allowlist is invalid") + prefixes = signer["allowed_ref_prefixes"] + if ( + not isinstance(prefixes, list) + or not prefixes + or prefixes != sorted(set(prefixes)) + or any( + not isinstance(item, str) + or not item.startswith("refs/") + or ".." in item + for item in prefixes + ) + ): + raise EvidenceRegistryError(f"{label} ref-prefix allowlist is invalid") + if signer["status"] == "active": + if signer["revoked_at"] is not None: + raise EvidenceRegistryError( + f"{label} active signer cannot have revoked_at" + ) + elif signer["status"] == "revoked": + revoked_at = _timestamp(signer["revoked_at"], f"{label} revoked_at") + if not generated_at <= revoked_at <= expires_at: + raise EvidenceRegistryError( + f"{label} revocation time is outside the registry lifetime" + ) + else: + raise EvidenceRegistryError(f"{label} status is invalid") + recovery_signers = [ + signer + for signer in signers + if RECOVERY_SIGNER_USAGE in signer.get("allowed_usages", []) + ] + if recovery_signers and sum( + signer["status"] == "active" for signer in recovery_signers + ) != 1: + raise EvidenceRegistryError( + "signer registry must identify exactly one active recovery signer" + ) + return registry -def _size(value: Any, label: str) -> int: - if not isinstance(value, int) or isinstance(value, bool) or value < 1: - raise EvidenceRegistryError(f"{label} must be a positive byte count") +def require_active_signer_for_usage( + value: Any, usage: str +) -> dict[str, Any]: + """Select the one active signer for a closed private signing usage.""" + + if usage not in SIGNER_USAGES: + raise EvidenceRegistryError("requested signer usage is invalid") + registry = validate_signer_registry(value) + matches = [ + signer + for signer in registry["signers"] + if signer["status"] == "active" and usage in signer["allowed_usages"] + ] + if len(matches) != 1: + raise EvidenceRegistryError( + f"signer registry must identify exactly one active signer for {usage}" + ) + return matches[0] + + +def _object_path(value: Any, kind: str, object_sha256: str) -> str: + if not isinstance(value, str) or not value or len(value) > 512: + raise EvidenceRegistryError("object path is invalid") + path = PurePosixPath(value) + if path.is_absolute() or any(part in {"", ".", ".."} for part in path.parts): + raise EvidenceRegistryError("object path must be a relative normalized path") + digest_hex = object_sha256.removeprefix("sha256:") + expected = ( + f"production-evidence/objects/sha256/{digest_hex[:2]}/" + f"{digest_hex}.{kind}.json" + ) + if value != expected: + raise EvidenceRegistryError( + f"object path must be the content-addressed path {expected}" + ) return value +def _validate_entry_projection(value: Any, label: str) -> dict[str, Any]: + entry = _closed(value, ENTRY_FIELDS, label) + kind = entry["kind"] + if not isinstance(kind, str) or KIND.fullmatch(kind) is None: + raise EvidenceRegistryError(f"{label} kind is invalid") + contract = OBJECT_KIND_CONTRACTS.get(kind) + if contract is None: + raise EvidenceRegistryError(f"{label} kind is not registered: {kind}") + if (entry["object_schema_version"], entry["object_media_type"]) != contract: + raise EvidenceRegistryError(f"{label} schema or media type differs from policy") + object_sha256 = _digest(entry["object_sha256"], f"{label} object digest") + _digest(entry["semantic_identity_sha256"], f"{label} semantic identity") + _positive_integer(entry["size_bytes"], f"{label} size") + _object_path(entry["object_path"], kind, object_sha256) + subject = entry["subject_sha256"] + if kind.endswith("-sigstore-bundle"): + _digest(subject, f"{label} bundle subject") + elif subject is not None: + raise EvidenceRegistryError(f"{label} regular object subject must be null") + return entry + + def entry_digest(entry: Mapping[str, Any]) -> str: - """Return the chained digest of one exact registry entry.""" - - canonical = { - "kind": entry["kind"], - "prior_entry_sha256": entry["prior_entry_sha256"], - "recorded_at": entry["recorded_at"], - "sequence": entry["sequence"], - "sha256": entry["sha256"], - "size_bytes": entry["size_bytes"], - "url": entry["url"], + projection = {field: entry[field] for field in ENTRY_FIELDS} + return "sha256:" + hashlib.sha256( + ENTRY_DIGEST_DOMAIN + canonical(projection) + ).hexdigest() + + +def registry_head_digest(document: Mapping[str, Any]) -> str: + payload = { + "schema_version": document["schema_version"], + "repository": document["repository"], + "repository_id": document["repository_id"], + "repository_owner_id": document["repository_owner_id"], + "revision": document["revision"], + "previous_registry_head_sha256": document["previous_registry_head_sha256"], + "signer_registry": document["signer_registry"], + "signer_registry_history": document["signer_registry_history"], + "entry_sha256s": [ + entry["registry_entry_sha256"] for entry in document["entries"] + ], } + return "sha256:" + hashlib.sha256( + HEAD_DIGEST_DOMAIN + canonical(payload) + ).hexdigest() + + +def raw_github_url(reference: Mapping[str, Any]) -> str: + """Derive the immutable raw GitHub URL from a validated reference.""" + + validated = validate_reference(reference) return ( - "sha256:" - + hashlib.sha256(ENTRY_DIGEST_DOMAIN + _canonical(canonical)).hexdigest() + f"{RAW_GITHUB_ORIGIN}/{validated['repository']}/" + f"{validated['registry_source_commit']}/{validated['object_path']}" ) -def build_entry( - *, - sequence: int, - kind: str, - url: str, - sha256: str, - size_bytes: int, - recorded_at: str, - prior_entry_sha256: str | None, -) -> dict[str, Any]: - """Build one validated registry entry with its computed chain digest.""" - - if not isinstance(sequence, int) or isinstance(sequence, bool) or sequence < 1: - raise EvidenceRegistryError("entry sequence must be a positive integer") - if kind not in ENTRY_KINDS: - raise EvidenceRegistryError(f"entry kind is not supported: {kind!r}") - entry = { - "sequence": sequence, - "kind": kind, - "url": _url(url, "entry url"), - "sha256": _digest(sha256, "entry digest"), - "size_bytes": _size(size_bytes, "entry size"), - "recorded_at": _timestamp(recorded_at, "entry recorded_at"), - "prior_entry_sha256": ( - None if prior_entry_sha256 is None - else _digest(prior_entry_sha256, "entry prior digest") - ), - } - entry["entry_sha256"] = entry_digest(entry) - return entry - +def validate_reference(value: Any) -> dict[str, Any]: + reference = _closed(value, REFERENCE_FIELDS, "production evidence reference") + if reference["schema_version"] != REFERENCE_SCHEMA: + raise EvidenceRegistryError("object reference schema is not supported") + if ( + reference["repository"], + reference["repository_id"], + reference["repository_owner_id"], + ) != (REPOSITORY, REPOSITORY_ID, REPOSITORY_OWNER_ID): + raise EvidenceRegistryError("object reference repository identity differs") + if not isinstance(reference["registry_source_commit"], str) or HEX40.fullmatch( + reference["registry_source_commit"] + ) is None: + raise EvidenceRegistryError("registry source commit must be exact lowercase hex") + _positive_integer(reference["registry_revision"], "registry revision") + _digest(reference["registry_head_sha256"], "registry head") + projection = {field: reference[field] for field in ENTRY_FIELDS} + _validate_entry_projection(projection, "production evidence reference") + if reference["registry_entry_sha256"] != entry_digest(projection): + raise EvidenceRegistryError("object reference entry digest is invalid") + return reference + + +def _validate_signer_pointer(value: Any) -> dict[str, Any] | None: + if value is None: + return None + pointer = _closed( + value, + { + "schema_version", + "object_path", + "object_sha256", + "registry_identity_sha256", + "registry_revision", + }, + "signer registry pointer", + ) + if pointer["schema_version"] != SIGNER_POINTER_SCHEMA: + raise EvidenceRegistryError("signer registry pointer schema is not supported") + digest = _digest(pointer["object_sha256"], "signer registry object digest") + _digest(pointer["registry_identity_sha256"], "signer registry identity") + _positive_integer(pointer["registry_revision"], "signer registry revision") + digest_hex = digest.removeprefix("sha256:") + expected = ( + f"production-evidence/signer-registries/sha256/{digest_hex[:2]}/" + f"{digest_hex}.qualification-signer-registry.json" + ) + if pointer["object_path"] != expected: + raise EvidenceRegistryError("signer registry pointer path is not content-addressed") + return pointer -def validate_registry(value: Any) -> list[dict[str, Any]]: - """Validate the complete registry document and return its entries.""" +def validate_registry(value: Any, *, root: Path | None = None) -> list[dict[str, Any]]: document = _closed( value, - {"$schema", "schema_version", "head_entry_sha256", "entries"}, + { + "$schema", + "schema_version", + "repository", + "repository_id", + "repository_owner_id", + "revision", + "previous_registry_head_sha256", + "registry_head_sha256", + "signer_registry", + "signer_registry_history", + "entries", + }, "evidence registry", ) if document["$schema"] != "schemas/evidence-registry.schema.json": raise EvidenceRegistryError("evidence registry $schema is invalid") if document["schema_version"] != REGISTRY_SCHEMA: raise EvidenceRegistryError("evidence registry schema is not supported") + if ( + document["repository"], + document["repository_id"], + document["repository_owner_id"], + ) != (REPOSITORY, REPOSITORY_ID, REPOSITORY_OWNER_ID): + raise EvidenceRegistryError("evidence registry repository identity differs") + _positive_integer(document["revision"], "registry revision") + previous = document["previous_registry_head_sha256"] + if previous is not None: + _digest(previous, "previous registry head") + signer_pointer = _validate_signer_pointer(document["signer_registry"]) + history_value = document["signer_registry_history"] + if not isinstance(history_value, list): + raise EvidenceRegistryError("signer registry history must be a list") + signer_history = [_validate_signer_pointer(item) for item in history_value] + if any(item is None for item in signer_history): + raise EvidenceRegistryError("signer registry history cannot contain null") + typed_history = [item for item in signer_history if item is not None] + revisions = [item["registry_revision"] for item in typed_history] + if revisions != sorted(set(revisions)) or any( + current != previous + 1 + for previous, current in itertools.pairwise(revisions) + ): + raise EvidenceRegistryError( + "signer registry history must use unique consecutive revisions" + ) + if (signer_pointer is None) != (not typed_history) or ( + signer_pointer is not None and signer_pointer != typed_history[-1] + ): + raise EvidenceRegistryError( + "current signer registry must be the last historical pointer" + ) entries = document["entries"] if not isinstance(entries, list): - raise EvidenceRegistryError("evidence registry entries must be a list") - - previous_digest: str | None = None - for index, item in enumerate(entries): + raise EvidenceRegistryError("registry entries must be a list") + if entries and signer_pointer is None: + raise EvidenceRegistryError("a non-empty registry requires a signer registry pointer") + + seen_entries: set[str] = set() + seen_paths: set[str] = set() + seen_regular_identities: set[tuple[str, str]] = set() + previous_regular: dict[str, Any] | None = None + validated: list[dict[str, Any]] = [] + for index, value_entry in enumerate(entries): entry = _closed( - item, - { - "sequence", - "kind", - "url", - "sha256", - "size_bytes", - "recorded_at", - "prior_entry_sha256", - "entry_sha256", - }, + value_entry, + {"registry_entry_sha256", *ENTRY_FIELDS}, f"registry entry {index}", ) - if entry["sequence"] != index + 1: - raise EvidenceRegistryError( - f"registry entry {index} sequence must be exactly {index + 1}" - ) - expected_prior = previous_digest - actual_prior = entry["prior_entry_sha256"] - if expected_prior != actual_prior: - raise EvidenceRegistryError( - f"registry entry {index} does not chain to its predecessor" - ) - if entry["kind"] not in ENTRY_KINDS: - raise EvidenceRegistryError( - f"registry entry {index} kind is not supported" + projection = {field: entry[field] for field in ENTRY_FIELDS} + _validate_entry_projection(projection, f"registry entry {index}") + computed = entry_digest(projection) + if entry["registry_entry_sha256"] != computed: + raise EvidenceRegistryError(f"registry entry {index} digest is invalid") + if computed in seen_entries or entry["object_path"] in seen_paths: + raise EvidenceRegistryError("registry entries must be unique") + seen_entries.add(computed) + seen_paths.add(entry["object_path"]) + if entry["kind"].endswith("-sigstore-bundle"): + expected_regular_kind = entry["kind"].removesuffix("-sigstore-bundle") + if ( + previous_regular is None + or previous_regular["kind"] != expected_regular_kind + or entry["subject_sha256"] != previous_regular["object_sha256"] + ): + raise EvidenceRegistryError( + "a Sigstore bundle must immediately follow and bind its regular object" + ) + previous_regular = None + else: + regular_identity = ( + entry["kind"], entry["semantic_identity_sha256"] ) - _url(entry["url"], f"registry entry {index} url") - _digest(entry["sha256"], f"registry entry {index} digest") - _size(entry["size_bytes"], f"registry entry {index} size") - _timestamp(entry["recorded_at"], f"registry entry {index} recorded_at") - computed = entry_digest(entry) - if entry["entry_sha256"] != computed: - raise EvidenceRegistryError( - f"registry entry {index} chained digest is invalid" + if regular_identity in seen_regular_identities: + raise EvidenceRegistryError( + "a regular semantic identity can be registered only once" + ) + seen_regular_identities.add(regular_identity) + if previous_regular is not None: + raise EvidenceRegistryError( + "every regular object must be immediately followed by its Sigstore bundle" + ) + previous_regular = entry + if root is not None: + path = root / entry["object_path"] + try: + raw = path.read_bytes() + except OSError as exc: + raise EvidenceRegistryError(f"registered object is missing: {path}") from exc + actual = "sha256:" + hashlib.sha256(raw).hexdigest() + if actual != entry["object_sha256"] or len(raw) != entry["size_bytes"]: + raise EvidenceRegistryError(f"registered object bytes differ: {path}") + try: + object_value = json.loads(raw) + except json.JSONDecodeError as exc: + raise EvidenceRegistryError( + f"registered object is not JSON: {path}" + ) from exc + if entry["kind"].endswith("-sigstore-bundle"): + identity_value: Any = entry["subject_sha256"] + else: + if ( + not isinstance(object_value, dict) + or object_value.get("schema_version") + != entry["object_schema_version"] + ): + raise EvidenceRegistryError( + f"registered object schema differs: {path}" + ) + if raw != canonical(object_value) + b"\n": + raise EvidenceRegistryError( + f"registered regular object is not canonical JSON plus LF: {path}" + ) + identity_value = object_value + expected_identity = semantic_identity_digest( + kind=entry["kind"], + object_schema_version=entry["object_schema_version"], + object_value=identity_value, + object_sha256=entry["object_sha256"], ) - previous_digest = computed + if entry["semantic_identity_sha256"] != expected_identity: + raise EvidenceRegistryError( + f"registered object semantic identity differs: {path}" + ) + validated.append(entry) - head = document["head_entry_sha256"] - if entries: - if head != previous_digest: - raise EvidenceRegistryError("evidence registry head digest is stale") - elif head is not None: - raise EvidenceRegistryError("empty evidence registry must have a null head") - return entries + if previous_regular is not None: + raise EvidenceRegistryError( + "every regular object must be immediately followed by its Sigstore bundle" + ) + + if document["registry_head_sha256"] != registry_head_digest(document): + raise EvidenceRegistryError("registry head digest is invalid") + if root is not None: + for historical_pointer in typed_history: + signer_path = root / historical_pointer["object_path"] + try: + signer_raw = signer_path.read_bytes() + signer_value = json.loads(signer_raw) + except (OSError, json.JSONDecodeError) as exc: + raise EvidenceRegistryError( + "signer registry object is missing or invalid" + ) from exc + if ( + "sha256:" + hashlib.sha256(signer_raw).hexdigest() + != historical_pointer["object_sha256"] + ): + raise EvidenceRegistryError("signer registry object digest differs") + historical_registry = validate_signer_registry(signer_value) + if ( + historical_registry["revision"] + != historical_pointer["registry_revision"] + or signer_registry_identity_digest(historical_registry) + != historical_pointer["registry_identity_sha256"] + ): + raise EvidenceRegistryError("signer registry identity differs") + return validated def require_registered( - entries: list[dict[str, Any]], - *, - url: str, - sha256: str, - kind: str, - label: str, -) -> None: - """Fail closed unless the exact (url, digest) pair is registered.""" - - wanted_url = _url(url, f"{label} url") - wanted_digest = _digest(sha256, f"{label} digest") + entries: list[dict[str, Any]], *, reference: Mapping[str, Any], label: str +) -> dict[str, Any]: + wanted = validate_reference(reference) for entry in entries: - if entry["url"] == wanted_url and entry["sha256"] == wanted_digest: - if entry["kind"] != kind: - raise EvidenceRegistryError( - f"{label} is registered with kind {entry['kind']!r}, " - f"not {kind!r}" - ) - return - raise EvidenceRegistryError( - f"{label} is not registered in the central evidence registry" - ) + if entry["registry_entry_sha256"] == wanted["registry_entry_sha256"]: + for field in ENTRY_FIELDS: + if entry[field] != wanted[field]: + raise EvidenceRegistryError(f"{label} differs from its registry entry") + return entry + raise EvidenceRegistryError(f"{label} is not registered") def validate_append_only_history(previous_value: object, current_value: object) -> None: - """Reject registry rollback while allowing exact appends.""" - - previous = validate_registry(previous_value) - current = validate_registry(current_value) - if len(current) < len(previous): - raise EvidenceRegistryError("evidence registry history cannot remove entries") - for index, old_entry in enumerate(previous): - if current[index] != old_entry: + if isinstance(previous_value, dict) and previous_value.get("schema_version") == ( + "openadapt.production-evidence-registry/v1" + ): + expected_empty_v1 = { + "$schema": "schemas/evidence-registry.schema.json", + "schema_version": "openadapt.production-evidence-registry/v1", + "head_entry_sha256": None, + "entries": [], + } + if previous_value != expected_empty_v1: raise EvidenceRegistryError( - f"evidence registry entry {index} changed after recording" + "only the exact empty v1 registry can migrate to v2" ) + current = validate_registry(current_value) + assert isinstance(current_value, dict) + if ( + current_value["revision"] != 1 + or current_value["previous_registry_head_sha256"] is not None + or current_value["signer_registry"] is not None + or current_value["signer_registry_history"] != [] + or current != [] + ): + raise EvidenceRegistryError( + "the v1-to-v2 migration must create the exact empty v2 genesis" + ) + return + previous = validate_registry(previous_value) + current = validate_registry(current_value) + previous_doc = previous_value + current_doc = current_value + assert isinstance(previous_doc, dict) and isinstance(current_doc, dict) + if current_doc["revision"] != previous_doc["revision"] + 1: + raise EvidenceRegistryError("registry revision must increase by exactly one") + if current_doc["previous_registry_head_sha256"] != previous_doc["registry_head_sha256"]: + raise EvidenceRegistryError("registry revision does not bind the previous head") + if len(current) < len(previous) or current[: len(previous)] != previous: + raise EvidenceRegistryError("registry history must be an exact append") + previous_signers = previous_doc["signer_registry_history"] + current_signers = current_doc["signer_registry_history"] + if ( + len(current_signers) < len(previous_signers) + or current_signers[: len(previous_signers)] != previous_signers + ): + raise EvidenceRegistryError( + "signer registry pointer history must be an exact append" + ) def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser( - description="Validate the content-addressed evidence registry." - ) - parser.add_argument( - "--registry", - default=str(REGISTRY_PATH), - help="path to evidence-registry.json (default: repository root)", - ) - parser.add_argument( - "--previous-registry", - help="optional trusted previous registry; the current one may only " - "append exact entries", - ) + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--registry", default=str(REGISTRY_PATH)) + parser.add_argument("--previous-registry") + parser.add_argument("--skip-object-readback", action="store_true") args = parser.parse_args(argv) + registry_path = Path(args.registry) try: - raw = Path(args.registry).read_text(encoding="utf-8") - value = json.loads(raw) - validate_registry(value) + value = json.loads(registry_path.read_text(encoding="utf-8")) + validate_registry( + value, + root=None if args.skip_object_readback else registry_path.resolve().parent, + ) if args.previous_registry: - previous_raw = Path(args.previous_registry).read_text(encoding="utf-8") - validate_append_only_history(json.loads(previous_raw), value) + previous = json.loads(Path(args.previous_registry).read_text(encoding="utf-8")) + validate_append_only_history(previous, value) except (OSError, json.JSONDecodeError, EvidenceRegistryError) as exc: print(f"REFUSED: {exc}", file=sys.stderr) return 1 - print("evidence registry is valid") + print("production evidence registry v2 is valid") return 0 diff --git a/scripts/validate_production_lifecycle.py b/scripts/validate_production_lifecycle.py index 85b96bf..14061e0 100755 --- a/scripts/validate_production_lifecycle.py +++ b/scripts/validate_production_lifecycle.py @@ -11,11 +11,22 @@ No network request occurs while the admissions list is empty. A Production admission fails closed unless the referenced summary, attestation bundle, and evidence manifest can all be fetched, hashed, and verified. + +The lifecycle policy is a v2 document. It declares the schema versions and the +two admission windows that the signed checkpoint chain enforces, and it names +the protected feed ref that carries live Production state. It does not carry a +summary authority. For v2 objects the certificate identity that signs +Production acceptance evidence lives in production-evidence-policy.json, keyed +by evidence kind. The admission ledger this module reads is the retained v1 +ledger, so its records keep the exact trust root, target map and policy +revision they were issued under; those are pinned here as module constants +rather than read from the v2 policy file. """ from __future__ import annotations import argparse +import copy import hashlib import json import re @@ -29,6 +40,7 @@ from typing import Any from urllib.parse import quote, urlsplit +import production_trust import validate_evidence_registry as evidence_registry ROOT = Path(__file__).resolve().parents[1] @@ -36,10 +48,37 @@ ADMISSIONS_PATH = ROOT / "production-lifecycle-admissions.json" LIFECYCLE_PATH = ROOT / "repository-lifecycle.yml" -POLICY_SCHEMA = "openadapt.production-lifecycle-policy/v1" +POLICY_SCHEMA = "openadapt.production-lifecycle-policy/v2" +POLICY_DOCUMENT_SCHEMA = "schemas/production-lifecycle-policy.schema.json" +POLICY_REVISION_MINIMUM = 2 ADMISSIONS_SCHEMA = "openadapt.production-lifecycle-admissions/v1" SUMMARY_SCHEMA = "openadapt.production-lifecycle-evidence-summary/v1" RELEASE_IDENTITY_SCHEMA = "openadapt.monotonic-production-release/v1" +OBJECT_REFERENCE_SCHEMA = "openadapt.production-evidence-object-reference/v2" +RELEASE_ADMISSION_SCHEMA = "openadapt.qualification-release/v1" +WORKFLOW_ADMISSION_SCHEMA = "openadapt.qualification-admission/v3" +LIFECYCLE_CHECKPOINT_SCHEMA = "openadapt.production-lifecycle-checkpoint/v1" +LIFECYCLE_FEED_SCHEMA = "openadapt.production-lifecycle-feed/v1" +LIFECYCLE_FEED_REF = "refs/heads/production-lifecycle-feed" +# production_trust.validate_release enforces a 30 day window on every +# openadapt.qualification-release/v1 object, and +# production_trust.validate_qualification_admission enforces a 7 day window on +# every openadapt.qualification-admission/v3 object. The policy declares both +# numbers; this module refuses a policy that declares a different one. +RELEASE_ADMISSION_MAXIMUM_DAYS = 30 +WORKFLOW_ADMISSION_MAXIMUM_DAYS = 7 +# The retained v1 admission ledger holds release admissions on the production +# channel only, so the release admission window governs its expiry, and every +# retained record was issued under policy revision 1. Its records and their +# acceptance summaries carry the digest of the v1 policy document that issued +# them, which is not the digest of the live v2 policy document. +RETAINED_POLICY_REVISION = 1 +RETAINED_POLICY_SHA256 = ( + "sha256:e1444a08ce6b16736168cce027ce9d48abb2e0e246fc0cd79c0772fa8e423e11" +) +TARGET_RELEASE_KINDS = frozenset({"package", "deployment", "hybrid"}) +CLAIM_SCOPE = re.compile(r"^production_[a-z]+$") +DECIMAL_ID = re.compile(r"^[1-9][0-9]*$") SHA256 = re.compile(r"^sha256:[0-9a-f]{64}$") HEX40 = re.compile(r"^[0-9a-f]{40}$") SEMVER = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$") @@ -340,36 +379,34 @@ def load_lifecycle( return _parse_group(text, "lifecycle"), _parse_group(text, "public_surfaces") -def _validate_policy(value: object) -> tuple[dict[str, Any], dict[str, dict[str, Any]]]: - policy = _closed( - value, - { - "$schema", - "schema_version", - "revision", - "maximum_admission_days", - "summary_authority", - "targets", - }, - "production lifecycle policy", - ) - if policy["schema_version"] != POLICY_SCHEMA: - raise LifecycleError("production lifecycle policy schema is not supported") - if ( - not isinstance(policy["revision"], int) - or isinstance(policy["revision"], bool) - or policy["revision"] < 1 - ): - raise LifecycleError("production lifecycle policy revision must be positive") - maximum_days = policy["maximum_admission_days"] +def _admission_days(value: object, label: str, enforced: int) -> int: + """Validate one declared admission window against the enforced window.""" + if ( - not isinstance(maximum_days, int) - or isinstance(maximum_days, bool) - or not 1 <= maximum_days <= 30 + not isinstance(value, int) + or isinstance(value, bool) + or not 1 <= value <= 30 ): - raise LifecycleError("maximum_admission_days must be between 1 and 30") + raise LifecycleError(f"{label} must be between 1 and 30") + if value != enforced: + raise LifecycleError( + f"{label} differs from the window the Production trust core enforces" + ) + return value + + +def _validate_summary_authority(value: object) -> dict[str, Any]: + """Validate the attestation authority that verifies acceptance evidence. + + v1 carried this object in the policy document and checked it there. v2 + removed it: for v2 objects production-evidence-policy.json holds the + certificate identity per evidence kind, and the retained v1 ledger keeps the + authority that issued its records. The checks are unchanged; only their + subject moved from policy data to the pinned trust root. + """ + authority = _closed( - policy["summary_authority"], + value, { "repository", "workflow", @@ -405,112 +442,211 @@ def _validate_policy(value: object) -> tuple[dict[str, Any], dict[str, dict[str, "openadapt.production-acceptance/v1" ): raise LifecycleError("evidence manifest schema is not supported") + if authority["release_identity_schema_version"] != RELEASE_IDENTITY_SCHEMA: + raise LifecycleError("release identity schema is not supported") + if authority["production_channel"] != "production": + raise LifecycleError("summary authority channel is not production") + _digest(authority["acceptance_policy_sha256"], "acceptance policy digest") if authority["signer_provenance_digest_domain"] != ( "OpenAdapt production certificate signer provenance v1\0" ): raise LifecycleError("signer provenance digest domain is not supported") if authority != EXPECTED_AUTHORITY: raise LifecycleError("summary authority differs from the pinned trust root") + return authority - targets_value = policy["targets"] - if not isinstance(targets_value, list) or not targets_value: - raise LifecycleError("production lifecycle policy must declare targets") - targets: dict[str, dict[str, Any]] = {} - memberships: set[tuple[str, str]] = set() - for index, item in enumerate(targets_value): - target = _closed( - item, - { - "id", - "display_name", - "lifecycle_scope", - "lifecycle_subject", - "source_repository", - "release_kind", - "required_claim_scope", - "required_artifact_kinds", - "package_index_project", - "artifact_authority_by_kind", - }, - f"target {index}", + +def _retained_admission_contract(maximum_admission_days: int) -> dict[str, Any]: + """Pin the contract that the retained v1 admission ledger was issued under. + + v2 keeps live Production state in the signed checkpoint feed. The v1 ledger + stays readable so a recovery can replay it, and a v1 record is only + verifiable against the attestation authority, target map and policy revision + that issued it. The v2 policy carries none of those, so they are pinned + here. The expiry window follows the release admission maximum, because + every record in this ledger is a release admission on the production + channel. + """ + + return { + "revision": RETAINED_POLICY_REVISION, + "policy_sha256": RETAINED_POLICY_SHA256, + "maximum_admission_days": maximum_admission_days, + "summary_authority": _validate_summary_authority( + copy.deepcopy(EXPECTED_AUTHORITY) + ), + "targets": { + target_id: {"id": target_id, **copy.deepcopy(contract)} + for target_id, contract in EXPECTED_TARGETS.items() + }, + } + + +def _validate_policy_target( + item: object, index: int, seen: Mapping[str, Any] +) -> tuple[str, dict[str, Any]]: + target = _closed( + item, + { + "id", + "display_name", + "source_repository", + "source_repository_id", + "release_kind", + "claim_scope", + "required_artifact_kinds", + "package_index_project", + }, + f"target {index}", + ) + target_id = _nonempty(target["id"], f"target {index} id") + if TARGET_ID.fullmatch(target_id) is None or target_id in seen: + raise LifecycleError(f"target id is invalid or duplicate: {target_id!r}") + contract = production_trust.TARGET_CONTRACTS.get(target_id) + if contract is None: + raise LifecycleError( + f"target {target_id} has no Production trust contract" ) - target_id = _nonempty(target["id"], f"target {index} id") - if TARGET_ID.fullmatch(target_id) is None or target_id in targets: - raise LifecycleError(f"target id is invalid or duplicate: {target_id!r}") - scope = target["lifecycle_scope"] - if scope not in {"repository", "public_surface"}: - raise LifecycleError(f"target {target_id} lifecycle_scope is invalid") - subject = _nonempty(target["lifecycle_subject"], f"target {target_id} subject") - membership = (scope, subject) - if membership in memberships: - raise LifecycleError(f"lifecycle subject is duplicate: {membership}") - memberships.add(membership) - _nonempty(target["display_name"], f"target {target_id} display name") - source_repository = _nonempty( - target["source_repository"], f"target {target_id} source repository" + _nonempty(target["display_name"], f"target {target_id} display name") + source_repository = _nonempty( + target["source_repository"], f"target {target_id} source repository" + ) + if not source_repository.startswith("OpenAdaptAI/"): + raise LifecycleError( + f"target {target_id} source repository is not first-party" ) - if not source_repository.startswith("OpenAdaptAI/"): - raise LifecycleError( - f"target {target_id} source repository is not first-party" - ) - release_kind = target["release_kind"] - if release_kind not in { - "public_package", - "private_deployment", - "public_deployment", - }: - raise LifecycleError(f"target {target_id} release kind is invalid") - claim_scope = _nonempty( - target["required_claim_scope"], - f"target {target_id} required claim scope", + repository_id = target["source_repository_id"] + if ( + not isinstance(repository_id, str) + or DECIMAL_ID.fullmatch(repository_id) is None + ): + raise LifecycleError( + f"target {target_id} source repository id is invalid" ) - if re.fullmatch(r"^[a-z][a-z0-9_]{2,127}$", claim_scope) is None: - raise LifecycleError(f"target {target_id} required claim scope is invalid") - kinds = target["required_artifact_kinds"] - if not isinstance(kinds, list) or not all( - isinstance(kind, str) and kind for kind in kinds - ): - raise LifecycleError( - f"target {target_id} required artifact kinds are invalid" - ) - if kinds != sorted(set(kinds)): - raise LifecycleError( - f"target {target_id} required artifact kinds must be unique and sorted" - ) - if release_kind == "private_deployment" and kinds: - raise LifecycleError( - f"private target {target_id} cannot require public artifacts" - ) - if release_kind != "private_deployment" and not kinds: - raise LifecycleError(f"public target {target_id} must require artifacts") - package_project = target["package_index_project"] - if package_project is not None and ( - not isinstance(package_project, str) - or re.fullmatch(r"^[a-z0-9][a-z0-9._-]+$", package_project) is None - ): - raise LifecycleError(f"target {target_id} package project is invalid") - authorities = target["artifact_authority_by_kind"] - if not isinstance(authorities, dict) or set(authorities) != set(kinds): - raise LifecycleError( - f"target {target_id} artifact authority map differs from artifact kinds" - ) - if not all( - value in {"pypi", "github_release", "managed_evidence"} - for value in authorities.values() - ): - raise LifecycleError(f"target {target_id} artifact authority is invalid") - if ("pypi" in authorities.values()) != (package_project is not None): + if target["release_kind"] not in TARGET_RELEASE_KINDS: + raise LifecycleError(f"target {target_id} release kind is invalid") + claim_scope = _nonempty( + target["claim_scope"], f"target {target_id} claim scope" + ) + if CLAIM_SCOPE.fullmatch(claim_scope) is None: + raise LifecycleError(f"target {target_id} claim scope is invalid") + kinds = target["required_artifact_kinds"] + if ( + not isinstance(kinds, list) + or not kinds + or not all(isinstance(kind, str) and kind for kind in kinds) + ): + raise LifecycleError( + f"target {target_id} required artifact kinds are invalid" + ) + if kinds != sorted(set(kinds)): + raise LifecycleError( + f"target {target_id} required artifact kinds must be unique and sorted" + ) + package_project = target["package_index_project"] + if package_project is not None and ( + not isinstance(package_project, str) + or re.fullmatch(r"^[a-z0-9][a-z0-9._-]+$", package_project) is None + ): + raise LifecycleError(f"target {target_id} package project is invalid") + if ( + claim_scope != contract["claim_scope"] + or source_repository != contract["repository"] + or repository_id != contract["repository_id"] + ): + raise LifecycleError( + f"target {target_id} differs from the Production trust contract" + ) + undefined = sorted(set(kinds) - set(contract["artifacts"])) + if undefined: + raise LifecycleError( + f"target {target_id} requires artifact kinds the Production trust " + f"contract does not define: {undefined}" + ) + return target_id, target + + +def _validate_policy(value: object) -> tuple[dict[str, Any], dict[str, Any]]: + """Validate the v2 policy and pin the retained v1 admission contract. + + The v2 policy states which schema versions the signed checkpoint chain + accepts and how long each admission kind may live. Every target it declares + must agree with the Production trust contract that + production_trust.validate_release applies to the matching + openadapt.qualification-release/v1 object. + """ + + policy = _closed( + value, + { + "$schema", + "schema_version", + "revision", + "maximum_release_admission_days", + "maximum_workflow_admission_days", + "object_reference_schema_version", + "release_admission_schema_version", + "workflow_admission_schema_version", + "lifecycle_checkpoint_schema_version", + "lifecycle_feed_schema_version", + "lifecycle_feed_ref", + "targets", + }, + "production lifecycle policy", + ) + if policy["schema_version"] != POLICY_SCHEMA: + raise LifecycleError("production lifecycle policy schema is not supported") + if policy["$schema"] != POLICY_DOCUMENT_SCHEMA: + raise LifecycleError("production lifecycle policy document schema differs") + if ( + not isinstance(policy["revision"], int) + or isinstance(policy["revision"], bool) + or policy["revision"] < POLICY_REVISION_MINIMUM + ): + raise LifecycleError( + "production lifecycle policy revision must be at least " + f"{POLICY_REVISION_MINIMUM}" + ) + release_days = _admission_days( + policy["maximum_release_admission_days"], + "maximum_release_admission_days", + RELEASE_ADMISSION_MAXIMUM_DAYS, + ) + workflow_days = _admission_days( + policy["maximum_workflow_admission_days"], + "maximum_workflow_admission_days", + WORKFLOW_ADMISSION_MAXIMUM_DAYS, + ) + if workflow_days > release_days: + raise LifecycleError( + "maximum_workflow_admission_days cannot exceed " + "maximum_release_admission_days" + ) + for key, expected in ( + ("object_reference_schema_version", OBJECT_REFERENCE_SCHEMA), + ("release_admission_schema_version", RELEASE_ADMISSION_SCHEMA), + ("workflow_admission_schema_version", WORKFLOW_ADMISSION_SCHEMA), + ("lifecycle_checkpoint_schema_version", LIFECYCLE_CHECKPOINT_SCHEMA), + ("lifecycle_feed_schema_version", LIFECYCLE_FEED_SCHEMA), + ("lifecycle_feed_ref", LIFECYCLE_FEED_REF), + ): + if policy[key] != expected: raise LifecycleError( - f"target {target_id} package project does not match PyPI authority" + f"production lifecycle policy {key} is not supported" ) + + targets_value = policy["targets"] + if not isinstance(targets_value, list) or not targets_value: + raise LifecycleError("production lifecycle policy must declare targets") + targets: dict[str, dict[str, Any]] = {} + for index, item in enumerate(targets_value): + target_id, target = _validate_policy_target(item, index, targets) targets[target_id] = target - actual_targets = { - target_id: {key: value for key, value in target.items() if key != "id"} - for target_id, target in targets.items() - } - if actual_targets != EXPECTED_TARGETS: - raise LifecycleError("production target map differs from the pinned target map") - return policy, targets + if set(targets) != set(EXPECTED_TARGETS): + raise LifecycleError( + "production target map differs from the pinned target map" + ) + return policy, _retained_admission_contract(release_days) def _clean_https_url(value: object, label: str) -> str: @@ -1433,7 +1569,17 @@ def validate( """Validate the complete lifecycle state and return target to admission IDs.""" now = (now or datetime.now(timezone.utc)).astimezone(timezone.utc) - policy, targets = _validate_policy(policy_value) + policy, retained = _validate_policy(policy_value) + targets = retained["targets"] + if retained["revision"] >= policy["revision"]: + raise LifecycleError( + "the retained admission revision must precede the live policy revision" + ) + _digest(policy_sha256, "production lifecycle policy digest") + if policy_sha256 == retained["policy_sha256"]: + raise LifecycleError( + "the live policy document cannot be the retained policy revision" + ) admissions_doc = _closed( admissions_value, {"$schema", "schema_version", "policy_sha256", "admissions"}, @@ -1441,7 +1587,7 @@ def validate( ) if admissions_doc["schema_version"] != ADMISSIONS_SCHEMA: raise LifecycleError("production lifecycle admissions schema is not supported") - if admissions_doc["policy_sha256"] != policy_sha256: + if admissions_doc["policy_sha256"] != retained["policy_sha256"]: raise LifecycleError("production lifecycle admissions policy digest differs") admissions_value_list = admissions_doc["admissions"] if not isinstance(admissions_value_list, list): @@ -1494,7 +1640,7 @@ def validate( raise LifecycleError( f"production admission target is not eligible: {target_id!r}" ) - if admission["policy_revision"] != policy["revision"]: + if admission["policy_revision"] != retained["revision"]: raise LifecycleError(f"admission {target_id} policy revision differs") reference = admission["acceptance_evidence"] if not isinstance(reference, dict): @@ -1533,7 +1679,7 @@ def validate( ) _validate_release_identity( admission["release_identity"], - policy["summary_authority"], + retained["summary_authority"], f"admission {target_id} release identity", ) issued_at = _timestamp( @@ -1545,7 +1691,7 @@ def validate( if issued_at > now: raise LifecycleError(f"admission {target_id} is not valid yet") if expires_at <= issued_at or expires_at > issued_at + timedelta( - days=policy["maximum_admission_days"] + days=retained["maximum_admission_days"] ): raise LifecycleError( f"admission {target_id} validity window is outside policy" @@ -1598,8 +1744,8 @@ def validate( _validate_remote_summary( latest, latest_release, - policy["summary_authority"], - policy_sha256, + retained["summary_authority"], + retained["policy_sha256"], now, fetch=fetch, verify_attestation=verify_attestation, @@ -1656,6 +1802,21 @@ def validate_files(root: Path = ROOT, *, now: datetime | None = None) -> dict[st ) +def validate_history_document(value: object, label: str) -> None: + """Validate one retained v1 admission-ledger document without its policy.""" + + document = _closed( + value, + {"$schema", "schema_version", "policy_sha256", "admissions"}, + label, + ) + if document["schema_version"] != ADMISSIONS_SCHEMA: + raise LifecycleError(f"{label} schema is not supported") + _digest(document["policy_sha256"], f"{label} policy digest") + if not isinstance(document["admissions"], list): + raise LifecycleError(f"{label} admissions must be a list") + + def validate_append_only_history(previous_value: object, current_value: object) -> None: """Reject release-ledger rollback while allowing one-way current revocation.""" @@ -1700,8 +1861,33 @@ def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--root", type=Path, default=ROOT) parser.add_argument("--previous-admissions", type=Path) + parser.add_argument( + "--history-only", + action="store_true", + help=( + "validate only the retained v1 admission-ledger append-only relation; " + "requires --previous-admissions" + ), + ) args = parser.parse_args() + if args.history_only and args.previous_admissions is None: + parser.error("--history-only requires --previous-admissions") try: + if args.history_only: + previous = _load_json( + args.previous_admissions, "previous Production lifecycle admissions" + ) + current = _load_json( + args.root / ADMISSIONS_PATH.name, + "current Production lifecycle admissions", + ) + validate_history_document( + previous, "previous Production admission history" + ) + validate_history_document(current, "current Production admission history") + validate_append_only_history(previous, current) + print("Validated retained v1 Production admission history.") + return 0 active = validate_files(args.root) if args.previous_admissions is not None: previous = _load_json( diff --git a/scripts/validate_production_lifecycle_ref.py b/scripts/validate_production_lifecycle_ref.py new file mode 100644 index 0000000..916a06e --- /dev/null +++ b/scripts/validate_production_lifecycle_ref.py @@ -0,0 +1,680 @@ +#!/usr/bin/env python3 +"""Validate a closed lifecycle feed update against an exact local Git commit.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path + +import production_trust as trust +import validate_evidence_registry as evidence +from verify_production_release_admission import verify_bytes, verify_sigstore + +ROOT = Path(__file__).resolve().parents[1] +POLICY = json.loads( + (ROOT / "production-evidence-policy.json").read_text(encoding="utf-8") +) + + +def git(*args: str) -> bytes: + return subprocess.run( + ["git", *args], check=True, capture_output=True + ).stdout + + +def ensure_commit(commit: str) -> None: + try: + git("cat-file", "-e", f"{commit}^{{commit}}") + except subprocess.CalledProcessError: + subprocess.run( + ["git", "fetch", "--no-tags", "origin", commit], check=True + ) + + +def git_file(commit: str, path: str) -> bytes: + ensure_commit(commit) + return git("show", f"{commit}:{path}") + + +def registered_object( + reference: dict[str, object], *, label: str +) -> tuple[bytes, dict[str, object]]: + validated = evidence.validate_reference(reference) + commit = validated["registry_source_commit"] + registry_raw = git_file(commit, "evidence-registry.json") + try: + registry_value = json.loads(registry_raw) + entries = evidence.validate_registry(registry_value) + except (json.JSONDecodeError, evidence.EvidenceRegistryError) as exc: + raise trust.TrustError(f"{label} registry is invalid") from exc + if ( + registry_value["revision"] != validated["registry_revision"] + or registry_value["registry_head_sha256"] + != validated["registry_head_sha256"] + ): + raise trust.TrustError(f"{label} registry identity differs") + try: + evidence.require_registered(entries, reference=validated, label=label) + except evidence.EvidenceRegistryError as exc: + raise trust.TrustError(str(exc)) from exc + raw = git_file(commit, validated["object_path"]) + value = verify_bytes(raw, validated, label) + if not isinstance(value, dict): + raise trust.TrustError(f"{label} must be one JSON object") + return raw, value + + +def registered_pair( + pair: dict[str, object], *, kind: str, prefix: str +) -> dict[str, object]: + regular_reference, bundle_reference = trust.validate_reference_pair( + pair["checkpoint_reference"] + if kind == "production-lifecycle-checkpoint" + else pair["admission_reference"], + pair["checkpoint_bundle_reference"] + if kind == "production-lifecycle-checkpoint" + else pair["admission_bundle_reference"], + kind=kind, + ) + regular_raw, regular = registered_object( + regular_reference, label=f"{prefix} object" + ) + bundle_raw, _ = registered_object( + bundle_reference, label=f"{prefix} bundle" + ) + verify_sigstore( + regular_raw, + bundle_raw, + kind=kind, + object_value=regular, + policy=POLICY, + ) + return regular + + +def reference_pair( + regular_reference: dict[str, object], + bundle_reference: dict[str, object], + *, + kind: str, + prefix: str, +) -> dict[str, object]: + regular, bundle = trust.validate_reference_pair( + regular_reference, bundle_reference, kind=kind + ) + regular_raw, value = registered_object(regular, label=f"{prefix} object") + bundle_raw, _ = registered_object(bundle, label=f"{prefix} bundle") + verify_sigstore( + regular_raw, + bundle_raw, + kind=kind, + object_value=value, + policy=POLICY, + ) + return value + + +def signer_registry(feed: dict[str, object]) -> dict[str, object]: + commit = feed["registry_source_commit"] + registry_document = json.loads(git_file(commit, "evidence-registry.json")) + evidence.validate_registry(registry_document) + pointer = evidence._validate_signer_pointer(feed["signer_registry"]) + if pointer is None or registry_document["signer_registry"] != pointer: + raise trust.TrustError("feed signer registry pointer differs") + raw = git_file(commit, pointer["object_path"]) + if ( + "sha256:" + hashlib.sha256(raw).hexdigest() + != pointer["object_sha256"] + or raw[-1:] != b"\n" + ): + raise trust.TrustError("feed signer registry bytes differ") + value = evidence.validate_signer_registry(json.loads(raw)) + if ( + evidence.canonical(value) + b"\n" != raw + or evidence.signer_registry_identity_digest(value) + != pointer["registry_identity_sha256"] + or value["revision"] != pointer["registry_revision"] + ): + raise trust.TrustError("feed signer registry identity differs") + return value + + +def historical_signer_registry( + reference: dict[str, object], object_value: dict[str, object] +) -> dict[str, object]: + commit = reference["registry_source_commit"] + registry_document = json.loads(git_file(commit, "evidence-registry.json")) + evidence.validate_registry(registry_document) + identity = object_value.get("signer_registry_identity_sha256") + if identity is None: + identity = object_value.get("signer_registry_sha256") + matches = [ + pointer + for pointer in registry_document["signer_registry_history"] + if pointer["registry_identity_sha256"] == identity + ] + if len(matches) != 1: + raise trust.TrustError( + "signed object registry is not in append-only pointer history" + ) + pointer = matches[0] + raw = git_file(commit, pointer["object_path"]) + if ( + "sha256:" + hashlib.sha256(raw).hexdigest() != pointer["object_sha256"] + or raw[-1:] != b"\n" + ): + raise trust.TrustError("historical signer registry bytes differ") + value = evidence.validate_signer_registry(json.loads(raw)) + if ( + evidence.canonical(value) + b"\n" != raw + or evidence.signer_registry_identity_digest(value) + != pointer["registry_identity_sha256"] + or value["revision"] != pointer["registry_revision"] + ): + raise trust.TrustError("historical signer registry identity differs") + return value + + +def checkpoint_children(checkpoint: dict[str, object]) -> tuple[ + dict[str, object], + list[dict[str, object]], + list[dict[str, object]], + dict[str, object], + dict[str, object], +]: + current = reference_pair( + checkpoint["current_default_reference"], + checkpoint["current_default_bundle_reference"], + kind="production-current-default", + prefix="current default", + ) + releases = [ + reference_pair( + item["admission_reference"], + item["admission_bundle_reference"], + kind="qualification-release", + prefix=f"{item['target']} release admission", + ) + for item in checkpoint["release_admissions"] + ] + workflows = [ + reference_pair( + item["admission_reference"], + item["admission_bundle_reference"], + kind="qualification-admission", + prefix="workflow admission", + ) + for item in checkpoint["workflow_admissions"] + ] + authority = reference_pair( + checkpoint["authority_state_reference"], + checkpoint["authority_state_bundle_reference"], + kind="qualification-authority-state-receipt", + prefix="authority state", + ) + revocation = reference_pair( + checkpoint["revocation_state_reference"], + checkpoint["revocation_state_bundle_reference"], + kind="qualification-revocation-state-receipt", + prefix="revocation state", + ) + return current, releases, workflows, authority, revocation + + +def resolve_release_chain( + release: dict[str, object], + *, + active_signer_registry: dict[str, object], + verification_time: datetime, +) -> tuple[dict[str, object], dict[str, object], dict[str, object], dict[str, object]]: + summary = reference_pair( + release["production_acceptance_summary_reference"], + release["production_acceptance_summary_bundle_reference"], + kind="production-acceptance-summary", + prefix=f"{release['target']} production acceptance summary", + ) + manifest = reference_pair( + summary["production_acceptance_manifest_reference"], + summary["production_acceptance_manifest_bundle_reference"], + kind="production-acceptance-manifest", + prefix=f"{release['target']} production acceptance manifest", + ) + receipt = reference_pair( + summary["qualification_evidence_decision_receipt_reference"], + summary["qualification_evidence_decision_receipt_bundle_reference"], + kind="qualification-evidence-decision-receipt", + prefix=f"{release['target']} qualification decision receipt", + ) + admission = reference_pair( + summary["qualification_admission_reference"], + summary["qualification_admission_bundle_reference"], + kind="qualification-admission", + prefix=f"{release['target']} workflow admission", + ) + receipt_signer_registry = historical_signer_registry( + summary["qualification_evidence_decision_receipt_reference"], receipt + ) + trust.verify_embedded_signature( + receipt, + signer_registry=receipt_signer_registry, + object_schema_version="openadapt.qualification-evidence-decision-receipt/v1", + signature_domain=trust.DECISION_RECEIPT_SIGNATURE_DOMAIN, + usage="qualification-evidence-decision-receipt", + now=trust.require_timestamp(receipt["issued_at"], "receipt issued_at"), + ) + trust.verify_embedded_signature( + receipt, + signer_registry=active_signer_registry, + object_schema_version="openadapt.qualification-evidence-decision-receipt/v1", + signature_domain=trust.DECISION_RECEIPT_SIGNATURE_DOMAIN, + usage="qualification-evidence-decision-receipt", + now=verification_time, + ) + trust.validate_release_evidence_chain( + release, + summary=summary, + manifest=manifest, + receipt=receipt, + qualification_admission=admission, + receipt_signer_registry=active_signer_registry, + now=verification_time, + ) + return summary, manifest, receipt, admission + + +def resolve_workflow_receipt( + admission: dict[str, object], + *, + active_signer_registry: dict[str, object], + verification_time: datetime, +) -> dict[str, object]: + receipt = reference_pair( + admission["decision_receipt_reference"], + admission["decision_receipt_bundle_reference"], + kind="qualification-evidence-decision-receipt", + prefix="workflow qualification decision receipt", + ) + receipt_signer_registry = historical_signer_registry( + admission["decision_receipt_reference"], receipt + ) + trust.verify_embedded_signature( + receipt, + signer_registry=receipt_signer_registry, + object_schema_version="openadapt.qualification-evidence-decision-receipt/v1", + signature_domain=trust.DECISION_RECEIPT_SIGNATURE_DOMAIN, + usage="qualification-evidence-decision-receipt", + now=trust.require_timestamp(receipt["issued_at"], "receipt issued_at"), + ) + trust.verify_embedded_signature( + receipt, + signer_registry=active_signer_registry, + object_schema_version="openadapt.qualification-evidence-decision-receipt/v1", + signature_domain=trust.DECISION_RECEIPT_SIGNATURE_DOMAIN, + usage="qualification-evidence-decision-receipt", + now=verification_time, + ) + trust._validate_receipt_admission_binding( + receipt, + admission, + signer_registry=active_signer_registry, + now=verification_time, + ) + return receipt + + +def refuse_revoked( + revocation: dict[str, object], + *, + subject_kind: str, + subject_id: str, +) -> None: + if any( + item["subject_kind"] == subject_kind and item["subject_id"] == subject_id + for item in revocation["revocations"] + ): + raise trust.TrustError(f"{subject_kind} is revoked") + + +def revocation_ancestry( + feed: dict[str, object], + current_state: dict[str, object], + *, + active_signer_registry: dict[str, object], + verification_time: datetime, +) -> set[str]: + """Resolve the complete signed revocation chain from the current registry.""" + + commit = feed["registry_source_commit"] + document = json.loads(git_file(commit, "evidence-registry.json")) + entries = evidence.validate_registry(document) + regular_by_identity = { + entry["semantic_identity_sha256"]: (index, entry) + for index, entry in enumerate(entries) + if entry["kind"] == "qualification-revocation-state-receipt" + } + seen: set[str] = set() + state = current_state + while True: + identity = state["revocation_state_sha256"] + if identity in seen: + raise trust.TrustError("revocation state history contains a cycle") + seen.add(identity) + previous_identity = state["previous_revocation_state_sha256"] + if previous_identity is None: + if state["revision"] != 1: + raise trust.TrustError("initial revocation state revision is not one") + return seen + match = regular_by_identity.get(previous_identity) + if match is None: + raise trust.TrustError("revocation state history is incomplete") + index, regular_entry = match + if index + 1 >= len(entries): + raise trust.TrustError("revocation state bundle is missing") + bundle_entry = entries[index + 1] + reference_common = { + "schema_version": evidence.REFERENCE_SCHEMA, + "repository": evidence.REPOSITORY, + "repository_id": evidence.REPOSITORY_ID, + "repository_owner_id": evidence.REPOSITORY_OWNER_ID, + "registry_source_commit": commit, + "registry_revision": document["revision"], + "registry_head_sha256": document["registry_head_sha256"], + } + regular_reference = {**reference_common, **regular_entry} + bundle_reference = {**reference_common, **bundle_entry} + previous_state = reference_pair( + regular_reference, + bundle_reference, + kind="qualification-revocation-state-receipt", + prefix=f"revocation state revision {state['revision'] - 1}", + ) + if ( + previous_state["revocation_state_sha256"] != previous_identity + or previous_state["revision"] != state["revision"] - 1 + ): + raise trust.TrustError("revocation state history skips or forks") + previous_revocations = { + (item["subject_kind"], item["subject_id"], item["revoked_at"], item["reason_code"]) + for item in previous_state["revocations"] + } + current_revocations = { + (item["subject_kind"], item["subject_id"], item["revoked_at"], item["reason_code"]) + for item in state["revocations"] + } + if not previous_revocations <= current_revocations: + raise trust.TrustError("revocation state removed an earlier revocation") + historical_registry = historical_signer_registry( + regular_reference, previous_state + ) + trust.verify_embedded_signature( + previous_state, + signer_registry=historical_registry, + object_schema_version=( + "openadapt.qualification-revocation-state-receipt/v1" + ), + signature_domain=trust.REVOCATION_STATE_SIGNATURE_DOMAIN, + usage="qualification-revocation-state-receipt", + now=trust.require_timestamp( + previous_state["observed_at"], "revocation observed_at" + ), + ) + trust.verify_embedded_signature( + previous_state, + signer_registry=active_signer_registry, + object_schema_version=( + "openadapt.qualification-revocation-state-receipt/v1" + ), + signature_domain=trust.REVOCATION_STATE_SIGNATURE_DOMAIN, + usage="qualification-revocation-state-receipt", + now=verification_time, + ) + state = previous_state + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--update-json", required=True) + parser.add_argument("--trusted-main", required=True) + args = parser.parse_args(argv) + try: + update = trust.validate_feed_update(json.loads(args.update_json)) + new_commit = update["new_commit"] + commit = git("rev-parse", f"{new_commit}^{{commit}}").decode().strip() + if commit != new_commit: + raise trust.TrustError("new lifecycle commit is not exact") + parent = git("rev-parse", f"{new_commit}^").decode().strip() + if parent != args.trusted_main: + raise trust.TrustError("new lifecycle commit is not based on current main") + changed = git( + "diff", "--name-only", "--diff-filter=ACMR", args.trusted_main, new_commit + ).decode().splitlines() + if changed != ["production-lifecycle-feed.json"]: + raise trust.TrustError("lifecycle commit changes files outside the feed") + feed_raw = git("show", f"{new_commit}:production-lifecycle-feed.json") + if "sha256:" + hashlib.sha256(feed_raw).hexdigest() != update["feed_sha256"]: + raise trust.TrustError("feed bytes differ from update digest") + verification_time = datetime.now(timezone.utc) + feed = trust.validate_feed(json.loads(feed_raw), now=verification_time) + if feed_raw != evidence.canonical(feed) + b"\n": + raise trust.TrustError("feed is not canonical JSON followed by one LF") + expected_old = update["expected_old_commit"] + if expected_old is None: + if feed["feed_revision"] != 1: + raise trust.TrustError("initial lifecycle feed revision must be one") + else: + previous_raw = git("show", f"{expected_old}:production-lifecycle-feed.json") + previous = json.loads(previous_raw) + if previous_raw != evidence.canonical(previous) + b"\n": + raise trust.TrustError("previous feed bytes are not canonical") + trust.validate_feed_transition(previous, feed) + if feed["registry_source_commit"] != args.trusted_main: + raise trust.TrustError("feed registry commit is not current protected main") + if feed["expires_at"] != update["expires_at"] or feed["registry_head_sha256"] != update["registry_head_sha256"]: + raise trust.TrustError("feed state differs from update payload") + checkpoint_digests = { + pair["checkpoint_reference"]["object_sha256"] + for pair in feed["checkpoints"] + } + if update["checkpoint_sha256"] not in checkpoint_digests: + raise trust.TrustError("update checkpoint is not selected by the feed") + active_signer_registry = signer_registry(feed) + checkpoints = [ + registered_pair( + pair, + kind="production-lifecycle-checkpoint", + prefix=f"lifecycle checkpoint {index}", + ) + for index, pair in enumerate(feed["checkpoints"]) + ] + decision_series: dict[tuple[str, int], str] = {} + for checkpoint in checkpoints: + current, releases, workflows, authority, revocation = ( + checkpoint_children(checkpoint) + ) + registry_identity = evidence.signer_registry_identity_digest( + active_signer_registry + ) + signer_pointer = evidence._validate_signer_pointer( + checkpoint["signer_registry"] + ) + assert signer_pointer is not None + if ( + signer_pointer["registry_identity_sha256"] != registry_identity + or authority["signer_registry_identity_sha256"] + != registry_identity + or authority["signer_registry_sha256"] + != signer_pointer["object_sha256"] + or authority["signer_registry_revision"] + != active_signer_registry["revision"] + or revocation["signer_registry_sha256"] != registry_identity + or revocation["authority_state_sha256"] + != authority["authority_state_sha256"] + ): + raise trust.TrustError( + "checkpoint authority, revocation, or signer registry binding differs" + ) + trust.verify_embedded_signature( + authority, + signer_registry=active_signer_registry, + object_schema_version=( + "openadapt.qualification-authority-state-receipt/v2" + ), + signature_domain=trust.AUTHORITY_STATE_SIGNATURE_DOMAIN, + usage="qualification-authority-state-receipt", + now=verification_time, + ) + trust.verify_embedded_signature( + revocation, + signer_registry=active_signer_registry, + object_schema_version=( + "openadapt.qualification-revocation-state-receipt/v1" + ), + signature_domain=trust.REVOCATION_STATE_SIGNATURE_DOMAIN, + usage="qualification-revocation-state-receipt", + now=verification_time, + ) + for signer in active_signer_registry["signers"]: + if signer["status"] == "active": + refuse_revoked( + revocation, + subject_kind="qualification-signer-key", + subject_id=signer["public_key_sha256"], + ) + + summaries: list[dict[str, object]] = [] + manifests: list[dict[str, object]] = [] + receipts: list[dict[str, object]] = [] + for release, pair_value in zip( + releases, checkpoint["release_admissions"], strict=True + ): + summary, manifest, receipt, admission = resolve_release_chain( + release, + active_signer_registry=active_signer_registry, + verification_time=verification_time, + ) + summaries.append(summary) + manifests.append(manifest) + receipts.append(receipt) + for item, item_reference in ( + (release, pair_value["admission_reference"]), + (admission, summary["qualification_admission_reference"]), + ( + receipt, + summary[ + "qualification_evidence_decision_receipt_reference" + ], + ), + ): + refuse_revoked( + revocation, + subject_kind=item_reference["kind"], + subject_id=item_reference["semantic_identity_sha256"], + ) + if ( + release["authority_state_sha256"] + != authority["authority_state_sha256"] + or release["revocation_state_sha256"] + != revocation["revocation_state_sha256"] + or release["signer_registry_sha256"] != registry_identity + ): + raise trust.TrustError( + "release chain current authority state differs" + ) + + for admission, pair_value in zip( + workflows, checkpoint["workflow_admissions"], strict=True + ): + receipt = resolve_workflow_receipt( + admission, + active_signer_registry=active_signer_registry, + verification_time=verification_time, + ) + receipts.append(receipt) + for item_reference in ( + pair_value["admission_reference"], + admission["decision_receipt_reference"], + ): + refuse_revoked( + revocation, + subject_kind=item_reference["kind"], + subject_id=item_reference["semantic_identity_sha256"], + ) + if ( + admission["revocation_state_sha256"] + != revocation["revocation_state_sha256"] + or admission["signer_registry_sha256"] != registry_identity + ): + raise trust.TrustError( + "workflow admission current trust state differs" + ) + + for receipt in receipts: + series = ( + receipt["decision_identity_sha256"], + receipt["decision_revision"], + ) + object_sha = "sha256:" + hashlib.sha256( + evidence.canonical(receipt) + b"\n" + ).hexdigest() + previous_sha = decision_series.setdefault(series, object_sha) + if previous_sha != object_sha: + raise trust.TrustError( + "one qualification decision series has conflicting receipts" + ) + if ( + receipt["revocation_state_sha256"] + != revocation["revocation_state_sha256"] + or receipt["signer_registry_sha256"] != registry_identity + ): + raise trust.TrustError( + "qualification decision receipt current trust state differs" + ) + + for default_target, release in zip( + current["targets"], releases, strict=True + ): + if ( + default_target["target"] != release["target"] + or default_target["release_sha256"] != release["release_sha256"] + or default_target["artifact_inventory_sha256"] + != release["artifact_inventory_sha256"] + or default_target["qualification_release_sha256"] + != checkpoint["release_admissions"][ + trust.TARGETS.index(release["target"]) + ]["admission_reference"]["object_sha256"] + ): + raise trust.TrustError( + "current default differs from its release admission" + ) + trust.validate_checkpoint_expiry_containment( + checkpoint, + signer_registry=active_signer_registry, + current_default=current, + release_admissions=releases, + workflow_admissions=workflows, + authority_state=authority, + revocation_state=revocation, + acceptance_summaries=summaries, + acceptance_manifests=manifests, + decision_receipts=receipts, + ) + trust.validate_feed_expiry_containment( + feed, + checkpoints=checkpoints, + signer_registry=active_signer_registry, + now=verification_time, + ) + print("production lifecycle feed update is valid") + except (OSError, ValueError, json.JSONDecodeError, subprocess.SubprocessError) as exc: + print(f"REFUSED: {exc}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/verify_evidence_reference.py b/scripts/verify_evidence_reference.py new file mode 100644 index 0000000..4cac991 --- /dev/null +++ b/scripts/verify_evidence_reference.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +"""Fetch and verify one registered regular evidence object and its paired bundle.""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + +import verify_production_release_admission as verifier + +ROOT = Path(__file__).resolve().parents[1] + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--reference", required=True) + parser.add_argument("--expected-kind", required=True) + parser.add_argument("--output", required=True) + args = parser.parse_args(argv) + try: + reference = verifier.load_json_argument(args.reference) + if reference.get("kind") != args.expected_kind: + raise ValueError("reference kind differs from expected kind") + bundle_reference = verifier.derive_bundle_reference(reference) + regular_raw, bundle_raw, value = verifier.fetch_pair( + reference, bundle_reference + ) + policy = json.loads( + (ROOT / "production-evidence-policy.json").read_text(encoding="utf-8") + ) + verifier.verify_sigstore( + regular_raw, bundle_raw, kind=args.expected_kind, policy=policy + ) + Path(args.output).write_text( + json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n", + encoding="utf-8", + ) + except (OSError, ValueError, json.JSONDecodeError) as exc: + print(f"REFUSED: {exc}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/verify_production_release_admission.py b/scripts/verify_production_release_admission.py new file mode 100644 index 0000000..b2aed4c --- /dev/null +++ b/scripts/verify_production_release_admission.py @@ -0,0 +1,614 @@ +#!/usr/bin/env python3 +"""Verify one exact Production release admission and its local candidate bytes.""" + +from __future__ import annotations + +import argparse +import base64 +import binascii +import hashlib +import json +import os +import shutil +import subprocess +import sys +import tempfile +import urllib.request +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +import production_trust as trust +import validate_evidence_registry as evidence + +ROOT = Path(__file__).resolve().parents[1] +POLICY_PATH = ROOT / "production-evidence-policy.json" + + +def load_json_argument(value: str) -> Any: + path = Path(value) + if path.is_file(): + return json.loads(path.read_text(encoding="utf-8")) + return json.loads(value) + + +def fetch(url: str) -> bytes: + request = urllib.request.Request(url, headers={"User-Agent": "OpenAdapt-trust/1"}) + with urllib.request.urlopen(request, timeout=30) as response: + return response.read() + + +def raw_url(commit: str, path: str) -> str: + return f"https://raw.githubusercontent.com/OpenAdaptAI/.github/{commit}/{path}" + + +def verify_bytes(raw: bytes, reference: dict[str, Any], label: str) -> Any: + actual = "sha256:" + hashlib.sha256(raw).hexdigest() + if actual != reference["object_sha256"] or len(raw) != reference["size_bytes"]: + raise trust.TrustError(f"{label} bytes or size differ from the reference") + try: + value = json.loads(raw) + except json.JSONDecodeError as exc: + raise trust.TrustError(f"{label} is not JSON") from exc + identity_value = reference["subject_sha256"] if reference["kind"].endswith("-sigstore-bundle") else value + if not reference["kind"].endswith("-sigstore-bundle") and raw != evidence.canonical(value) + b"\n": + raise trust.TrustError(f"{label} is not canonical JSON followed by one LF") + expected_identity = evidence.semantic_identity_digest( + kind=reference["kind"], + object_schema_version=reference["object_schema_version"], + object_value=identity_value, + object_sha256=reference["object_sha256"], + ) + if expected_identity != reference["semantic_identity_sha256"]: + raise trust.TrustError(f"{label} semantic identity differs") + return value + + +def fetch_pair( + regular_reference: dict[str, Any], bundle_reference: dict[str, Any] +) -> tuple[ + bytes, + bytes, + dict[str, Any], + dict[str, Any], + dict[str, Any], +]: + regular, bundle = trust.validate_reference_pair( + regular_reference, + bundle_reference, + kind=regular_reference.get("kind", ""), + ) + commit = regular["registry_source_commit"] + registry_raw = fetch(raw_url(commit, "evidence-registry.json")) + try: + registry_value = json.loads(registry_raw) + entries = evidence.validate_registry(registry_value) + except (json.JSONDecodeError, evidence.EvidenceRegistryError) as exc: + raise trust.TrustError("the referenced registry is invalid") from exc + if ( + registry_value["revision"] != regular["registry_revision"] + or registry_value["registry_head_sha256"] != regular["registry_head_sha256"] + ): + raise trust.TrustError("reference registry revision or head differs") + try: + evidence.require_registered(entries, reference=regular, label="regular object") + evidence.require_registered(entries, reference=bundle, label="bundle object") + except evidence.EvidenceRegistryError as exc: + raise trust.TrustError(str(exc)) from exc + current_pointer = registry_value["signer_registry"] + if current_pointer is None: + raise trust.TrustError("the referenced registry has no signer registry") + regular_raw = fetch(raw_url(commit, regular["object_path"])) + bundle_raw = fetch(raw_url(commit, bundle["object_path"])) + value = verify_bytes(regular_raw, regular, "regular object") + verify_bytes(bundle_raw, bundle, "bundle object") + + def load_signer(pointer: dict[str, Any], *, require_active: bool) -> dict[str, Any]: + signer_raw = fetch(raw_url(commit, pointer["object_path"])) + if ( + "sha256:" + hashlib.sha256(signer_raw).hexdigest() + != pointer["object_sha256"] + or signer_raw[-1:] != b"\n" + ): + raise trust.TrustError("signer registry bytes differ") + try: + signer_value = evidence.validate_signer_registry(json.loads(signer_raw)) + except (json.JSONDecodeError, evidence.EvidenceRegistryError) as exc: + raise trust.TrustError("signer registry is invalid") from exc + if ( + evidence.canonical(signer_value) + b"\n" != signer_raw + or evidence.signer_registry_identity_digest(signer_value) + != pointer["registry_identity_sha256"] + or signer_value["revision"] != pointer["registry_revision"] + ): + raise trust.TrustError("signer registry identity differs") + if require_active: + now = datetime.now(timezone.utc) + if not ( + evidence._timestamp(signer_value["generated_at"], "generated_at") + <= now + < evidence._timestamp(signer_value["expires_at"], "expires_at") + ): + raise trust.TrustError("current signer registry is not active") + return signer_value + + current_signer_registry = load_signer(current_pointer, require_active=True) + bound_identity = value.get("signer_registry_sha256") + if bound_identity is None: + bound_pointer = current_pointer + else: + matches = [ + item + for item in registry_value["signer_registry_history"] + if item["registry_identity_sha256"] == bound_identity + ] + if len(matches) != 1: + raise trust.TrustError( + "object signer registry is not in append-only pointer history" + ) + bound_pointer = matches[0] + bound_signer_registry = ( + current_signer_registry + if bound_pointer == current_pointer + else load_signer(bound_pointer, require_active=False) + ) + return ( + regular_raw, + bundle_raw, + value, + bound_signer_registry, + current_signer_registry, + ) + + +def resolve_pair( + regular_reference: dict[str, Any], + bundle_reference: dict[str, Any], + *, + kind: str, + policy: dict[str, Any], +) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]: + if regular_reference.get("kind") != kind: + raise trust.TrustError(f"the referenced object is not {kind}") + regular_raw, bundle_raw, value, bound_signer_registry, current_signer_registry = fetch_pair( + regular_reference, bundle_reference + ) + verify_sigstore( + regular_raw, + bundle_raw, + kind=kind, + object_value=value, + policy=policy, + ) + return value, bound_signer_registry, current_signer_registry + + +def derive_bundle_reference(regular_reference: dict[str, Any]) -> dict[str, Any]: + regular = evidence.validate_reference(regular_reference) + commit = regular["registry_source_commit"] + registry_value = json.loads(fetch(raw_url(commit, "evidence-registry.json"))) + entries = evidence.validate_registry(registry_value) + for index, entry in enumerate(entries): + if entry["registry_entry_sha256"] != regular["registry_entry_sha256"]: + continue + if index + 1 >= len(entries): + break + bundle_entry = entries[index + 1] + return { + "schema_version": evidence.REFERENCE_SCHEMA, + "repository": evidence.REPOSITORY, + "repository_id": evidence.REPOSITORY_ID, + "repository_owner_id": evidence.REPOSITORY_OWNER_ID, + "registry_source_commit": commit, + "registry_revision": regular["registry_revision"], + "registry_head_sha256": regular["registry_head_sha256"], + **bundle_entry, + } + raise trust.TrustError("the admission bundle does not immediately follow its object") + + +def verify_sigstore( + regular_raw: bytes, + bundle_raw: bytes, + *, + kind: str, + object_value: dict[str, Any], + policy: dict[str, Any], +) -> None: + sigstore = policy["sigstore"] + identities = { + item["kind"]: item for item in sigstore["certificate_identities"] + } + identity = identities.get(kind) + if identity is None: + raise trust.TrustError(f"no keyless identity is registered for {kind}") + profile = identity.get("bundle_profile") + if profile == "sigstore-message-signature": + verify_message_signature( + regular_raw, + bundle_raw, + kind=kind, + object_value=object_value, + identity=identity, + policy=policy, + ) + return + if profile != "github-attestation": + raise trust.TrustError(f"{kind} has an unsupported Sigstore profile") + version = subprocess.run( + ["gh", "--version"], check=True, capture_output=True, text=True + ).stdout.splitlines()[0] + if version != f"gh version {sigstore['version']} (2026-08-20)": + raise trust.TrustError( + f"GitHub CLI version differs; required {sigstore['version']}; got {version}" + ) + with tempfile.TemporaryDirectory() as temporary: + directory = Path(temporary) + subject = directory / "subject.json" + bundle = directory / "bundle.json" + subject.write_bytes(regular_raw) + bundle.write_bytes(bundle_raw) + command = [ + "gh", "attestation", "verify", str(subject), "--bundle", str(bundle), + "--repo", identity["issuer_repository"], "--cert-identity", + identity["certificate_identity"], "--cert-oidc-issuer", + sigstore["cert_oidc_issuer"], "--deny-self-hosted-runners", + "--no-public-good", "--format", "json", + ] + result = subprocess.run( + command, capture_output=True, text=True, check=False + ) + if result.returncode: + raise trust.TrustError( + "Sigstore bundle verification failed: " + result.stderr.strip() + ) + try: + results = json.loads(result.stdout) + if not isinstance(results, list) or len(results) != 1: + raise trust.TrustError( + "Sigstore verifier must return exactly one verified statement" + ) + verification = results[0]["verificationResult"] + statement = verification["statement"] + certificate = verification["signature"]["certificate"] + subjects = statement["subject"] + predicate_type = statement["predicateType"] + dependencies = statement["predicate"]["buildDefinition"][ + "resolvedDependencies" + ] + except (json.JSONDecodeError, KeyError, TypeError) as exc: + raise trust.TrustError("Sigstore verifier output is not JSON") from exc + if ( + predicate_type != "https://slsa.dev/provenance/v1" + or not isinstance(subjects, list) + or len(subjects) != 1 + or subjects[0].get("digest") + != {"sha256": hashlib.sha256(regular_raw).hexdigest()} + ): + raise trust.TrustError("Sigstore statement subject or predicate differs") + issuer = object_value.get("issuer") + if not isinstance(issuer, dict): + raise trust.TrustError("signed object has no closed issuer identity") + source_commit = issuer.get("source_commit") + source_ref = issuer.get("ref") + if ( + certificate.get("githubWorkflowSHA") != source_commit + or certificate.get("sourceRepositoryDigest") != source_commit + or certificate.get("githubWorkflowRef") != source_ref + or certificate.get("githubWorkflowRepository") + != identity["issuer_repository"] + ): + raise trust.TrustError("Sigstore certificate source identity differs") + expected_dependency = { + "uri": ( + f"git+https://github.com/{identity['issuer_repository']}@{source_ref}" + ), + "digest": {"gitCommit": source_commit}, + } + if dependencies != [expected_dependency]: + raise trust.TrustError("Sigstore resolved source dependency differs") + + +def _canonical_base64(value: Any, *, label: str) -> bytes: + if not isinstance(value, str): + raise trust.TrustError(f"{label} is not base64") + try: + raw = base64.b64decode(value, validate=True) + except (ValueError, binascii.Error) as exc: + raise trust.TrustError(f"{label} is not canonical base64") from exc + if base64.b64encode(raw).decode("ascii") != value: + raise trust.TrustError(f"{label} is not canonical base64") + return raw + + +def verify_message_signature( + regular_raw: bytes, + bundle_raw: bytes, + *, + kind: str, + object_value: dict[str, Any], + identity: dict[str, Any], + policy: dict[str, Any], +) -> None: + """Verify the exact raw v0.3 messageSignature profile.""" + + try: + bundle = json.loads(bundle_raw) + if set(bundle) != {"mediaType", "verificationMaterial", "messageSignature"}: + raise trust.TrustError("messageSignature bundle top-level fields differ") + if bundle["mediaType"] != evidence.BUNDLE_MEDIA_TYPE: + raise trust.TrustError("messageSignature bundle media type differs") + message = bundle["messageSignature"] + if not isinstance(message, dict) or set(message) != { + "messageDigest", "signature" + }: + raise trust.TrustError("messageSignature fields differ") + digest = message["messageDigest"] + if not isinstance(digest, dict) or set(digest) != {"algorithm", "digest"}: + raise trust.TrustError("messageSignature digest fields differ") + if digest["algorithm"] != "SHA2_256": + raise trust.TrustError("messageSignature digest algorithm differs") + if _canonical_base64(digest["digest"], label="messageSignature digest") != ( + hashlib.sha256(regular_raw).digest() + ): + raise trust.TrustError("messageSignature digest differs from the object") + signature = _canonical_base64( + message["signature"], label="messageSignature signature" + ) + if not signature: + raise trust.TrustError("messageSignature signature is empty") + material = bundle["verificationMaterial"] + if not isinstance(material, dict) or "certificate" not in material: + raise trust.TrustError("messageSignature certificate is missing") + tlog_entries = material.get("tlogEntries") + if not isinstance(tlog_entries, list) or not tlog_entries: + raise trust.TrustError("messageSignature transparency log proof is missing") + certificate = material["certificate"] + if not isinstance(certificate, dict) or set(certificate) != {"rawBytes"}: + raise trust.TrustError("messageSignature certificate fields differ") + if not _canonical_base64( + certificate["rawBytes"], label="messageSignature certificate" + ): + raise trust.TrustError("messageSignature certificate is empty") + except json.JSONDecodeError as exc: + raise trust.TrustError("messageSignature bundle is not JSON") from exc + + message_policy = policy["message_signature"] + cosign_path = shutil.which("cosign") + if cosign_path is None: + raise trust.TrustError("the pinned Cosign verifier is not installed") + cosign_raw = Path(cosign_path).read_bytes() + if ( + len(cosign_raw) != message_policy["size_bytes"] + or "sha256:" + hashlib.sha256(cosign_raw).hexdigest() + != message_policy["sha256"] + ): + raise trust.TrustError("Cosign binary bytes differ from policy") + trusted_root_path = os.environ.get("OPENADAPT_SIGSTORE_TRUSTED_ROOT") + if not trusted_root_path: + raise trust.TrustError("the pinned Sigstore trusted root is not configured") + trusted_root = Path(trusted_root_path) + trusted_root_raw = trusted_root.read_bytes() + root_policy = message_policy["trusted_root"] + if ( + len(trusted_root_raw) != root_policy["size_bytes"] + or "sha256:" + hashlib.sha256(trusted_root_raw).hexdigest() + != root_policy["sha256"] + ): + raise trust.TrustError("Sigstore trusted root bytes differ from policy") + issuer = object_value.get("issuer") + if not isinstance(issuer, dict): + raise trust.TrustError("messageSignature object has no signed issuer") + if ( + issuer.get("repository") != identity["issuer_repository"] + or issuer.get("ref") != "refs/heads/main" + or not isinstance(issuer.get("source_commit"), str) + or trust.HEX40.fullmatch(issuer["source_commit"]) is None + ): + raise trust.TrustError("messageSignature signed issuer differs") + with tempfile.TemporaryDirectory() as temporary: + directory = Path(temporary) + subject_path = directory / "subject.json" + bundle_path = directory / "bundle.json" + subject_path.write_bytes(regular_raw) + bundle_path.write_bytes(bundle_raw) + result = subprocess.run( + [ + cosign_path, + "verify-blob", + "--allow-certificate-chain", + "--bundle", + str(bundle_path), + "--trusted-root", + str(trusted_root), + "--certificate-identity", + identity["certificate_identity"], + "--certificate-oidc-issuer", + policy["sigstore"]["cert_oidc_issuer"], + "--certificate-github-workflow-repository", + identity["issuer_repository"], + "--certificate-github-workflow-ref", + issuer["ref"], + "--certificate-github-workflow-sha", + issuer["source_commit"], + "--certificate-github-workflow-trigger", + "workflow_dispatch", + str(subject_path), + ], + capture_output=True, + text=True, + check=False, + ) + if result.returncode: + raise trust.TrustError( + "messageSignature verification failed: " + result.stderr.strip() + ) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--admission-reference", required=True) + parser.add_argument("--admission-bundle-reference") + parser.add_argument("--artifact-inventory", required=True) + parser.add_argument("--artifact-root", required=True) + parser.add_argument("--expected-target", required=True) + parser.add_argument("--expected-repository", required=True) + parser.add_argument("--expected-repository-id", required=True) + parser.add_argument("--expected-source-commit", required=True) + parser.add_argument("--expected-version", required=True) + parser.add_argument("--expected-tag", required=True) + parser.add_argument("--github-output") + args = parser.parse_args(argv) + try: + reference = load_json_argument(args.admission_reference) + bundle_reference = ( + load_json_argument(args.admission_bundle_reference) + if args.admission_bundle_reference + else derive_bundle_reference(reference) + ) + inventory = trust.validate_artifact_inventory( + load_json_argument(args.artifact_inventory) + ) + ( + regular_raw, + bundle_raw, + admission_value, + release_signer_registry, + _current_signer_registry, + ) = fetch_pair( + reference, bundle_reference + ) + policy = json.loads(POLICY_PATH.read_text(encoding="utf-8")) + verify_sigstore( + regular_raw, + bundle_raw, + kind="qualification-release", + object_value=admission_value, + policy=policy, + ) + now = datetime.now(timezone.utc) + admission = trust.validate_release(admission_value) + if ( + admission["signer_registry_sha256"] + != evidence.signer_registry_identity_digest(release_signer_registry) + ): + raise trust.TrustError("release signer registry identity differs") + summary, _, _ = resolve_pair( + admission["production_acceptance_summary_reference"], + admission["production_acceptance_summary_bundle_reference"], + kind="production-acceptance-summary", + policy=policy, + ) + manifest, _, _ = resolve_pair( + summary["production_acceptance_manifest_reference"], + summary["production_acceptance_manifest_bundle_reference"], + kind="production-acceptance-manifest", + policy=policy, + ) + receipt, receipt_signer_registry, receipt_current_signer_registry = resolve_pair( + summary["qualification_evidence_decision_receipt_reference"], + summary["qualification_evidence_decision_receipt_bundle_reference"], + kind="qualification-evidence-decision-receipt", + policy=policy, + ) + qualification_admission, _, _ = resolve_pair( + summary["qualification_admission_reference"], + summary["qualification_admission_bundle_reference"], + kind="qualification-admission", + policy=policy, + ) + if ( + receipt["signer_registry_sha256"] + != evidence.signer_registry_identity_digest(receipt_signer_registry) + ): + raise trust.TrustError( + "decision receipt signer registry identity differs" + ) + trust.verify_embedded_signature( + receipt, + signer_registry=receipt_signer_registry, + object_schema_version=( + "openadapt.qualification-evidence-decision-receipt/v1" + ), + signature_domain=trust.DECISION_RECEIPT_SIGNATURE_DOMAIN, + usage="qualification-evidence-decision-receipt", + now=trust.require_timestamp(receipt["issued_at"], "receipt issued_at"), + ) + trust.verify_embedded_signature( + receipt, + signer_registry=receipt_current_signer_registry, + object_schema_version=( + "openadapt.qualification-evidence-decision-receipt/v1" + ), + signature_domain=trust.DECISION_RECEIPT_SIGNATURE_DOMAIN, + usage="qualification-evidence-decision-receipt", + now=now, + ) + admission = trust.validate_release_evidence_chain( + admission, + summary=summary, + manifest=manifest, + receipt=receipt, + qualification_admission=qualification_admission, + receipt_signer_registry=receipt_current_signer_registry, + now=now, + ) + release = admission["release"] + expected = { + "target": args.expected_target, + "repository": args.expected_repository, + "repository_id": args.expected_repository_id, + "source_commit": args.expected_source_commit, + "version": args.expected_version, + "tag": args.expected_tag, + } + actual = { + "target": admission["target"], + "repository": release["source_repository"], + "repository_id": release["source_repository_id"], + "source_commit": release["source_commit"], + "version": release["version"], + "tag": release["tag"], + } + if actual != expected: + raise trust.TrustError("release identity differs from caller expectations") + if ( + inventory["target"] != admission["target"] + or inventory["claim_scope"] != admission["claim_scope"] + or inventory["artifacts"] != release["artifacts"] + or trust.artifact_inventory_digest(inventory) + != admission["artifact_inventory_sha256"] + ): + raise trust.TrustError("caller artifact inventory differs from admission") + trust.verify_local_artifacts(Path(args.artifact_root), release["artifacts"]) + outputs = { + "admission_object_sha256": reference["object_sha256"], + "release_sha256": admission["release_sha256"], + "artifact_inventory_sha256": admission["artifact_inventory_sha256"], + "publication_staging_json": trust.canonical( + admission["publication_staging"] + ).decode("utf-8"), + "publication_staging_sha256": admission["publication_staging_sha256"], + "draft_release_id": admission["publication_staging"]["draft_release_id"], + "expires_at": admission["expires_at"], + "registry_source_commit": reference["registry_source_commit"], + } + if args.github_output: + with Path(args.github_output).open("a", encoding="utf-8") as handle: + handle.writelines( + f"{key}={value}\n" for key, value in outputs.items() + ) + print(json.dumps(outputs, sort_keys=True)) + except ( + OSError, + ValueError, + json.JSONDecodeError, + subprocess.SubprocessError, + ) as exc: + print(f"REFUSED: {exc}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/support-release-policy.json b/support-release-policy.json new file mode 100644 index 0000000..2e0d206 --- /dev/null +++ b/support-release-policy.json @@ -0,0 +1 @@ +{"$schema":"schemas/support-release-policy.schema.json","schema_version":"openadapt.support-release-policy/v1","lifecycle_state":"Support","production_projection":false,"maximum_admission_days":7,"release_authority":{"app_id":"4730708","app_slug":"openadapt-release","installation_id":"156835568","bot_user_id":"321543906","bot_login":"openadapt-release[bot]","admission_environment":"support-release-admission","effect_environment":"release-identity","tag_creation_ruleset":"OpenAdapt policy: release tag creation","tag_immutability_ruleset":"OpenAdapt policy: immutable release tags"},"targets":[{"id":"openadapt-tray","repository":"OpenAdaptAI/openadapt-tray","repository_id":"1136122737","claim_scope":"support_openadapt_tray","release_kind":"package","tag_pattern":"refs/tags/v*","artifacts":[{"kind":"python-sdist","media_type":"application/gzip","publish_destinations":["github-release","pypi"]},{"kind":"python-wheel","media_type":"application/zip","publish_destinations":["github-release","pypi"]}]}]} diff --git a/tests/test_evidence_registry.py b/tests/test_evidence_registry.py index e35e4a7..ea7030c 100644 --- a/tests/test_evidence_registry.py +++ b/tests/test_evidence_registry.py @@ -1,153 +1,590 @@ -"""Fail-closed tests for the content-addressed evidence registry.""" +"""Fail-closed tests for production evidence object references v2.""" from __future__ import annotations +import base64 import copy +import hashlib import json import sys +import tempfile import unittest from pathlib import Path +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ec + ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "scripts")) +import public_trust_kms as kms # noqa: E402 +import stage_production_evidence as stage # noqa: E402 import validate_evidence_registry as registry # noqa: E402 -def make_entry(sequence: int = 1, prior: str | None = None, **overrides) -> dict: - entry = registry.build_entry( - sequence=sequence, - kind=overrides.pop("kind", "evidence-summary"), - url=overrides.pop( - "url", "https://evidence.openadapt.ai/objects/" + "a" * 64 + "/summary.json" +def sha(character: str) -> str: + return "sha256:" + character * 64 + + +def entry(kind: str = "production-acceptance-manifest", **overrides) -> dict: + schema, media = registry.OBJECT_KIND_CONTRACTS[kind] + object_sha = overrides.pop("object_sha256", sha("a")) + digest_hex = object_sha.removeprefix("sha256:") + value = { + "kind": kind, + "object_schema_version": schema, + "object_path": ( + f"production-evidence/objects/sha256/{digest_hex[:2]}/" + f"{digest_hex}.{kind}.json" ), - sha256=overrides.pop("sha256", "sha256:" + "b" * 64), - size_bytes=overrides.pop("size_bytes", 128), - recorded_at=overrides.pop("recorded_at", "2026-08-21T00:00:00.000Z"), - prior_entry_sha256=prior, - ) - entry.update(overrides) - return entry + "object_sha256": object_sha, + "size_bytes": 123, + "object_media_type": media, + "semantic_identity_sha256": sha("b"), + "subject_sha256": None, + } + value.update(overrides) + value["registry_entry_sha256"] = registry.entry_digest(value) + return value -def wrap(entries: list[dict]) -> dict: - head = entries[-1]["entry_sha256"] if entries else None - return { +def document(entries: list[dict] | None = None, **overrides) -> dict: + values = entries or [] + value = { "$schema": "schemas/evidence-registry.schema.json", "schema_version": registry.REGISTRY_SCHEMA, - "head_entry_sha256": head, - "entries": entries, + "repository": registry.REPOSITORY, + "repository_id": registry.REPOSITORY_ID, + "repository_owner_id": registry.REPOSITORY_OWNER_ID, + "revision": 1, + "previous_registry_head_sha256": None, + "registry_head_sha256": sha("0"), + "signer_registry": None, + "signer_registry_history": [], + "entries": values, + } + value.update(overrides) + value["registry_head_sha256"] = registry.registry_head_digest(value) + return value + + +def reference(value: dict, **overrides) -> dict: + result = { + "schema_version": registry.REFERENCE_SCHEMA, + "repository": registry.REPOSITORY, + "repository_id": registry.REPOSITORY_ID, + "repository_owner_id": registry.REPOSITORY_OWNER_ID, + "registry_source_commit": "c" * 40, + "registry_revision": 7, + "registry_head_sha256": sha("d"), + **value, + } + result.update(overrides) + return result + + +def ed25519_signer( + key_byte: int, + usage: str, + *, + status: str = "active", +) -> dict: + key = bytes([key_byte]) * 32 + spki = bytes.fromhex("302a300506032b6570032100") + key + return { + "algorithm": "ed25519", + "key_id": "qa-ed25519-" + hashlib.sha256(key).hexdigest()[:16], + "public_key": base64.urlsafe_b64encode(key).decode().rstrip("="), + "public_key_spki_der_base64": base64.b64encode(spki).decode(), + "public_key_sha256": "sha256:" + hashlib.sha256(spki).hexdigest(), + "statement_schema_versions": [ + "openadapt.qualification-evidence-signing-statement/v1" + ], + "allowed_usages": [usage], + "allowed_workflows": [ + ( + "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/OpenAdaptAI/openadapt-internal/" + ".github/workflows/issue-private-qualification-evidence-decision.yml" + "@refs/heads/main" + ) + ], + "allowed_ref_prefixes": ["refs/heads/main"], + "status": status, + "revoked_at": "2026-08-27T01:00:00Z" if status == "revoked" else None, + } + + +def signer_registry(*signers: dict) -> dict: + return { + "schema_version": registry.SIGNER_REGISTRY_SCHEMA, + "revision": 1, + "generated_at": "2026-08-27T00:00:00Z", + "expires_at": "2026-09-03T00:00:00Z", + "signers": list(signers), } class EvidenceRegistryTests(unittest.TestCase): - def test_empty_registry_is_valid(self) -> None: - self.assertEqual(registry.validate_registry(wrap([])), []) - - def test_chained_entries_are_valid(self) -> None: - first = make_entry(1, None) - second = make_entry(2, first["entry_sha256"]) - third = make_entry(3, second["entry_sha256"], kind="attestation-bundle") - result = registry.validate_registry(wrap([first, second, third])) - self.assertEqual(len(result), 3) - - def test_broken_chain_is_refused(self) -> None: - first = make_entry(1, None) - second = make_entry(2, "sha256:" + "0" * 64) - with self.assertRaisesRegex(registry.EvidenceRegistryError, "chain"): - registry.validate_registry(wrap([first, second])) - - def test_stale_head_is_refused(self) -> None: - first = make_entry(1, None) - document = wrap([first]) - document["head_entry_sha256"] = "sha256:" + "e" * 64 - with self.assertRaisesRegex(registry.EvidenceRegistryError, "head"): - registry.validate_registry(document) - - def test_sequence_gap_is_refused(self) -> None: - first = make_entry(1, None) - second = make_entry(3, first["entry_sha256"]) - with self.assertRaisesRegex(registry.EvidenceRegistryError, "sequence"): - registry.validate_registry(wrap([first, second])) - - def test_tampered_digest_breaks_the_chain(self) -> None: - first = make_entry(1, None) - tampered = dict(first) - tampered["sha256"] = "sha256:" + "f" * 64 - with self.assertRaisesRegex(registry.EvidenceRegistryError, "chained digest"): - registry.validate_registry(wrap([tampered])) - - def test_non_https_url_is_refused(self) -> None: - with self.assertRaisesRegex(registry.EvidenceRegistryError, "HTTPS"): - make_entry(1, None, url="http://evidence.openadapt.ai/summary.json") - - def test_unsupported_kind_is_refused(self) -> None: - entry = make_entry(1, None) - entry["kind"] = "screenshot" - with self.assertRaisesRegex(registry.EvidenceRegistryError, "kind"): - registry.validate_registry(wrap([entry])) - - def test_closed_schema_refuses_extra_fields(self) -> None: - document = wrap([make_entry(1, None)]) - document["extra"] = True - with self.assertRaisesRegex(registry.EvidenceRegistryError, "unexpected"): - registry.validate_registry(document) - - def test_require_registered_matches_exact_pair(self) -> None: - entry = make_entry(1, None) - entries = registry.validate_registry(wrap([entry])) - registry.require_registered( - entries, - url=entry["url"], - sha256=entry["sha256"], - kind="evidence-summary", - label="case summary", + def test_kind_map_has_17_regular_and_17_bundle_kinds(self) -> None: + self.assertEqual(len(registry.REGULAR_KIND_CONTRACTS), 17) + self.assertEqual(len(registry.OBJECT_KIND_CONTRACTS), 34) + self.assertEqual( + registry.REGULAR_KIND_CONTRACTS["support-release-admission"], + ( + "openadapt.support-release-admission/v1", + "application/vnd.openadapt.support-release-admission+json;version=1", + ), + ) + self.assertNotIn( + "openadapt-tray", + {"agent", "capture", "cloud", "desktop", "docs", "flow", "openadapt"}, + ) + + def test_repository_registry_is_valid_v2(self) -> None: + value = json.loads((ROOT / "evidence-registry.json").read_text()) + self.assertEqual(registry.validate_registry(value, root=ROOT), []) + + def test_reference_has_exact_16_keys_and_derives_raw_url(self) -> None: + value = reference(entry()) + self.assertEqual(set(value), registry.REFERENCE_FIELDS) + registry.validate_reference(value) + self.assertEqual( + registry.raw_github_url(value), + "https://raw.githubusercontent.com/OpenAdaptAI/.github/" + + "c" * 40 + + "/" + + value["object_path"], + ) + + def test_url_field_is_refused(self) -> None: + value = reference(entry()) + value["url"] = "https://evidence.openadapt.ai/object.json" + with self.assertRaisesRegex(registry.EvidenceRegistryError, "exactly"): + registry.validate_reference(value) + + def test_repository_id_and_commit_are_pinned(self) -> None: + value = reference(entry(), repository_id="1") + with self.assertRaisesRegex(registry.EvidenceRegistryError, "repository"): + registry.validate_reference(value) + value = reference(entry(), registry_source_commit="main") + with self.assertRaisesRegex(registry.EvidenceRegistryError, "commit"): + registry.validate_reference(value) + + def test_object_path_must_bind_digest_and_kind(self) -> None: + value = entry() + value["object_path"] = "production-evidence/objects/sha256/aa/wrong.json" + value["registry_entry_sha256"] = registry.entry_digest(value) + with self.assertRaisesRegex(registry.EvidenceRegistryError, "content-addressed"): + registry.validate_reference(reference(value)) + + def test_bundle_must_immediately_follow_and_bind_regular_object(self) -> None: + regular = entry() + bundle = entry( + "production-acceptance-manifest-sigstore-bundle", + object_sha256=sha("e"), + subject_sha256=regular["object_sha256"], + ) + with self.assertRaisesRegex(registry.EvidenceRegistryError, "signer registry"): + registry.validate_registry(document([regular, bundle])) + signer = { + "schema_version": registry.SIGNER_POINTER_SCHEMA, + "object_path": ( + "production-evidence/signer-registries/sha256/ff/" + + "f" * 64 + + ".qualification-signer-registry.json" + ), + "object_sha256": sha("f"), + "registry_identity_sha256": sha("1"), + "registry_revision": 1, + } + valid = document( + [regular, bundle], + signer_registry=signer, + signer_registry_history=[signer], + ) + registry.validate_registry(valid) + trailing = document( + [regular], signer_registry=signer, signer_registry_history=[signer] + ) + with self.assertRaisesRegex(registry.EvidenceRegistryError, "every regular"): + registry.validate_registry(trailing) + invalid = copy.deepcopy(valid) + invalid["entries"].reverse() + invalid["registry_head_sha256"] = registry.registry_head_digest(invalid) + with self.assertRaisesRegex(registry.EvidenceRegistryError, "immediately"): + registry.validate_registry(invalid) + + def test_registry_readback_binds_exact_bytes_and_size(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + raw = b'{"schema_version":"openadapt.production-acceptance/v2"}\n' + object_sha = "sha256:" + hashlib.sha256(raw).hexdigest() + regular = entry( + object_sha256=object_sha, + size_bytes=len(raw), + semantic_identity_sha256=registry.semantic_identity_digest( + kind="production-acceptance-manifest", + object_schema_version="openadapt.production-acceptance/v2", + object_value=json.loads(raw), + object_sha256=object_sha, + ), + ) + path = root / regular["object_path"] + path.parent.mkdir(parents=True) + path.write_bytes(raw) + bundle_raw = b'{"mediaType":"application/vnd.dev.sigstore.bundle.v0.3+json"}\n' + bundle_sha = "sha256:" + hashlib.sha256(bundle_raw).hexdigest() + bundle = entry( + "production-acceptance-manifest-sigstore-bundle", + object_sha256=bundle_sha, + size_bytes=len(bundle_raw), + subject_sha256=regular["object_sha256"], + semantic_identity_sha256=registry.semantic_identity_digest( + kind="production-acceptance-manifest-sigstore-bundle", + object_schema_version=registry.BUNDLE_MEDIA_TYPE, + object_value=regular["object_sha256"], + object_sha256=bundle_sha, + ), + ) + bundle_path = root / bundle["object_path"] + bundle_path.parent.mkdir(parents=True) + bundle_path.write_bytes(bundle_raw) + signer = { + "schema_version": registry.SIGNER_POINTER_SCHEMA, + "object_path": ( + "production-evidence/signer-registries/sha256/ff/" + + "f" * 64 + + ".qualification-signer-registry.json" + ), + "object_sha256": sha("f"), + "registry_identity_sha256": sha("1"), + "registry_revision": 1, + } + value = document( + [regular, bundle], + signer_registry=signer, + signer_registry_history=[signer], + ) + with self.assertRaisesRegex(registry.EvidenceRegistryError, "signer registry"): + registry.validate_registry(value, root=root) + path.write_bytes(raw + b" ") + with self.assertRaisesRegex(registry.EvidenceRegistryError, "bytes differ"): + registry.validate_registry(value, root=root) + + def test_signer_registry_key_id_binds_canonical_key(self) -> None: + key = bytes(range(32)) + public_key = registry.base64.urlsafe_b64encode(key).decode().rstrip("=") + spki = bytes.fromhex("302a300506032b6570032100") + key + value = { + "schema_version": registry.SIGNER_REGISTRY_SCHEMA, + "revision": 1, + "generated_at": "2026-08-27T00:00:00Z", + "expires_at": "2026-09-03T00:00:00Z", + "signers": [ + { + "algorithm": "ed25519", + "key_id": "qa-ed25519-" + hashlib.sha256(key).hexdigest()[:16], + "public_key": public_key, + "public_key_spki_der_base64": registry.base64.b64encode(spki).decode(), + "public_key_sha256": ( + "sha256:" + hashlib.sha256(spki).hexdigest() + ), + "statement_schema_versions": [ + "openadapt.qualification-evidence-signing-statement/v1" + ], + "allowed_usages": [ + "qualification-evidence-decision-receipt" + ], + "allowed_workflows": [ + ( + "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/OpenAdaptAI/openadapt-internal/" + ".github/workflows/" + "issue-private-qualification-evidence-decision.yml" + "@refs/heads/main" + ) + ], + "allowed_ref_prefixes": ["refs/heads/main"], + "status": "active", + "revoked_at": None, + } + ], + } + registry.validate_signer_registry(value) + value["signers"][0]["key_id"] = "qa-ed25519-" + "0" * 16 + with self.assertRaisesRegex(registry.EvidenceRegistryError, "bind"): + registry.validate_signer_registry(value) + + def test_recovery_receipt_uses_one_distinct_active_signer(self) -> None: + schema = json.loads( + (ROOT / "schemas/qualification-signer-registry.schema.json").read_text() + ) + self.assertIn( + registry.RECOVERY_SIGNER_USAGE, + schema["$defs"]["ed25519_signer"]["properties"]["allowed_usages"] + ["items"]["enum"], + ) + recovery = ed25519_signer( + 17, registry.RECOVERY_SIGNER_USAGE + ) + storage = ed25519_signer( + 18, "private-qualification-evidence-decision-storage-seal" + ) + value = signer_registry(recovery, storage) + self.assertEqual(registry.validate_signer_registry(value), value) + self.assertEqual( + registry.require_active_signer_for_usage( + value, registry.RECOVERY_SIGNER_USAGE + )["key_id"], + recovery["key_id"], + ) + self.assertNotEqual(recovery["public_key_sha256"], storage["public_key_sha256"]) + + def test_recovery_signer_mismatch_revocation_and_reuse_fail_closed(self) -> None: + mismatch = signer_registry( + ed25519_signer(19, "qualification-evidence-decision-receipt") ) with self.assertRaisesRegex( - registry.EvidenceRegistryError, "not registered" + registry.EvidenceRegistryError, "exactly one active signer" ): - registry.require_registered( - entries, - url=entry["url"], - sha256="sha256:" + "9" * 64, - kind="evidence-summary", - label="case summary", + registry.require_active_signer_for_usage( + mismatch, registry.RECOVERY_SIGNER_USAGE ) - with self.assertRaisesRegex(registry.EvidenceRegistryError, "kind"): - registry.require_registered( - entries, - url=entry["url"], - sha256=entry["sha256"], - kind="attestation-bundle", - label="case bundle", + + revoked = signer_registry( + ed25519_signer( + 20, registry.RECOVERY_SIGNER_USAGE, status="revoked" ) + ) + with self.assertRaisesRegex( + registry.EvidenceRegistryError, "exactly one active recovery signer" + ): + registry.validate_signer_registry(revoked) + + reused = signer_registry( + ed25519_signer(21, registry.RECOVERY_SIGNER_USAGE) + ) + reused["signers"][0]["allowed_usages"] = sorted( + [ + registry.RECOVERY_SIGNER_USAGE, + "private-qualification-evidence-decision-storage-seal", + ] + ) + with self.assertRaisesRegex( + registry.EvidenceRegistryError, "distinct signer" + ): + registry.validate_signer_registry(reused) + + def test_signer_registry_accepts_only_exact_aws_kms_p256_profile(self) -> None: + public_key = ec.derive_private_key(7, ec.SECP256R1()).public_key() + point = public_key.public_bytes( + serialization.Encoding.X962, + serialization.PublicFormat.UncompressedPoint, + ) + spki = public_key.public_bytes( + serialization.Encoding.DER, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) + signer = { + "algorithm": "ecdsa-p256-sha256", + "key_id": kms.public_key_id(public_key), + "key_origin": "aws-kms", + "kms_key_arn": ( + "arn:aws:kms:us-east-1:992382684924:key/" + "12345678-1234-4abc-8def-1234567890ab" + ), + "public_key": base64.urlsafe_b64encode(point).decode().rstrip("="), + "public_key_spki_der_base64": base64.b64encode(spki).decode(), + "public_key_sha256": "sha256:" + hashlib.sha256(spki).hexdigest(), + "signature_encoding": "asn1-der-low-s-base64-rfc4648-padded", + "statement_schema_versions": [kms.STATEMENT_SCHEMA], + "allowed_usages": ["production-public-evidence"], + "allowed_kinds": ["qualification-release"], + "allowed_workflows": [kms.PUBLIC_SIGNING_WORKFLOW], + "allowed_ref_prefixes": ["refs/heads/main"], + "allowed_environments": ["public-trust-signing"], + "status": "active", + "revoked_at": None, + } + value = { + "schema_version": registry.SIGNER_REGISTRY_SCHEMA, + "revision": 1, + "generated_at": "2026-08-27T00:00:00Z", + "expires_at": "2026-09-03T00:00:00Z", + "signers": [signer], + } + self.assertEqual(registry.validate_signer_registry(value), value) + + alias = copy.deepcopy(value) + alias["signers"][0]["kms_key_arn"] = ( + "arn:aws:kms:us-east-1:992382684924:alias/openadapt-public-trust" + ) + with self.assertRaisesRegex(registry.EvidenceRegistryError, "ARN"): + registry.validate_signer_registry(alias) + + def test_signer_registry_install_is_canonical_consecutive_and_append_only(self) -> None: + key = bytes(range(32)) + spki = bytes.fromhex("302a300506032b6570032100") + key + + def signer_registry(revision: int) -> dict: + return { + "schema_version": registry.SIGNER_REGISTRY_SCHEMA, + "revision": revision, + "generated_at": f"2026-08-{26 + revision:02d}T00:00:00Z", + "expires_at": f"2026-09-{2 + revision:02d}T00:00:00Z", + "signers": [ + { + "algorithm": "ed25519", + "key_id": ( + "qa-ed25519-" + hashlib.sha256(key).hexdigest()[:16] + ), + "public_key": ( + registry.base64.urlsafe_b64encode(key) + .decode() + .rstrip("=") + ), + "public_key_spki_der_base64": ( + registry.base64.b64encode(spki).decode() + ), + "public_key_sha256": ( + "sha256:" + hashlib.sha256(spki).hexdigest() + ), + "statement_schema_versions": [ + "openadapt.qualification-evidence-signing-statement/v1" + ], + "allowed_usages": [ + "qualification-evidence-decision-receipt" + ], + "allowed_workflows": [ + ( + "/OpenAdaptAI/" + "openadapt-internal/.github/workflows/" + "issue-private-qualification-evidence-decision.yml" + "@refs/heads/main" + ) + ], + "allowed_ref_prefixes": ["refs/heads/main"], + "status": "active", + "revoked_at": None, + } + ], + } - def test_rewritten_history_is_refused(self) -> None: - first = make_entry(1, None) - second = make_entry(2, first["entry_sha256"]) - rewritten_first = dict(first) - rewritten_first["recorded_at"] = "2026-08-20T00:00:00.000Z" - # Rebuilding history from the edited first entry produces a different - # chained digest than the recorded one. - forged_second = registry.build_entry( - sequence=2, - kind="evidence-summary", - url=second["url"], - sha256=second["sha256"], - size_bytes=second["size_bytes"], - recorded_at=second["recorded_at"], - prior_entry_sha256=rewritten_first["entry_sha256"], - ) - document = wrap([rewritten_first, forged_second]) - with self.assertRaisesRegex(registry.EvidenceRegistryError, "chained digest"): - registry.validate_registry(document) - - def test_repository_file_is_valid_and_empty(self) -> None: - raw = (ROOT / "evidence-registry.json").read_text(encoding="utf-8") - value = json.loads(raw) - self.assertEqual(value["entries"], []) - self.assertIsNone(value["head_entry_sha256"]) - registry.validate_registry(copy.deepcopy(value)) + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + value = document() + first = signer_registry(1) + first_path = root / "signers-v1.json" + first_path.write_bytes(registry.canonical(first) + b"\n") + stage.install_signer_registry(value, first_path, root) + self.assertEqual(value["signer_registry_history"], [value["signer_registry"]]) + stage.install_signer_registry(value, first_path, root) + self.assertEqual(len(value["signer_registry_history"]), 1) + + third = signer_registry(3) + third_path = root / "signers-v3.json" + third_path.write_bytes(registry.canonical(third) + b"\n") + with self.assertRaisesRegex(registry.EvidenceRegistryError, "exactly one"): + stage.install_signer_registry(value, third_path, root) + + second = signer_registry(2) + second_path = root / "signers-v2.json" + second_path.write_bytes(registry.canonical(second) + b"\n") + stage.install_signer_registry(value, second_path, root) + self.assertEqual( + [item["registry_revision"] for item in value["signer_registry_history"]], + [1, 2], + ) + self.assertEqual(value["signer_registry"], value["signer_registry_history"][-1]) + + noncanonical = root / "noncanonical.json" + noncanonical.write_text(json.dumps(signer_registry(3), indent=2) + "\n") + with self.assertRaisesRegex(registry.EvidenceRegistryError, "canonical"): + stage.install_signer_registry(value, noncanonical, root) + + def test_append_only_revision_binds_previous_head(self) -> None: + previous = document() + current = document( + revision=2, + previous_registry_head_sha256=previous["registry_head_sha256"], + ) + registry.validate_append_only_history(previous, current) + current["previous_registry_head_sha256"] = sha("9") + current["registry_head_sha256"] = registry.registry_head_digest(current) + with self.assertRaisesRegex(registry.EvidenceRegistryError, "previous head"): + registry.validate_append_only_history(previous, current) + + def test_only_exact_empty_v1_registry_can_create_the_empty_v2_genesis(self) -> None: + previous = { + "$schema": "schemas/evidence-registry.schema.json", + "schema_version": "openadapt.production-evidence-registry/v1", + "head_entry_sha256": None, + "entries": [], + } + current = document() + registry.validate_append_only_history(previous, current) + + changed_previous = copy.deepcopy(previous) + changed_previous["head_entry_sha256"] = sha("1") + with self.assertRaisesRegex(registry.EvidenceRegistryError, "exact empty"): + registry.validate_append_only_history(changed_previous, current) + + changed_current = document(revision=2) + with self.assertRaisesRegex(registry.EvidenceRegistryError, "empty v2 genesis"): + registry.validate_append_only_history(previous, changed_current) + + def test_decision_revision_semantic_identity_cannot_conflict(self) -> None: + receipt_identity = { + "decision_identity_sha256": sha("1"), + "decision_revision": 7, + } + semantic = registry.semantic_identity_digest( + kind="qualification-evidence-decision-receipt", + object_schema_version=( + "openadapt.qualification-evidence-decision-receipt/v1" + ), + object_value=receipt_identity, + object_sha256=sha("2"), + ) + first = entry( + "qualification-evidence-decision-receipt", + object_sha256=sha("2"), + semantic_identity_sha256=semantic, + ) + first_bundle = entry( + "qualification-evidence-decision-receipt-sigstore-bundle", + object_sha256=sha("3"), + subject_sha256=first["object_sha256"], + ) + conflicting = entry( + "qualification-evidence-decision-receipt", + object_sha256=sha("4"), + semantic_identity_sha256=semantic, + ) + conflicting_bundle = entry( + "qualification-evidence-decision-receipt-sigstore-bundle", + object_sha256=sha("5"), + subject_sha256=conflicting["object_sha256"], + ) + signer = { + "schema_version": registry.SIGNER_POINTER_SCHEMA, + "object_path": ( + "production-evidence/signer-registries/sha256/ff/" + + "f" * 64 + + ".qualification-signer-registry.json" + ), + "object_sha256": sha("f"), + "registry_identity_sha256": sha("6"), + "registry_revision": 1, + } + value = document( + [first, first_bundle, conflicting, conflicting_bundle], + signer_registry=signer, + signer_registry_history=[signer], + ) + with self.assertRaisesRegex( + registry.EvidenceRegistryError, "semantic identity" + ): + registry.validate_registry(value) if __name__ == "__main__": diff --git a/tests/test_governance_workflows.py b/tests/test_governance_workflows.py index 1841820..c6fd4fb 100644 --- a/tests/test_governance_workflows.py +++ b/tests/test_governance_workflows.py @@ -1,17 +1,19 @@ from __future__ import annotations import json +import hashlib import re import sys import tempfile +import types import unittest from pathlib import Path +from unittest import mock ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "scripts")) -import prepare_lifecycle_change as prepare -import validate_evidence_registry as registry +import prepare_lifecycle_change as prepare # noqa: E402 LIFECYCLE_WORKFLOWS = { "production-lifecycle-activation.yml": "production-lifecycle-activation", @@ -28,6 +30,45 @@ def _write_json(path: Path, value: object) -> None: path.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8") +def _legacy_entry_digest(entry: dict) -> str: + projection = { + field: entry[field] + for field in ( + "kind", "prior_entry_sha256", "recorded_at", "sequence", + "sha256", "size_bytes", "url", + ) + } + encoded = json.dumps( + projection, sort_keys=True, separators=(",", ":"), ensure_ascii=False + ).encode() + return "sha256:" + hashlib.sha256( + b"OpenAdapt production evidence registry entry v1\0" + encoded + ).hexdigest() + + +def _legacy_build_entry(**fields: object) -> dict: + entry = dict(fields) + entry["entry_sha256"] = _legacy_entry_digest(entry) + return entry + + +def _legacy_registry_adapter() -> types.SimpleNamespace: + def validate_registry(value: dict) -> list[dict]: + if value.get("schema_version") != "openadapt.production-evidence-registry/v1": + raise ValueError("legacy registry schema differs") + return value["entries"] + + def validate_append_only_history(previous: dict, current: dict) -> None: + if current["entries"][:-1] != previous["entries"]: + raise ValueError("legacy registry history changed") + + return types.SimpleNamespace( + build_entry=_legacy_build_entry, + validate_registry=validate_registry, + validate_append_only_history=validate_append_only_history, + ) + + class GovernanceWorkflowContractTests(unittest.TestCase): def test_profile_check_reports_on_every_pull_request(self) -> None: content = _read(".github/workflows/profile-consistency.yml") @@ -39,6 +80,15 @@ def test_profile_check_reports_on_every_pull_request(self) -> None: self.assertIn("group: ${{ github.workflow }}-${{ github.event_name }}", content) self.assertIn("cancel-in-progress: false", content) + def test_v1_release_history_gate_does_not_parse_the_v2_policy_as_v1(self) -> None: + content = _read(".github/workflows/profile-consistency.yml") + rollback_step = content.split( + " - name: Refuse Production release-ledger rollback\n", 1 + )[1].split(" - name:", 1)[0] + self.assertIn("--history-only", rollback_step) + self.assertIn("--previous-admissions", rollback_step) + self.assertNotIn("previous-production-lifecycle-policy", rollback_step) + def test_profile_dispatch_refuses_the_lifecycle_app_in_every_job(self) -> None: content = _read(".github/workflows/profile-consistency.yml") self.assertIn(" reject-lifecycle-app:\n permissions: {}", content) @@ -66,7 +116,8 @@ def test_dispatch_inventory_is_exact(self) -> None: discovered.add(path.name) self.assertEqual( discovered, - set(LIFECYCLE_WORKFLOWS) | {"profile-consistency.yml"}, + set(LIFECYCLE_WORKFLOWS) + | {"production-lifecycle-ref.yml", "profile-consistency.yml"}, ) def test_lifecycle_workflows_are_app_only_review_paths(self) -> None: @@ -166,7 +217,7 @@ def test_authority_state_appends_one_hash_chained_entry(self) -> None: "entries": [], } _write_json(root / "evidence-registry.json", empty) - entry = registry.build_entry( + entry = _legacy_build_entry( sequence=1, kind="evidence-summary", url="https://example.test/summary.json", @@ -179,16 +230,19 @@ def test_authority_state_appends_one_hash_chained_entry(self) -> None: prepare.AUTHORITY_DOMAIN, {"source_commit": self.SOURCE_COMMIT, "entry": entry}, ) - prepare.record_authority( - root=root, - source_commit=self.SOURCE_COMMIT, - kind=entry["kind"], - url=entry["url"], - sha256=entry["sha256"], - size_bytes=entry["size_bytes"], - recorded_at=entry["recorded_at"], - idempotency_key=key, - ) + with mock.patch.object( + prepare, "evidence_registry", _legacy_registry_adapter() + ): + prepare.record_authority( + root=root, + source_commit=self.SOURCE_COMMIT, + kind=entry["kind"], + url=entry["url"], + sha256=entry["sha256"], + size_bytes=entry["size_bytes"], + recorded_at=entry["recorded_at"], + idempotency_key=key, + ) current = json.loads((root / "evidence-registry.json").read_text()) self.assertEqual(current["entries"], [entry]) self.assertEqual(current["head_entry_sha256"], entry["entry_sha256"]) diff --git a/tests/test_production_lifecycle.py b/tests/test_production_lifecycle.py index 90ab5dc..d5aa0bf 100644 --- a/tests/test_production_lifecycle.py +++ b/tests/test_production_lifecycle.py @@ -4,33 +4,127 @@ import copy import hashlib +import inspect import json +import subprocess import sys +import types import unittest from datetime import datetime, timezone from pathlib import Path +from unittest import mock ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "scripts")) +import production_trust as trust # noqa: E402 import validate_production_lifecycle as lifecycle # noqa: E402 -import validate_evidence_registry as evidence_registry # noqa: E402 NOW = datetime(2026, 8, 18, 12, 0, 0, tzinfo=timezone.utc) SOURCE_COMMIT = "1" * 40 +# The digest of the live v2 policy document. The value is opaque to these +# tests; the validator only requires that it is a digest and that it is not the +# retained v1 policy revision. POLICY_DIGEST = "sha256:" + "a" * 64 +# The digest of the v1 policy document that issued the retained admission +# ledger. Every retained record and every acceptance summary carries it. +LEDGER_DIGEST = lifecycle.RETAINED_POLICY_SHA256 def digest_bytes(value: bytes) -> str: return "sha256:" + hashlib.sha256(value).hexdigest() +class LegacyRegistryError(ValueError): + pass + + +def legacy_entry_digest(entry: dict) -> str: + projection = { + field: entry[field] + for field in ( + "kind", "prior_entry_sha256", "recorded_at", "sequence", + "sha256", "size_bytes", "url", + ) + } + return digest_bytes( + b"OpenAdapt production evidence registry entry v1\0" + + json.dumps( + projection, sort_keys=True, separators=(",", ":"), ensure_ascii=False + ).encode() + ) + + +def legacy_build_entry(**fields: object) -> dict: + entry = dict(fields) + entry["entry_sha256"] = legacy_entry_digest(entry) + return entry + + +def legacy_registry_adapter() -> types.SimpleNamespace: + def validate_registry(value: object) -> list[dict]: + if not isinstance(value, dict) or set(value) != { + "$schema", "schema_version", "head_entry_sha256", "entries" + }: + raise LegacyRegistryError("evidence registry object is not closed") + if value["schema_version"] != "openadapt.production-evidence-registry/v1": + raise LegacyRegistryError("evidence registry schema is not supported") + entries = value["entries"] + if not isinstance(entries, list): + raise LegacyRegistryError("evidence registry entries must be a list") + prior = None + for index, entry in enumerate(entries): + if ( + entry["sequence"] != index + 1 + or entry["prior_entry_sha256"] != prior + or entry["entry_sha256"] != legacy_entry_digest(entry) + ): + raise LegacyRegistryError("evidence registry chain is invalid") + prior = entry["entry_sha256"] + if value["head_entry_sha256"] != prior: + raise LegacyRegistryError("evidence registry head digest is stale") + return entries + + def require_registered( + entries: list[dict], *, url: str, sha256: str, kind: str, label: str + ) -> None: + matches = [ + entry for entry in entries + if entry["url"] == url and entry["sha256"] == sha256 + ] + if len(matches) != 1: + raise LegacyRegistryError( + f"{label} is not registered in the central evidence registry" + ) + if matches[0]["kind"] != kind: + raise LegacyRegistryError(f"{label} is registered with the wrong kind") + + return types.SimpleNamespace( + EvidenceRegistryError=LegacyRegistryError, + validate_registry=validate_registry, + require_registered=require_registered, + ) + + def load_policy() -> dict: + """Return the live v2 policy document the repository actually publishes. + + The validator reads this file in production, so the tests read it too. A + fixture that mirrors the validator's own constants cannot catch the case + where the data and the validator disagree. + """ + return json.loads( (ROOT / "production-lifecycle-policy.json").read_text(encoding="utf-8") ) +def retained_target(target: str) -> dict: + """Return the retained v1 contract a retained admission is verified against.""" + + return copy.deepcopy(lifecycle.EXPECTED_TARGETS[target]) + + def flow_release() -> dict: artifacts = [ { @@ -133,13 +227,8 @@ def build_case( expires_at: str = "2026-08-20T11:00:00Z", ) -> tuple[dict, dict, dict[str, bytes]]: release = copy.deepcopy(release or flow_release()) - policy = load_policy() - claim_scope = next( - item["required_claim_scope"] - for item in policy["targets"] - if item["id"] == target - ) - acceptance_policy_digest = policy["summary_authority"]["acceptance_policy_sha256"] + claim_scope = retained_target(target)["required_claim_scope"] + acceptance_policy_digest = lifecycle.EXPECTED_AUTHORITY["acceptance_policy_sha256"] release_digest = lifecycle._target_release_digest(target, claim_scope, release) artifact_digest = lifecycle._artifact_inventory_digest( target, claim_scope, release.get("artifacts", []) @@ -158,7 +247,7 @@ def build_case( "claim_scope": claim_scope, "verdict": "accepted", "acceptance_policy_sha256": acceptance_policy_digest, - "lifecycle_policy_sha256": POLICY_DIGEST, + "lifecycle_policy_sha256": LEDGER_DIGEST, "target_release_sha256": release_digest, "target_artifact_inventory_sha256": artifact_digest, "evidence_identity_sha256": evidence_identity, @@ -242,7 +331,7 @@ def build_case( "verdict": "accepted", "claim_scope": claim_scope, "acceptance_policy_sha256": acceptance_policy_digest, - "lifecycle_policy_sha256": POLICY_DIGEST, + "lifecycle_policy_sha256": LEDGER_DIGEST, "release_identity": release_identity, "release_sha256": release_digest, "artifact_inventory_sha256": artifact_digest, @@ -304,7 +393,7 @@ def build_case( admissions = { "$schema": "schemas/production-lifecycle-admissions.schema.json", "schema_version": lifecycle.ADMISSIONS_SCHEMA, - "policy_sha256": POLICY_DIGEST, + "policy_sha256": LEDGER_DIGEST, "admissions": [admission], } remote = { @@ -316,11 +405,7 @@ def build_case( item for item in release.get("artifacts", []) if item["authority"] == "pypi" ] if pypi_artifacts: - project = next( - item["package_index_project"] - for item in policy["targets"] - if item["id"] == target - ) + project = retained_target(target)["package_index_project"] metadata_url = f"https://pypi.org/pypi/{project}/{release['version']}/json" remote[metadata_url] = json.dumps( { @@ -362,7 +447,7 @@ def empty_admissions() -> dict: return { "$schema": "schemas/production-lifecycle-admissions.schema.json", "schema_version": lifecycle.ADMISSIONS_SCHEMA, - "policy_sha256": POLICY_DIGEST, + "policy_sha256": LEDGER_DIGEST, "admissions": [], } @@ -407,6 +492,7 @@ def build_registry(admissions: dict, remote: dict[str, bytes]) -> dict | None: """Build a valid central evidence registry for the given admissions.""" entries = [] + seen_references: set[tuple[str, str, str]] = set() prior: str | None = None sequence = 0 for admission in admissions.get("admissions", []): @@ -415,8 +501,12 @@ def build_registry(admissions: dict, remote: dict[str, bytes]) -> dict | None: ("evidence-summary", "summary_url", "summary_sha256"), ("attestation-bundle", "attestation_bundle_url", "attestation_bundle_sha256"), ): + identity = (kind, reference[url_key], reference[digest_key]) + if identity in seen_references: + continue + seen_references.add(identity) sequence += 1 - entry = evidence_registry.build_entry( + entry = legacy_build_entry( sequence=sequence, kind=kind, url=reference[url_key], @@ -433,7 +523,7 @@ def build_registry(admissions: dict, remote: dict[str, bytes]) -> dict | None: return None return { "$schema": "schemas/evidence-registry.schema.json", - "schema_version": evidence_registry.REGISTRY_SCHEMA, + "schema_version": "openadapt.production-evidence-registry/v1", "head_entry_sha256": prior, "entries": entries, } @@ -446,6 +536,7 @@ def validate_case( surfaces: list[str] | None = None, attestation_valid: bool = True, registry: dict | None = None, + authority_sink: list[dict] | None = None, ) -> dict[str, str]: attestation_calls: list[tuple[bytes, bytes]] = [] @@ -457,8 +548,10 @@ def fetch(url: str) -> bytes: raise OSError(f"synthetic URL is absent: {url}") from exc def verify( - summary: bytes, bundle: bytes, _authority: dict, _source_commit: str + summary: bytes, bundle: bytes, authority: dict, _source_commit: str ) -> None: + if authority_sink is not None: + authority_sink.append(copy.deepcopy(authority)) if not attestation_valid: raise lifecycle.LifecycleError("synthetic attestation failure") attestation_calls.append((summary, bundle)) @@ -477,19 +570,24 @@ def verify( if group != "production" and subject in subjects: subjects.remove(subject) - result = lifecycle.validate( - load_policy(), - admissions, - repository_lifecycle, - surface_lifecycle, - policy_sha256=POLICY_DIGEST, - now=NOW, - fetch=fetch, - verify_attestation=verify, - registry_value=( - registry if registry is not None else build_registry(admissions, remote or {}) - ), - ) + with mock.patch.object( + lifecycle, "evidence_registry", legacy_registry_adapter() + ): + result = lifecycle.validate( + load_policy(), + admissions, + repository_lifecycle, + surface_lifecycle, + policy_sha256=POLICY_DIGEST, + now=NOW, + fetch=fetch, + verify_attestation=verify, + registry_value=( + registry + if registry is not None + else build_registry(admissions, remote or {}) + ), + ) return result @@ -799,13 +897,14 @@ def test_target_claim_scope_mismatch_is_refused(self) -> None: ): validate_case(admissions, remote, repositories=["openadapt-flow"]) - def test_signer_provenance_domain_mismatch_is_refused(self) -> None: + def test_policy_cannot_carry_a_substitute_summary_authority(self) -> None: policy = load_policy() + policy["summary_authority"] = copy.deepcopy(lifecycle.EXPECTED_AUTHORITY) policy["summary_authority"]["signer_provenance_digest_domain"] = ( "OpenAdapt production certificate signer provenance v2\0" ) with self.assertRaisesRegex( - lifecycle.LifecycleError, "signer provenance digest domain" + lifecycle.LifecycleError, "must contain exactly" ): lifecycle.validate( policy, @@ -923,11 +1022,45 @@ def test_history_gate_refuses_revocation_rollback(self) -> None: with self.assertRaisesRegex(lifecycle.LifecycleError, "only append"): lifecycle.validate_append_only_history(previous, current) - def test_pinned_target_map_refuses_subject_substitution(self) -> None: + def test_history_gate_refuses_nonclosed_legacy_document(self) -> None: + previous, _summary, _remote = build_case() + current = copy.deepcopy(previous) + current["unexpected"] = True + with self.assertRaisesRegex(lifecycle.LifecycleError, "must contain exactly"): + lifecycle.validate_history_document( + current, "current Production admission history" + ) + + def test_history_gate_refuses_non_v1_document(self) -> None: + previous, _summary, _remote = build_case() + current = copy.deepcopy(previous) + current["schema_version"] = "openadapt.production-lifecycle-admissions/v2" + with self.assertRaisesRegex(lifecycle.LifecycleError, "not supported"): + lifecycle.validate_history_document( + current, "current Production admission history" + ) + + def test_policy_target_cannot_substitute_its_source_repository(self) -> None: policy = load_policy() next(item for item in policy["targets"] if item["id"] == "flow")[ - "lifecycle_subject" - ] = "openadapt-evals" + "source_repository" + ] = "OpenAdaptAI/openadapt-evals" + with self.assertRaisesRegex( + lifecycle.LifecycleError, "differs from the Production trust contract" + ): + lifecycle.validate( + policy, + empty_admissions(), + *lifecycle.load_lifecycle(), + policy_sha256=POLICY_DIGEST, + now=NOW, + ) + + def test_policy_target_cannot_drop_a_target(self) -> None: + policy = load_policy() + policy["targets"] = [ + item for item in policy["targets"] if item["id"] != "flow" + ] with self.assertRaisesRegex(lifecycle.LifecycleError, "pinned target map"): lifecycle.validate( policy, @@ -937,16 +1070,14 @@ def test_pinned_target_map_refuses_subject_substitution(self) -> None: now=NOW, ) - def test_pinned_attestation_authority_refuses_substitution(self) -> None: + def test_policy_target_cannot_require_an_undefined_artifact_kind(self) -> None: policy = load_policy() - authority = policy["summary_authority"] - authority["repository"] = "OpenAdaptAI/attacker" - authority["certificate_identity"] = ( - "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/OpenAdaptAI/attacker/" - + authority["workflow"] - + "@refs/heads/main" - ) - with self.assertRaisesRegex(lifecycle.LifecycleError, "pinned trust root"): + next(item for item in policy["targets"] if item["id"] == "flow")[ + "required_artifact_kinds" + ] = ["python-sdist", "python-wheel", "unregistered-kind"] + with self.assertRaisesRegex( + lifecycle.LifecycleError, "does not define" + ): lifecycle.validate( policy, empty_admissions(), @@ -960,5 +1091,305 @@ def test_schema_files_are_valid_json(self) -> None: self.assertIsInstance(json.loads(path.read_text(encoding="utf-8")), dict) +class PublishedPolicyTests(unittest.TestCase): + """Bind the validator to the policy document the repository publishes.""" + + def test_published_policy_is_accepted(self) -> None: + self.assertEqual(lifecycle.validate_files(ROOT, now=NOW), {}) + + def test_check_profile_accepts_the_published_repository(self) -> None: + completed = subprocess.run( + [sys.executable, str(ROOT / "scripts" / "check_profile.py")], + capture_output=True, + text=True, + ) + self.assertEqual(completed.returncode, 0, completed.stderr) + + def test_published_policy_declares_the_v2_contract(self) -> None: + policy = load_policy() + self.assertEqual( + policy["schema_version"], "openadapt.production-lifecycle-policy/v2" + ) + self.assertEqual( + lifecycle.POLICY_SCHEMA, "openadapt.production-lifecycle-policy/v2" + ) + self.assertGreaterEqual(policy["revision"], 2) + self.assertNotIn("summary_authority", policy) + self.assertNotIn("maximum_admission_days", policy) + self.assertEqual( + policy["lifecycle_feed_ref"], "refs/heads/production-lifecycle-feed" + ) + + def test_published_policy_matches_its_own_schema_key_set(self) -> None: + schema = json.loads( + ( + ROOT / "schemas" / "production-lifecycle-policy.schema.json" + ).read_text(encoding="utf-8") + ) + self.assertEqual(set(load_policy()), set(schema["required"])) + + def test_retained_ledger_carries_the_v1_policy_digest(self) -> None: + ledger = json.loads( + (ROOT / "production-lifecycle-admissions.json").read_text( + encoding="utf-8" + ) + ) + self.assertEqual(ledger["policy_sha256"], lifecycle.RETAINED_POLICY_SHA256) + self.assertNotEqual( + lifecycle.RETAINED_POLICY_SHA256, + "sha256:" + + hashlib.sha256( + (ROOT / "production-lifecycle-policy.json").read_bytes() + ).hexdigest(), + ) + + +class AdmissionWindowTests(unittest.TestCase): + """The two v2 maximums must be the windows the trust core enforces.""" + + def test_release_maximum_governs_the_retained_expiry(self) -> None: + policy = load_policy() + self.assertEqual(policy["maximum_release_admission_days"], 30) + self.assertEqual(lifecycle.RELEASE_ADMISSION_MAXIMUM_DAYS, 30) + admissions, _summary, remote = build_case( + issued_at="2026-08-18T11:00:00Z", + expires_at="2026-09-17T11:00:00Z", + ) + self.assertEqual( + validate_case(admissions, remote), {"flow": "production:flow:1"} + ) + admissions, _summary, remote = build_case( + issued_at="2026-08-18T11:00:00Z", + expires_at="2026-09-17T11:00:01Z", + ) + with self.assertRaisesRegex( + lifecycle.LifecycleError, "validity window is outside policy" + ): + validate_case(admissions, remote) + + def test_policy_maximums_stay_within_the_historical_bound(self) -> None: + policy = load_policy() + for key in ( + "maximum_release_admission_days", + "maximum_workflow_admission_days", + ): + with self.subTest(key=key): + self.assertIsInstance(policy[key], int) + self.assertGreaterEqual(policy[key], 1) + self.assertLessEqual(policy[key], 30) + self.assertLessEqual( + policy["maximum_workflow_admission_days"], + policy["maximum_release_admission_days"], + ) + + def test_declared_maximums_match_the_enforced_windows(self) -> None: + # production_trust is the v2 trust core. validate_release applies the + # release window to openadapt.qualification-release/v1 and + # validate_qualification_admission applies the workflow window to + # openadapt.qualification-admission/v3. A drift between the declared + # policy numbers and those call sites must fail here. + policy = load_policy() + for function, days in ( + (trust.validate_release, policy["maximum_release_admission_days"]), + ( + trust.validate_qualification_admission, + policy["maximum_workflow_admission_days"], + ), + ): + with self.subTest(function=function.__name__): + source = inspect.getsource(function) + self.assertIn("validate_window(", source) + self.assertIn(f"maximum=timedelta(days={days})", source) + + def test_policy_that_declares_another_maximum_is_refused(self) -> None: + for key in ( + "maximum_release_admission_days", + "maximum_workflow_admission_days", + ): + with self.subTest(key=key): + policy = load_policy() + policy[key] = 3 + with self.assertRaisesRegex( + lifecycle.LifecycleError, "the Production trust core enforces" + ): + lifecycle.validate( + policy, + empty_admissions(), + *lifecycle.load_lifecycle(), + policy_sha256=POLICY_DIGEST, + now=NOW, + ) + + def test_policy_maximum_outside_the_historical_bound_is_refused(self) -> None: + policy = load_policy() + policy["maximum_release_admission_days"] = 31 + with self.assertRaisesRegex( + lifecycle.LifecycleError, "must be between 1 and 30" + ): + lifecycle.validate( + policy, + empty_admissions(), + *lifecycle.load_lifecycle(), + policy_sha256=POLICY_DIGEST, + now=NOW, + ) + + +class CertificateIdentityBindingTests(unittest.TestCase): + """The v2 policy dropped the summary authority. The binding must remain. + + Removing summary_authority from the validator without a replacement would + leave it reporting success while nothing checks who signed the acceptance + evidence. These tests fail if that happens. + """ + + def active_authority(self) -> dict: + admissions, _summary, remote = build_case() + captured: list[dict] = [] + self.assertEqual( + validate_case(admissions, remote, authority_sink=captured), + {"flow": "production:flow:1"}, + ) + self.assertEqual(len(captured), 1) + return captured[0] + + def test_attestation_verifier_receives_the_exact_certificate_identity( + self, + ) -> None: + authority = self.active_authority() + # Literal values, not lifecycle.EXPECTED_AUTHORITY: a refactor that + # empties or rewrites the pinned trust root must fail here too. + self.assertEqual( + authority["certificate_identity"], + "/OpenAdaptAI/openadapt-evals/.github/workflows/" + "production-lifecycle-evidence.yml@refs/heads/main", + ) + self.assertEqual( + authority["oidc_issuer"], "https://token.actions.githubusercontent.com" + ) + self.assertEqual(authority["repository"], "OpenAdaptAI/openadapt-evals") + self.assertEqual(authority["source_ref"], "refs/heads/main") + + def test_certificate_identity_is_derived_from_repository_and_workflow( + self, + ) -> None: + authority = self.active_authority() + self.assertEqual( + authority["certificate_identity"], + "/" + + authority["repository"] + + "/" + + authority["workflow"] + + "@" + + authority["source_ref"], + ) + + def test_published_policy_no_longer_carries_the_authority(self) -> None: + policy = load_policy() + self.assertNotIn("summary_authority", json.dumps(policy)) + # The authority is absent from the policy and present at the verifier. + self.assertTrue(self.active_authority()["certificate_identity"]) + + def test_refused_attestation_removes_derived_production(self) -> None: + admissions, _summary, remote = build_case() + with self.assertRaisesRegex(lifecycle.LifecycleError, "attestation"): + validate_case(admissions, remote, attestation_valid=False) + + def test_verifier_command_pins_the_identity_flags(self) -> None: + recorded: list[list[str]] = [] + + class Completed: + returncode = 0 + stdout = "not json" + stderr = "" + + def fake_run(command, **_kwargs): + recorded.append(list(command)) + return Completed() + + with mock.patch.object(lifecycle.subprocess, "run", fake_run): + with self.assertRaises(lifecycle.LifecycleError): + lifecycle._verify_attestation( + b"{}", b"{}", lifecycle.EXPECTED_AUTHORITY, "1" * 40 + ) + self.assertEqual(len(recorded), 1) + command = recorded[0] + self.assertIn("--cert-identity", command) + self.assertEqual( + command[command.index("--cert-identity") + 1], + "/OpenAdaptAI/openadapt-evals/.github/workflows/" + "production-lifecycle-evidence.yml@refs/heads/main", + ) + self.assertIn("--cert-oidc-issuer", command) + self.assertEqual( + command[command.index("--cert-oidc-issuer") + 1], + "https://token.actions.githubusercontent.com", + ) + self.assertIn("--deny-self-hosted-runners", command) + + def test_pinned_authority_checks_still_bite(self) -> None: + # v1 checked the authority where the policy declared it. v2 removed the + # declaration, so the same checks now run against the pinned trust root. + # Each mutation must still be refused. + cases = ( + ("oidc_issuer", "https://token.actions.example.com", "OIDC issuer"), + ("source_ref", "refs/heads/release", "refs/heads/main"), + ( + "certificate_identity", + "/OpenAdaptAI/attacker/w.yml@refs/heads/main", + "certificate identity is not exact", + ), + ( + "summary_schema_version", + "openadapt.production-lifecycle-evidence-summary/v2", + "summary authority schema", + ), + ( + "evidence_manifest_schema_version", + "openadapt.production-acceptance/v2", + "evidence manifest schema", + ), + ( + "release_identity_schema_version", + "openadapt.monotonic-production-release/v2", + "release identity schema", + ), + ("production_channel", "staging", "channel is not production"), + ( + "signer_provenance_digest_domain", + "OpenAdapt production certificate signer provenance v2\0", + "signer provenance digest domain", + ), + ) + for key, value, message in cases: + with self.subTest(key=key): + authority = copy.deepcopy(lifecycle.EXPECTED_AUTHORITY) + authority[key] = value + with self.assertRaisesRegex(lifecycle.LifecycleError, message): + lifecycle._validate_summary_authority(authority) + + def test_pinned_authority_cannot_gain_or_lose_a_field(self) -> None: + authority = copy.deepcopy(lifecycle.EXPECTED_AUTHORITY) + authority["extra"] = "value" + with self.assertRaisesRegex( + lifecycle.LifecycleError, "must contain exactly" + ): + lifecycle._validate_summary_authority(authority) + authority = copy.deepcopy(lifecycle.EXPECTED_AUTHORITY) + del authority["certificate_identity"] + with self.assertRaisesRegex( + lifecycle.LifecycleError, "must contain exactly" + ): + lifecycle._validate_summary_authority(authority) + + def test_published_authority_passes_every_pinned_check(self) -> None: + self.assertEqual( + lifecycle._validate_summary_authority( + copy.deepcopy(lifecycle.EXPECTED_AUTHORITY) + ), + lifecycle.EXPECTED_AUTHORITY, + ) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_production_trust.py b/tests/test_production_trust.py new file mode 100644 index 0000000..871ef19 --- /dev/null +++ b/tests/test_production_trust.py @@ -0,0 +1,942 @@ +"""Fail-closed tests for release, workflow, and lifecycle trust contracts.""" + +from __future__ import annotations + +import base64 +import copy +import hashlib +import sys +import tempfile +import unittest +from datetime import datetime, timezone +from pathlib import Path +from unittest import mock + +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "scripts")) + +import production_trust as trust +import validate_evidence_registry as registry + + +def sha(character: str) -> str: + return "sha256:" + character * 64 + + +def reference(kind: str, object_character: str, *, subject: str | None = None) -> dict: + schema, media = registry.OBJECT_KIND_CONTRACTS[kind] + object_sha = sha(object_character) + digest_hex = object_sha.removeprefix("sha256:") + entry = { + "kind": kind, + "object_schema_version": schema, + "object_path": ( + f"production-evidence/objects/sha256/{digest_hex[:2]}/" + f"{digest_hex}.{kind}.json" + ), + "object_sha256": object_sha, + "size_bytes": 100, + "object_media_type": media, + "semantic_identity_sha256": sha("9"), + "subject_sha256": subject, + } + return { + "schema_version": registry.REFERENCE_SCHEMA, + "repository": registry.REPOSITORY, + "repository_id": registry.REPOSITORY_ID, + "repository_owner_id": registry.REPOSITORY_OWNER_ID, + "registry_source_commit": "a" * 40, + "registry_revision": 3, + "registry_head_sha256": sha("8"), + "registry_entry_sha256": registry.entry_digest(entry), + **entry, + } + + +def pair(kind: str, regular_character: str, bundle_character: str) -> tuple[dict, dict]: + regular = reference(kind, regular_character) + bundle = reference( + f"{kind}-sigstore-bundle", + bundle_character, + subject=regular["object_sha256"], + ) + return regular, bundle + + +def campaign_summary() -> dict: + result = {} + for campaign_class in trust.CAMPAIGN_CLASSES: + counts = {field: 0 for field in trust.CAMPAIGN_COUNT_FIELDS} + counts.update( + task_condition_cell_count=1, + minimum_trials_per_cell=3, + observed_trial_count=3, + ) + if campaign_class == "uncertain_delivery": + counts["reconciliation_required_count"] = 3 + if campaign_class == "declared_attended": + counts["authenticated_bound_decision_count"] = 3 + counts["live_target_revalidation_count"] = 3 + if campaign_class == "governed_repair": + for field in ( + "policy_approved_repair_count", + "approved_repair_count", + "retained_repair_evidence_count", + "live_target_revalidation_count", + ): + counts[field] = 3 + result[campaign_class] = counts + return result + + +RECEIPT_NOW = datetime(2026, 8, 27, 12, 30, tzinfo=timezone.utc) + + +def _resign_receipt(value: dict, private_key: Ed25519PrivateKey) -> None: + value["signing_statement"] = trust.signing_statement( + value, + object_schema_version=( + "openadapt.qualification-evidence-decision-receipt/v1" + ), + signature_domain=trust.DECISION_RECEIPT_SIGNATURE_DOMAIN, + ) + value["signature"] = base64.b64encode( + private_key.sign(trust.canonical(value["signing_statement"]) + b"\n") + ).decode("ascii") + + +def decision_receipt_fixture( + private_key: Ed25519PrivateKey | None = None, +) -> tuple[dict, dict, Ed25519PrivateKey]: + key = private_key or Ed25519PrivateKey.from_private_bytes(bytes(range(1, 33))) + public_key = key.public_key().public_bytes( + serialization.Encoding.Raw, + serialization.PublicFormat.Raw, + ) + spki = bytes.fromhex("302a300506032b6570032100") + public_key + key_id = "qa-ed25519-" + hashlib.sha256(public_key).hexdigest()[:16] + signer = { + "algorithm": "ed25519", + "key_id": key_id, + "public_key": base64.urlsafe_b64encode(public_key).decode().rstrip("="), + "public_key_spki_der_base64": base64.b64encode(spki).decode(), + "public_key_sha256": "sha256:" + hashlib.sha256(spki).hexdigest(), + "statement_schema_versions": [ + "openadapt.qualification-evidence-signing-statement/v1" + ], + "allowed_usages": ["qualification-evidence-decision-receipt"], + "allowed_workflows": [ + ( + "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/OpenAdaptAI/openadapt-internal/" + ".github/workflows/" + "issue-private-qualification-evidence-decision.yml" + "@refs/heads/main" + ) + ], + "allowed_ref_prefixes": ["refs/heads/main"], + "status": "active", + "revoked_at": None, + } + signer_registry = { + "schema_version": "openadapt.qualification-signer-registry/v2", + "revision": 1, + "generated_at": "2026-08-27T12:00:00Z", + "expires_at": "2026-08-29T12:00:00Z", + "signers": [signer], + } + receipt = { + "schema_version": ( + "openadapt.qualification-evidence-decision-receipt/v1" + ), + "decision_identity_sha256": sha("a"), + "decision_revision": 1, + "decision_commitment_sha256": sha("b"), + "evidence_manifest_sha256": sha("c"), + "evidence_manifest_readback_sha256": sha("d"), + "campaign_artifact_sha256": sha("e"), + "organization_id_sha256": sha("f"), + "workflow_id_sha256": sha("1"), + "workflow_version_id_sha256": sha("2"), + "bundle_version": "3.0.0-rc.1", + "bundle_sha256": sha("3"), + "admitted_runtime_sha256": sha("4"), + "application_contract_sha256": sha("5"), + "environment_contract_sha256": sha("6"), + "input_contract_sha256": sha("7"), + "action_contract_sha256": sha("8"), + "identity_contract_sha256": sha("9"), + "effect_contract_sha256": sha("a"), + "policy_contract_sha256": sha("b"), + "evidence_authority_contract_sha256": sha("c"), + "campaign_permit_sha256": sha("d"), + "signer_registry_sha256": registry.signer_registry_identity_digest( + signer_registry + ), + "revocation_state_sha256": sha("e"), + "entity_class": "record", + "campaign_summary": { + "schema_version": ( + "openadapt.qualification-evidence-decision-campaign-summary/v1" + ), + "minimum_trials_per_task_condition": 3, + "task_count": 1, + "classes": campaign_summary(), + }, + "verdict": "ADMIT", + "issued_at": "2026-08-27T12:00:00Z", + "not_before": "2026-08-27T12:00:00Z", + "expires_at": "2026-08-28T12:00:00Z", + "issuer_key_id": key_id, + "algorithm": "ed25519", + "signing_statement": None, + "signature": "", + "issuer": { + "repository": "OpenAdaptAI/openadapt-internal", + "repository_id": "1170060695", + "repository_owner_id": "132681217", + "workflow": ( + ".github/workflows/" + "issue-private-qualification-evidence-decision.yml" + ), + "ref": "refs/heads/main", + "source_commit": "a" * 40, + "environment": "private-qualification-evidence-decision", + }, + } + _resign_receipt(receipt, key) + return receipt, signer_registry, key + + +def rulesets() -> list[dict]: + common = { + "schema_version": "openadapt.production-release-tag-ruleset/v1", + "repository": "OpenAdaptAI/openadapt-capture", + "repository_id": "1115283835", + "target": "tag", + "enforcement": "active", + "conditions": {"ref_name": {"include": ["refs/tags/v*"], "exclude": []}}, + } + return [ + { + **common, + "name": "OpenAdapt policy: release tag creation", + "role": "creation_authority", + "ruleset_id": "10", + "bypass_actors": [ + { + "actor_id": "4730708", + "actor_type": "Integration", + "bypass_mode": "always", + } + ], + "rules": [{"type": "creation"}], + }, + { + **common, + "name": "OpenAdapt policy: immutable release tags", + "role": "immutability", + "ruleset_id": "11", + "bypass_actors": [], + "rules": [ + {"type": "deletion"}, + {"type": "non_fast_forward"}, + { + "type": "update", + "parameters": {"update_allows_fetch_and_merge": False}, + }, + ], + }, + ] + + +def release_admission() -> dict: + artifacts = [ + { + "name": "openadapt-capture-extension-1.0.0.zip", + "kind": "chrome-extension-zip", + "sha256": sha("1"), + "size_bytes": 4, + "media_type": "application/zip", + "publish_destinations": ["github-release"], + }, + { + "name": "openadapt_capture-1.0.0.tar.gz", + "kind": "python-sdist", + "sha256": sha("2"), + "size_bytes": 4, + "media_type": "application/gzip", + "publish_destinations": ["github-release", "pypi"], + }, + { + "name": "openadapt_capture-1.0.0-py3-none-any.whl", + "kind": "python-wheel", + "sha256": sha("3"), + "size_bytes": 4, + "media_type": "application/zip", + "publish_destinations": ["github-release", "pypi"], + }, + { + "name": "openadapt-capture-1.0.0.spdx.json", + "kind": "spdx-sbom", + "sha256": sha("4"), + "size_bytes": 4, + "media_type": "application/spdx+json", + "publish_destinations": ["github-release"], + }, + ] + normalized_rulesets = rulesets() + staging = { + "schema_version": "openadapt.production-release-staging-evidence/v1", + "repository": "OpenAdaptAI/openadapt-capture", + "repository_id": "1115283835", + "draft_release_id": "20", + "tag": "v1.0.0", + "target_commitish": "b" * 40, + "draft": True, + "prerelease": False, + "release_app_id": "4730708", + "release_app_installation_id": "156835568", + "release_app_bot_user_id": "321543906", + "release_author_login": "openadapt-release[bot]", + "assets": [ + { + "asset_id": str(30 + index), + "name": artifact["name"], + "sha256": artifact["sha256"], + "size_bytes": artifact["size_bytes"], + "kind": artifact["kind"], + "media_type": artifact["media_type"], + "publish_destinations": artifact["publish_destinations"], + "uploader_id": "321543906", + "uploader_login": "openadapt-release[bot]", + } + for index, artifact in enumerate(sorted(artifacts, key=lambda item: item["name"])) + ], + "immutable_releases": {"enabled": True, "enforced_by_owner": False}, + "immutable_releases_sha256": trust.digest_bytes( + trust.IMMUTABLE_RELEASES_DOMAIN, + {"enabled": True, "enforced_by_owner": False}, + ), + "tag_rulesets": normalized_rulesets, + "tag_rulesets_sha256": trust.digest_bytes( + trust.TAG_RULESETS_DOMAIN, normalized_rulesets + ), + "tag_ref_state": {"ref": "refs/tags/v1.0.0", "exists": False}, + "tag_ref_state_sha256": trust.digest_bytes( + trust.TAG_REF_STATE_DOMAIN, + {"ref": "refs/tags/v1.0.0", "exists": False}, + ), + "observed_at": "2026-08-27T12:00:00Z", + } + release = { + "schema_version": "openadapt.production-release-candidate/v1", + "kind": "package", + "source_repository": "OpenAdaptAI/openadapt-capture", + "source_repository_id": "1115283835", + "source_commit": "b" * 40, + "version": "1.0.0", + "tag": "v1.0.0", + "deployment_id": None, + "deployment_sha256": None, + "artifacts": artifacts, + } + summary, summary_bundle = pair("production-acceptance-summary", "2", "3") + value = { + "schema_version": "openadapt.qualification-release/v1", + "admission_id_sha256": sha("0"), + "target": "capture", + "verdict": "accepted", + "claim_scope": "production_capture", + "release_identity": { + "schema_version": "openadapt.monotonic-production-release/v1", + "channel": "production", + "sequence": 1, + "previous_admission_sha256": None, + }, + "release": release, + "release_sha256": trust.digest_bytes( + trust.RELEASE_DOMAIN, + { + "target": "capture", + "claim_scope": "production_capture", + "release": release, + }, + ), + "artifact_inventory_sha256": trust.digest_bytes( + trust.ARTIFACT_INVENTORY_DOMAIN, + { + "target": "capture", + "claim_scope": "production_capture", + "artifacts": artifacts, + }, + ), + "publication_staging": staging, + "publication_staging_sha256": trust.staging_digest(staging), + "production_acceptance_summary_reference": summary, + "production_acceptance_summary_bundle_reference": summary_bundle, + "authority_state_sha256": sha("4"), + "revocation_state_sha256": sha("5"), + "signer_registry_sha256": sha("6"), + "publication_policy_sha256": sha("7"), + "issued_at": "2026-08-27T12:00:00Z", + "not_before": "2026-08-27T12:00:00Z", + "expires_at": "2026-09-03T12:00:00Z", + "issuer": { + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "workflow": ".github/workflows/issue-production-release-admission.yml", + "ref": "refs/heads/main", + "source_commit": "c" * 40, + "environment": "production-release-admission", + }, + } + projection = dict(value) + projection.pop("admission_id_sha256") + value["admission_id_sha256"] = trust.digest_bytes( + trust.RELEASE_ADMISSION_DOMAIN, projection + ) + return value + + +def cloud_handoff() -> dict: + authorization, authorization_bundle = pair( + "production-cloud-deploy-authorization", "a", "b" + ) + source_commit = "d" * 40 + source_commitment = "sha256:" + hashlib.sha256( + trust.CLOUD_SOURCE_COMMIT_DOMAIN + source_commit.encode("ascii") + ).hexdigest() + request = { + "schema_version": "openadapt.production-secret-source-proof-request/v2", + "authorization_sha256": "1" * 64, + "cloud_source_commit": source_commit, + "expected_live_attestation_sha256": "2" * 64, + "expires_at": "2026-08-27T12:05:00Z", + "issued_at": "2026-08-27T12:00:00Z", + "nonce": "A" * 43, + "production_origin": "https://app.openadapt.ai", + "profile_commit": "e" * 40, + "profile_environment": "production-cloud-deploy", + "profile_repository": "OpenAdaptAI/.github", + "profile_workflow_ref": ( + "OpenAdaptAI/.github/.github/workflows/" + "production-cloud-deploy.yml@refs/heads/main" + ), + "run_attempt": "1", + "run_id": "100", + "site_id": "ccf1a48a-d934-48ea-a9a8-36cbddf27cb5", + "source_names": list(trust.CLOUD_LIVE_SOURCE_NAMES), + } + request_sha_raw = hashlib.sha256( + trust.CLOUD_SOURCE_PROOF_REQUEST_DOMAIN + trust.canonical(request) + ).hexdigest() + request_sha = "sha256:" + request_sha_raw + public_values = {trust.CLOUD_PUBLIC_SOURCE_NAME: "answer_v1"} + response = { + "schema_version": "openadapt.production-secret-source-proof-response/v2", + "request_sha256": request_sha_raw, + "authorization_sha256": request["authorization_sha256"], + "live_attestation_sha256": "4" * 64, + "live_deployment": { + "build_id": sha("5"), + "source_commit": source_commit, + }, + "oidc_identity_sha256": "6" * 64, + "production_origin": request["production_origin"], + "proofs": { + name: hashlib.sha256(name.encode("ascii")).hexdigest() + for name in trust.CLOUD_SECRET_SOURCE_NAMES + }, + "public_values": public_values, + "site_id": request["site_id"], + } + provider_idempotency_key = "cloud-deploy:" + hashlib.sha256( + trust.CLOUD_PROVIDER_IDEMPOTENCY_DOMAIN + + trust.canonical( + { + "authorization_sha256": "sha256:" + request["authorization_sha256"], + "source_proof_request_sha256": request_sha, + "profile_run_id": request["run_id"], + "profile_run_attempt": request["run_attempt"], + } + ) + ).hexdigest() + value = { + "schema_version": "openadapt.production-cloud-deployment-result/v1", + "handoff_id_sha256": sha("0"), + "authorization_reference": authorization, + "authorization_bundle_reference": authorization_bundle, + "authorization_sha256": "sha256:" + request["authorization_sha256"], + "cloud_source_commitment_sha256": source_commitment, + "profile_commit": request["profile_commit"], + "profile_run_id": request["run_id"], + "profile_run_attempt": request["run_attempt"], + "source_proof_request": request, + "source_proof_request_sha256": request_sha, + "source_proof_response": response, + "source_proof_response_sha256": "sha256:" + + hashlib.sha256(trust.canonical(response)).hexdigest(), + "reviewed_public_values_sha256": trust.digest_bytes( + trust.CLOUD_PUBLIC_VALUES_DOMAIN, public_values + ), + "expected_live_attestation_sha256": "sha256:" + + request["expected_live_attestation_sha256"], + "live_attestation_sha256": "sha256:" + + response["live_attestation_sha256"], + "provider_idempotency_key": provider_idempotency_key, + "audience": "openadapt-private-cloud-production-deploy", + "signer_registry_sha256": sha("8"), + "revocation_state_sha256": sha("9"), + "verdict": "accepted", + "issued_at": request["issued_at"], + "not_before": request["issued_at"], + "expires_at": request["expires_at"], + "issuer": { + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "workflow": ".github/workflows/production-cloud-deploy.yml", + "ref": "refs/heads/main", + "source_commit": "f" * 40, + "environment": "production-cloud-deploy", + }, + } + projection = dict(value) + projection.pop("handoff_id_sha256") + value["handoff_id_sha256"] = trust.digest_bytes( + trust.CLOUD_HANDOFF_DOMAIN, projection + ) + return value + + +def rotation_fixture() -> tuple[dict, dict, dict, dict]: + signer_registry = { + "schema_version": registry.SIGNER_REGISTRY_SCHEMA, + "revision": 4, + "generated_at": "2026-08-27T11:00:00Z", + "expires_at": "2026-08-27T14:00:00Z", + "signers": [], + } + signer_raw = registry.canonical(signer_registry) + b"\n" + signer_sha = "sha256:" + hashlib.sha256(signer_raw).hexdigest() + signer_hex = signer_sha.removeprefix("sha256:") + pointer = { + "schema_version": registry.SIGNER_POINTER_SCHEMA, + "object_path": ( + "production-evidence/signer-registries/sha256/" + f"{signer_hex[:2]}/{signer_hex}.qualification-signer-registry.json" + ), + "object_sha256": signer_sha, + "registry_identity_sha256": registry.signer_registry_identity_digest( + signer_registry + ), + "registry_revision": 4, + } + common = { + "registry_source_commit": "a" * 40, + "registry_revision": 9, + "registry_head_sha256": sha("2"), + "signer_registry": pointer, + } + older = { + **common, + "checkpoint_id_sha256": sha("3"), + "checkpoint_revision": 8, + "previous_checkpoint_sha256": sha("4"), + "generated_at": "2026-08-27T11:15:00Z", + "not_before": "2026-08-27T11:30:00Z", + "expires_at": "2026-08-27T12:30:00Z", + "signature": "older-wrapper", + } + older_sha = "sha256:" + hashlib.sha256( + trust.canonical(older) + b"\n" + ).hexdigest() + newer = { + **common, + "checkpoint_id_sha256": sha("5"), + "checkpoint_revision": 9, + "previous_checkpoint_sha256": older_sha, + "generated_at": "2026-08-27T11:45:00Z", + "not_before": "2026-08-27T12:30:00Z", + "expires_at": "2026-08-27T13:30:00Z", + "signature": "newer-wrapper", + } + + def checkpoint_reference(checkpoint: dict) -> dict: + return { + "object_sha256": "sha256:" + hashlib.sha256( + trust.canonical(checkpoint) + b"\n" + ).hexdigest(), + "semantic_identity_sha256": checkpoint["checkpoint_id_sha256"], + "registry_source_commit": checkpoint["registry_source_commit"], + "registry_revision": checkpoint["registry_revision"], + "registry_head_sha256": checkpoint["registry_head_sha256"], + } + + feed = { + **common, + "feed_revision": 12, + "generated_at": "2026-08-27T12:00:00Z", + "expires_at": "2026-08-27T13:00:00Z", + "checkpoints": [ + { + "checkpoint_reference": checkpoint_reference(newer), + "checkpoint_bundle_reference": {}, + }, + { + "checkpoint_reference": checkpoint_reference(older), + "checkpoint_bundle_reference": {}, + }, + ], + } + return feed, newer, older, signer_registry + + +class ProductionTrustTests(unittest.TestCase): + def test_decision_receipt_requires_active_registered_signature(self) -> None: + receipt, signer_registry, key = decision_receipt_fixture() + self.assertEqual( + trust.validate_receipt( + receipt, signer_registry=signer_registry, now=RECEIPT_NOW + ), + receipt, + ) + + zero_signature = copy.deepcopy(receipt) + zero_signature["signature"] = base64.b64encode(bytes(64)).decode("ascii") + with self.assertRaisesRegex(trust.TrustError, "verification failed"): + trust.validate_receipt( + zero_signature, + signer_registry=signer_registry, + now=RECEIPT_NOW, + ) + + changed_payload = copy.deepcopy(receipt) + changed_payload["bundle_sha256"] = sha("f") + with self.assertRaisesRegex(trust.TrustError, "signing statement differs"): + trust.validate_receipt( + changed_payload, + signer_registry=signer_registry, + now=RECEIPT_NOW, + ) + + _, wrong_registry, _ = decision_receipt_fixture( + Ed25519PrivateKey.from_private_bytes(bytes(range(33, 65))) + ) + wrong_key = copy.deepcopy(receipt) + wrong_key["issuer_key_id"] = wrong_registry["signers"][0]["key_id"] + wrong_key["signer_registry_sha256"] = ( + registry.signer_registry_identity_digest(wrong_registry) + ) + _resign_receipt(wrong_key, key) + with self.assertRaisesRegex(trust.TrustError, "verification failed"): + trust.validate_receipt( + wrong_key, signer_registry=wrong_registry, now=RECEIPT_NOW + ) + + with self.assertRaisesRegex(trust.TrustError, "not active"): + trust.validate_receipt( + receipt, + signer_registry=signer_registry, + now=datetime(2026, 8, 28, 12, 0, tzinfo=timezone.utc), + ) + + revoked_registry = copy.deepcopy(signer_registry) + revoked_registry["signers"][0]["status"] = "revoked" + revoked_registry["signers"][0]["revoked_at"] = ( + "2026-08-27T12:15:00Z" + ) + revoked = copy.deepcopy(receipt) + revoked["signer_registry_sha256"] = ( + registry.signer_registry_identity_digest(revoked_registry) + ) + _resign_receipt(revoked, key) + with self.assertRaisesRegex(trust.TrustError, "not an active registered"): + trust.validate_receipt( + revoked, signer_registry=revoked_registry, now=RECEIPT_NOW + ) + + def test_feed_expiry_boundary_is_exclusive(self) -> None: + checkpoint_reference, checkpoint_bundle_reference = pair( + "production-lifecycle-checkpoint", "1", "2" + ) + feed = { + "schema_version": "openadapt.production-lifecycle-feed/v1", + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "ref": "refs/heads/production-lifecycle-feed", + "feed_revision": 1, + "generated_at": "2026-08-27T12:00:00Z", + "expires_at": "2026-08-27T13:00:00Z", + "registry_source_commit": "a" * 40, + "registry_revision": 3, + "registry_head_sha256": sha("8"), + "signer_registry": {}, + "checkpoints": [ + { + "checkpoint_reference": checkpoint_reference, + "checkpoint_bundle_reference": checkpoint_bundle_reference, + } + ], + } + trust.validate_feed( + feed, + now=trust.require_timestamp("2026-08-27T12:59:59Z", "test time"), + ) + with self.assertRaisesRegex(trust.TrustError, "not current"): + trust.validate_feed( + feed, + now=trust.require_timestamp( + "2026-08-27T13:00:00Z", "test time" + ), + ) + + def test_future_checkpoint_rotation_has_one_exact_boundary(self) -> None: + feed, newer, older, signer_registry = rotation_fixture() + with ( + mock.patch.object(trust, "validate_feed", return_value=feed), + mock.patch.object(trust, "validate_checkpoint", side_effect=lambda item, **_: item), + mock.patch.object( + registry, "validate_signer_registry", return_value=signer_registry + ), + ): + trust.validate_feed_expiry_containment( + feed, + checkpoints=[newer, older], + signer_registry=signer_registry, + now=trust.require_timestamp( + "2026-08-27T12:29:59Z", "test time" + ), + ) + trust.validate_feed_expiry_containment( + feed, + checkpoints=[newer, older], + signer_registry=signer_registry, + now=trust.require_timestamp( + "2026-08-27T12:30:00Z", "test time" + ), + ) + + def test_checkpoint_rotation_refuses_overlap_gap_fork_and_reorder(self) -> None: + for defect in ("overlap", "gap", "fork", "skip", "reorder"): + with self.subTest(defect=defect): + feed, newer, older, signer_registry = rotation_fixture() + checkpoints = [newer, older] + if defect == "overlap": + older["expires_at"] = "2026-08-27T12:30:01Z" + elif defect == "gap": + older["expires_at"] = "2026-08-27T12:29:59Z" + elif defect == "fork": + newer["previous_checkpoint_sha256"] = sha("6") + elif defect == "skip": + newer["checkpoint_revision"] = 10 + else: + checkpoints.reverse() + feed["checkpoints"].reverse() + for pair_value, checkpoint in zip( + feed["checkpoints"], checkpoints, strict=True + ): + pair_value["checkpoint_reference"]["object_sha256"] = ( + "sha256:" + hashlib.sha256( + trust.canonical(checkpoint) + b"\n" + ).hexdigest() + ) + with ( + mock.patch.object(trust, "validate_feed", return_value=feed), + mock.patch.object( + trust, + "validate_checkpoint", + side_effect=lambda item, **_: item, + ), + mock.patch.object( + registry, + "validate_signer_registry", + return_value=signer_registry, + ), + self.assertRaises(trust.TrustError), + ): + trust.validate_feed_expiry_containment( + feed, + checkpoints=checkpoints, + signer_registry=signer_registry, + ) + + def test_checkpoint_reference_binds_the_exact_signed_wrapper(self) -> None: + feed, newer, older, signer_registry = rotation_fixture() + newer["signature"] = "alternate-wrapper" + with ( + mock.patch.object(trust, "validate_feed", return_value=feed), + mock.patch.object(trust, "validate_checkpoint", side_effect=lambda item, **_: item), + mock.patch.object( + registry, "validate_signer_registry", return_value=signer_registry + ), + self.assertRaisesRegex(trust.TrustError, "bytes differ"), + ): + trust.validate_feed_expiry_containment( + feed, + checkpoints=[newer, older], + signer_registry=signer_registry, + ) + + def test_feed_transition_refuses_rollback_or_skipped_history(self) -> None: + previous, _newer, _older, _ = rotation_fixture() + previous["checkpoints"] = [previous["checkpoints"][1]] + previous["feed_revision"] = 11 + previous["generated_at"] = "2026-08-27T11:50:00Z" + current, _, _, _ = rotation_fixture() + with mock.patch.object(trust, "validate_feed", side_effect=lambda item: item): + trust.validate_feed_transition(previous, current) + rollback = copy.deepcopy(current) + rollback["checkpoints"] = [rollback["checkpoints"][1]] + rollback["feed_revision"] = 13 + rollback["generated_at"] = "2026-08-27T12:10:00Z" + with self.assertRaisesRegex(trust.TrustError, "skips or forks"): + trust.validate_feed_transition(current, rollback) + skipped_revision = copy.deepcopy(current) + skipped_revision["feed_revision"] = 13 + with self.assertRaisesRegex(trust.TrustError, "revision"): + trust.validate_feed_transition(previous, skipped_revision) + + def test_feed_expiry_cannot_outlive_checkpoint_or_signer_registry(self) -> None: + signer_registry = { + "schema_version": registry.SIGNER_REGISTRY_SCHEMA, + "revision": 4, + "generated_at": "2026-08-27T11:00:00Z", + "expires_at": "2026-08-27T13:00:00Z", + "signers": [], + } + signer_raw = registry.canonical(signer_registry) + b"\n" + signer_sha = "sha256:" + hashlib.sha256(signer_raw).hexdigest() + signer_hex = signer_sha.removeprefix("sha256:") + pointer = { + "schema_version": registry.SIGNER_POINTER_SCHEMA, + "object_path": ( + "production-evidence/signer-registries/sha256/" + f"{signer_hex[:2]}/{signer_hex}.qualification-signer-registry.json" + ), + "object_sha256": signer_sha, + "registry_identity_sha256": registry.signer_registry_identity_digest( + signer_registry + ), + "registry_revision": 4, + } + checkpoint = { + "checkpoint_id_sha256": sha("1"), + "checkpoint_revision": 8, + "previous_checkpoint_sha256": None, + "registry_source_commit": "a" * 40, + "registry_revision": 9, + "registry_head_sha256": sha("2"), + "signer_registry": pointer, + "generated_at": "2026-08-27T11:15:00Z", + "not_before": "2026-08-27T11:30:00Z", + "expires_at": "2026-08-27T12:30:00Z", + } + checkpoint_sha = "sha256:" + hashlib.sha256( + trust.canonical(checkpoint) + b"\n" + ).hexdigest() + feed = { + "generated_at": "2026-08-27T12:00:00Z", + "expires_at": "2026-08-27T12:30:00Z", + "registry_source_commit": checkpoint["registry_source_commit"], + "registry_revision": checkpoint["registry_revision"], + "registry_head_sha256": checkpoint["registry_head_sha256"], + "signer_registry": pointer, + "checkpoints": [ + { + "checkpoint_reference": { + "object_sha256": checkpoint_sha, + "registry_source_commit": checkpoint[ + "registry_source_commit" + ], + "registry_revision": checkpoint["registry_revision"], + "registry_head_sha256": checkpoint[ + "registry_head_sha256" + ], + }, + "checkpoint_bundle_reference": {}, + } + ], + } + with ( + mock.patch.object(trust, "validate_feed", return_value=feed), + mock.patch.object( + trust, "validate_checkpoint", return_value=checkpoint + ), + mock.patch.object( + registry, "validate_signer_registry", return_value=signer_registry + ), + ): + trust.validate_feed_expiry_containment( + feed, checkpoints=[checkpoint], signer_registry=signer_registry + ) + feed["expires_at"] = "2026-08-27T12:30:01Z" + with self.assertRaisesRegex(trust.TrustError, "checkpoint"): + trust.validate_feed_expiry_containment( + feed, + checkpoints=[checkpoint], + signer_registry=signer_registry, + ) + + def test_cloud_handoff_binds_remote_safe_request_and_response(self) -> None: + value = cloud_handoff() + trust.validate_cloud_deployment_handoff(value) + invalid = copy.deepcopy(value) + invalid["source_proof_response"]["proofs"][ + "BUSINESS_DECISION_ANSWER_HMAC_KEY" + ] = "a" * 64 + with self.assertRaisesRegex(trust.TrustError, "response digest"): + trust.validate_cloud_deployment_handoff(invalid) + + def test_campaign_requires_reconciliation_without_replay(self) -> None: + value = campaign_summary() + trust.validate_campaign_summary(value) + value["uncertain_delivery"]["replay_dispatch_count"] = 1 + with self.assertRaisesRegex(trust.TrustError, "replay"): + trust.validate_campaign_summary(value) + + def test_release_binds_exact_draft_assets_and_two_rulesets(self) -> None: + value = release_admission() + trust.validate_release(value) + invalid = copy.deepcopy(value) + invalid["publication_staging"]["tag_rulesets"][1]["bypass_actors"] = [ + { + "actor_id": "4730708", + "actor_type": "Integration", + "bypass_mode": "always", + } + ] + with self.assertRaisesRegex(trust.TrustError, "bypass"): + trust.validate_release(invalid) + + def test_local_artifact_inventory_refuses_extra_file(self) -> None: + value = release_admission() + artifacts = value["release"]["artifacts"] + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + for artifact in artifacts: + path = root / artifact["name"] + path.write_bytes(b"test") + artifact["sha256"] = ( + "sha256:" + hashlib.sha256(b"test").hexdigest() + ) + trust.verify_local_artifacts(root, artifacts) + (root / "SHA256SUMS").write_text("not admitted") + with self.assertRaisesRegex(trust.TrustError, "extra"): + trust.verify_local_artifacts(root, artifacts) + + def test_v2_reference_ids_are_strings_and_digests_are_prefixed(self) -> None: + regular, bundle = pair("qualification-release", "4", "5") + trust.validate_reference_pair(regular, bundle, kind="qualification-release") + regular["repository_id"] = 858454062 + with self.assertRaisesRegex(trust.TrustError, "repository"): + trust.validate_reference_pair(regular, bundle, kind="qualification-release") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_public_trust_kms.py b/tests/test_public_trust_kms.py new file mode 100644 index 0000000..f8e5323 --- /dev/null +++ b/tests/test_public_trust_kms.py @@ -0,0 +1,493 @@ +"""Tests for the offline AWS KMS P-256 public-trust DSSE profile.""" + +from __future__ import annotations + +import base64 +import copy +import hashlib +import json +import sys +import unittest +from datetime import datetime, timezone +from pathlib import Path + +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric.utils import ( + decode_dss_signature, + encode_dss_signature, +) + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "scripts")) + +import public_trust_kms as kms # noqa: E402 +import public_trust_resolver as resolver # noqa: E402 +import validate_evidence_registry as evidence # noqa: E402 + +NOW = datetime(2026, 8, 27, 12, 0, 30, tzinfo=timezone.utc) +KMS_ARN = ( + "arn:aws:kms:us-east-1:992382684924:key/" + "12345678-1234-4abc-8def-1234567890ab" +) + + +def sha(label: str) -> str: + return "sha256:" + hashlib.sha256(label.encode("utf-8")).hexdigest() + + +def private_key() -> ec.EllipticCurvePrivateKey: + return ec.derive_private_key( + int("2f29c1f30c9db50a15e08ddd4f5f49d1f5c621d40ad72835d11065448148d8c7", 16), + ec.SECP256R1(), + ) + + +def signer(*, status: str = "active") -> dict: + public_key = private_key().public_key() + point = public_key.public_bytes( + serialization.Encoding.X962, + serialization.PublicFormat.UncompressedPoint, + ) + spki = public_key.public_bytes( + serialization.Encoding.DER, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) + return { + "algorithm": "ecdsa-p256-sha256", + "key_id": kms.public_key_id(public_key), + "key_origin": "aws-kms", + "kms_key_arn": KMS_ARN, + "public_key": base64.urlsafe_b64encode(point).decode().rstrip("="), + "public_key_spki_der_base64": base64.b64encode(spki).decode(), + "public_key_sha256": "sha256:" + hashlib.sha256(spki).hexdigest(), + "signature_encoding": "asn1-der-low-s-base64-rfc4648-padded", + "statement_schema_versions": [kms.STATEMENT_SCHEMA], + "allowed_usages": ["production-public-evidence"], + "allowed_kinds": ["qualification-release"], + "allowed_workflows": [ + ( + "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/OpenAdaptAI/.github/" + ".github/workflows/sign-production-evidence.yml@refs/heads/main" + ) + ], + "allowed_ref_prefixes": ["refs/heads/main"], + "allowed_environments": ["public-trust-signing"], + "status": status, + "revoked_at": "2026-08-27T11:59:00Z" if status == "revoked" else None, + } + + +def source_issuer() -> dict: + return { + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "workflow": ".github/workflows/issue-production-release-admission.yml", + "ref": "refs/heads/main", + "source_commit": "a" * 40, + "environment": "production-release-admission", + } + + +def object_value(registry_sha256: str = sha("registry")) -> dict: + return { + "schema_version": "test/v1", + "authority_state_sha256": sha("authority"), + "revocation_state_sha256": sha("revocation"), + "signer_registry_sha256": registry_sha256, + "issuer": source_issuer(), + } + + +def statement(registry_sha256: str = sha("registry")) -> dict: + value = object_value(registry_sha256) + raw = kms.canonical_lf(value) + active_signer = signer() + return { + "schema_version": kms.STATEMENT_SCHEMA, + "object_kind": "qualification-release", + "object_schema_version": "openadapt.qualification-release/v1", + "object_media_type": "application/vnd.openadapt.qualification-release+json;version=1", + "object_sha256": "sha256:" + hashlib.sha256(raw).hexdigest(), + "object_size_bytes": len(raw), + "semantic_identity_sha256": sha("release-identity"), + "source_issuer": source_issuer(), + "signer_registry_sha256": registry_sha256, + "authority_state_sha256": sha("authority"), + "revocation_state_sha256": sha("revocation"), + "issued_at": "2026-08-27T12:00:00Z", + "not_before": "2026-08-27T12:00:00Z", + "expires_at": "2026-08-27T12:05:00Z", + "request_id_sha256": sha("request"), + "signing_authority": { + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "workflow": ".github/workflows/sign-production-evidence.yml", + "ref": "refs/heads/main", + "source_commit": "b" * 40, + "environment": "public-trust-signing", + "aws_account_id": "992382684924", + "aws_region": "us-east-1", + "kms_key_arn": KMS_ARN, + "kms_key_spec": "ECC_NIST_P256", + "kms_signing_algorithm": "ECDSA_SHA_256", + "kms_message_type": "DIGEST", + "role_arn": "arn:aws:iam::992382684924:role/openadapt-public-trust-signer", + }, + "key_id": active_signer["key_id"], + "signature_profile": "aws-kms-p256-dsse-v1", + } + + +def bundle(value: dict | None = None) -> dict: + signed_statement = value or statement() + payload = kms.canonical_lf(signed_statement) + signature = private_key().sign( + kms.dsse_pae(kms.STATEMENT_MEDIA_TYPE, payload), ec.ECDSA(hashes.SHA256()) + ) + signature_r, signature_s = decode_dss_signature(signature) + if signature_s > kms.P256_ORDER // 2: + signature_s = kms.P256_ORDER - signature_s + signature = encode_dss_signature(signature_r, signature_s) + key_id = signer()["key_id"] + return { + "mediaType": kms.BUNDLE_MEDIA_TYPE, + "verificationMaterial": {"publicKey": {"hint": key_id}}, + "dsseEnvelope": { + "payload": base64.b64encode(payload).decode(), + "payloadType": kms.STATEMENT_MEDIA_TYPE, + "signatures": [ + {"keyid": key_id, "sig": base64.b64encode(signature).decode()} + ], + }, + } + + +def reference(kind: str, raw: bytes, *, semantic: str, subject: str | None) -> dict: + object_sha256 = "sha256:" + hashlib.sha256(raw).hexdigest() + digest_hex = object_sha256.removeprefix("sha256:") + schema, media = evidence.OBJECT_KIND_CONTRACTS[kind] + entry = { + "kind": kind, + "object_schema_version": schema, + "object_path": ( + f"production-evidence/objects/sha256/{digest_hex[:2]}/" + f"{digest_hex}.{kind}.json" + ), + "object_sha256": object_sha256, + "size_bytes": len(raw), + "object_media_type": media, + "semantic_identity_sha256": semantic, + "subject_sha256": subject, + } + entry["registry_entry_sha256"] = evidence.entry_digest(entry) + return { + "schema_version": evidence.REFERENCE_SCHEMA, + "repository": evidence.REPOSITORY, + "repository_id": evidence.REPOSITORY_ID, + "repository_owner_id": evidence.REPOSITORY_OWNER_ID, + "registry_source_commit": "c" * 40, + "registry_revision": 5, + "registry_head_sha256": sha("registry-head"), + **entry, + } + + +def registry_raw() -> bytes: + value = { + "schema_version": evidence.SIGNER_REGISTRY_SCHEMA, + "revision": 5, + "generated_at": "2026-08-27T11:00:00Z", + "expires_at": "2026-08-27T13:00:00Z", + "signers": [signer()], + } + return evidence.canonical(value) + b"\n" + + +class PublicTrustKmsTests(unittest.TestCase): + def test_fixed_key_and_dsse_pae_digest_vector(self) -> None: + active_signer = signer() + self.assertEqual( + active_signer["key_id"], "oa-public-trust-p256-93dbc7899ed2892d" + ) + self.assertEqual( + active_signer["public_key_sha256"], + "sha256:93dbc7899ed2892d0215567d8f8f7a2b00d44246d8c6a9f8cb40c4355c1234d3", + ) + self.assertEqual( + kms.kms_message_digest(statement()).hex(), + "272b5a49125aac7591703c97fc50bc42cc31c678f762e557830279ba09772aa1", + ) + + def test_exact_statement_signer_and_bundle_verify_offline(self) -> None: + active_signer = signer() + signed_statement = statement() + self.assertEqual(kms.validate_public_signer(active_signer), active_signer) + self.assertEqual( + kms.validate_signing_statement( + signed_statement, signer=active_signer, now=NOW + ), + signed_statement, + ) + signed_bundle = bundle(signed_statement) + self.assertEqual( + kms.verify_bundle( + signed_bundle, + expected_statement=signed_statement, + signer=active_signer, + now=NOW, + ), + signed_bundle, + ) + + def test_statement_binds_exact_canonical_object_and_signed_issuer(self) -> None: + value = object_value() + raw = kms.canonical_lf(value) + self.assertEqual( + kms.validate_statement_object_binding( + statement(), + object_raw=raw, + object_value=value, + object_kind="qualification-release", + object_schema_version="openadapt.qualification-release/v1", + object_media_type="application/vnd.openadapt.qualification-release+json;version=1", + semantic_identity_sha256=sha("release-identity"), + expected_signer_registry_sha256=sha("registry"), + expected_authority_state_sha256=sha("authority"), + expected_revocation_state_sha256=sha("revocation"), + ), + statement(), + ) + with self.assertRaisesRegex(kms.PublicTrustKmsError, "canonical JSON"): + kms.validate_statement_object_binding( + statement(), + object_raw=json.dumps(value, indent=2).encode() + b"\n", + object_value=value, + object_kind="qualification-release", + object_schema_version="openadapt.qualification-release/v1", + object_media_type="application/vnd.openadapt.qualification-release+json;version=1", + semantic_identity_sha256=sha("release-identity"), + expected_signer_registry_sha256=sha("registry"), + expected_authority_state_sha256=sha("authority"), + expected_revocation_state_sha256=sha("revocation"), + ) + + def test_kms_digest_is_sha256_of_exact_dsse_pae(self) -> None: + value = statement() + expected = hashlib.sha256( + kms.dsse_pae(kms.STATEMENT_MEDIA_TYPE, kms.canonical_lf(value)) + ).digest() + self.assertEqual(kms.kms_message_digest(value), expected) + self.assertEqual(len(expected), 32) + + def test_tamper_profile_workflow_expiry_revocation_and_alias_fail(self) -> None: + active_signer = signer() + signed_statement = statement() + signed_bundle = bundle(signed_statement) + + tampered = copy.deepcopy(signed_bundle) + tampered["dsseEnvelope"]["payloadType"] = "application/json" + with self.assertRaisesRegex(kms.PublicTrustKmsError, "payload type"): + kms.verify_bundle( + tampered, + expected_statement=signed_statement, + signer=active_signer, + ) + + wrong_profile = copy.deepcopy(signed_statement) + wrong_profile["signature_profile"] = "github-attestation" + with self.assertRaisesRegex(kms.PublicTrustKmsError, "profile"): + kms.validate_signing_statement(wrong_profile) + + wrong_workflow = copy.deepcopy(signed_statement) + wrong_workflow["signing_authority"]["workflow"] = ".github/workflows/release.yml" + with self.assertRaisesRegex(kms.PublicTrustKmsError, "workflow"): + kms.validate_signing_statement(wrong_workflow) + + with self.assertRaisesRegex(kms.PublicTrustKmsError, "not active"): + kms.verify_bundle( + signed_bundle, + expected_statement=signed_statement, + signer=active_signer, + now=datetime(2026, 8, 27, 12, 5, 0, tzinfo=timezone.utc), + ) + + with self.assertRaisesRegex(kms.PublicTrustKmsError, "not active"): + kms.verify_bundle( + signed_bundle, + expected_statement=signed_statement, + signer=signer(status="revoked"), + now=NOW, + ) + + alias_signer = signer() + alias_signer["kms_key_arn"] = ( + "arn:aws:kms:us-east-1:992382684924:alias/openadapt-public-trust" + ) + with self.assertRaisesRegex(kms.PublicTrustKmsError, "ARN"): + kms.validate_public_signer(alias_signer) + + def test_bundle_rejects_profile_substitution_and_extra_fields(self) -> None: + active_signer = signer() + signed_statement = statement() + for label, mutate in ( + ( + "message signature", + lambda value: value.__setitem__("messageSignature", {}), + ), + ( + "keyless material", + lambda value: value["verificationMaterial"].__setitem__( + "x509CertificateChain", {} + ), + ), + ( + "second signature", + lambda value: value["dsseEnvelope"]["signatures"].append( + copy.deepcopy(value["dsseEnvelope"]["signatures"][0]) + ), + ), + ): + with self.subTest(label=label): + changed = bundle(signed_statement) + mutate(changed) + with self.assertRaises(kms.PublicTrustKmsError): + kms.verify_bundle( + changed, + expected_statement=signed_statement, + signer=active_signer, + ) + + def test_registered_pair_resolves_raw_bytes_and_current_state_offline(self) -> None: + signer_registry_raw = registry_raw() + registry_identity = evidence.signer_registry_identity_digest( + json.loads(signer_registry_raw) + ) + signed_object = object_value(registry_identity) + object_raw = kms.canonical_lf(signed_object) + semantic = evidence.semantic_identity_digest( + kind="qualification-release", + object_schema_version="openadapt.qualification-release/v1", + object_value=signed_object, + object_sha256="sha256:" + hashlib.sha256(object_raw).hexdigest(), + ) + signed_statement = statement(registry_identity) + signed_statement["object_sha256"] = "sha256:" + hashlib.sha256(object_raw).hexdigest() + signed_statement["object_size_bytes"] = len(object_raw) + signed_statement["semantic_identity_sha256"] = semantic + signed_bundle = bundle(signed_statement) + bundle_raw = kms.canonical_lf(signed_bundle) + object_ref = reference( + "qualification-release", + object_raw, + semantic=semantic, + subject=None, + ) + bundle_sha = "sha256:" + hashlib.sha256(bundle_raw).hexdigest() + bundle_ref = reference( + "qualification-release-sigstore-bundle", + bundle_raw, + semantic=evidence.semantic_identity_digest( + kind="qualification-release-sigstore-bundle", + object_schema_version="sigstore.bundle/v0.3", + object_value=object_ref["object_sha256"], + object_sha256=bundle_sha, + ), + subject=object_ref["object_sha256"], + ) + result = resolver.verify_registered_public_trust_pair( + object_raw=object_raw, + object_reference=object_ref, + bundle_raw=bundle_raw, + bundle_reference=bundle_ref, + signer_registry_raw=signer_registry_raw, + expected_signer_registry_sha256=registry_identity, + expected_authority_state_sha256=sha("authority"), + expected_revocation_state_sha256=sha("revocation"), + now=NOW, + ) + self.assertEqual(result["object"], signed_object) + self.assertEqual(result["signing_statement"], signed_statement) + + def test_registered_pair_refuses_raw_tamper_wrong_state_and_wrong_key(self) -> None: + signer_registry_raw = registry_raw() + registry_identity = evidence.signer_registry_identity_digest( + json.loads(signer_registry_raw) + ) + signed_object = object_value(registry_identity) + object_raw = kms.canonical_lf(signed_object) + semantic = evidence.semantic_identity_digest( + kind="qualification-release", + object_schema_version="openadapt.qualification-release/v1", + object_value=signed_object, + object_sha256="sha256:" + hashlib.sha256(object_raw).hexdigest(), + ) + signed_statement = statement(registry_identity) + signed_statement["object_sha256"] = "sha256:" + hashlib.sha256(object_raw).hexdigest() + signed_statement["object_size_bytes"] = len(object_raw) + signed_statement["semantic_identity_sha256"] = semantic + signed_bundle = bundle(signed_statement) + bundle_raw = kms.canonical_lf(signed_bundle) + object_ref = reference( + "qualification-release", + object_raw, + semantic=semantic, + subject=None, + ) + bundle_sha = "sha256:" + hashlib.sha256(bundle_raw).hexdigest() + bundle_ref = reference( + "qualification-release-sigstore-bundle", + bundle_raw, + semantic=evidence.semantic_identity_digest( + kind="qualification-release-sigstore-bundle", + object_schema_version="sigstore.bundle/v0.3", + object_value=object_ref["object_sha256"], + object_sha256=bundle_sha, + ), + subject=object_ref["object_sha256"], + ) + base = { + "object_raw": object_raw, + "object_reference": object_ref, + "bundle_raw": bundle_raw, + "bundle_reference": bundle_ref, + "signer_registry_raw": signer_registry_raw, + "expected_signer_registry_sha256": registry_identity, + "expected_authority_state_sha256": sha("authority"), + "expected_revocation_state_sha256": sha("revocation"), + "now": NOW, + } + for label, mutate, message in ( + ( + "object bytes", + lambda value: value.__setitem__("object_raw", object_raw + b" "), + "bytes or size", + ), + ( + "authority state", + lambda value: value.__setitem__( + "expected_authority_state_sha256", sha("other-authority") + ), + "authority_state_sha256", + ), + ( + "registry identity", + lambda value: value.__setitem__( + "expected_signer_registry_sha256", sha("other-registry") + ), + "not current", + ), + ): + with self.subTest(label=label): + changed = copy.deepcopy(base) + mutate(changed) + with self.assertRaisesRegex( + resolver.PublicTrustResolutionError, message + ): + resolver.verify_registered_public_trust_pair(**changed) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_public_trust_schemas.py b/tests/test_public_trust_schemas.py new file mode 100644 index 0000000..14447a1 --- /dev/null +++ b/tests/test_public_trust_schemas.py @@ -0,0 +1,767 @@ +"""Structural tests for the public Production trust JSON schemas.""" + +from __future__ import annotations + +import json +import unittest +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parents[1] +SCHEMA_ROOT = ROOT / "schemas" + + +EXPECTED_TOP_LEVEL_FIELDS = { + "production-public-trust-signing-statement.schema.json": { + "schema_version", + "object_kind", + "object_schema_version", + "object_media_type", + "object_sha256", + "object_size_bytes", + "semantic_identity_sha256", + "source_issuer", + "signer_registry_sha256", + "authority_state_sha256", + "revocation_state_sha256", + "issued_at", + "not_before", + "expires_at", + "request_id_sha256", + "signing_authority", + "key_id", + "signature_profile", + }, + "production-public-trust-dsse-bundle.schema.json": { + "mediaType", + "verificationMaterial", + "dsseEnvelope", + }, + "qualification-worker-admission.schema.json": { + "schema_version", + "admission_id_sha256", + "provider_identity_sha256", + "worker_identity_sha256", + "live_provider_observation_sha256", + "admitted_runtime_sha256", + "worker_image_sha256", + "baseline_sha256", + "host_identity_sha256", + "tls_identity_sha256", + "egress_policy_sha256", + "campaign_permit_sha256", + "capability_handle_sha256", + "authority_state_sha256", + "revocation_state_sha256", + "signer_registry_sha256", + "issued_at", + "not_before", + "expires_at", + "issuer", + }, + "qualification-worker-admission-verifier-result.schema.json": { + "schema_version", + "verdict", + "admission_object_sha256", + "provider_identity_sha256", + "worker_identity_sha256", + "live_provider_observation_sha256", + "admitted_runtime_sha256", + "worker_image_sha256", + "baseline_sha256", + "host_identity_sha256", + "tls_identity_sha256", + "egress_policy_sha256", + "campaign_permit_sha256", + "capability_handle_sha256", + "authority_state_sha256", + "revocation_state_sha256", + "signer_registry_sha256", + "not_before", + "expires_at", + "verified_at", + "verifier", + }, + "qualification-worker-dispatch.schema.json": { + "schema_version", + "dispatch_id_sha256", + "worker_admission_sha256", + "provider_identity_sha256", + "worker_identity_sha256", + "live_provider_observation_sha256", + "admitted_runtime_sha256", + "run_id", + "run_attempt", + "start_id_sha256", + "task_id_sha256", + "task_condition_sha256", + "campaign_artifact_sha256", + "process_lease_sha256", + "capability_handle_sha256", + "idempotency_key", + "issued_at", + "not_before", + "expires_at", + "issuer", + }, + "qualification-worker-terminal-receipt.schema.json": { + "schema_version", + "receipt_id_sha256", + "worker_admission_sha256", + "dispatch_id_sha256", + "provider_identity_sha256", + "worker_identity_sha256", + "live_provider_observation_sha256", + "admitted_runtime_sha256", + "run_id", + "run_attempt", + "start_id_sha256", + "task_id_sha256", + "task_condition_sha256", + "capability_handle_sha256", + "launch_attempt", + "launch_attempt_sha256", + "process", + "oracle_sha256", + "result_sha256", + "log_sha256", + "burned_identities_sha256", + "burn_ledger_revision", + "burn_receipt_sha256", + "burned_at", + "ledger_readback_sha256", + "effect_started", + "delivery_state", + "terminal_state", + "exit_code", + "uncertainty_sha256", + "quarantine", + "completed_at", + "issuer", + }, + "support-release-policy.schema.json": { + "$schema", + "schema_version", + "lifecycle_state", + "production_projection", + "maximum_admission_days", + "release_authority", + "targets", + }, + "qualification-trial-receipt-authority-policy.schema.json": { + "$schema", + "schema_version", + "repository", + "repository_id", + "repository_owner_id", + "workflow", + "ref", + "trigger", + "input", + "signing", + "authorities", + }, + "support-release-admission.schema.json": { + "schema_version", + "admission_id_sha256", + "lifecycle_state", + "support_target", + "verdict", + "claim_scope", + "release_identity", + "release", + "release_sha256", + "artifact_inventory_sha256", + "publication_staging", + "publication_staging_sha256", + "authority_state_sha256", + "revocation_state_sha256", + "signer_registry_sha256", + "support_policy_sha256", + "issued_at", + "not_before", + "expires_at", + "issuer", + }, + "production-release-authority-policy.schema.json": { + "$schema", + "schema_version", + "release_app", + "repository_selection", + "permissions", + "repositories", + "tag_rulesets", + "support_release_authorities", + }, + "production-publication-recovery-authorization.schema.json": { + "schema_version", + "authorization_id_sha256", + "qualification_release_reference", + "qualification_release_object_sha256", + "release_sha256", + "artifact_inventory_sha256", + "publication_staging_sha256", + "repository", + "repository_id", + "target", + "tag", + "tag_ref", + "tag_object_id", + "target_commit", + "draft_release_id", + "requested_effect", + "run_id", + "run_attempt", + "dispatcher_actor_id", + "environment", + "release_app", + "idempotency_key", + "issued_at", + "not_before", + "expires_at", + "issuer", + }, + "production-cloud-deploy-authorization.schema.json": { + "schema_version", + "authorization_sha256", + "deployment_intent_sha256", + "profile_repository", + "profile_repository_id", + "profile_commit", + "profile_workflow", + "profile_run_id", + "profile_run_attempt", + "cloud_source_commitment_sha256", + "expected_live_attestation_sha256", + "provider_idempotency_key", + "audience", + "signer_registry_sha256", + "revocation_state_sha256", + "issued_at", + "not_before", + "expires_at", + "issuer", + }, + "production-cloud-deployment-result.schema.json": { + "schema_version", + "handoff_id_sha256", + "authorization_reference", + "authorization_bundle_reference", + "authorization_sha256", + "cloud_source_commitment_sha256", + "profile_commit", + "profile_run_id", + "profile_run_attempt", + "source_proof_request", + "source_proof_request_sha256", + "source_proof_response", + "source_proof_response_sha256", + "reviewed_public_values_sha256", + "expected_live_attestation_sha256", + "live_attestation_sha256", + "provider_idempotency_key", + "audience", + "signer_registry_sha256", + "revocation_state_sha256", + "verdict", + "issued_at", + "not_before", + "expires_at", + "issuer", + }, + "production-lifecycle-evidence-manifest.schema.json": { + "schema_version", + "target", + "verdict", + "claim_scope", + "acceptance_policy_sha256", + "lifecycle_policy_sha256", + "release_identity", + "release", + "release_sha256", + "artifact_inventory", + "artifact_inventory_sha256", + "publication_staging", + "publication_staging_sha256", + "qualification_evidence_decision_receipt_reference", + "qualification_evidence_decision_receipt_bundle_reference", + "qualification_admission_reference", + "qualification_admission_bundle_reference", + "campaign_summary", + "authority_state_sha256", + "revocation_state_sha256", + "signer_registry_sha256", + "issued_at", + "not_before", + "expires_at", + "issuer", + }, + "production-current-default.schema.json": { + "schema_version", + "default_set_revision", + "previous_default_set_sha256", + "targets", + "issued_at", + "not_before", + "expires_at", + "issuer", + }, + "production-evidence-object-reference.schema.json": { + "schema_version", + "repository", + "repository_id", + "repository_owner_id", + "registry_source_commit", + "registry_revision", + "registry_head_sha256", + "registry_entry_sha256", + "kind", + "object_schema_version", + "object_path", + "object_sha256", + "size_bytes", + "object_media_type", + "semantic_identity_sha256", + "subject_sha256", + }, + "production-evidence-verification-policy.schema.json": { + "schema_version", + "repository", + "repository_id", + "repository_owner_id", + "reference_schema_version", + "transport", + "signer_registry_schema_version", + "embedded_signatures", + "message_signature", + "sigstore", + "public_trust_dsse", + }, + "production-lifecycle-checkpoint.schema.json": { + "schema_version", + "checkpoint_id_sha256", + "checkpoint_revision", + "previous_checkpoint_sha256", + "registry_source_commit", + "registry_revision", + "registry_head_sha256", + "signer_registry", + "lifecycle_policy_sha256", + "current_default_reference", + "current_default_bundle_reference", + "lifecycle_projection", + "lifecycle_projection_sha256", + "release_admissions", + "release_admission_set_sha256", + "workflow_admissions", + "workflow_admission_set_sha256", + "authority_state_reference", + "authority_state_bundle_reference", + "authority_state_sha256", + "revocation_state_reference", + "revocation_state_bundle_reference", + "revocation_state_sha256", + "generated_at", + "not_before", + "expires_at", + "issuer", + }, + "production-lifecycle-evidence-summary.schema.json": { + "schema_version", + "target", + "verdict", + "claim_scope", + "acceptance_policy_sha256", + "lifecycle_policy_sha256", + "release_identity", + "release_sha256", + "artifact_inventory_sha256", + "publication_staging", + "publication_staging_sha256", + "evidence_identity_sha256", + "qualification_evidence_decision_receipt_reference", + "qualification_evidence_decision_receipt_bundle_reference", + "qualification_admission_reference", + "qualification_admission_bundle_reference", + "production_acceptance_manifest_reference", + "production_acceptance_manifest_bundle_reference", + "campaign_summary", + "authority_state_sha256", + "revocation_state_sha256", + "signer_registry_sha256", + "issued_at", + "not_before", + "expires_at", + "issuer", + }, + "production-lifecycle-feed.schema.json": { + "schema_version", + "repository", + "repository_id", + "repository_owner_id", + "ref", + "feed_revision", + "generated_at", + "expires_at", + "registry_source_commit", + "registry_revision", + "registry_head_sha256", + "signer_registry", + "checkpoints", + }, + "production-lifecycle-feed-update.schema.json": { + "schema_version", + "event_type", + "source_repository", + "source_repository_id", + "source_ref", + "source_commit", + "target_repository", + "target_repository_id", + "target_ref", + "expected_old_commit", + "new_commit", + "feed_path", + "feed_sha256", + "checkpoint_sha256", + "registry_head_sha256", + "expires_at", + "idempotency_key", + }, + "production-lifecycle-policy.schema.json": { + "$schema", + "schema_version", + "revision", + "maximum_release_admission_days", + "maximum_workflow_admission_days", + "object_reference_schema_version", + "release_admission_schema_version", + "workflow_admission_schema_version", + "lifecycle_checkpoint_schema_version", + "lifecycle_feed_schema_version", + "lifecycle_feed_ref", + "targets", + }, + "production-release-artifact-inventory.schema.json": { + "schema_version", + "target", + "claim_scope", + "artifacts", + }, + "qualification-admission.schema.json": { + "schema_version", + "admission_id_sha256", + "organization_id_sha256", + "workflow_id_sha256", + "workflow_version_id_sha256", + "bundle_version", + "bundle_sha256", + "admitted_runtime_sha256", + "application_contract_sha256", + "environment_contract_sha256", + "input_contract_sha256", + "action_contract_sha256", + "identity_contract_sha256", + "effect_contract_sha256", + "policy_contract_sha256", + "evidence_authority_sha256", + "campaign_artifact_sha256", + "campaign_permit_sha256", + "decision_receipt_reference", + "decision_receipt_bundle_reference", + "signer_registry_sha256", + "revocation_state_sha256", + "entity_class", + "campaign_summary", + "local_identity_opening", + "verdict", + "issued_at", + "not_before", + "expires_at", + "issuer", + }, + "qualification-evidence-decision-receipt.schema.json": { + "schema_version", + "decision_identity_sha256", + "decision_revision", + "decision_commitment_sha256", + "evidence_manifest_sha256", + "evidence_manifest_readback_sha256", + "organization_id_sha256", + "workflow_id_sha256", + "workflow_version_id_sha256", + "bundle_version", + "bundle_sha256", + "admitted_runtime_sha256", + "application_contract_sha256", + "environment_contract_sha256", + "input_contract_sha256", + "action_contract_sha256", + "identity_contract_sha256", + "effect_contract_sha256", + "policy_contract_sha256", + "evidence_authority_contract_sha256", + "campaign_permit_sha256", + "signer_registry_sha256", + "revocation_state_sha256", + "entity_class", + "campaign_summary", + "verdict", + "issued_at", + "not_before", + "expires_at", + "issuer_key_id", + "algorithm", + "signing_statement", + "signature", + "campaign_artifact_sha256", + "issuer", + }, + "qualification-release.schema.json": { + "schema_version", + "admission_id_sha256", + "target", + "verdict", + "claim_scope", + "release_identity", + "release", + "release_sha256", + "artifact_inventory_sha256", + "publication_staging", + "publication_staging_sha256", + "production_acceptance_summary_reference", + "production_acceptance_summary_bundle_reference", + "authority_state_sha256", + "revocation_state_sha256", + "signer_registry_sha256", + "publication_policy_sha256", + "issued_at", + "not_before", + "expires_at", + "issuer", + }, +} + + +def iter_nodes(value: Any): + yield value + if isinstance(value, dict): + for child in value.values(): + yield from iter_nodes(child) + elif isinstance(value, list): + for child in value: + yield from iter_nodes(child) + + +class PublicTrustSchemaTests(unittest.TestCase): + def test_every_schema_parses_and_declared_objects_are_closed(self) -> None: + paths = sorted(SCHEMA_ROOT.glob("*.schema.json")) + self.assertTrue(paths) + for path in paths: + with self.subTest(path=path.name): + schema = json.loads(path.read_text(encoding="utf-8")) + for node in iter_nodes(schema): + if not isinstance(node, dict): + continue + if node.get("type") != "object" or "properties" not in node: + continue + self.assertIs(node.get("additionalProperties"), False) + self.assertEqual(set(node.get("required", [])), set(node["properties"])) + + def test_frozen_top_level_contracts_are_exact(self) -> None: + for filename, expected in EXPECTED_TOP_LEVEL_FIELDS.items(): + with self.subTest(path=filename): + schema = json.loads((SCHEMA_ROOT / filename).read_text(encoding="utf-8")) + self.assertIs(schema["additionalProperties"], False) + self.assertEqual(set(schema["required"]), expected) + self.assertEqual(set(schema["properties"]), expected) + + def test_v2_reference_has_no_url_transport_field(self) -> None: + schema = json.loads( + (SCHEMA_ROOT / "production-evidence-object-reference.schema.json").read_text( + encoding="utf-8" + ) + ) + self.assertEqual(len(schema["required"]), 16) + self.assertNotIn("url", schema["properties"]) + self.assertEqual( + schema["properties"]["schema_version"]["const"], + "openadapt.production-evidence-object-reference/v2", + ) + + def test_current_default_and_checkpoint_bind_complete_signed_issuers(self) -> None: + current = json.loads( + (SCHEMA_ROOT / "production-current-default.schema.json").read_text() + ) + checkpoint = json.loads( + (SCHEMA_ROOT / "production-lifecycle-checkpoint.schema.json").read_text() + ) + common = { + "repository", "repository_id", "repository_owner_id", "workflow", + "ref", "source_commit", "environment", + } + current_issuer = current["$defs"]["issuer"] + checkpoint_issuer = checkpoint["$defs"]["issuer"] + self.assertEqual(set(current_issuer["required"]), common) + self.assertEqual( + current_issuer["properties"]["repository_owner_id"]["const"], + "132681217", + ) + self.assertEqual( + current_issuer["properties"]["environment"]["const"], + "production-current-default", + ) + self.assertEqual( + set(checkpoint_issuer["required"]), + common + | { + "policy_repository", "policy_repository_id", + "policy_source_commit", "policy_path", + }, + ) + self.assertEqual( + checkpoint_issuer["properties"]["environment"]["const"], + "production-lifecycle-checkpoint", + ) + + def test_release_artifacts_bind_publish_destinations(self) -> None: + schema = json.loads( + (SCHEMA_ROOT / "qualification-release.schema.json").read_text( + encoding="utf-8" + ) + ) + artifact = schema["$defs"]["artifact"] + staged = schema["$defs"]["staged_asset"] + projection = { + "name", + "kind", + "sha256", + "size_bytes", + "media_type", + "publish_destinations", + } + self.assertEqual(set(artifact["required"]), projection) + self.assertEqual( + set(staged["required"]), + projection | {"asset_id", "uploader_id", "uploader_login"}, + ) + self.assertEqual(schema["$defs"]["publication_staging"]["properties"]["tag_rulesets"]["minItems"], 2) + self.assertEqual(schema["$defs"]["publication_staging"]["properties"]["tag_rulesets"]["maxItems"], 2) + + def test_verification_policy_keeps_sigstore_and_freezes_future_kms_profile( + self, + ) -> None: + policy = json.loads( + (ROOT / "production-evidence-policy.json").read_text(encoding="utf-8") + ) + identities = policy["sigstore"]["certificate_identities"] + self.assertEqual( + [item["kind"] for item in identities], + sorted(item["kind"] for item in identities), + ) + profiles = {item["kind"]: item["bundle_profile"] for item in identities} + self.assertEqual( + profiles["qualification-evidence-decision-receipt"], + "sigstore-message-signature", + ) + self.assertEqual(profiles["qualification-release"], "github-attestation") + message = policy["message_signature"] + self.assertEqual(message["version"], "3.1.3") + self.assertEqual(message["runner"], "ubuntu-24.04") + self.assertEqual(message["architecture"], "linux-amd64") + kms = policy["public_trust_dsse"] + self.assertEqual(kms["profile"], "aws-kms-p256-dsse-v1") + self.assertEqual(kms["aws_account_id"], "992382684924") + self.assertEqual(kms["kms_key_spec"], "ECC_NIST_P256") + self.assertEqual(kms["kms_signing_algorithm"], "ECDSA_SHA_256") + self.assertEqual(kms["kms_message_type"], "DIGEST") + self.assertIs(kms["offline_verification_required"], True) + self.assertIs(kms["alias_allowed"], False) + self.assertEqual(kms["allowed_kinds"], sorted(kms["allowed_kinds"])) + self.assertEqual(len(kms["allowed_kinds"]), 17) + + def test_support_policy_cannot_enter_the_production_projection(self) -> None: + policy = json.loads((ROOT / "support-release-policy.json").read_text()) + self.assertEqual(policy["lifecycle_state"], "Support") + self.assertIs(policy["production_projection"], False) + self.assertEqual([item["id"] for item in policy["targets"]], ["openadapt-tray"]) + production_targets = { + "openadapt", "flow", "cloud", "desktop", "capture", "agent", "docs" + } + self.assertTrue(production_targets.isdisjoint({"openadapt-tray"})) + + def test_trial_receipt_authorities_are_fixed_per_receipt_type(self) -> None: + policy = json.loads( + (ROOT / "qualification-trial-receipt-authority-policy.json").read_text() + ) + self.assertEqual(policy["repository"], "OpenAdaptAI/openadapt-evals") + self.assertEqual(policy["workflow"], ".github/workflows/issue-qualification-trial-receipts.yml") + self.assertEqual(policy["trigger"], "workflow_dispatch") + self.assertEqual(len(policy["authorities"]), 8) + self.assertEqual( + len({item["environment"] for item in policy["authorities"]}), 8 + ) + for item in policy["authorities"]: + self.assertEqual( + item["runner_labels"], + ["self-hosted", "evidence-authority", item["environment"]], + ) + + def test_worker_contracts_pin_central_authority_and_terminal_uncertainty(self) -> None: + admission = json.loads( + (SCHEMA_ROOT / "qualification-worker-admission.schema.json").read_text() + ) + dispatch = json.loads( + (SCHEMA_ROOT / "qualification-worker-dispatch.schema.json").read_text() + ) + terminal = json.loads( + (SCHEMA_ROOT / "qualification-worker-terminal-receipt.schema.json").read_text() + ) + self.assertEqual( + admission["$defs"]["issuer"]["properties"]["repository"]["const"], + "OpenAdaptAI/.github", + ) + self.assertEqual(dispatch["properties"]["run_attempt"]["const"], "1") + self.assertEqual( + dispatch["$defs"]["issuer"]["properties"]["workflow"]["const"], + ".github/workflows/issue-qualification-worker-dispatch.yml", + ) + process_schema = terminal["properties"]["process"]["oneOf"][0] + self.assertEqual( + set(process_schema["required"]), + { + "pid", + "process_group_id", + "process_start_ticks", + "launched_at", + "executable_sha256", + "process_start_identity_sha256", + }, + ) + prelaunch = terminal["allOf"][0]["then"]["properties"] + self.assertIs(prelaunch["effect_started"]["const"], False) + self.assertEqual(prelaunch["delivery_state"]["const"], "not_started") + self.assertEqual( + prelaunch["terminal_state"]["const"], "PRELAUNCH_QUARANTINED" + ) + self.assertIs( + prelaunch["quarantine"]["properties"]["active"]["const"], True + ) + uncertain = next( + rule["then"]["properties"] + for rule in terminal["allOf"] + if rule["if"]["properties"].get("delivery_state") + == {"const": "uncertain"} + ) + self.assertIs(uncertain["effect_started"]["const"], True) + self.assertEqual( + uncertain["terminal_state"]["enum"], + ["RECONCILIATION_REQUIRED", "QUARANTINED"], + ) + self.assertIs( + uncertain["quarantine"]["properties"]["active"]["const"], True + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_qualification_worker_contract.py b/tests/test_qualification_worker_contract.py new file mode 100644 index 0000000..7264976 --- /dev/null +++ b/tests/test_qualification_worker_contract.py @@ -0,0 +1,262 @@ +"""Tests for the qualification worker terminal authority contract.""" + +from __future__ import annotations + +import copy +import hashlib +import sys +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "scripts")) + +import qualification_worker_contract as worker + + +def sha(label: str) -> str: + return "sha256:" + hashlib.sha256(label.encode()).hexdigest() + + +def admission() -> dict: + return { + "admission_id_sha256": sha("admission"), + "provider_identity_sha256": sha("provider"), + "worker_identity_sha256": sha("worker"), + "live_provider_observation_sha256": sha("observation"), + "admitted_runtime_sha256": sha("runtime"), + "host_identity_sha256": sha("host"), + "capability_handle_sha256": sha("capability"), + } + + +def dispatch() -> dict: + return { + "dispatch_id_sha256": sha("dispatch"), + "worker_admission_sha256": sha("admission"), + "provider_identity_sha256": sha("provider"), + "worker_identity_sha256": sha("worker"), + "live_provider_observation_sha256": sha("observation"), + "admitted_runtime_sha256": sha("runtime"), + "run_id": "100", + "run_attempt": "1", + "start_id_sha256": sha("start"), + "task_id_sha256": sha("task"), + "task_condition_sha256": sha("condition"), + "capability_handle_sha256": sha("capability"), + } + + +def terminal(*, process: bool = True) -> dict: + value = { + "schema_version": worker.TERMINAL_SCHEMA, + "receipt_id_sha256": sha("pending"), + "worker_admission_sha256": sha("admission"), + "dispatch_id_sha256": sha("dispatch"), + "provider_identity_sha256": sha("provider"), + "worker_identity_sha256": sha("worker"), + "live_provider_observation_sha256": sha("observation"), + "admitted_runtime_sha256": sha("runtime"), + "run_id": "100", + "run_attempt": "1", + "start_id_sha256": sha("start"), + "task_id_sha256": sha("task"), + "task_condition_sha256": sha("condition"), + "capability_handle_sha256": sha("capability"), + "launch_attempt": { + "attempted_at": "2026-08-27T11:59:59Z", + "host_identity_sha256": sha("host"), + "executable_sha256": sha("executable"), + "capability_handle_sha256": sha("capability"), + "evidence_sha256": sha("launch-evidence"), + "child_created": process, + "failure_classification": "PROCESS_START_FAILED" if not process else None, + }, + "launch_attempt_sha256": sha("pending-launch"), + "process": None, + "oracle_sha256": sha("oracle"), + "result_sha256": sha("result"), + "log_sha256": sha("log"), + "burned_identities_sha256": sha("burned"), + "burn_ledger_revision": 7, + "burn_receipt_sha256": sha("burn-receipt"), + "burned_at": "2026-08-27T12:01:00Z", + "ledger_readback_sha256": sha("readback"), + "effect_started": False, + "delivery_state": "not_started", + "terminal_state": "SAFE_HALT" if process else "PRELAUNCH_QUARANTINED", + "exit_code": None, + "uncertainty_sha256": None, + "quarantine": { + "active": True, + "reason_code": "PROCESS_START_FAILED" if not process else "SAFE_HALT", + "evidence_sha256": sha("launch-evidence") if not process else sha("quarantine"), + }, + "completed_at": "2026-08-27T12:01:01Z", + "issuer": { + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "workflow": ".github/workflows/issue-qualification-worker-terminal-receipt.yml", + "ref": "refs/heads/main", + "source_commit": "a" * 40, + "environment": "qualification-worker-terminal-receipt", + }, + } + if process: + value["process"] = { + "pid": 4242, + "process_group_id": 4242, + "process_start_ticks": "987654321", + "launched_at": "2026-08-27T12:01:00Z", + "executable_sha256": sha("executable"), + "process_start_identity_sha256": sha("pending-process"), + } + value["process"]["process_start_identity_sha256"] = worker.digest( + worker.PROCESS_START_DOMAIN, worker.process_start_projection(value) + ) + value["launch_attempt_sha256"] = worker.digest( + worker.LAUNCH_ATTEMPT_DOMAIN, worker.launch_attempt_projection(value) + ) + value["receipt_id_sha256"] = worker.digest( + worker.TERMINAL_RECEIPT_DOMAIN, worker.terminal_receipt_projection(value) + ) + return value + + +def refresh(value: dict) -> None: + if value["process"] is not None: + value["process"]["process_start_identity_sha256"] = worker.digest( + worker.PROCESS_START_DOMAIN, worker.process_start_projection(value) + ) + value["launch_attempt_sha256"] = worker.digest( + worker.LAUNCH_ATTEMPT_DOMAIN, worker.launch_attempt_projection(value) + ) + value["receipt_id_sha256"] = worker.digest( + worker.TERMINAL_RECEIPT_DOMAIN, worker.terminal_receipt_projection(value) + ) + + +class QualificationWorkerContractTests(unittest.TestCase): + def test_terminal_binds_admission_dispatch_and_kernel_process_generation(self) -> None: + value = terminal() + self.assertEqual( + worker.validate_terminal_bindings( + value, admission=admission(), dispatch=dispatch() + ), + value, + ) + self.assertEqual(value["process"]["process_start_ticks"], "987654321") + + def test_pid_reuse_or_process_start_tick_tamper_is_refused(self) -> None: + original = terminal() + tampered = copy.deepcopy(original) + tampered["process"]["process_start_ticks"] = "987654322" + with self.assertRaisesRegex( + worker.QualificationWorkerContractError, "process start identity" + ): + worker.validate_terminal_receipt(tampered) + + conflicting = copy.deepcopy(original) + conflicting["process"]["process_start_ticks"] = "987654322" + refresh(conflicting) + worker.validate_terminal_receipt(conflicting) + with self.assertRaisesRegex( + worker.QualificationWorkerContractError, "conflicting terminal" + ): + worker.validate_terminal_replay(original, conflicting) + + def test_capability_burn_must_precede_process_launch(self) -> None: + launched_before_burn = terminal() + launched_before_burn["process"]["launched_at"] = "2026-08-27T11:59:59Z" + refresh(launched_before_burn) + with self.assertRaisesRegex( + worker.QualificationWorkerContractError, "time order" + ): + worker.validate_terminal_receipt(launched_before_burn) + + launch_attempt_after_burn = terminal() + launch_attempt_after_burn["launch_attempt"]["attempted_at"] = ( + "2026-08-27T12:00:01Z" + ) + launch_attempt_after_burn["process"]["launched_at"] = ( + "2026-08-27T12:00:02Z" + ) + refresh(launch_attempt_after_burn) + with self.assertRaisesRegex( + worker.QualificationWorkerContractError, "time order" + ): + worker.validate_terminal_receipt(launch_attempt_after_burn) + + def test_dispatch_id_uniquely_selects_one_terminal_receipt(self) -> None: + previous = terminal() + for field, changed_value in ( + ("worker_admission_sha256", sha("other-admission")), + ("run_id", "101"), + ("start_id_sha256", sha("other-start")), + ): + current = copy.deepcopy(previous) + current[field] = changed_value + refresh(current) + worker.validate_terminal_receipt(current) + with self.assertRaisesRegex( + worker.QualificationWorkerContractError, + "one worker dispatch has conflicting terminal receipts", + ): + worker.validate_terminal_replay(previous, current) + + def test_prelaunch_failure_is_a_bound_quarantined_terminal_receipt(self) -> None: + value = terminal(process=False) + self.assertEqual(worker.validate_terminal_receipt(value), value) + changed = copy.deepcopy(value) + changed["quarantine"]["active"] = False + refresh(changed) + with self.assertRaisesRegex( + worker.QualificationWorkerContractError, "prelaunch terminal" + ): + worker.validate_terminal_receipt(changed) + + def test_prelaunch_refuses_post_pid_classification_and_split_evidence(self) -> None: + identity_unavailable = terminal(process=False) + identity_unavailable["launch_attempt"]["failure_classification"] = ( + "PROCESS_IDENTITY_UNAVAILABLE" + ) + identity_unavailable["quarantine"]["reason_code"] = ( + "PROCESS_IDENTITY_UNAVAILABLE" + ) + refresh(identity_unavailable) + with self.assertRaisesRegex( + worker.QualificationWorkerContractError, "prelaunch terminal" + ): + worker.validate_terminal_receipt(identity_unavailable) + + process_exists = terminal(process=True) + process_exists["terminal_state"] = "PRELAUNCH_QUARANTINED" + refresh(process_exists) + with self.assertRaisesRegex( + worker.QualificationWorkerContractError, "cannot contain a process" + ): + worker.validate_terminal_receipt(process_exists) + + split_evidence = terminal(process=False) + split_evidence["quarantine"]["evidence_sha256"] = sha("other-evidence") + refresh(split_evidence) + with self.assertRaisesRegex( + worker.QualificationWorkerContractError, "prelaunch terminal" + ): + worker.validate_terminal_receipt(split_evidence) + + def test_cross_contract_identity_drift_is_refused(self) -> None: + changed_dispatch = dispatch() + changed_dispatch["live_provider_observation_sha256"] = sha("other") + with self.assertRaisesRegex( + worker.QualificationWorkerContractError, + "live_provider_observation_sha256 differs", + ): + worker.validate_terminal_bindings( + terminal(), admission=admission(), dispatch=changed_dispatch + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_qualification_worker_task_contract.py b/tests/test_qualification_worker_task_contract.py new file mode 100644 index 0000000..0b2f583 --- /dev/null +++ b/tests/test_qualification_worker_task_contract.py @@ -0,0 +1,241 @@ +"""Tests for qualification worker task and condition identities.""" + +from __future__ import annotations + +import copy +import hashlib +import json +import sys +import unittest +from pathlib import Path + +from jsonschema import Draft202012Validator + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "scripts")) + +import qualification_worker_task_contract as task_contract + + +def sha(label: str) -> str: + return "sha256:" + hashlib.sha256(label.encode()).hexdigest() + + +def selector() -> dict: + value = { + "schema_version": task_contract.TASK_SELECTOR_SCHEMA, + "campaign_artifact_sha256": sha("campaign"), + "task_source_sha256": sha("private-task-source"), + "task_ordinal": 7, + "task_id_sha256": sha("pending-task"), + } + value["task_id_sha256"] = task_contract.digest( + task_contract.TASK_SELECTOR_DOMAIN, + task_contract.task_selector_projection(value), + ) + return value + + +def condition(task: dict | None = None) -> dict: + selected_task = selector() if task is None else task + value = { + "schema_version": task_contract.TASK_CONDITION_SCHEMA, + "task_id_sha256": selected_task["task_id_sha256"], + "condition_source_sha256": sha("private-condition-source"), + "condition_ordinal": 3, + "task_condition_sha256": sha("pending-condition"), + } + value["task_condition_sha256"] = task_contract.digest( + task_contract.TASK_CONDITION_DOMAIN, + task_contract.task_condition_projection(value), + ) + return value + + +def schema(name: str) -> dict: + return json.loads((ROOT / "schemas" / name).read_text()) + + +class QualificationWorkerTaskContractTests(unittest.TestCase): + def test_domains_projections_and_digests_are_frozen(self) -> None: + selected_task = selector() + selected_condition = condition(selected_task) + self.assertEqual( + task_contract.TASK_SELECTOR_DOMAIN, + b"OpenAdapt qualification worker task selector v1\0", + ) + self.assertEqual( + task_contract.TASK_CONDITION_DOMAIN, + b"OpenAdapt qualification worker task condition v1\0", + ) + self.assertEqual( + task_contract.canonical( + task_contract.task_selector_projection(selected_task) + ), + ( + b'{"campaign_artifact_sha256":"sha256:3dc260b2472062d9c57b' + b'd930b02f23831d917b8f3e3234b6d63964a53c31d3aa","schema_version"' + b':"openadapt.qualification-worker-task-selector/v1","task_ordinal"' + b':7,"task_source_sha256":"sha256:8057301060c8a6a36e2b425e4d26' + b'eb10a6d110cdaffe5c80ebe1c515286c3e17"}' + ), + ) + self.assertEqual( + selected_task["task_id_sha256"], + "sha256:3e688825cd4e6ce312d15f47ddb1d3fa57441e3b0c3a5ca9ccc2f1f285a52cc7", + ) + self.assertEqual( + task_contract.canonical( + task_contract.task_condition_projection(selected_condition) + ), + ( + b'{"condition_ordinal":3,"condition_source_sha256":"sha256:' + b'b9079b50782122c328dae360cb2d9766601668d1e1842bc4f73d0a0404b' + b'f38df","schema_version":"openadapt.qualification-worker-task-condition' + b'/v1","task_id_sha256":"sha256:3e688825cd4e6ce312d15f47ddb1d3' + b'fa57441e3b0c3a5ca9ccc2f1f285a52cc7"}' + ), + ) + self.assertEqual( + selected_condition["task_condition_sha256"], + "sha256:e09987d522fd290c2d07994428027bebd5deabcf2156258dd9633a465095b16f", + ) + + def test_closed_objects_validate_and_cross_bind(self) -> None: + selected_task = selector() + selected_condition = condition(selected_task) + self.assertEqual( + task_contract.validate_task_contract( + selected_task, + selected_condition, + campaign_artifact_sha256=sha("campaign"), + ), + (selected_task, selected_condition), + ) + Draft202012Validator( + schema("qualification-worker-task-selector.schema.json") + ).validate(selected_task) + Draft202012Validator( + schema("qualification-worker-task-condition.schema.json") + ).validate(selected_condition) + + def test_task_selector_tamper_is_refused(self) -> None: + for field, replacement in ( + ("campaign_artifact_sha256", sha("other-campaign")), + ("task_source_sha256", sha("other-task-source")), + ("task_ordinal", 8), + ): + changed = copy.deepcopy(selector()) + changed[field] = replacement + with self.subTest(field=field), self.assertRaisesRegex( + task_contract.QualificationWorkerTaskContractError, + "task identity differs", + ): + task_contract.validate_task_selector(changed) + + def test_task_condition_tamper_is_refused(self) -> None: + for field, replacement in ( + ("task_id_sha256", sha("other-task")), + ("condition_source_sha256", sha("other-condition-source")), + ("condition_ordinal", 4), + ): + changed = copy.deepcopy(condition()) + changed[field] = replacement + with self.subTest(field=field), self.assertRaisesRegex( + task_contract.QualificationWorkerTaskContractError, + "task condition identity differs", + ): + task_contract.validate_task_condition(changed) + + def test_recomputed_cross_contract_substitution_is_refused(self) -> None: + selected_task = selector() + different_task = copy.deepcopy(selected_task) + different_task["task_ordinal"] = 8 + different_task["task_id_sha256"] = task_contract.digest( + task_contract.TASK_SELECTOR_DOMAIN, + task_contract.task_selector_projection(different_task), + ) + substituted = condition(different_task) + task_contract.validate_task_condition(substituted) + with self.assertRaisesRegex( + task_contract.QualificationWorkerTaskContractError, + "selects a different task identity", + ): + task_contract.validate_task_condition_binding( + selected_task, substituted + ) + + different_campaign = copy.deepcopy(selected_task) + different_campaign["campaign_artifact_sha256"] = sha("other-campaign") + different_campaign["task_id_sha256"] = task_contract.digest( + task_contract.TASK_SELECTOR_DOMAIN, + task_contract.task_selector_projection(different_campaign), + ) + different_campaign_condition = condition(different_campaign) + with self.assertRaisesRegex( + task_contract.QualificationWorkerTaskContractError, + "different campaign artifact", + ): + task_contract.validate_task_contract( + different_campaign, + different_campaign_condition, + campaign_artifact_sha256=sha("campaign"), + ) + + def test_invalid_digest_ordinal_and_shape_are_refused(self) -> None: + for invalid in (0, -1, True, "1"): + changed = selector() + changed["task_ordinal"] = invalid + with self.subTest(invalid=invalid), self.assertRaisesRegex( + task_contract.QualificationWorkerTaskContractError, + "positive integer", + ): + task_contract.validate_task_selector(changed) + + changed_condition = condition() + changed_condition["condition_ordinal"] = invalid + with self.subTest(condition_invalid=invalid), self.assertRaisesRegex( + task_contract.QualificationWorkerTaskContractError, + "positive integer", + ): + task_contract.validate_task_condition(changed_condition) + + invalid_digest = selector() + invalid_digest["task_source_sha256"] = "sha256:" + "A" * 64 + with self.assertRaisesRegex( + task_contract.QualificationWorkerTaskContractError, + "lowercase sha256 digest", + ): + task_contract.validate_task_selector(invalid_digest) + + extra = condition() + extra["private_condition"] = "must-not-egress" + with self.assertRaisesRegex( + task_contract.QualificationWorkerTaskContractError, + "must contain exactly", + ): + task_contract.validate_task_condition(extra) + + def test_json_schemas_refuse_extra_fields_and_nonpositive_ordinals(self) -> None: + selector_validator = Draft202012Validator( + schema("qualification-worker-task-selector.schema.json") + ) + condition_validator = Draft202012Validator( + schema("qualification-worker-task-condition.schema.json") + ) + + changed_selector = selector() + changed_selector["task_ordinal"] = 0 + self.assertTrue(list(selector_validator.iter_errors(changed_selector))) + + changed_condition = condition() + changed_condition["condition_ordinal"] = True + self.assertTrue(list(condition_validator.iter_errors(changed_condition))) + + changed_condition = condition() + changed_condition["raw_condition"] = {"secret": "must-not-egress"} + self.assertTrue(list(condition_validator.iter_errors(changed_condition))) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_support_release_and_recovery_contracts.py b/tests/test_support_release_and_recovery_contracts.py new file mode 100644 index 0000000..b80a727 --- /dev/null +++ b/tests/test_support_release_and_recovery_contracts.py @@ -0,0 +1,1075 @@ +"""Focused fail-closed tests for Support releases and publication recovery.""" + +from __future__ import annotations + +import copy +import hashlib +import json +import sys +import unittest +from datetime import datetime, timezone +from pathlib import Path +from unittest import mock + + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "scripts")) + +import production_trust as trust # noqa: E402 +import validate_evidence_registry as registry # noqa: E402 + + +NOW = datetime(2026, 8, 27, 12, 4, 59, tzinfo=timezone.utc) + + +def sha(label: str) -> str: + return "sha256:" + hashlib.sha256(label.encode("utf-8")).hexdigest() + + +def evidence_entry( + kind: str, + *, + label: str, + subject_sha256: str | None = None, +) -> dict: + schema, media = registry.OBJECT_KIND_CONTRACTS[kind] + object_sha256 = sha(f"object:{label}") + digest_hex = object_sha256.removeprefix("sha256:") + entry = { + "kind": kind, + "object_schema_version": schema, + "object_path": ( + f"production-evidence/objects/sha256/{digest_hex[:2]}/" + f"{digest_hex}.{kind}.json" + ), + "object_sha256": object_sha256, + "size_bytes": 100, + "object_media_type": media, + "semantic_identity_sha256": sha(f"identity:{label}"), + "subject_sha256": subject_sha256, + } + entry["registry_entry_sha256"] = registry.entry_digest(entry) + return entry + + +def evidence_reference( + kind: str = "qualification-release", + *, + object_sha256: str | None = None, + object_value: object | None = None, +) -> dict: + entry = evidence_entry(kind, label=f"reference:{kind}") + if object_sha256 is not None: + entry["object_sha256"] = object_sha256 + digest_hex = object_sha256.removeprefix("sha256:") + entry["object_path"] = ( + f"production-evidence/objects/sha256/{digest_hex[:2]}/" + f"{digest_hex}.{kind}.json" + ) + if object_value is not None: + entry["semantic_identity_sha256"] = registry.semantic_identity_digest( + kind=kind, + object_schema_version=entry["object_schema_version"], + object_value=object_value, + object_sha256=object_sha256, + ) + entry["registry_entry_sha256"] = registry.entry_digest(entry) + return { + "schema_version": registry.REFERENCE_SCHEMA, + "repository": registry.REPOSITORY, + "repository_id": registry.REPOSITORY_ID, + "repository_owner_id": registry.REPOSITORY_OWNER_ID, + "registry_source_commit": "a" * 40, + "registry_revision": 3, + "registry_head_sha256": sha("registry-head"), + **entry, + } + + +def tag_rulesets( + repository: str = "OpenAdaptAI/openadapt-tray", + repository_id: str = "1136122737", +) -> list[dict]: + common = { + "schema_version": "openadapt.production-release-tag-ruleset/v1", + "repository": repository, + "repository_id": repository_id, + "target": "tag", + "enforcement": "active", + "conditions": {"ref_name": {"include": ["refs/tags/v*"], "exclude": []}}, + } + return [ + { + **common, + "role": "creation_authority", + "ruleset_id": "10", + "name": "OpenAdapt policy: release tag creation", + "bypass_actors": [ + { + "actor_id": "4730708", + "actor_type": "Integration", + "bypass_mode": "always", + } + ], + "rules": [{"type": "creation"}], + }, + { + **common, + "role": "immutability", + "ruleset_id": "11", + "name": "OpenAdapt policy: immutable release tags", + "bypass_actors": [], + "rules": [ + {"type": "deletion"}, + {"type": "non_fast_forward"}, + { + "type": "update", + "parameters": {"update_allows_fetch_and_merge": False}, + }, + ], + }, + ] + + +def support_artifacts() -> list[dict]: + return [ + { + "name": "openadapt_tray-1.0.0.tar.gz", + "kind": "python-sdist", + "sha256": sha("tray-sdist"), + "size_bytes": 101, + "media_type": "application/gzip", + "publish_destinations": ["github-release", "pypi"], + }, + { + "name": "openadapt_tray-1.0.0-py3-none-any.whl", + "kind": "python-wheel", + "sha256": sha("tray-wheel"), + "size_bytes": 102, + "media_type": "application/zip", + "publish_destinations": ["github-release", "pypi"], + }, + ] + + +def refresh_staging(staging: dict) -> None: + staging["immutable_releases_sha256"] = trust.digest_bytes( + trust.IMMUTABLE_RELEASES_DOMAIN, staging["immutable_releases"] + ) + staging["tag_rulesets_sha256"] = trust.digest_bytes( + trust.TAG_RULESETS_DOMAIN, staging["tag_rulesets"] + ) + staging["tag_ref_state_sha256"] = trust.digest_bytes( + trust.TAG_REF_STATE_DOMAIN, staging["tag_ref_state"] + ) + + +def refresh_support_admission(admission: dict) -> None: + release = admission["release"] + admission["release_sha256"] = trust.digest_bytes( + trust.SUPPORT_RELEASE_DOMAIN, + { + "support_target": admission["support_target"], + "claim_scope": admission["claim_scope"], + "release": release, + }, + ) + admission["artifact_inventory_sha256"] = trust.digest_bytes( + trust.SUPPORT_ARTIFACT_INVENTORY_DOMAIN, + { + "support_target": admission["support_target"], + "claim_scope": admission["claim_scope"], + "artifacts": release["artifacts"], + }, + ) + refresh_staging(admission["publication_staging"]) + admission["publication_staging_sha256"] = trust.staging_digest( + admission["publication_staging"] + ) + projection = dict(admission) + projection.pop("admission_id_sha256", None) + admission["admission_id_sha256"] = trust.digest_bytes( + trust.SUPPORT_RELEASE_ADMISSION_DOMAIN, projection + ) + + +def support_admission() -> dict: + artifacts = support_artifacts() + staged_assets = [ + { + "asset_id": str(30 + index), + **artifact, + "uploader_id": "321543906", + "uploader_login": "openadapt-release[bot]", + } + for index, artifact in enumerate( + sorted(artifacts, key=lambda item: item["name"]) + ) + ] + staging = { + "schema_version": "openadapt.production-release-staging-evidence/v1", + "repository": "OpenAdaptAI/openadapt-tray", + "repository_id": "1136122737", + "draft_release_id": "20", + "tag": "v1.0.0", + "target_commitish": "b" * 40, + "draft": True, + "prerelease": False, + "release_app_id": "4730708", + "release_app_installation_id": "156835568", + "release_app_bot_user_id": "321543906", + "release_author_login": "openadapt-release[bot]", + "assets": staged_assets, + "immutable_releases": {"enabled": True, "enforced_by_owner": False}, + "immutable_releases_sha256": sha("placeholder"), + "tag_rulesets": tag_rulesets(), + "tag_rulesets_sha256": sha("placeholder"), + "tag_ref_state": {"ref": "refs/tags/v1.0.0", "exists": False}, + "tag_ref_state_sha256": sha("placeholder"), + "observed_at": "2026-08-27T12:00:00Z", + } + release = { + "schema_version": "openadapt.production-release-candidate/v1", + "kind": "package", + "source_repository": "OpenAdaptAI/openadapt-tray", + "source_repository_id": "1136122737", + "source_commit": "b" * 40, + "version": "1.0.0", + "tag": "v1.0.0", + "deployment_id": None, + "deployment_sha256": None, + "artifacts": artifacts, + } + admission = { + "schema_version": "openadapt.support-release-admission/v1", + "admission_id_sha256": sha("placeholder"), + "lifecycle_state": "Support", + "support_target": "openadapt-tray", + "verdict": "accepted", + "claim_scope": "support_openadapt_tray", + "release_identity": { + "schema_version": "openadapt.monotonic-support-release/v1", + "channel": "support", + "sequence": 1, + "previous_admission_sha256": None, + }, + "release": release, + "release_sha256": sha("placeholder"), + "artifact_inventory_sha256": sha("placeholder"), + "publication_staging": staging, + "publication_staging_sha256": sha("placeholder"), + "authority_state_sha256": sha("authority-current"), + "revocation_state_sha256": sha("revocation-current"), + "signer_registry_sha256": sha("signer-current"), + "support_policy_sha256": trust.digest_bytes( + trust.SUPPORT_POLICY_DOMAIN, + json.loads((ROOT / "support-release-policy.json").read_text()), + ), + "issued_at": "2026-08-27T12:00:00Z", + "not_before": "2026-08-27T12:00:00Z", + "expires_at": "2026-09-03T12:00:00Z", + "issuer": { + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "workflow": ".github/workflows/issue-support-release-admission.yml", + "ref": "refs/heads/main", + "source_commit": "c" * 40, + "environment": "support-release-admission", + }, + } + refresh_support_admission(admission) + return admission + + +def refresh_recovery_authorization(authorization: dict) -> None: + projection = { + "qualification_release_object_sha256": authorization[ + "qualification_release_object_sha256" + ], + "repository": authorization["repository"], + "repository_id": authorization["repository_id"], + "target": authorization["target"], + "tag": authorization["tag"], + "tag_ref": authorization["tag_ref"], + "tag_object_id": authorization["tag_object_id"], + "target_commit": authorization["target_commit"], + "draft_release_id": authorization["draft_release_id"], + "release_sha256": authorization["release_sha256"], + "artifact_inventory_sha256": authorization["artifact_inventory_sha256"], + "publication_staging_sha256": authorization["publication_staging_sha256"], + "run_id": authorization["run_id"], + "run_attempt": authorization["run_attempt"], + "dispatcher_actor_id": authorization["dispatcher_actor_id"], + "requested_effect": authorization["requested_effect"], + "environment": authorization["environment"], + "release_app": authorization["release_app"], + } + authorization["idempotency_key"] = "publication-recovery:" + hashlib.sha256( + trust.PUBLICATION_RECOVERY_IDEMPOTENCY_DOMAIN + trust.canonical(projection) + ).hexdigest() + identity_projection = dict(authorization) + identity_projection.pop("authorization_id_sha256", None) + authorization["authorization_id_sha256"] = trust.digest_bytes( + trust.PUBLICATION_RECOVERY_AUTHORIZATION_DOMAIN, identity_projection + ) + + +def recovery_authorization() -> dict: + release_reference = evidence_reference() + authorization = { + "schema_version": ( + "openadapt.production-publication-recovery-authorization/v1" + ), + "authorization_id_sha256": sha("placeholder"), + "qualification_release_reference": release_reference, + "qualification_release_object_sha256": release_reference["object_sha256"], + "release_sha256": sha("capture-release"), + "artifact_inventory_sha256": sha("capture-inventory"), + "publication_staging_sha256": sha("capture-staging"), + "repository": "OpenAdaptAI/openadapt-capture", + "repository_id": "1115283835", + "target": "capture", + "tag": "v1.0.0", + "tag_ref": "refs/tags/v1.0.0", + "tag_object_id": "d" * 40, + "target_commit": "b" * 40, + "draft_release_id": "20", + "requested_effect": "publish-github-release", + "run_id": "100", + "run_attempt": "1", + "dispatcher_actor_id": "774615", + "environment": "release-identity", + "release_app": { + "app_id": "4730708", + "installation_id": "156835568", + "bot_user_id": "321543906", + "bot_login": "openadapt-release[bot]", + }, + "idempotency_key": "publication-recovery:" + "0" * 64, + "issued_at": "2026-08-27T12:00:00Z", + "not_before": "2026-08-27T12:00:00Z", + "expires_at": "2026-08-27T12:05:00Z", + "issuer": { + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "workflow": ( + ".github/workflows/issue-production-publication-recovery.yml" + ), + "ref": "refs/heads/main", + "source_commit": "c" * 40, + "environment": "release-identity", + }, + } + refresh_recovery_authorization(authorization) + return authorization + + +class SupportReleaseAdmissionTests(unittest.TestCase): + def test_exact_tray_wheel_and_sdist_app_staged_draft_is_valid(self) -> None: + value = support_admission() + self.assertEqual(trust.validate_support_release(value, now=NOW), value) + self.assertEqual( + [item["kind"] for item in value["release"]["artifacts"]], + ["python-sdist", "python-wheel"], + ) + self.assertTrue(value["publication_staging"]["draft"]) + self.assertEqual( + { + item["uploader_login"] + for item in value["publication_staging"]["assets"] + }, + {"openadapt-release[bot]"}, + ) + + def test_support_is_not_an_eighth_production_target(self) -> None: + self.assertNotIn("openadapt-tray", trust.TARGETS) + value = support_admission() + value.pop("lifecycle_state") + value.pop("support_target") + value["schema_version"] = "openadapt.qualification-release/v1" + value["target"] = "openadapt-tray" + value["production_acceptance_summary_reference"] = evidence_reference( + "production-acceptance-summary" + ) + summary_bundle = evidence_reference( + "production-acceptance-summary-sigstore-bundle" + ) + summary_bundle["subject_sha256"] = value[ + "production_acceptance_summary_reference" + ]["object_sha256"] + projection = { + field: summary_bundle[field] for field in registry.ENTRY_FIELDS + } + summary_bundle["registry_entry_sha256"] = registry.entry_digest(projection) + value["production_acceptance_summary_bundle_reference"] = summary_bundle + value["publication_policy_sha256"] = value.pop("support_policy_sha256") + with self.assertRaisesRegex(trust.TrustError, "target is invalid"): + trust.validate_release(value) + + def test_missing_extra_and_cross_profile_artifacts_are_refused(self) -> None: + cases: list[tuple[str, object]] = [ + ( + "missing wheel", + lambda value: value["release"]["artifacts"].pop(), + ), + ( + "extra wheel", + lambda value: value["release"]["artifacts"].append( + { + "name": "extra.whl", + "kind": "python-wheel", + "sha256": sha("extra"), + "size_bytes": 1, + "media_type": "application/zip", + "publish_destinations": ["github-release", "pypi"], + } + ), + ), + ( + "wheel media", + lambda value: value["release"]["artifacts"][1].__setitem__( + "media_type", "application/gzip" + ), + ), + ( + "sdist destination", + lambda value: value["release"]["artifacts"][0].__setitem__( + "publish_destinations", ["github-release"] + ), + ), + ( + "foreign kind", + lambda value: value["release"]["artifacts"][0].__setitem__( + "kind", "spdx-sbom" + ), + ), + ] + for label, mutate in cases: + with self.subTest(label=label): + value = support_admission() + mutate(value) + with self.assertRaises(trust.TrustError): + trust.validate_support_release(value) + + def test_staged_inventory_must_exactly_match_admitted_inventory(self) -> None: + value = support_admission() + value["publication_staging"]["assets"][0]["sha256"] = sha( + "other-staged-bytes" + ) + refresh_support_admission(value) + with self.assertRaisesRegex(trust.TrustError, "staged assets differ"): + trust.validate_support_release(value) + + def test_wrong_app_ruleset_tag_and_staging_state_are_refused(self) -> None: + cases: list[tuple[str, object]] = [ + ( + "release App", + lambda value: value["publication_staging"].__setitem__( + "release_app_id", "1" + ), + ), + ( + "asset uploader", + lambda value: value["publication_staging"]["assets"][0].__setitem__( + "uploader_id", "1" + ), + ), + ( + "creation ruleset", + lambda value: value["publication_staging"]["tag_rulesets"][0][ + "bypass_actors" + ][0].__setitem__("actor_id", "1"), + ), + ( + "immutability ruleset", + lambda value: value["publication_staging"]["tag_rulesets"][1][ + "rules" + ].pop(), + ), + ( + "existing tag", + lambda value: value["publication_staging"]["tag_ref_state"].__setitem__( + "exists", True + ), + ), + ( + "immutable releases disabled", + lambda value: value["publication_staging"][ + "immutable_releases" + ].__setitem__("enabled", False), + ), + ( + "not a draft", + lambda value: value["publication_staging"].__setitem__( + "draft", False + ), + ), + ( + "prerelease", + lambda value: value["publication_staging"].__setitem__( + "prerelease", True + ), + ), + ( + "wrong repository", + lambda value: value["publication_staging"].__setitem__( + "repository", "OpenAdaptAI/openadapt-flow" + ), + ), + ( + "wrong target commit", + lambda value: value["publication_staging"].__setitem__( + "target_commitish", "d" * 40 + ), + ), + ] + for label, mutate in cases: + with self.subTest(label=label): + value = support_admission() + mutate(value) + with self.assertRaises(trust.TrustError): + trust.validate_support_release(value) + + def test_window_is_half_open_and_limited_to_seven_days(self) -> None: + value = support_admission() + trust.validate_support_release( + value, + now=datetime(2026, 8, 27, 12, 0, 0, tzinfo=timezone.utc), + ) + with self.assertRaisesRegex(trust.TrustError, "not active"): + trust.validate_support_release( + value, + now=datetime(2026, 9, 3, 12, 0, 0, tzinfo=timezone.utc), + ) + value["expires_at"] = "2026-09-03T12:00:01Z" + refresh_support_admission(value) + with self.assertRaisesRegex(trust.TrustError, "validity window"): + trust.validate_support_release(value) + + def test_signer_and_revocation_digests_are_admission_identity_bound(self) -> None: + for field in ( + "authority_state_sha256", + "revocation_state_sha256", + "signer_registry_sha256", + ): + with self.subTest(field=field): + value = support_admission() + value[field] = sha(f"changed:{field}") + with self.assertRaisesRegex(trust.TrustError, "admission id"): + trust.validate_support_release(value) + + def test_support_release_requires_exact_current_trust_state(self) -> None: + value = support_admission() + signer_registry = { + "schema_version": "test-signer-registry", + "revision": 7, + "signers": [ + { + "status": "active", + "public_key_sha256": sha("current-signer-key"), + } + ], + } + signer_identity = registry.signer_registry_identity_digest(signer_registry) + signer_raw_sha = "sha256:" + hashlib.sha256( + registry.canonical(signer_registry) + b"\n" + ).hexdigest() + authority = { + "authority_state_sha256": sha("current-authority"), + "signer_registry_sha256": signer_raw_sha, + "signer_registry_identity_sha256": signer_identity, + "signer_registry_revision": 7, + } + revocation = { + "revocation_state_sha256": sha("current-revocation"), + "authority_state_sha256": authority["authority_state_sha256"], + "signer_registry_sha256": signer_identity, + "revocations": [], + } + value["authority_state_sha256"] = authority["authority_state_sha256"] + value["revocation_state_sha256"] = revocation[ + "revocation_state_sha256" + ] + value["signer_registry_sha256"] = signer_identity + refresh_support_admission(value) + object_sha256 = "sha256:" + hashlib.sha256( + trust.canonical(value) + b"\n" + ).hexdigest() + admission_reference = evidence_reference( + "support-release-admission", + object_sha256=object_sha256, + object_value=value, + ) + patches = ( + mock.patch.object( + registry, "validate_signer_registry", return_value=signer_registry + ), + mock.patch.object( + trust, "validate_authority_state", return_value=authority + ), + mock.patch.object( + trust, "validate_revocation_state", return_value=revocation + ), + mock.patch.object(trust, "verify_embedded_signature"), + ) + with patches[0], patches[1], patches[2], patches[3]: + self.assertEqual( + trust.validate_admission_current_state( + value, + admission_reference=admission_reference, + authority_state=authority, + revocation_state=revocation, + signer_registry=signer_registry, + now=NOW, + ), + value, + ) + for field in ( + "authority_state_sha256", + "revocation_state_sha256", + "signer_registry_sha256", + ): + with self.subTest(field=field): + changed = copy.deepcopy(value) + changed[field] = sha(f"not-current:{field}") + refresh_support_admission(changed) + changed_sha = "sha256:" + hashlib.sha256( + trust.canonical(changed) + b"\n" + ).hexdigest() + changed_reference = evidence_reference( + "support-release-admission", + object_sha256=changed_sha, + object_value=changed, + ) + with self.assertRaisesRegex(trust.TrustError, "current"): + trust.validate_admission_current_state( + changed, + admission_reference=changed_reference, + authority_state=authority, + revocation_state=revocation, + signer_registry=signer_registry, + now=NOW, + ) + + changed_identity_reference = copy.deepcopy(admission_reference) + changed_identity_reference["semantic_identity_sha256"] = sha( + "unbound-admission-identity" + ) + projection = { + field: changed_identity_reference[field] + for field in registry.ENTRY_FIELDS + } + changed_identity_reference["registry_entry_sha256"] = ( + registry.entry_digest(projection) + ) + with self.assertRaisesRegex( + trust.TrustError, "semantic identity|reference differs" + ): + trust.validate_admission_current_state( + value, + admission_reference=changed_identity_reference, + authority_state=authority, + revocation_state=revocation, + signer_registry=signer_registry, + now=NOW, + ) + + revocation["revocations"] = [ + { + "subject_kind": "support-release-admission", + "subject_id": admission_reference[ + "semantic_identity_sha256" + ], + } + ] + with self.assertRaisesRegex(trust.TrustError, "admission is revoked"): + trust.validate_admission_current_state( + value, + admission_reference=admission_reference, + authority_state=authority, + revocation_state=revocation, + signer_registry=signer_registry, + now=NOW, + ) + revocation["revocations"] = [ + { + "subject_kind": "qualification-signer-key", + "subject_id": signer_registry["signers"][0][ + "public_key_sha256" + ], + } + ] + with self.assertRaisesRegex(trust.TrustError, "signer key is revoked"): + trust.validate_admission_current_state( + value, + admission_reference=admission_reference, + authority_state=authority, + revocation_state=revocation, + signer_registry=signer_registry, + now=NOW, + ) + + def test_support_release_refuses_stale_staging_observation(self) -> None: + boundary = support_admission() + boundary["publication_staging"]["observed_at"] = "2026-08-27T11:00:00Z" + refresh_support_admission(boundary) + trust.validate_support_release(boundary, now=NOW) + + for observed_at in ( + "2026-08-27T10:59:59Z", + "2026-08-27T12:00:01Z", + ): + with self.subTest(observed_at=observed_at): + value = support_admission() + value["publication_staging"]["observed_at"] = observed_at + refresh_support_admission(value) + with self.assertRaisesRegex( + trust.TrustError, "staging observation.*stale" + ): + trust.validate_support_release(value, now=NOW) + + +class PublicationRecoveryAuthorizationTests(unittest.TestCase): + def test_exact_five_minute_single_attempt_authorization_is_valid(self) -> None: + value = recovery_authorization() + self.assertEqual( + trust.validate_publication_recovery_authorization(value, now=NOW), + value, + ) + + def test_five_minute_window_is_half_open_and_cannot_be_longer(self) -> None: + value = recovery_authorization() + with self.assertRaisesRegex(trust.TrustError, "not active"): + trust.validate_publication_recovery_authorization( + value, + now=datetime(2026, 8, 27, 12, 5, 0, tzinfo=timezone.utc), + ) + value["expires_at"] = "2026-08-27T12:05:01Z" + refresh_recovery_authorization(value) + with self.assertRaisesRegex(trust.TrustError, "validity window"): + trust.validate_publication_recovery_authorization(value) + + def test_attempt_dispatcher_environment_and_issuer_are_exact(self) -> None: + cases: list[tuple[str, object]] = [ + ("attempt", lambda value: value.__setitem__("run_attempt", "2")), + ( + "dispatcher", + lambda value: value.__setitem__("dispatcher_actor_id", "1"), + ), + ( + "environment", + lambda value: value.__setitem__("environment", "pypi"), + ), + ( + "issuer workflow", + lambda value: value["issuer"].__setitem__( + "workflow", ".github/workflows/release.yml" + ), + ), + ( + "issuer environment", + lambda value: value["issuer"].__setitem__("environment", "pypi"), + ), + ] + for label, mutate in cases: + with self.subTest(label=label): + value = recovery_authorization() + mutate(value) + with self.assertRaises(trust.TrustError): + trust.validate_publication_recovery_authorization(value) + + def test_release_app_identity_is_exact(self) -> None: + for field in ("app_id", "installation_id", "bot_user_id", "bot_login"): + with self.subTest(field=field): + value = recovery_authorization() + value["release_app"][field] = "1" + with self.assertRaisesRegex(trust.TrustError, "release App"): + trust.validate_publication_recovery_authorization(value) + + def test_repository_target_and_release_reference_profiles_are_exact(self) -> None: + cases: list[tuple[str, object]] = [ + ( + "repository", + lambda value: value.__setitem__( + "repository", "OpenAdaptAI/openadapt-flow" + ), + ), + ( + "repository id", + lambda value: value.__setitem__("repository_id", "1"), + ), + ("deployment target", lambda value: value.__setitem__("target", "cloud")), + ( + "reference kind", + lambda value: value.__setitem__( + "qualification_release_reference", + evidence_reference("support-release-admission"), + ), + ), + ( + "reference object", + lambda value: value.__setitem__( + "qualification_release_object_sha256", sha("other-release") + ), + ), + ] + for label, mutate in cases: + with self.subTest(label=label): + value = recovery_authorization() + mutate(value) + with self.assertRaises(trust.TrustError): + trust.validate_publication_recovery_authorization(value) + + def test_tag_and_staging_bindings_are_closed(self) -> None: + cases: list[tuple[str, object]] = [ + ("tag", lambda value: value.__setitem__("tag", "1.0.0")), + ( + "tag ref", + lambda value: value.__setitem__("tag_ref", "refs/tags/v2.0.0"), + ), + ( + "tag object", + lambda value: value.__setitem__("tag_object_id", "main"), + ), + ( + "target commit", + lambda value: value.__setitem__("target_commit", "main"), + ), + ( + "draft release", + lambda value: value.__setitem__("draft_release_id", "0"), + ), + ( + "staging digest", + lambda value: value.__setitem__( + "publication_staging_sha256", "not-a-digest" + ), + ), + ] + for label, mutate in cases: + with self.subTest(label=label): + value = recovery_authorization() + mutate(value) + with self.assertRaises(trust.TrustError): + trust.validate_publication_recovery_authorization(value) + + malformed = recovery_authorization() + malformed["tag"] = "vbad/path" + malformed["tag_ref"] = "refs/tags/vbad/path" + refresh_recovery_authorization(malformed) + with self.assertRaisesRegex(trust.TrustError, "tag"): + trust.validate_publication_recovery_authorization(malformed) + + def test_effect_environment_and_idempotency_key_are_exact(self) -> None: + cases: list[tuple[str, object]] = [ + ("empty", lambda value: value.__setitem__("requested_effect", "")), + ( + "unknown", + lambda value: value.__setitem__("requested_effect", "docker"), + ), + ( + "PyPI environment", + lambda value: ( + value.__setitem__("requested_effect", "publish-pypi"), + value.__setitem__("environment", "release-identity"), + ), + ), + ( + "MCP environment", + lambda value: ( + value.__setitem__("target", "agent"), + value.__setitem__("repository", "OpenAdaptAI/openadapt-agent"), + value.__setitem__("repository_id", "1136136670"), + value.__setitem__("requested_effect", "publish-mcp-registry"), + value.__setitem__("environment", "release-identity"), + ), + ), + ( + "idempotency mismatch", + lambda value: value.__setitem__( + "idempotency_key", "publication-recovery:" + "0" * 64 + ), + ), + ] + for label, mutate in cases: + with self.subTest(label=label): + value = recovery_authorization() + mutate(value) + with self.assertRaises(trust.TrustError): + trust.validate_publication_recovery_authorization(value) + + def test_each_single_effect_has_one_exact_environment(self) -> None: + profiles = { + "stage-draft-assets": "release-identity", + "publish-pypi": "pypi", + "publish-github-release": "release-identity", + } + for effect, environment in profiles.items(): + with self.subTest(effect=effect): + value = recovery_authorization() + value["requested_effect"] = effect + value["environment"] = environment + value["issuer"]["environment"] = environment + refresh_recovery_authorization(value) + trust.validate_publication_recovery_authorization(value, now=NOW) + + agent = recovery_authorization() + agent["target"] = "agent" + agent["repository"] = "OpenAdaptAI/openadapt-agent" + agent["repository_id"] = "1136136670" + agent["requested_effect"] = "publish-mcp-registry" + agent["environment"] = "mcp-registry" + agent["issuer"]["environment"] = "mcp-registry" + refresh_recovery_authorization(agent) + trust.validate_publication_recovery_authorization(agent, now=NOW) + + def test_replay_key_binds_every_effect_critical_recovery_field(self) -> None: + first = recovery_authorization() + mutations: list[tuple[str, object]] = [ + ("target commit", lambda value: value.__setitem__("target_commit", "e" * 40)), + ("draft", lambda value: value.__setitem__("draft_release_id", "21")), + ( + "release", + lambda value: value.__setitem__( + "release_sha256", sha("different-release") + ), + ), + ( + "inventory", + lambda value: value.__setitem__( + "artifact_inventory_sha256", sha("different-inventory") + ), + ), + ( + "staging", + lambda value: value.__setitem__( + "publication_staging_sha256", sha("different-staging") + ), + ), + ("effect", lambda value: value.__setitem__("requested_effect", "stage-draft-assets")), + ("App", lambda value: value["release_app"].__setitem__("installation_id", "1")), + ] + for label, mutate in mutations: + with self.subTest(label=label): + second = copy.deepcopy(first) + mutate(second) + refresh_recovery_authorization(second) + self.assertNotEqual(first["idempotency_key"], second["idempotency_key"]) + + def test_replay_accepts_exact_bytes_and_refuses_one_use_conflict(self) -> None: + first = recovery_authorization() + self.assertEqual( + trust.validate_publication_recovery_replay( + first, copy.deepcopy(first), now=NOW + ), + first, + ) + conflict = copy.deepcopy(first) + conflict["draft_release_id"] = "21" + refresh_recovery_authorization(conflict) + with self.assertRaisesRegex(trust.TrustError, "one-use claim conflicts"): + trust.validate_publication_recovery_replay(first, conflict, now=NOW) + + next_tag = copy.deepcopy(first) + next_tag["tag"] = "v1.0.1" + next_tag["tag_ref"] = "refs/tags/v1.0.1" + next_tag["tag_object_id"] = "e" * 40 + next_tag["target_commit"] = "e" * 40 + next_tag["draft_release_id"] = "21" + next_tag["run_id"] = "101" + refresh_recovery_authorization(next_tag) + self.assertEqual( + trust.validate_publication_recovery_replay(first, next_tag, now=NOW), + next_tag, + ) + + with self.assertRaisesRegex(trust.TrustError, "not active"): + trust.validate_publication_recovery_replay( + first, + copy.deepcopy(first), + now=datetime(2026, 8, 27, 12, 5, 0, tzinfo=timezone.utc), + ) + + +class EvidenceKindProfileTests(unittest.TestCase): + def registry_document(self, entries: list[dict]) -> dict: + signer = { + "schema_version": registry.SIGNER_POINTER_SCHEMA, + "object_path": ( + "production-evidence/signer-registries/sha256/" + + sha("signer-object").removeprefix("sha256:")[:2] + + "/" + + sha("signer-object").removeprefix("sha256:") + + ".qualification-signer-registry.json" + ), + "object_sha256": sha("signer-object"), + "registry_identity_sha256": sha("signer-identity"), + "registry_revision": 1, + } + value = { + "$schema": "schemas/evidence-registry.schema.json", + "schema_version": registry.REGISTRY_SCHEMA, + "repository": registry.REPOSITORY, + "repository_id": registry.REPOSITORY_ID, + "repository_owner_id": registry.REPOSITORY_OWNER_ID, + "revision": 1, + "previous_registry_head_sha256": None, + "registry_head_sha256": sha("placeholder"), + "signer_registry": signer, + "signer_registry_history": [signer], + "entries": entries, + } + value["registry_head_sha256"] = registry.registry_head_digest(value) + return value + + def test_all_17_regular_kinds_have_adjacent_bound_bundle_profiles(self) -> None: + entries: list[dict] = [] + for kind in registry.REGULAR_KIND_CONTRACTS: + regular = evidence_entry(kind, label=f"regular:{kind}") + bundle = evidence_entry( + f"{kind}-sigstore-bundle", + label=f"bundle:{kind}", + subject_sha256=regular["object_sha256"], + ) + entries.extend((regular, bundle)) + self.assertEqual(len(entries), 34) + self.assertEqual(registry.validate_registry(self.registry_document(entries)), entries) + + def test_profile_and_adjacency_substitution_are_refused_for_every_kind(self) -> None: + kinds = list(registry.REGULAR_KIND_CONTRACTS) + for index, kind in enumerate(kinds): + other_kind = kinds[(index + 1) % len(kinds)] + with self.subTest(kind=kind, substitution="regular profile"): + regular = evidence_entry(kind, label=f"regular:{kind}") + regular["object_schema_version"], regular["object_media_type"] = ( + registry.REGULAR_KIND_CONTRACTS[other_kind] + ) + regular["registry_entry_sha256"] = registry.entry_digest(regular) + bundle = evidence_entry( + f"{kind}-sigstore-bundle", + label=f"bundle:{kind}", + subject_sha256=regular["object_sha256"], + ) + with self.assertRaisesRegex( + registry.EvidenceRegistryError, "schema or media type" + ): + registry.validate_registry(self.registry_document([regular, bundle])) + with self.subTest(kind=kind, substitution="bundle adjacency"): + regular = evidence_entry(kind, label=f"regular:{kind}") + substituted_bundle = evidence_entry( + f"{other_kind}-sigstore-bundle", + label=f"substitute:{kind}", + subject_sha256=regular["object_sha256"], + ) + with self.assertRaisesRegex( + registry.EvidenceRegistryError, "immediately follow" + ): + registry.validate_registry( + self.registry_document([regular, substituted_bundle]) + ) + + +if __name__ == "__main__": + unittest.main()