Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 63 additions & 29 deletions docs/eval_results/PRIVATE_EXPORT_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,59 +13,92 @@ below are blank.
> workflow and ref, and the approval authority. A broad enable flag or approval
> of only the admission and campaign digests is not sufficient.

That bar was written when the store was an S3 bucket with Object Lock. The store
is now a signed git commit, so "container and prefix" reads as repository and
path prefix, and "immutable retention mode and period" reads as the transparency
log entry plus a recorded commitment. Every other clause holds unchanged. The
next section explains why the medium changed.

## Why an approval is needed at all, and what it fixes

The certificate already carries a `retention` block, and the importer already
validates its shape: every digest is well formed, the mode is Object Lock
`COMPLIANCE`, four verification booleans are true, the period is between one and
ten years, and the chronology runs `acceptance_verified_at <= retained_at <
retention_until`.
validates its shape: every digest is well formed, the retention commit is an
exact 40-character hash, four verification booleans are true, and the chronology
runs `acceptance_verified_at <= retained_at` and no later than now.

Shape is not identity. Today the certificate supplies its own
`storage_identity_sha256`, `kms_key_identity_sha256`, and
`uploader_identity_sha256`, and the importer checks only that they look like
digests. A certificate that named some other bucket, some other key, and some
other uploader would pass every check in the file.
digests. A certificate that named some other repository, some other key, and
some other uploader would pass every check in the file.

The approval closes that. It supplies the expected digests from outside the
evidence, the way `--trusted-admission-signers` and
`APPROVED_CLOUD_ACCEPTANCE_COMMIT` already do for the qualification authority
and the Cloud commit. After approval the evidence can no longer select its own
destination, its own key, or its own uploader.

## What the store is, and why it is not S3

The retained evidence is a signed git commit in a private repository, not an
object in an S3 bucket with Object Lock. That choice was made deliberately,
before the first write, when changing it was still free.

The reasoning, kept here so nobody has to reconstruct it:

- The Cloud writer already encrypts the envelope client-side with an AES-256-GCM
data key wrapped by KMS. The store holds ciphertext either way, so GitHub
cannot read the evidence and confidentiality does not depend on the medium.
- What makes the claim credible to a reader is the signature chain plus the
public Rekor entry, not the storage. Object Lock stops neither the suppression
of an unpublished result nor the alteration of a published one; the signature
and the transparency log already do both.
- Git is content-addressed. A commit hash covers the whole tree and history, so
it binds the relationships between certificate, admission and campaign. A
per-object lock binds only each blob.
- Object Lock does buy one thing git does not: if a third party is granted
direct read access to the store, they can trust nothing was pruned before they
looked. No buyer has asked for that. If one does, this decision should be
revisited before the next write rather than patched around.

Object Lock in COMPLIANCE mode is also irreversible for its full term. Choosing
it commits storage for years and cannot be undone by anyone, including the
account root.

## The bindings

The approval fixes seven destination fields plus the uploader, the importer, and
the authority. **Every value is blank in this repository.** They are deployment
facts, and only the account owner can supply them.
The approval fixes six destination fields plus the uploader, the importer, and
the authority. The store and its path are settled: `openadapt-evidence` exists
and is empty by design. The remaining blanks are AWS facts and your signature.

| Contract field | What it fixes | Value |
| --- | --- | --- |
| `destination.account_id` | the AWS account holding the evidence | **TO BE SUPPLIED** |
| `destination.region` | the region | **TO BE SUPPLIED** |
| `destination.bucket` | the S3 bucket | **TO BE SUPPLIED** |
| `destination.object_prefix` | the prefix inside it | **TO BE SUPPLIED** |
| `destination.kms_key_arn` | the encryption key | **TO BE SUPPLIED** |
| `destination.retention_mode` | Object Lock mode | `COMPLIANCE`, fixed |
| `destination.retention_days` | the immutable period | default 2555, inside 365 to 3650 |
| `destination.account_id` | the AWS account holding the KMS key | **TO BE SUPPLIED** |
| `destination.region` | the region of that key | **TO BE SUPPLIED** |
| `destination.repository` | the private repository holding the ciphertext | `OpenAdaptAI/openadapt-evidence` |
| `destination.ref` | the branch it lands on | `refs/heads/main` |
| `destination.path_prefix` | the path inside it | `production-acceptance` |
| `destination.kms_key_arn` | the key wrapping each data key | **TO BE SUPPLIED** |
| `destination.retention_commitment_days` | how long you commit to keep it | default 2555 |
| `uploader_arn` | the principal allowed to write | **TO BE SUPPLIED** |
| `importer_workflow_ref` | the one workflow allowed to import | pre-filled |
| `approval_authority`, `approved_at` | who approved, and when | **TO BE SUPPLIED** |

These are the same seven destination fields the Cloud retention writer hashes,
in the same order, so the approval digest derived here is the one that writer
already requires.
`retention_commitment_days` is a commitment the mechanism records and does not
enforce. A git commit has no expiry. Enforcing a number nothing can hold would
be theatre, so the verifier does not police it, and this document says so
plainly rather than leaving a reader to assume otherwise.

Two things the certificate carries are not fixed by the approval, because they
differ per export: the ciphertext, envelope, and candidate digests, and the
object and locator version digests. The approval fixes where evidence may go and
who may put it there, not the content of any one export.
differ per export: the ciphertext, envelope and candidate digests, and the
commit and locator versions. The approval fixes where evidence may go and who
may put it there, not the content of any one export.

## Where the filled copy lives

This repository is public. The filled contract names an account, a bucket, a
prefix, a key ARN, and an uploader ARN, which are deployment-derived facts and
fall under the source-availability boundary.
This repository is public. The filled contract names an account, a repository, a
path prefix, a key ARN, and an uploader ARN, which are deployment-derived facts
and fall under the source-availability boundary.

So: the shape stays here, the values do not. The approved instance belongs in
`openadapt-internal`, and this repository receives only its digest, supplied to
Expand Down Expand Up @@ -104,7 +137,7 @@ values from the contract, so one approval governs both repositories:

| Digest | Domain | Preimage |
| --- | --- | --- |
| `storage_identity_sha256` | `retention store` | `destination.bucket` |
| `storage_identity_sha256` | `retention store` | `destination.repository` |
| `kms_key_identity_sha256` | `retention KMS key` | `destination.kms_key_arn` |
| `uploader_identity_sha256` | `AWS retention uploader` | `uploader_arn` |
| `destination_approval_sha256` | `Execute acceptance retention destination` | canonical JSON of all seven `destination` fields |
Expand All @@ -120,11 +153,12 @@ contract produces it.

The contract carries values and never a digest, so an approval cannot assert a
hash whose input nobody can see. `verify_retention_against_contract` compares
all three identities against the certificate and refuses a retention period
shorter than the approved one. A longer lock is allowed: the approval sets a
floor. `verify_importer_identity` requires `GITHUB_WORKFLOW_REF` to equal the
all three identities against the certificate. `verify_importer_identity` requires `GITHUB_WORKFLOW_REF` to equal the
approved ref exactly. The destination is checked the way the Cloud writer checks
it, including that the KMS key lives in the approved account and region.
The certificate must also carry an exact 40-character retention commit and prove
that the push, the commit read-back, and the transparency-log entry were all
verified.

## Open items that block approval

Expand Down
14 changes: 8 additions & 6 deletions docs/eval_results/PRODUCTION_READINESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ repository. Do not set `production_acceptance: true` while this gate is closed.
The checked-in fixtures are synthetic test vectors only.

The future approval must bind the complete payload schema and digest, the
destination account, service, container, and prefix, the encryption-key
identity, the immutable retention mode and period, the authorized importer
workflow and ref, and the approval authority. A broad enable flag or approval
destination account, repository, ref, and path prefix, the encryption-key
identity, the recorded retention commitment, the authorized importer workflow
and ref, and the approval authority. A broad enable flag or approval
of only the admission and campaign digests is not sufficient.

After approval, the importer will compose four evidence inputs:
Expand All @@ -39,7 +39,8 @@ After approval, the importer will compose four evidence inputs:
3. The full `openadapt.qualification-campaign/v2` artifact with every retained
trial row and normalized evidence receipt for the exact qualification
contract.
4. The GitHub artifact-attestation bundle for the certificate bytes.
4. The Sigstore bundle signing the certificate bytes on the public-good
instance.

The future importer also needs external control inputs: the approved Cloud
source commit, the approved qualification signer registry, the admission and
Expand Down Expand Up @@ -219,8 +220,9 @@ that output. Until then, the importer refuses. A bare registry boolean or
campaign label fails the check.

Cloud does not issue a complete acceptance record before it verifies durable
retention. The private evidence envelope uses encrypted immutable storage with
Object Lock and KMS. The public record retains only opaque digests and the
retention. The private evidence envelope is encrypted with an AES-256-GCM data
key wrapped by KMS, then committed to a private repository and signed on the
Sigstore public-good instance. The public record retains only opaque digests and the
verified retention facts. The importer verifies the public retention receipt
and its exact binding to the candidate and retained envelope. GitHub artifact
retention alone does not satisfy this contract.
Expand Down
33 changes: 14 additions & 19 deletions docs/eval_results/private-export-contract.template.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,26 @@
"_README": [
"TEMPLATE. This file is not an approval and the importer will refuse it.",
"Copy it into openadapt-internal, replace every FILL_ value, delete this",
"_README key, and approve the result there. This public repository keeps",
"the shape; the filled instance and its values stay private.",
"_README key, and approve the result there.",
"",
"The destination fields are the same seven the Cloud retention writer",
"hashes in scripts/retain-execute-private-evidence.mjs. Its approval digest",
"EXECUTE_ACCEPTANCE_RETENTION_DESTINATION_APPROVAL_SHA256 is derived from",
"exactly these values, so one approval governs both repositories.",
"The evidence is encrypted to an age recipient before it is committed, so",
"the repository holds ciphertext. The private half lives in the founder's",
"keychain and never reaches CI: the writer only encrypts, so nothing in any",
"workflow can read the evidence back.",
"",
"Validate a filled copy with:",
" python scripts/import_production_acceptance.py --private-export-contract <path> ...",
"It derives every digest from the values below, so a reviewer recomputes",
"each one rather than trusting an asserted hash."
"retention_commitment_days is a commitment this mechanism records and does",
"not enforce. A git commit carries no expiry."
],
"schema_version": "openadapt.private-export-contract/v1",
"destination": {
"account_id": "FILL_AWS_ACCOUNT_ID",
"region": "FILL_AWS_REGION",
"bucket": "FILL_RETENTION_BUCKET",
"object_prefix": "FILL_RETENTION_OBJECT_PREFIX",
"kms_key_arn": "FILL_RETENTION_KMS_KEY_ARN",
"retention_mode": "COMPLIANCE",
"retention_days": 2555
"repository": "OpenAdaptAI/openadapt-evidence",
"ref": "refs/heads/main",
"path_prefix": "production-acceptance",
"encryption_recipient": "age19gxzh0n7d6r8hdwpdfvuwxgc46gtcq0fd298nq2v8puqprtphetsg7sdh2",
"retention_commitment_days": 2555
},
"uploader_arn": "FILL_UPLOADER_ROLE_OR_USER_ARN",
"importer_workflow_ref": "OpenAdaptAI/openadapt-evals/.github/workflows/import-production-acceptance.yml@refs/heads/main",
"approval_authority": "FILL_APPROVAL_AUTHORITY",
"approved_at": "FILL_APPROVED_AT"
"approved_at": "FILL_APPROVED_AT",
"uploader_identity": "FILL_PUSHING_IDENTITY"
}
Loading
Loading