Skip to content
Closed
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
82 changes: 55 additions & 27 deletions docs/eval_results/PRIVATE_EXPORT_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,56 +16,83 @@ below are blank.
## 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 approval fixes six 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.

| 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 | **TO BE SUPPLIED** |
| `destination.ref` | the branch it lands on | `refs/heads/main` |
| `destination.path_prefix` | the path inside it | **TO BE SUPPLIED** |
| `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 +131,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 +147,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
21 changes: 12 additions & 9 deletions docs/eval_results/private-export-contract.template.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
"_README key, and approve the result there. This public repository keeps",
"the shape; the filled instance and its values stay private.",
"",
"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 retained evidence is a signed git commit, not an S3 object. The Cloud",
"writer still encrypts the envelope client-side with an AES-256-GCM data",
"key wrapped by KMS, so the repository holds ciphertext and GitHub cannot",
"read it. The KMS key is the only part that still costs money.",
"",
"retention_commitment_days is a commitment this mechanism records and does",
"not enforce. A git commit carries no expiry. Nothing here pretends it does.",
"",
"Validate a filled copy with:",
" python scripts/import_production_acceptance.py --private-export-contract <path> ...",
Expand All @@ -19,11 +22,11 @@
"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": "FILL_EVIDENCE_REPOSITORY",
"ref": "refs/heads/main",
"path_prefix": "FILL_EVIDENCE_PATH_PREFIX",
"kms_key_arn": "FILL_DATA_KEY_KMS_ARN",
"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",
Expand Down
Loading
Loading