Skip to content
Open
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
3 changes: 3 additions & 0 deletions protos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ predicates have protobuf definitions:
chain attributes.
- [Test Result]: Expresses the result of a test run in the software supply
chain.
- [Adversarial Execution Evidence]: Records recomputable evidence from a
bounded adversarial-stimulus run against a system under test.

## Supported language bindings

Expand Down Expand Up @@ -55,6 +57,7 @@ testing the supported language bindings.
[VULNS]: in_toto_attestation/predicates/vulns/
[in-toto Link]: in_toto_attestation/predicates/link/
[Test Result]: in_toto_attestation/predicates/test_result/
[Adversarial Execution Evidence]: in_toto_attestation/predicates/adversarial_execution_evidence/
[documentation]: ../docs/protos.md
[go]: ../go/
[python]: ../python/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,351 @@
// Proto representation of predicate type
// https://in-toto.io/attestation/adversarial-execution-evidence/v0.7
//
// Validation of all fields is left to the users of this proto. The predicate's
// soundness lives in the verifier rules (statement well-formedness, the two
// coverage/tier consumption gates, the offline `result` recompute, the
// digest-integrity checks, and the per-record DSSE signature), never in this
// schema: a proto-valid object is not necessarily a sound attestation.
//
// -----------------------------------------------------------------------------
// TWO PROPERTIES OF THIS PREDICATE MAKE THIS PROTO SUBTLE. READ BOTH BEFORE
// EDITING OR GENERATING CODE FROM IT.
// -----------------------------------------------------------------------------
//
// 1. STRING-TYPED VOCABULARIES, NEVER proto3 ENUMS.
//
// `result`, `containmentObserved`, `basis`, `method`, `attribution`,
// `actualLayer`, and
// `networkPosture.posture` carry closed value vocabularies, yet every one is
// a `string` here, not a proto3 `enum`. This is deliberate and load-bearing:
//
// - The AEE vocabularies are consumer-side, FAIL-CLOSED closed vocabularies.
// A value outside the carried vocabulary is STRUCTURALLY VALID and
// fail-closes at the verifier's recompute gate (a fail-closed row forces
// `result` to `fail`), exactly as an absent `basis`/`method` does. A
// proto3 enum cannot express this: an unknown wire value would deserialize
// to the zero member (`UNKNOWN = 0`) and be silently swallowed, erasing
// the very fail-closed signal the recompute depends on. String preserves
// the unknown value verbatim for the verifier to reject. The altitude of
// that rejection differs by member and the markdown states which: an
// unknown `basis`/`method`/`attribution`/`containmentObserved`
// fail-closes a row, while
// an unknown `actualLayer` or `networkPosture.posture` makes the whole
// statement malformed. Either way the verifier must see the value it was
// given, which is what an enum would take away.
// - `containmentObserved`'s admissible label set is not even fixed by this
// predicate: it is producer vocabulary carried on-wire in
// `observationEnvironment.observationVocabulary.{labels,caught}` and read
// by the verifier, so no enum could enumerate it.
// - It matches the in-toto house convention: there are ZERO proto3 `enum`
// declarations across any merged in-toto predicate `.proto`
// (vsa, scai, test_result, provenance, release, vulns, svr, ...); every
// closed vocabulary in those predicates travels as a `string`.
//
// 2. THIS PROTO IS FOR TRANSPORT/CODEGEN ONLY. ITS JSON OUTPUT MUST NEVER BE
// RE-CANONICALIZED FOR SIGNING OR RE-HASHED.
//
// The CANONICAL BYTES of an AEE attestation are the on-wire JSON emitted by
// the producer. The DSSE signature is computed over
// `PAE(payloadType, SERIALIZED_BODY)` -- the Pre-Authentication Encoding over
// the body BYTES VERBATIM -- and every downstream digest (corpus manifest
// digest, observation-vocabulary digest, the network-posture object digest
// the run binding folds in, `batchRoot` leaves, and the run binding itself)
// is taken over RFC 8785 (JCS) canonical bytes. proto3 canonical ProtoJSON is
// NOT RFC 8785 / JCS and cannot be made so; at least two divergences are
// unfixable:
//
// - ProtoJSON emits 64-bit integers (`int64`/`uint64`) as QUOTED strings,
// a JSON token-type change JCS never makes (JCS keeps them as numbers).
// - ProtoJSON preserves proto field declaration order; JCS lexicographically
// sorts object member names by UTF-16 code unit. The two orderings differ.
// (ProtoJSON/Struct also re-formats timestamps, re-pads base64, and may
// drop or reformat members, each a further divergence.)
//
// Therefore a consumer MUST NOT parse an attestation into this proto, re-emit
// its JSON, and re-hash or re-verify against that output: it would compute a
// digest over bytes the producer never signed and reject a genuine bundle
// (or, worse, mask a tampered one). Verify DSSE over the producer's exact
// on-wire bytes; use this proto only for language bindings and transport.
// (This mirrors sigstore's decision to DELETE its proto-derived JSON-Schema
// rather than maintain a co-equal signed schema.)
//
// Consequences of (2) visible below: `manifest.classes`,
// `manifest.expectedPayloads` and `_ext` use google.protobuf.Struct (proto3
// maps cannot carry repeated/array values, so a message-wrapped modeling
// would ALTER the JSON shape); `payload` is the
// opaque base64 string carried verbatim (not proto `bytes`, whose base64
// decode/re-encode round-trip could alter padding); and the reserved
// `aee*` payload members (aeeKind, aeeMethod, aeeRunBinding, armedAt,
// aeePostureDigest, aeeStillArmed, aeeDropCount, aeeChainScope, aeeRunSeq,
// aeePrevRunBinding, aeePayloadCommitment, aeeAssessedAttacks,
// aeeObservedSet, aeeObservedAttacks, aeeVersion, ...) are NOT modeled as
// proto fields: they
// live INSIDE the base64 `payload` bytes and are producer-signed and
// opaque-until-verified (verify-then-read), never sibling wire fields.

syntax = "proto3";

package in_toto_attestation.predicates.adversarial_execution_evidence.v07;

import "in_toto_attestation/v1/resource_descriptor.proto";
import "google/protobuf/struct.proto";
import "google/protobuf/timestamp.proto";

option go_package = "github.com/in-toto/attestation/go/predicates/adversarial_execution_evidence/v07";
option java_package = "io.github.intoto.attestation.predicates.adversarial_execution_evidence.v07";

// The single signed artifact one bounded adversarial-stimulus run emits per
// system under test.
message AdversarialExecutionEvidence {
// The fused fail|degraded|pass_indirect|pass reduction, recomputed offline
// from the rest of the predicate as the MINIMUM of three independent
// conditions under that order. `pass_indirect` is the value a
// coverage-complete statement takes when some clean row declares a `basis`
// other than `substrate` or a `method` other than `intercepted`; it says
// nothing about signature verification, which is the evidence tier's
// question and is not recomputable. STRING, not enum (see header rule 1):
// the recompute reads this value, and a value the recompute does not
// reproduce is invalid.
string result = 1;

ObservationEnvironment observation_environment = 2 [json_name = "observationEnvironment"];

Coverage coverage = 3;

repeated AttackResult attack_results = 4 [json_name = "attackResults"];

// Optional. Absent on an artifact-only statement. When present and non-empty,
// `batch_root` is required (a rule the verifier enforces, not this proto).
repeated ObservationRecord observation_records = 5 [json_name = "observationRecords"];

// RFC 6962 Merkle root over `observation_records` (SHA-256, domain-separated,
// recursive split). Required iff `observation_records` is non-empty, omitted
// otherwise. lowercase 64-hex.
string batch_root = 6 [json_name = "batchRoot"];

// Optional, advisory. Explicit negative-scope statements; a verifier MUST NOT
// require it and nothing in it weakens a required check.
repeated string does_not_assert = 7 [json_name = "doesNotAssert"];

// When the producer signed the evidence bundle (RFC 3339). Modeled as a
// Timestamp to match the in-toto house style (cf. vsa `timeVerified`); on the
// wire this is the producer's exact RFC 3339 string, which is the canonical
// byte -- see header rule 2 on why ProtoJSON's reformatted output must not be
// re-hashed.
//
// The markdown pins the profile this type leaves open: the date-time
// separator and the zone designator are uppercase, and the zone designator is
// `Z`, `+00:00` or `-00:00`. ProtoJSON parsers accept spellings outside that
// profile, so parsing a statement into this message is not a conformance
// check on the field; the verifier checks the carried string.
google.protobuf.Timestamp issued_at = 8 [json_name = "issuedAt"];

// Optional producer extension.
ExecutionEnvelope execution_envelope = 9 [json_name = "executionEnvelope"];

// Optional producer extension bag (`_ext` on the wire). Free-form JSON object.
google.protobuf.Struct ext = 10 [json_name = "_ext"];
}

// The digest-pinned context the evidence was earned under. Two of its six
// members are ResourceDescriptors and the other four are locally typed; the
// markdown states the rule that decides which, and its reason.
message ObservationEnvironment {
// The subject reference of the substrate's own attestation. A
// ResourceDescriptor: the member carries a name and a digest and nothing
// else, which is what the descriptor holds, and the verifier's requirement
// of a `sha256` digest is one the descriptor specification permits a context
// using the type to impose. The JSON member names are identical either way,
// so this typing is not a wire change.
in_toto_attestation.v1.ResourceDescriptor substrate = 1;

// The corpus thrown at the subject.
message Corpus {
string name = 1;
// RECOMMENDED as a purl, e.g. pkg:<producer>/<corpus>@<version>.
string uri = 2;
// JCS digest of the embedded `manifest` (algorithm -> lowercase-hex).
map<string, string> digest = 3;

// A map from assessment class code to the complete array of attack
// identifiers it defines, e.g. {"CO":["CO-EXFIL-1"]}. Modeled as a Struct,
// NOT map<string, repeated string>: proto3 map values cannot be `repeated`,
// so any message-wrapped modeling would inject an extra JSON nesting level
// and change the on-wire shape. A Struct's ProtoJSON reproduces the
// map-of-arrays exactly. (Verifier re-derives `corpus.digest` from these
// bytes; see header rule 2.)
message Manifest {
google.protobuf.Struct classes = 1;

// Optional. attackId -> the array of lowercase 64-hex commitment values
// a substrate is expected to carry when it observes that attack. A
// Struct for the same reason `classes` is one: the values are arrays,
// which a proto3 map cannot hold without injecting a nesting level the
// wire shape does not have. It sits inside the pre-image
// `corpus.digest` is taken over, so adding, editing or removing an
// entry after the arming record is signed derives a run binding the
// statement's own records do not carry.
google.protobuf.Struct expected_payloads = 2 [json_name = "expectedPayloads"];
}
Manifest manifest = 4;
}
Corpus corpus = 2;

// JCS digest of the parsed catch-policy document. A ResourceDescriptor for
// the same reason `substrate` is one: a digest and nothing beside it.
in_toto_attestation.v1.ResourceDescriptor catch_policy = 3 [json_name = "catchPolicy"];

// The substrate-authoritative egress posture and its configuration digest.
//
// The run binding takes the RFC 8785 canonical digest of THIS WHOLE MESSAGE
// as carried on the wire, not the value of its `digest` member, so every
// member here is inside the substrate's signature and a member added or
// removed after the arming record is signed invalidates the statement. That
// is a reason not to extend this message casually: unlike the descriptors
// above, a producer-specific member here changes a signed digest.
message NetworkPosture {
// STRING, not enum (see header rule 1): the registered posture vocabulary
// is closed and fail-closed at exactly four values -- no_network,
// allowlist, sinkhole, unsafe_bypass_egress -- and an unregistered value
// makes the statement malformed. A proto3 enum would deserialize the
// unregistered value to the zero member and erase the rejection, which is
// the same reason every other closed vocabulary here is a string.
string posture = 1;
map<string, string> digest = 2;
}
NetworkPosture network_posture = 4 [json_name = "networkPosture"];

// The producer's on-wire, versioned observation label set. The recompute and
// the coverage-validity gate read THIS carried set (not the producer's docs),
// so archived attestations verify without external documentation.
message ObservationVocabulary {
// JCS digest of {"caught":[...],"labels":[...]} (algorithm -> lowercase-hex).
// This value is also a run-binding input, so narrowing `caught` after the
// run changes every record's required aeeRunBinding rather than merely
// re-deriving this digest against the arrays beside it.
map<string, string> digest = 1;
// Every `containmentObserved` value the producer can emit; sorted ascending
// by UTF-16 code unit, no duplicates, BMP-only (verifier soundness rules).
repeated string labels = 2;
// The subset of `labels` whose observation is a caught containment event.
repeated string caught = 3;
}
ObservationVocabulary observation_vocabulary = 5 [json_name = "observationVocabulary"];

// The substrate-emitted run-start value folded into the run binding. OPTIONAL
// here; the verifier requires it exactly when any `attack_results` row is
// basis:substrate.
message RunEntropy {
map<string, string> digest = 1;
}
RunEntropy run_entropy = 6 [json_name = "runEntropy"];
}

// The coverage bound: the disjoint partition of the manifest's classes into
// assessed / out-of-scope / routed-elsewhere.
message Coverage {
// Class codes actually assessed.
repeated string assessed_classes = 1 [json_name = "assessedClasses"];
// Class code -> reason string; empty when complete. A non-empty map forces
// `result` to degraded.
map<string, string> out_of_scope = 2 [json_name = "outOfScope"];
// Class code -> reason string; empty when complete. A non-empty map forces
// `result` to degraded.
map<string, string> routed_elsewhere = 3 [json_name = "routedElsewhere"];
}

// One row per executed attack.
message AttackResult {
// Must appear in the manifest; unique across rows (one row per executed attack).
string attack_id = 1 [json_name = "attackId"];

// A label from the carried `observationVocabulary.labels`. STRING, not enum
// (see header rule 1): the admissible set travels on-wire in
// `observationVocabulary`; a label outside the carried set is valid here and
// fail-closes at the recompute gate.
string containment_observed = 2 [json_name = "containmentObserved"];

// The observation's vantage (producer MUST emit `substrate` or `artifact`).
// STRING, not enum: an absent or out-of-vocabulary value is valid-but-
// fail-closed at the recompute gate, never proto/schema-rejected. NOT a
// proto-required field for the same reason.
string basis = 3;

// The observation's directness (producer MUST emit `intercepted` or
// `reconstructed`). STRING, not enum; same fail-closed rule as `basis`.
string method = 4;

// Which enforcement layer acted, drawn from the registered layer vocabulary,
// OR the literal string "none" (the clean-row sentinel). STRING, not enum: a
// missing value is a MALFORMED statement (a different altitude than the
// fail-closed row members); the closed layer set is producer identity the
// verifier checks.
string actual_layer = 5 [json_name = "actualLayer"];

// Indexes into `observation_records` binding this row to its covering records.
// uint32 (not int64) so ProtoJSON emits bare JSON numbers, matching the
// on-wire `[0]` shape (see header rule 2 on int64 -> quoted-string divergence).
// An out-of-range index makes the statement malformed (verifier rule).
repeated uint32 observation_refs = 6 [json_name = "observationRefs"];

// Optional producer-defined tokens, positionally parallel to
// `observation_refs`, naming the sub-observation within a referenced record's
// committed payload. Producer vocabulary; nothing normative reads it.
repeated string observation_selectors = 7 [json_name = "observationSelectors"];

// How firmly this row is bound to the records that cover it (producer MUST
// emit `pinned` or `paired`). STRING, not enum; same fail-closed rule as
// `basis`. A `pinned` row is checked against the corpus manifest's
// `expectedPayloads` and the covering records' `aeePayloadCommitment`; a
// `paired` row rests on a producer-asserted correspondence. The value
// constrains the binding and never the result, the method or the evidence
// tier. It is declared last because its field number is new, not because it
// is optional: the verifier requires it on every row.
string attribution = 8;
}

// One DSSE envelope per observation. The producer signs at observation time,
// before attribution; a consumer verifies the signature (DSSE PAE over
// `payload_type` and the base64-decoded `payload`) BEFORE reading any field
// inside the payload (verify-then-read).
//
// NOTE (header rule 2): the reserved `aee*` members the verifier reads
// post-verify (aeeRunBinding, aeeKind, aeeMethod, and per-kind members
// armedAt / aeePostureDigest / aeeStillArmed / aeeDropCount / aeeDropBound /
// aeeRunSeq / aeePrevRunBinding / aeeChainScope / aeeVersion, ...) live INSIDE
// the base64 `payload` bytes as top-level members of the signed +json object.
// They are producer-signed and opaque-until-verified and are deliberately NOT
// modeled as proto fields of this record.
message ObservationRecord {
// base64(exact canonical +json bytes the substrate signed). Carried as the
// opaque base64 STRING verbatim (not proto `bytes`): the DSSE PAE is over the
// producer's exact base64-decoded bytes, and proto `bytes` would round-trip
// through its own base64 decode/re-encode, which may alter padding.
string payload = 1;

// A producer-defined media type ending in `+json`. The exact bytes are the
// first PAE input, so a differing value changes the signed bytes.
string payload_type = 2 [json_name = "payloadType"];

// DSSE signature. `keyid` is an unauthenticated lookup hint and OPTIONAL
// (never the check itself); only `sig` carries the signature.
message Signature {
string keyid = 1;
string sig = 2;
}
repeated Signature signatures = 3;

// Optional predicate-layer emission sequence (1-based) of this record within
// the run's signed stream: the stable join key a vmi.attribution record's
// catch_seq resolves against, robust to a later filter or reorder of
// `observation_records`. The signed payload carries no seq. uint32 so
// ProtoJSON emits a bare number (see header rule 2). Absent only when the
// producer emitted an un-sequenced stream.
uint32 seq = 4;
}

// Optional producer extension: linkage to a distributed-trace context.
message ExecutionEnvelope {
string otel_trace_id = 1 [json_name = "otelTraceId"];
}
Loading