litzki-sovp-agentrust-bridge 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- litzki_sovp_agentrust_bridge-0.1.0/.gitignore +11 -0
- litzki_sovp_agentrust_bridge-0.1.0/PKG-INFO +77 -0
- litzki_sovp_agentrust_bridge-0.1.0/README.md +64 -0
- litzki_sovp_agentrust_bridge-0.1.0/examples/sovp-attestation.json +25 -0
- litzki_sovp_agentrust_bridge-0.1.0/integration.yaml +34 -0
- litzki_sovp_agentrust_bridge-0.1.0/pyproject.toml +28 -0
- litzki_sovp_agentrust_bridge-0.1.0/src/litzki_sovp/__init__.py +5 -0
- litzki_sovp_agentrust_bridge-0.1.0/src/litzki_sovp/adapter.py +40 -0
- litzki_sovp_agentrust_bridge-0.1.0/src/litzki_sovp/trace.py +157 -0
- litzki_sovp_agentrust_bridge-0.1.0/tests/test_trace.py +96 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: litzki-sovp-agentrust-bridge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SOVP to AgenTrust TRACE bridge
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: agent-manifest>=0.11.1
|
|
8
|
+
Requires-Dist: agentrust-trace-tests>=0.5.1
|
|
9
|
+
Requires-Dist: agentrust-trace>=0.9.0
|
|
10
|
+
Requires-Dist: cmcp-runtime>=0.4.0
|
|
11
|
+
Requires-Dist: cryptography<51,>=42
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Litzki SOVP AgenTrust Integration
|
|
15
|
+
|
|
16
|
+
Exports SOVP attestation results as signed AgenTrust TRACE records.
|
|
17
|
+
|
|
18
|
+
## What it does
|
|
19
|
+
|
|
20
|
+
This integration provides a Python adapter that:
|
|
21
|
+
|
|
22
|
+
- loads a SOVP attestation result from JSON;
|
|
23
|
+
- maps explicitly supplied SOVP and AgenTrust fields into a TRACE envelope;
|
|
24
|
+
- signs the TRACE envelope with Ed25519 over a canonical JSON pre-image that excludes only the `signature` field;
|
|
25
|
+
- writes the signed record as JSON for downstream validation with `agentrust-trace-tests`.
|
|
26
|
+
|
|
27
|
+
## What it does not claim
|
|
28
|
+
|
|
29
|
+
- It does not certify SOVP or any SOVP deployment.
|
|
30
|
+
- It does not call non-attested data attested.
|
|
31
|
+
- It does not mark the integration as Verified until the generated records pass `agentrust-trace-tests` in CI or a reviewer reproduction environment.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
python -m pip install -e integrations/litzki-sovp
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The package declares runtime dependencies on released PyPI packages: `cmcp-runtime`, `agentrust-trace`, `agentrust-trace-tests`, and `agent-manifest`.
|
|
40
|
+
|
|
41
|
+
## Input contract
|
|
42
|
+
|
|
43
|
+
The adapter expects a JSON object with an `agentrust` object containing the TRACE envelope fields and a `sovp` object containing the source attestation payload. The adapter only signs fields present in `agentrust`; it does not infer missing attestation claims.
|
|
44
|
+
|
|
45
|
+
See `examples/sovp-attestation.json` for the minimal local fixture shape.
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
First generate a local Ed25519 signing key. This is a throwaway demo key; it is
|
|
50
|
+
git-ignored and must not be committed. Use a persistent, protected key for any
|
|
51
|
+
record you intend to be re-verified later.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
openssl genpkey -algorithm Ed25519 \
|
|
55
|
+
-out integrations/litzki-sovp/examples/ed25519-private.pem
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Then produce a signed TRACE record:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
litzki-sovp-trace \
|
|
62
|
+
--input integrations/litzki-sovp/examples/sovp-attestation.json \
|
|
63
|
+
--private-key integrations/litzki-sovp/examples/ed25519-private.pem \
|
|
64
|
+
--output /tmp/sovp.trace.json
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Reproduction target
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
python -m pytest integrations/litzki-sovp/tests
|
|
71
|
+
trace-tests verify --record /tmp/sovp.trace.json --level 0
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The generated record passes TRACE conformance at **Level 0** (`TR-ENV`, `TR-SIG`,
|
|
75
|
+
`TR-POL`). Level 0 is the honest ceiling for this bridge: it transcribes a SOVP
|
|
76
|
+
attestation rather than measuring a hardware TEE, so `runtime.platform` is
|
|
77
|
+
`software-only`, which the suite accepts only at Level 0.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Litzki SOVP AgenTrust Integration
|
|
2
|
+
|
|
3
|
+
Exports SOVP attestation results as signed AgenTrust TRACE records.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
This integration provides a Python adapter that:
|
|
8
|
+
|
|
9
|
+
- loads a SOVP attestation result from JSON;
|
|
10
|
+
- maps explicitly supplied SOVP and AgenTrust fields into a TRACE envelope;
|
|
11
|
+
- signs the TRACE envelope with Ed25519 over a canonical JSON pre-image that excludes only the `signature` field;
|
|
12
|
+
- writes the signed record as JSON for downstream validation with `agentrust-trace-tests`.
|
|
13
|
+
|
|
14
|
+
## What it does not claim
|
|
15
|
+
|
|
16
|
+
- It does not certify SOVP or any SOVP deployment.
|
|
17
|
+
- It does not call non-attested data attested.
|
|
18
|
+
- It does not mark the integration as Verified until the generated records pass `agentrust-trace-tests` in CI or a reviewer reproduction environment.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
python -m pip install -e integrations/litzki-sovp
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The package declares runtime dependencies on released PyPI packages: `cmcp-runtime`, `agentrust-trace`, `agentrust-trace-tests`, and `agent-manifest`.
|
|
27
|
+
|
|
28
|
+
## Input contract
|
|
29
|
+
|
|
30
|
+
The adapter expects a JSON object with an `agentrust` object containing the TRACE envelope fields and a `sovp` object containing the source attestation payload. The adapter only signs fields present in `agentrust`; it does not infer missing attestation claims.
|
|
31
|
+
|
|
32
|
+
See `examples/sovp-attestation.json` for the minimal local fixture shape.
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
First generate a local Ed25519 signing key. This is a throwaway demo key; it is
|
|
37
|
+
git-ignored and must not be committed. Use a persistent, protected key for any
|
|
38
|
+
record you intend to be re-verified later.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
openssl genpkey -algorithm Ed25519 \
|
|
42
|
+
-out integrations/litzki-sovp/examples/ed25519-private.pem
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Then produce a signed TRACE record:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
litzki-sovp-trace \
|
|
49
|
+
--input integrations/litzki-sovp/examples/sovp-attestation.json \
|
|
50
|
+
--private-key integrations/litzki-sovp/examples/ed25519-private.pem \
|
|
51
|
+
--output /tmp/sovp.trace.json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Reproduction target
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
python -m pytest integrations/litzki-sovp/tests
|
|
58
|
+
trace-tests verify --record /tmp/sovp.trace.json --level 0
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The generated record passes TRACE conformance at **Level 0** (`TR-ENV`, `TR-SIG`,
|
|
62
|
+
`TR-POL`). Level 0 is the honest ceiling for this bridge: it transcribes a SOVP
|
|
63
|
+
attestation rather than measuring a hardware TEE, so `runtime.platform` is
|
|
64
|
+
`software-only`, which the suite accepts only at Level 0.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"agentrust": {
|
|
3
|
+
"trace_id": "0192f3a0-0000-7000-8000-0000000000aa",
|
|
4
|
+
"agent_id": "spiffe://litzki.systems/agent/sovp-demo",
|
|
5
|
+
"agent_manifest_id": "0192f3a0-0000-7000-8000-000000000001",
|
|
6
|
+
"manifest_verification_result": "VALID",
|
|
7
|
+
"tool_id": "systems.litzki.sovp.attestation",
|
|
8
|
+
"policy_hash": "sha256:a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
|
|
9
|
+
"catalog_hash": "sha256:b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
|
|
10
|
+
"decision": "allow",
|
|
11
|
+
"decision_reason": "SOVP attestation result accepted by caller policy.",
|
|
12
|
+
"payload_classification": "internal",
|
|
13
|
+
"egress_destination": "trace.local",
|
|
14
|
+
"hitl_required": false,
|
|
15
|
+
"hitl_approval_id": null,
|
|
16
|
+
"timestamp": "2026-08-22T00:00:00Z",
|
|
17
|
+
"tee_measurement": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd"
|
|
18
|
+
},
|
|
19
|
+
"sovp": {
|
|
20
|
+
"result": "passed",
|
|
21
|
+
"measurement": "sha256:cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd",
|
|
22
|
+
"nonce": "example-nonce",
|
|
23
|
+
"subject": "sovp-demo"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: litzki-sovp
|
|
2
|
+
vendor: Litzki Systems
|
|
3
|
+
product: SOVP
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
category: attestation
|
|
6
|
+
summary: Emits signed AgenTrust TRACE records from SOVP attestation results.
|
|
7
|
+
maintainer:
|
|
8
|
+
name: Litzki Systems
|
|
9
|
+
email: maintainers@litzki.systems
|
|
10
|
+
links:
|
|
11
|
+
homepage: https://github.com/litzki-systems/sovp-agentrust-bridge
|
|
12
|
+
repository: https://github.com/litzki-systems/sovp-agentrust-bridge
|
|
13
|
+
runtime:
|
|
14
|
+
language: python
|
|
15
|
+
package: litzki-sovp-agentrust-bridge
|
|
16
|
+
requires_python: '>=3.11'
|
|
17
|
+
dependencies:
|
|
18
|
+
- cmcp-runtime
|
|
19
|
+
- agentrust-trace
|
|
20
|
+
- agentrust-trace-tests
|
|
21
|
+
- agent-manifest
|
|
22
|
+
capabilities:
|
|
23
|
+
emits:
|
|
24
|
+
- trace-record
|
|
25
|
+
consumes:
|
|
26
|
+
- sovp-attestation-result
|
|
27
|
+
claims:
|
|
28
|
+
tier: community
|
|
29
|
+
trace_conformance: level-0
|
|
30
|
+
verified: false
|
|
31
|
+
notes:
|
|
32
|
+
- Generated records pass agentrust-trace-tests at Level 0 (TR-ENV, TR-SIG, TR-POL). Reproduce with `trace-tests verify --record <record> --level 0`; this repository's CI runs that reproduction on every push and pull request (.github/workflows/ci.yml, Python 3.11 and 3.12).
|
|
33
|
+
- Level 0 is the honest ceiling for this bridge. It transcribes a SOVP attestation rather than measuring a hardware TEE, so runtime.platform is software-only, which the suite accepts only at Level 0.
|
|
34
|
+
- The Verified tier is granted by the AgenTrust maintainers during marketplace review, never self-declared. This field stays false until then.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "litzki-sovp-agentrust-bridge"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "SOVP to AgenTrust TRACE bridge"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = {text = "Apache-2.0"}
|
|
12
|
+
dependencies = [
|
|
13
|
+
"agent-manifest>=0.11.1",
|
|
14
|
+
"agentrust-trace>=0.9.0",
|
|
15
|
+
"agentrust-trace-tests>=0.5.1",
|
|
16
|
+
"cmcp-runtime>=0.4.0",
|
|
17
|
+
"cryptography>=42,<51",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
litzki-sovp-trace = "litzki_sovp.adapter:main"
|
|
22
|
+
|
|
23
|
+
[tool.hatch.build.targets.wheel]
|
|
24
|
+
packages = ["src/litzki_sovp"]
|
|
25
|
+
|
|
26
|
+
[tool.pytest.ini_options]
|
|
27
|
+
testpaths = ["tests"]
|
|
28
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Command line adapter for SOVP attestation JSON to signed TRACE JSON."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from cryptography.hazmat.primitives import serialization
|
|
10
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
11
|
+
|
|
12
|
+
from litzki_sovp.trace import build_trace_record, sign_trace_record
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_private_key(path: Path) -> Ed25519PrivateKey:
|
|
16
|
+
key = serialization.load_pem_private_key(path.read_bytes(), password=None)
|
|
17
|
+
if not isinstance(key, Ed25519PrivateKey):
|
|
18
|
+
raise ValueError("private key must be an Ed25519 PEM key")
|
|
19
|
+
return key
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def convert_file(input_path: Path, private_key_path: Path, output_path: Path) -> None:
|
|
23
|
+
sovp_result = json.loads(input_path.read_text(encoding="utf-8"))
|
|
24
|
+
private_key = load_private_key(private_key_path)
|
|
25
|
+
record = build_trace_record(sovp_result)
|
|
26
|
+
signed = sign_trace_record(record, private_key)
|
|
27
|
+
output_path.write_text(json.dumps(signed, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main() -> None:
|
|
31
|
+
parser = argparse.ArgumentParser(description="Convert SOVP attestation JSON to a signed AgenTrust TRACE record")
|
|
32
|
+
parser.add_argument("--input", required=True, type=Path, help="SOVP attestation result JSON")
|
|
33
|
+
parser.add_argument("--private-key", required=True, type=Path, help="Ed25519 private key in PEM format")
|
|
34
|
+
parser.add_argument("--output", required=True, type=Path, help="Output TRACE JSON path")
|
|
35
|
+
args = parser.parse_args()
|
|
36
|
+
convert_file(args.input, args.private_key, args.output)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
main()
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""TRACE record construction for SOVP attestation results.
|
|
2
|
+
|
|
3
|
+
Builds an AgenTrust TRACE v0.2 Trust Record from a SOVP attestation document and
|
|
4
|
+
signs it with the released ``agentrust-trace`` signing implementation, so the
|
|
5
|
+
output verifies under ``agentrust-trace-tests`` (TR-ENV / TR-SIG / TR-POL) at
|
|
6
|
+
Level 0.
|
|
7
|
+
|
|
8
|
+
Design stance: this bridge only *transcribes* a SOVP attestation into TRACE
|
|
9
|
+
shape. It measures nothing itself, so every field that would otherwise assert a
|
|
10
|
+
strong guarantee is set to its weakest honest value:
|
|
11
|
+
|
|
12
|
+
- ``runtime.platform`` is ``software-only`` (no hardware TEE in the bridge);
|
|
13
|
+
- ``origin.kind`` is ``third-party-control-plane`` (the evidence is asserted by
|
|
14
|
+
SOVP, not measured by this runtime);
|
|
15
|
+
- ``appraisal.status`` is ``none`` (no verifier appraised the result);
|
|
16
|
+
- ``build_provenance.slsa_level`` is ``0`` (no build-provenance guarantee claimed).
|
|
17
|
+
|
|
18
|
+
This mirrors the project README's "What it does not claim" section: the record
|
|
19
|
+
never dresses transcribed evidence up as hardware-attested.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import time
|
|
25
|
+
from collections.abc import Mapping
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
from agentrust_trace.sign import TRACE_PROFILE_V0_2, sign_record
|
|
29
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
30
|
+
|
|
31
|
+
# Fields the caller must supply explicitly in the ``agentrust`` object. The bridge
|
|
32
|
+
# never infers an attestation claim it was not given.
|
|
33
|
+
TRACE_REQUIRED_FIELDS = (
|
|
34
|
+
"agent_id",
|
|
35
|
+
"policy_hash",
|
|
36
|
+
"catalog_hash",
|
|
37
|
+
"payload_classification",
|
|
38
|
+
"tee_measurement",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# TR-POL-002 accepts only these; the model's fourth value ``declared`` passes the
|
|
42
|
+
# schema but fails Level-0 conformance, so it is deliberately not offered here.
|
|
43
|
+
_VALID_ENFORCEMENT_MODES = frozenset({"enforce", "advisory", "silent"})
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _enforcement_mode(agentrust: Mapping[str, Any]) -> str:
|
|
47
|
+
"""Return a conformance-valid ``policy.enforcement_mode`` for the record.
|
|
48
|
+
|
|
49
|
+
Honest mapping from the caller's own governance decision:
|
|
50
|
+
|
|
51
|
+
- an explicit ``policy_enforcement_mode`` wins, but must be one the TRACE
|
|
52
|
+
conformance suite accepts;
|
|
53
|
+
- otherwise, a caller ``decision`` that was acted on means the policy was
|
|
54
|
+
*enforced*; with no decision at all the honest value is ``advisory`` (a
|
|
55
|
+
policy is named but nothing is asserted to have acted on it).
|
|
56
|
+
"""
|
|
57
|
+
mode = agentrust.get("policy_enforcement_mode")
|
|
58
|
+
if mode is not None:
|
|
59
|
+
if mode not in _VALID_ENFORCEMENT_MODES:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f"policy_enforcement_mode must be one of {sorted(_VALID_ENFORCEMENT_MODES)}, "
|
|
62
|
+
f"got {mode!r} (the schema also allows 'declared', but it fails TRACE "
|
|
63
|
+
"conformance TR-POL-002)"
|
|
64
|
+
)
|
|
65
|
+
return mode
|
|
66
|
+
return "enforce" if agentrust.get("decision") else "advisory"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def build_trace_record(sovp_result: Mapping[str, Any]) -> dict[str, Any]:
|
|
70
|
+
"""Build an unsigned AgenTrust TRACE v0.2 Trust Record from a SOVP result.
|
|
71
|
+
|
|
72
|
+
``sovp_result`` must contain an ``agentrust`` object with the fields named in
|
|
73
|
+
:data:`TRACE_REQUIRED_FIELDS` and may contain a ``sovp`` object with the source
|
|
74
|
+
attestation payload. Missing required fields raise ``ValueError``; nothing is
|
|
75
|
+
inferred.
|
|
76
|
+
"""
|
|
77
|
+
agentrust = sovp_result.get("agentrust")
|
|
78
|
+
if not isinstance(agentrust, Mapping):
|
|
79
|
+
raise ValueError("SOVP result must contain an 'agentrust' object")
|
|
80
|
+
|
|
81
|
+
missing = [field for field in TRACE_REQUIRED_FIELDS if field not in agentrust]
|
|
82
|
+
if missing:
|
|
83
|
+
raise ValueError(f"missing TRACE fields: {', '.join(missing)}")
|
|
84
|
+
|
|
85
|
+
sovp = sovp_result.get("sovp", {})
|
|
86
|
+
if not isinstance(sovp, Mapping):
|
|
87
|
+
raise ValueError("'sovp' must be an object when present")
|
|
88
|
+
|
|
89
|
+
now = int(time.time())
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
"eat_profile": TRACE_PROFILE_V0_2,
|
|
93
|
+
"iat": now,
|
|
94
|
+
"subject": agentrust["agent_id"],
|
|
95
|
+
"model": {
|
|
96
|
+
"provider": str(agentrust.get("model_provider", "litzki-systems")),
|
|
97
|
+
"model_id": str(agentrust.get("model_id", "sovp")),
|
|
98
|
+
"version": str(agentrust.get("model_version", "0.1.0")),
|
|
99
|
+
},
|
|
100
|
+
"runtime": {
|
|
101
|
+
# No hardware TEE in the bridge; the measurement is transcribed from
|
|
102
|
+
# SOVP and the software-only platform says so.
|
|
103
|
+
"platform": "software-only",
|
|
104
|
+
"measurement": agentrust["tee_measurement"],
|
|
105
|
+
"nonce": sovp.get("nonce"),
|
|
106
|
+
},
|
|
107
|
+
"policy": {
|
|
108
|
+
"bundle_hash": agentrust["policy_hash"],
|
|
109
|
+
"enforcement_mode": _enforcement_mode(agentrust),
|
|
110
|
+
},
|
|
111
|
+
"data_class": agentrust["payload_classification"],
|
|
112
|
+
"origin": {
|
|
113
|
+
# The evidence is asserted by SOVP, not measured here.
|
|
114
|
+
"kind": "third-party-control-plane",
|
|
115
|
+
"producer": "SOVP",
|
|
116
|
+
"source_event_id": str(
|
|
117
|
+
sovp.get("subject", agentrust.get("trace_id", "sovp-attestation"))
|
|
118
|
+
),
|
|
119
|
+
"ingested_at": now,
|
|
120
|
+
},
|
|
121
|
+
"build_provenance": {
|
|
122
|
+
# slsa_level 0 == no build-provenance guarantee claimed. The catalog
|
|
123
|
+
# hash is the one contextual digest the caller supplies.
|
|
124
|
+
"slsa_level": 0,
|
|
125
|
+
"digest": agentrust["catalog_hash"],
|
|
126
|
+
},
|
|
127
|
+
"appraisal": {
|
|
128
|
+
"status": "none",
|
|
129
|
+
"verifier": "litzki-sovp-agentrust-bridge",
|
|
130
|
+
"timestamp": now,
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def sign_trace_record(
|
|
136
|
+
record: Mapping[str, Any], private_key: Ed25519PrivateKey
|
|
137
|
+
) -> dict[str, Any]:
|
|
138
|
+
"""Return a signed TRACE record.
|
|
139
|
+
|
|
140
|
+
Delegates to ``agentrust_trace.sign.sign_record``, which populates ``cnf.jwk``
|
|
141
|
+
with the public key and adds an Ed25519 ``signature`` over the RFC 8785 (JCS)
|
|
142
|
+
canonical form of the record with only the ``signature`` field absent. This is
|
|
143
|
+
the exact convention ``agentrust-trace-tests`` TR-SIG verifies.
|
|
144
|
+
"""
|
|
145
|
+
return sign_record(dict(record), private_key)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def trace_signing_pre_image(record: Mapping[str, Any]) -> bytes:
|
|
149
|
+
"""Return the canonical signing pre-image for a TRACE record.
|
|
150
|
+
|
|
151
|
+
Kept for API stability (re-exported from the package root). Signing itself is
|
|
152
|
+
owned by ``agentrust-trace``; this delegates to the same canonicalization so a
|
|
153
|
+
caller inspecting the pre-image sees exactly the bytes that get signed.
|
|
154
|
+
"""
|
|
155
|
+
from agentrust_trace.sign import _canonical_bytes
|
|
156
|
+
|
|
157
|
+
return _canonical_bytes({k: v for k, v in record.items() if k != "signature"})
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from agentrust_trace import verify_record
|
|
5
|
+
from agentrust_trace.models import TrustRecord
|
|
6
|
+
from agentrust_trace.sign import TRACE_PROFILE_V0_2
|
|
7
|
+
from cryptography.exceptions import InvalidSignature
|
|
8
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
9
|
+
from trace_tests.result import Status
|
|
10
|
+
from trace_tests.runner import run
|
|
11
|
+
|
|
12
|
+
from litzki_sovp.trace import build_trace_record, sign_trace_record, trace_signing_pre_image
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _sovp_result():
|
|
16
|
+
return {
|
|
17
|
+
"agentrust": {
|
|
18
|
+
"trace_id": "0192f3a0-0000-7000-8000-0000000000aa",
|
|
19
|
+
"agent_id": "spiffe://litzki.systems/agent/sovp-demo",
|
|
20
|
+
"policy_hash": "sha256:" + "a1" * 32,
|
|
21
|
+
"catalog_hash": "sha256:" + "b2" * 32,
|
|
22
|
+
"decision": "allow",
|
|
23
|
+
"decision_reason": "SOVP attestation result accepted by caller policy.",
|
|
24
|
+
"payload_classification": "internal",
|
|
25
|
+
"tee_measurement": "sha256:" + "cd" * 32,
|
|
26
|
+
},
|
|
27
|
+
"sovp": {"result": "passed", "measurement": "sha256:" + "cd" * 32, "nonce": "example-nonce", "subject": "sovp-demo"},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_build_trace_record_requires_explicit_trace_fields():
|
|
32
|
+
source = _sovp_result()
|
|
33
|
+
del source["agentrust"]["policy_hash"]
|
|
34
|
+
|
|
35
|
+
with pytest.raises(ValueError, match="policy_hash"):
|
|
36
|
+
build_trace_record(source)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_build_trace_record_is_a_structurally_valid_v0_2_record():
|
|
40
|
+
record = build_trace_record(_sovp_result())
|
|
41
|
+
|
|
42
|
+
assert record["eat_profile"] == TRACE_PROFILE_V0_2
|
|
43
|
+
# The unsigned record is a valid TrustRecord except for the cnf/signature that
|
|
44
|
+
# signing adds, so validate the signed form.
|
|
45
|
+
signed = sign_trace_record(record, Ed25519PrivateKey.generate())
|
|
46
|
+
TrustRecord.model_validate(signed)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_enforcement_mode_rejects_non_conformant_override():
|
|
50
|
+
source = _sovp_result()
|
|
51
|
+
source["agentrust"]["policy_enforcement_mode"] = "declared"
|
|
52
|
+
|
|
53
|
+
with pytest.raises(ValueError, match="TR-POL-002"):
|
|
54
|
+
build_trace_record(source)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_signed_record_verifies_with_agentrust_trace():
|
|
58
|
+
private_key = Ed25519PrivateKey.generate()
|
|
59
|
+
signed = sign_trace_record(build_trace_record(_sovp_result()), private_key)
|
|
60
|
+
|
|
61
|
+
# Real verifier: profile, freshness, and Ed25519 signature over the JCS pre-image.
|
|
62
|
+
verify_record(signed, private_key.public_key())
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_tampered_field_invalidates_signature():
|
|
66
|
+
private_key = Ed25519PrivateKey.generate()
|
|
67
|
+
signed = sign_trace_record(build_trace_record(_sovp_result()), private_key)
|
|
68
|
+
|
|
69
|
+
tampered = copy.deepcopy(signed)
|
|
70
|
+
tampered["data_class"] = "public"
|
|
71
|
+
|
|
72
|
+
with pytest.raises(InvalidSignature):
|
|
73
|
+
verify_record(tampered, private_key.public_key())
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_pre_image_excludes_only_signature():
|
|
77
|
+
record = sign_trace_record(build_trace_record(_sovp_result()), Ed25519PrivateKey.generate())
|
|
78
|
+
record["signature"] = "one"
|
|
79
|
+
first = trace_signing_pre_image(record)
|
|
80
|
+
record["signature"] = "two"
|
|
81
|
+
|
|
82
|
+
assert trace_signing_pre_image(record) == first
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_record_passes_trace_tests_level_0():
|
|
86
|
+
signed = sign_trace_record(build_trace_record(_sovp_result()), Ed25519PrivateKey.generate())
|
|
87
|
+
|
|
88
|
+
results = run(signed, "trace", level=0)
|
|
89
|
+
failures = [
|
|
90
|
+
finding
|
|
91
|
+
for findings in results.values()
|
|
92
|
+
for finding in findings
|
|
93
|
+
if finding.status is Status.FAIL
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
assert not failures, [f.message for f in failures]
|