fkyh 0.1.0__py3-none-any.whl

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.
fkyh/__init__.py ADDED
@@ -0,0 +1,37 @@
1
+ """
2
+ FK YH — the reference implementation of the FK YH protocol.
3
+
4
+ An open protocol for agent discovery, coordination, observability, and
5
+ independently verifiable attestations. Piggybacks on existing protocol
6
+ fields (mDNS, HTTP headers, Prometheus) — no new infrastructure.
7
+
8
+ Modules:
9
+ state agent and fleet state, and the FK YH response headers
10
+ discovery mDNS advertisement and browsing
11
+ metrics Prometheus exposition
12
+ gossip the fleet scraper and the observer
13
+ attestation the consumer side of the attestation protocol
14
+ node the HTTP service that serves all of the above
15
+
16
+ The package depends on the standard library only. `cryptography` is
17
+ optional, and is needed for the two things that use a signature: the
18
+ attestation layer (ATTESTATION.md) and the signed gossip headers of
19
+ SPEC.md section 4.4.
20
+ """
21
+
22
+ from fkyh.state import (
23
+ PROTOCOL_VERSION,
24
+ AgentState,
25
+ FleetState,
26
+ status_for,
27
+ )
28
+
29
+ __all__ = [
30
+ "PROTOCOL_VERSION",
31
+ "AgentState",
32
+ "FleetState",
33
+ "status_for",
34
+ "__version__",
35
+ ]
36
+
37
+ __version__ = "0.1.0"
fkyh/__main__.py ADDED
@@ -0,0 +1,199 @@
1
+ """
2
+ FK YH command line.
3
+
4
+ python3 -m fkyh keys create --keystore ./keys --name verifier-1
5
+ python3 -m fkyh keys show --keystore ./keys
6
+ python3 -m fkyh keys revoke --keystore ./keys --name verifier-1 --reason "..."
7
+ python3 -m fkyh keys rotate --keystore ./keys --name verifier-1 --new-name verifier-2
8
+
9
+ python3 -m fkyh verify --attestation ./attestation.json --trust ./trust.json
10
+ python3 -m fkyh verify --attestation ./attestation.json --trust ./trust.json --json
11
+
12
+ Key management exists because the reference node used to generate an
13
+ ephemeral key on every start. A key that changes on every restart cannot be
14
+ referenced by a trust configuration, so it cannot be authorised by anyone.
15
+
16
+ `verify` exists because a specification nobody can run is a document, and a
17
+ document is not a product. It is the one piece of this protocol that a
18
+ stranger can point at their own code and use: give it an attestation and a
19
+ trust configuration, and it tells you what can be relied upon. The three
20
+ outcomes are kept distinct because they are three different facts:
21
+ `verified` is an authentic attestation reporting success, `failed` is an
22
+ authentic attestation reporting failure, and `inconclusive` is an authentic
23
+ attestation that could not decide. All three are authentic; only the first
24
+ is a success, and a verifier that collapses them would let a caller act on
25
+ a report of failure.
26
+ """
27
+ from __future__ import annotations
28
+
29
+ import argparse
30
+ import json
31
+ import os
32
+ import sys
33
+
34
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
35
+
36
+ from fkyh.attestation import AttestationError, Consumer, TrustConfig # noqa: E402
37
+ from fkyh.keys import KeyStore, KeyStoreError # noqa: E402
38
+
39
+
40
+ def _cmd_keys(args) -> int:
41
+ store = KeyStore(args.keystore)
42
+ try:
43
+ if args.action == "create":
44
+ key = store.create(args.name, policies=args.policy or [],
45
+ overwrite=args.force)
46
+ print(f"created {key.name}")
47
+ print(f" key id : {key.key_id}")
48
+ print(f" private : {key.private_path} (mode 0600)")
49
+ print(f" policies : {key.policies or ['(any the consumer allows)']}")
50
+ print(f" trust : {store.trust_path}")
51
+ return 0
52
+
53
+ if args.action == "show":
54
+ config = store.trust_config()
55
+ print(json.dumps(config, indent=2, sort_keys=True))
56
+ return 0
57
+
58
+ if args.action == "revoke":
59
+ key_id = store.key_id_for(args.name)
60
+ if key_id is None:
61
+ print(f"no key named {args.name!r}", file=sys.stderr)
62
+ return 2
63
+ store.revoke(key_id, reason=args.reason)
64
+ print(f"revoked {key_id} ({args.name}): {args.reason}")
65
+ return 0
66
+
67
+ if args.action == "rotate":
68
+ successor = store.rotate(args.name, args.new_name,
69
+ policies=args.policy or None)
70
+ print(f"rotated {args.name} -> {args.new_name}")
71
+ print(f" new key id : {successor.key_id}")
72
+ print(f" private : {successor.private_path} (mode 0600)")
73
+ return 0
74
+ except KeyStoreError as exc:
75
+ print(f"error: {exc}", file=sys.stderr)
76
+ return 2
77
+ return 2
78
+
79
+
80
+ def _cmd_verify(args) -> int:
81
+ """Validate one attestation against a trust configuration.
82
+
83
+ The exit code is the part that matters to a caller, so it is defined
84
+ precisely and it is not a boolean. Zero means the attestation is
85
+ authentic AND reports success. One means it is authentic and reports
86
+ something else — a caller must not act on that, but the record is real
87
+ and the report may be exactly what a reader needs to see. Two means the
88
+ attestation could not be relied upon at all: forged, expired, replayed,
89
+ or malformed. Three is a usage or I/O failure, which is a different
90
+ thing again: nothing was concluded.
91
+
92
+ Collapsing those into pass/fail would lose the distinction that
93
+ ATTESTATION.md section 9 is about — an authentic report of failure is
94
+ evidence, and a forged report is not.
95
+ """
96
+ try:
97
+ with open(args.attestation, encoding="utf-8") as handle:
98
+ payload = json.load(handle)
99
+ except (OSError, ValueError) as exc:
100
+ print(f"error: cannot read attestation: {exc}", file=sys.stderr)
101
+ return 3
102
+
103
+ try:
104
+ trust = TrustConfig.load(args.trust)
105
+ except (OSError, ValueError) as exc:
106
+ print(f"error: cannot read trust configuration: {exc}",
107
+ file=sys.stderr)
108
+ return 3
109
+
110
+ consumer = Consumer(trust, evidence_root=args.evidence)
111
+ try:
112
+ result = consumer.validate(payload,
113
+ expected_task_id=args.task_id,
114
+ expected_attempt_id=args.attempt_id)
115
+ except AttestationError as exc:
116
+ # The package is standard library only, and `cryptography` is an
117
+ # optional extra because only the signature paths need it. That is
118
+ # the right shape for a LIBRARY, and the wrong shape for a COMMAND:
119
+ # a stranger who typed `pip install fkyh` and then `fkyh verify`
120
+ # must not be handed a traceback. The fix is one pip extra, and the
121
+ # message says exactly which one.
122
+ if "cryptography package is required" in str(exc):
123
+ print("error: verifying a signature needs the optional "
124
+ "cryptography dependency.\n"
125
+ " install it with: pip install 'fkyh[signing]'",
126
+ file=sys.stderr)
127
+ return 3
128
+ raise
129
+
130
+ if args.json:
131
+ print(json.dumps(result.to_dict(), indent=2, sort_keys=True))
132
+ else:
133
+ print(f"outcome : {result.result or result.code}")
134
+ print(f"accepted: {result.accepted}")
135
+ print(f"trusted : {result.trusted}")
136
+ if result.code and result.code != result.result:
137
+ print(f"code : {result.code}")
138
+ if result.detail:
139
+ print(f"detail : {result.detail}")
140
+ if result.task_id:
141
+ print(f"task : {result.task_id}")
142
+ if result.attempt_id:
143
+ print(f"attempt : {result.attempt_id}")
144
+ if result.policy_id:
145
+ print(f"policy : {result.policy_id}")
146
+ if result.verifier_key_id:
147
+ print(f"verifier: {result.verifier_key_id}")
148
+ for check in result.checks:
149
+ print(f" check : {check}")
150
+
151
+ if result.trusted:
152
+ return 0
153
+ if result.accepted:
154
+ return 1
155
+ return 2
156
+
157
+
158
+ def main(argv=None) -> int:
159
+ parser = argparse.ArgumentParser(prog="fkyh")
160
+ sub = parser.add_subparsers(dest="command", required=True)
161
+
162
+ keys = sub.add_parser("keys", help="manage verifier keys")
163
+ keys.add_argument("action", choices=["create", "show", "revoke", "rotate"])
164
+ keys.add_argument("--keystore", required=True,
165
+ help="directory holding the keys and trust.json")
166
+ keys.add_argument("--name", default="verifier-1",
167
+ help="name of the key")
168
+ keys.add_argument("--new-name", default=None,
169
+ help="name of the successor key (rotate)")
170
+ keys.add_argument("--policy", action="append",
171
+ help="a policy this key may attest under; repeatable")
172
+ keys.add_argument("--reason", default="unspecified",
173
+ help="why the key is being revoked")
174
+ keys.add_argument("--force", action="store_true",
175
+ help="overwrite an existing private key")
176
+ keys.set_defaults(func=_cmd_keys)
177
+
178
+ verify = sub.add_parser("verify", help="validate an attestation")
179
+ verify.add_argument("--attestation", required=True,
180
+ help="the attestation JSON to validate")
181
+ verify.add_argument("--trust", required=True,
182
+ help="the trust configuration to validate against")
183
+ verify.add_argument("--evidence", default=None,
184
+ help="the evidence directory, so the consumer can "
185
+ "re-derive digests (ATTESTATION.md section 10)")
186
+ verify.add_argument("--task-id", default=None,
187
+ help="require this task_id")
188
+ verify.add_argument("--attempt-id", default=None,
189
+ help="require this attempt_id")
190
+ verify.add_argument("--json", action="store_true",
191
+ help="print the verdict as JSON")
192
+ verify.set_defaults(func=_cmd_verify)
193
+
194
+ args = parser.parse_args(argv)
195
+ return args.func(args)
196
+
197
+
198
+ if __name__ == "__main__":
199
+ raise SystemExit(main())