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 +37 -0
- fkyh/__main__.py +199 -0
- fkyh/attestation.py +623 -0
- fkyh/discovery.py +802 -0
- fkyh/gossip.py +377 -0
- fkyh/jcs.py +236 -0
- fkyh/keys.py +233 -0
- fkyh/metrics.py +216 -0
- fkyh/node.py +349 -0
- fkyh/servicetype.py +399 -0
- fkyh/signer.py +163 -0
- fkyh/signing.py +598 -0
- fkyh/state.py +269 -0
- fkyh-0.1.0.dist-info/METADATA +485 -0
- fkyh-0.1.0.dist-info/RECORD +19 -0
- fkyh-0.1.0.dist-info/WHEEL +5 -0
- fkyh-0.1.0.dist-info/entry_points.txt +2 -0
- fkyh-0.1.0.dist-info/licenses/LICENSE +202 -0
- fkyh-0.1.0.dist-info/top_level.txt +1 -0
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())
|