notbefore 0.2.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.
- notbefore-0.2.0/.gitignore +14 -0
- notbefore-0.2.0/LICENSE +25 -0
- notbefore-0.2.0/PKG-INFO +58 -0
- notbefore-0.2.0/README.md +31 -0
- notbefore-0.2.0/notbefore/__init__.py +12 -0
- notbefore-0.2.0/notbefore/check.py +115 -0
- notbefore-0.2.0/notbefore/cli.py +112 -0
- notbefore-0.2.0/notbefore/derive.py +51 -0
- notbefore-0.2.0/notbefore/log.py +90 -0
- notbefore-0.2.0/notbefore/verifier/VENDORED.json +31 -0
- notbefore-0.2.0/notbefore/verifier/__init__.py +0 -0
- notbefore-0.2.0/notbefore/verifier/bls_drand.py +88 -0
- notbefore-0.2.0/notbefore/verifier/ci/KNOWN_NONCOMPLIANT.json +7 -0
- notbefore-0.2.0/notbefore/verifier/ci/__init__.py +0 -0
- notbefore-0.2.0/notbefore/verifier/ci/anchor_lib.py +197 -0
- notbefore-0.2.0/notbefore/verifier/drand_anchor.py +71 -0
- notbefore-0.2.0/notbefore/verifier/hosts/EXPECTED.json +21 -0
- notbefore-0.2.0/notbefore/verifier/hosts/__init__.py +0 -0
- notbefore-0.2.0/notbefore/verifier/hosts/attest_host.py +27 -0
- notbefore-0.2.0/notbefore/verifier/hosts/attest_lib.py +86 -0
- notbefore-0.2.0/notbefore/verifier/hosts/entropy_host.py +152 -0
- notbefore-0.2.0/notbefore/verifier/hosts/gnss_probe.py +105 -0
- notbefore-0.2.0/notbefore/verifier/hosts/stamp_probe.py +189 -0
- notbefore-0.2.0/notbefore/verifier/keys/KEYS.json +78 -0
- notbefore-0.2.0/notbefore/verifier/keys/anchor.pub +4 -0
- notbefore-0.2.0/notbefore/verifier/keys/drand-quicknet.json +18 -0
- notbefore-0.2.0/notbefore/verifier/keys/entropy_signer.pub +6 -0
- notbefore-0.2.0/notbefore/verifier/keys/rekor.pub +4 -0
- notbefore-0.2.0/notbefore/verifier/keys/time_attester.f9t.RETIRED.pub +6 -0
- notbefore-0.2.0/notbefore/verifier/keys/time_attester.pub +6 -0
- notbefore-0.2.0/notbefore/verifier/keys/time_witness.pub +6 -0
- notbefore-0.2.0/notbefore/verifier/schema.py +33 -0
- notbefore-0.2.0/notbefore/verifier/tsa.py +91 -0
- notbefore-0.2.0/notbefore/verifier/verify.py +343 -0
- notbefore-0.2.0/pyproject.toml +41 -0
- notbefore-0.2.0/tests/test_acceptance.py +54 -0
- notbefore-0.2.0/vendor.py +29 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
chain/pending/*.secret
|
|
2
|
+
chain/pending/*.revealed
|
|
3
|
+
cycle.log
|
|
4
|
+
cycle.out
|
|
5
|
+
tsa-certs/
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.pyc
|
|
8
|
+
|
|
9
|
+
# vendored inside the CLI package (the repo-root tsa-certs/ cache stays ignored)
|
|
10
|
+
!cli/notbefore/verifier/tsa-certs/
|
|
11
|
+
cli/dist/
|
|
12
|
+
cli/.venv/
|
|
13
|
+
cli/*.egg-info/
|
|
14
|
+
.pytest_cache/
|
notbefore-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bill Dailey (docdailey)
|
|
4
|
+
|
|
5
|
+
Applies to the software in this repository: verify.py, pulse.py, tsa.py, drand_anchor.py,
|
|
6
|
+
bls_drand.py, watcher.py, beacon-cycle.py, make_pulse.py, ci/, merkle/*.py, systemd/.
|
|
7
|
+
The published log data and documents are licensed separately — see LICENSE-DATA.
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
notbefore-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: notbefore
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: NotBefore: verify an hourly commit-then-reveal seed from the public qrng-beacon-log and derive labeled seeds, shuffles and splits from it — with a pinned, vendored verifier.
|
|
5
|
+
Project-URL: Homepage, https://github.com/docdailey/qrng-beacon-log
|
|
6
|
+
Project-URL: Specification, https://github.com/docdailey/qrng-beacon-log/blob/main/NOTBEFORE.md
|
|
7
|
+
Project-URL: Log, https://github.com/docdailey/qrng-beacon-log/tree/main/chain
|
|
8
|
+
Author: docdailey
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: beacon,commit-reveal,drand,randomness,reproducibility,transparency-log
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Topic :: Security :: Cryptography
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: cryptography>=42
|
|
21
|
+
Requires-Dist: py-ecc>=7
|
|
22
|
+
Provides-Extra: anchors
|
|
23
|
+
Requires-Dist: opentimestamps-client>=0.7; extra == 'anchors'
|
|
24
|
+
Provides-Extra: test
|
|
25
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# notbefore
|
|
29
|
+
|
|
30
|
+
Consumer CLI for the public **qrng-beacon-log** — an hourly, commit-then-reveal, host-attested randomness log
|
|
31
|
+
anchored to drand quicknet, RFC 3161 timestamps, Rekor and OpenTimestamps. Spec: `NOTBEFORE.md` (draft 0.2) in
|
|
32
|
+
<https://github.com/docdailey/qrng-beacon-log>.
|
|
33
|
+
|
|
34
|
+
**What it proves:** the 32-byte attested value \(V\) of hour \(N\) was **fixed before** drand round \(R\) and
|
|
35
|
+
**not selected after** \(R\) existed. **What it does not prove:** that the bits are quantum, secret, certified, or
|
|
36
|
+
unique. This is not a certification of anything; see `CLAIMS.md` in the log repository.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install notbefore # needs git and openssl on PATH; add [anchors] for OpenTimestamps proofs
|
|
40
|
+
notbefore verify 41 # commit 40 + reveal 41: signatures (pinned keys), drand BLS offline, RFC 3161, Rekor anchors
|
|
41
|
+
notbefore value 41 # V, only if verify passes
|
|
42
|
+
notbefore seed 41 --purpose clinic-qi-roster-2026-09-12 # S = SHA256("notbefore/derive/v1" || V || purpose)
|
|
43
|
+
notbefore shuffle 41 --purpose split:iris-csv:v3 rows.txt # deterministic shuffle of the lines of rows.txt
|
|
44
|
+
notbefore split 41 --purpose split:iris-csv:v3 --frac 0.8 rows.txt
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`seed`, `shuffle` and `split` write a **transcript** JSON (`notbefore-<seq>-<purpose>.json`) — the engineering
|
|
48
|
+
artifact that lets anyone reproduce the result from the public log.
|
|
49
|
+
|
|
50
|
+
**Trust model.** The verifier, the signing keys, the drand group key, the Rekor log key, the freetsa CA and the
|
|
51
|
+
expected host configuration are **vendored inside this package**, pinned at a named commit of the log repository
|
|
52
|
+
(`notbefore --version` prints it). The CLI executes only those files; the log is read as data. Updating the
|
|
53
|
+
verifier means updating the package — deliberately.
|
|
54
|
+
|
|
55
|
+
Eligible pairs start at 0020/0021 (0026/0027 preferred, execution enforced). `verify 19` fails by design (ERR-007).
|
|
56
|
+
Pulses 0001–0041 carry retroactive anchors (2026-09-12 12:47 UTC); from 0042 anchors are contemporaneous.
|
|
57
|
+
|
|
58
|
+
MIT. Data in the log: CC BY 4.0.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# notbefore
|
|
2
|
+
|
|
3
|
+
Consumer CLI for the public **qrng-beacon-log** — an hourly, commit-then-reveal, host-attested randomness log
|
|
4
|
+
anchored to drand quicknet, RFC 3161 timestamps, Rekor and OpenTimestamps. Spec: `NOTBEFORE.md` (draft 0.2) in
|
|
5
|
+
<https://github.com/docdailey/qrng-beacon-log>.
|
|
6
|
+
|
|
7
|
+
**What it proves:** the 32-byte attested value \(V\) of hour \(N\) was **fixed before** drand round \(R\) and
|
|
8
|
+
**not selected after** \(R\) existed. **What it does not prove:** that the bits are quantum, secret, certified, or
|
|
9
|
+
unique. This is not a certification of anything; see `CLAIMS.md` in the log repository.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install notbefore # needs git and openssl on PATH; add [anchors] for OpenTimestamps proofs
|
|
13
|
+
notbefore verify 41 # commit 40 + reveal 41: signatures (pinned keys), drand BLS offline, RFC 3161, Rekor anchors
|
|
14
|
+
notbefore value 41 # V, only if verify passes
|
|
15
|
+
notbefore seed 41 --purpose clinic-qi-roster-2026-09-12 # S = SHA256("notbefore/derive/v1" || V || purpose)
|
|
16
|
+
notbefore shuffle 41 --purpose split:iris-csv:v3 rows.txt # deterministic shuffle of the lines of rows.txt
|
|
17
|
+
notbefore split 41 --purpose split:iris-csv:v3 --frac 0.8 rows.txt
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`seed`, `shuffle` and `split` write a **transcript** JSON (`notbefore-<seq>-<purpose>.json`) — the engineering
|
|
21
|
+
artifact that lets anyone reproduce the result from the public log.
|
|
22
|
+
|
|
23
|
+
**Trust model.** The verifier, the signing keys, the drand group key, the Rekor log key, the freetsa CA and the
|
|
24
|
+
expected host configuration are **vendored inside this package**, pinned at a named commit of the log repository
|
|
25
|
+
(`notbefore --version` prints it). The CLI executes only those files; the log is read as data. Updating the
|
|
26
|
+
verifier means updating the package — deliberately.
|
|
27
|
+
|
|
28
|
+
Eligible pairs start at 0020/0021 (0026/0027 preferred, execution enforced). `verify 19` fails by design (ERR-007).
|
|
29
|
+
Pulses 0001–0041 carry retroactive anchors (2026-09-12 12:47 UTC); from 0042 anchors are contemporaneous.
|
|
30
|
+
|
|
31
|
+
MIT. Data in the log: CC BY 4.0.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""NotBefore — the consumer side of the qrng-beacon-log: verify one hourly commit-then-reveal pair with a
|
|
2
|
+
pinned, vendored verifier, and derive labeled seeds / shuffles / splits from its attested value.
|
|
3
|
+
|
|
4
|
+
Spec: NOTBEFORE.md in https://github.com/docdailey/qrng-beacon-log (draft 0.2). Not a certification of anything.
|
|
5
|
+
"""
|
|
6
|
+
__version__ = "0.2.0"
|
|
7
|
+
SPEC = "notbefore/spec/0.2"
|
|
8
|
+
D_DERIVE = b"notbefore/derive/v1"
|
|
9
|
+
D_SHUFFLE = b"notbefore/shuffle/v1"
|
|
10
|
+
DEFAULT_REPO = "https://github.com/docdailey/qrng-beacon-log.git"
|
|
11
|
+
FIRST_ELIGIBLE_REVEAL = 21 # 0020/0021; 18/19 are KNOWN-NONCOMPLIANT (ERR-007)
|
|
12
|
+
ANCHOR_GRACE_S = 1500 # a pulse older than this with no anchor is non-compliant
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Verify one NotBefore pair (commit N-1, reveal N) with the VENDORED verifier and keys (NOTBEFORE.md §5–6).
|
|
2
|
+
The log supplies data only. Exit-code discipline: a CheckResult with ok=False must never yield V or S."""
|
|
3
|
+
import os, sys, json, subprocess, time, base64
|
|
4
|
+
from . import FIRST_ELIGIBLE_REVEAL, ANCHOR_GRACE_S
|
|
5
|
+
from .log import LogSource
|
|
6
|
+
|
|
7
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
8
|
+
VENDOR = os.path.join(HERE, "verifier")
|
|
9
|
+
KEYS = os.path.join(VENDOR, "keys")
|
|
10
|
+
sys.path.insert(0, os.path.join(VENDOR, "ci"))
|
|
11
|
+
|
|
12
|
+
def vendored_meta():
|
|
13
|
+
try: return json.load(open(os.path.join(VENDOR, "VENDORED.json")))
|
|
14
|
+
except Exception: return {}
|
|
15
|
+
|
|
16
|
+
def known_noncompliant():
|
|
17
|
+
try: return {int(k): v for k, v in json.load(open(os.path.join(VENDOR, "ci", "KNOWN_NONCOMPLIANT.json"))).get("pulses", {}).items()}
|
|
18
|
+
except Exception: return {}
|
|
19
|
+
|
|
20
|
+
class CheckResult:
|
|
21
|
+
def __init__(self, seq):
|
|
22
|
+
self.seq = seq; self.ok = True; self.lines = []; self.verbose = []
|
|
23
|
+
self.commit_seq = self.pulse_hash_reveal = self.pulse_hash_commit = self.attested_value = self.drand_round = None
|
|
24
|
+
self.bls_offline = False; self.tsa_pass = 0; self.anchors = "not checked"
|
|
25
|
+
self.log_git_sha = self.log_ref = None; self.verifier_git_sha = vendored_meta().get("git_sha")
|
|
26
|
+
def say(self, ok, msg, level=None):
|
|
27
|
+
tag = level or ("PASS" if ok else "FAIL")
|
|
28
|
+
if tag == "FAIL": self.ok = False
|
|
29
|
+
self.lines.append(f"[{tag}] {msg}")
|
|
30
|
+
def summary(self):
|
|
31
|
+
return {"ok": self.ok, "bls_offline": self.bls_offline, "tsa_tokens_verified": self.tsa_pass, "anchors": self.anchors,
|
|
32
|
+
"lines": self.lines}
|
|
33
|
+
|
|
34
|
+
def _run(args, cwd=None):
|
|
35
|
+
r = subprocess.run([sys.executable, *args], cwd=cwd, capture_output=True, text=True); return r.returncode, r.stdout + r.stderr
|
|
36
|
+
|
|
37
|
+
def check_pair(seq: int, src: LogSource, refetch=True, anchors=True, verbose=False) -> CheckResult:
|
|
38
|
+
R = CheckResult(seq); R.log_git_sha, R.log_ref = src.log_git_sha, (src.ref or "working tree")
|
|
39
|
+
knc = known_noncompliant()
|
|
40
|
+
# ---- eligibility (§6) before anything is executed
|
|
41
|
+
if seq < FIRST_ELIGIBLE_REVEAL: R.say(False, f"seq {seq} is below the first eligible reveal ({FIRST_ELIGIBLE_REVEAL}); pulses 1–17 are legacy (ERR-005), 18–19 KNOWN-NONCOMPLIANT (ERR-007)")
|
|
42
|
+
for s in (seq, seq - 1):
|
|
43
|
+
if s in knc: R.say(False, f"seq {s} is KNOWN-NONCOMPLIANT ({knc[s].get('erratum')}): {knc[s].get('reason')}")
|
|
44
|
+
if not R.ok: return R
|
|
45
|
+
rev = src.pulse(seq); com = src.pulse(seq - 1)
|
|
46
|
+
if rev is None: R.say(False, f"pulse {seq:04d} is not in the log at {R.log_ref}"); return R
|
|
47
|
+
if com is None: R.say(False, f"predecessor pulse {seq-1:04d} is not in the log"); return R
|
|
48
|
+
rc_, cc_ = rev["core"], com["core"]
|
|
49
|
+
R.pulse_hash_reveal, R.pulse_hash_commit = rev["pulse_hash"], com["pulse_hash"]
|
|
50
|
+
R.say(rc_.get("v") == "0.5", f"reveal {seq:04d} is protocol v0.5 (got {rc_.get('v') or rc_.get('version') or 'legacy'})")
|
|
51
|
+
R.say(rc_.get("type") == "reveal", f"pulse {seq:04d} is a reveal (type {rc_.get('type')})")
|
|
52
|
+
R.say(cc_.get("type") == "commit", f"pulse {seq-1:04d} is a commit (type {cc_.get('type')})")
|
|
53
|
+
R.say((rc_.get("derived") or {}).get("commit_seq") == seq - 1, f"reveal names commit_seq {seq-1} (derived.commit_seq = {(rc_.get('derived') or {}).get('commit_seq')})")
|
|
54
|
+
if not R.ok: return R
|
|
55
|
+
R.commit_seq = seq - 1; R.attested_value = rc_["derived"]["attested_value"]; R.drand_round = rc_["drand"]["round"]
|
|
56
|
+
# ---- materialize the pair (+ the commit's predecessor for its chain link) and run the vendored verifier
|
|
57
|
+
p_rev, p_com = src.materialize(seq), src.materialize(seq - 1)
|
|
58
|
+
p_pp = src.materialize(seq - 2) if src.has_pulse(seq - 2) else None
|
|
59
|
+
vpy = os.path.join(VENDOR, "verify.py"); extra = ["--refetch"] if refetch else []
|
|
60
|
+
rc, out = _run([vpy, p_com, "--pin", KEYS] + (["--prev", p_pp] if p_pp else []) + extra); R.verbose.append(out)
|
|
61
|
+
R.say(rc == 0 and "ALL CHECKS PASSED" in out, f"commit {seq-1:04d}: vendored verify.py (pinned keys{', chained to %04d' % (seq-2) if p_pp else ''}{', drand refetched' if refetch else ''})")
|
|
62
|
+
_bls_c = "[full BLS, offline]" in out and "BLS verification skipped" not in out
|
|
63
|
+
rc, out = _run([vpy, p_rev, "--pin", KEYS, "--prev", p_com] + extra); R.verbose.append(out)
|
|
64
|
+
R.say(rc == 0 and "ALL CHECKS PASSED" in out, f"reveal {seq:04d}: vendored verify.py (pinned keys, chained to {seq-1:04d}, C = SHA256(D_commit||E), V recomputed, timing contract{', drand refetched' if refetch else ''})")
|
|
65
|
+
R.bls_offline = _bls_c and "[full BLS, offline]" in out and "BLS verification skipped" not in out
|
|
66
|
+
R.say(R.bls_offline, "drand round BLS-verified offline under the pinned quicknet group key (py_ecc)")
|
|
67
|
+
if f"Attested value {R.attested_value}" not in out: R.say(False, "verifier's attested value differs from the pulse's derived.attested_value")
|
|
68
|
+
# ---- RFC 3161 on the commit
|
|
69
|
+
rc, out = _run([os.path.join(VENDOR, "tsa.py"), "verify", p_com]); R.verbose.append(out)
|
|
70
|
+
R.tsa_pass = out.count("[PASS] RFC3161")
|
|
71
|
+
R.say(R.tsa_pass >= 2 and "[FAIL]" not in out, f"commit {seq-1:04d}: {R.tsa_pass} RFC 3161 token(s) verify (need ≥ 2: freetsa + DigiCert)")
|
|
72
|
+
# ---- publication anchors (SHOULD; missing anchors fail once the pulse is older than the grace period)
|
|
73
|
+
if anchors:
|
|
74
|
+
R.anchors = _check_anchors(R, src, (seq - 1, seq), rc_, refetch)
|
|
75
|
+
else: R.anchors = "skipped (--no-anchors)"
|
|
76
|
+
return R
|
|
77
|
+
|
|
78
|
+
def _check_anchors(R, src, seqs, rev_core, refetch):
|
|
79
|
+
if not src.anchors_available():
|
|
80
|
+
R.say(True, "publication anchors: no anchors branch reachable from this log source — not checked", "WARN"); return "unavailable"
|
|
81
|
+
try:
|
|
82
|
+
import anchor_lib as L
|
|
83
|
+
except Exception as e:
|
|
84
|
+
R.say(True, f"publication anchors: vendored anchor_lib unavailable ({e})", "WARN"); return "unavailable"
|
|
85
|
+
anchor_pub = L.load_pub(open(os.path.join(KEYS, "anchor.pub"), "rb").read()); rekor_pub = L.load_pub(open(os.path.join(KEYS, "rekor.pub"), "rb").read())
|
|
86
|
+
age = time.time() - float((rev_core.get("derived") or {}).get("round_release_unix_s") or time.time())
|
|
87
|
+
status = []
|
|
88
|
+
for s in seqs:
|
|
89
|
+
rec, stmt = src.anchor(s)
|
|
90
|
+
if rec is None:
|
|
91
|
+
if age > ANCHOR_GRACE_S: R.say(False, f"pulse {s:04d}: no publication anchor {age/60:.0f} min after its round (grace {ANCHOR_GRACE_S//60} min)")
|
|
92
|
+
else: R.say(True, f"pulse {s:04d}: anchor pending ({age:.0f} s since its round)", "WARN")
|
|
93
|
+
status.append("missing"); continue
|
|
94
|
+
pf = os.path.join(src.tmp, "chain", f"pulse-{s:04d}.json")
|
|
95
|
+
want, _ = L.statement_for(pf); entry = rec["rekor"]["entry"]
|
|
96
|
+
ok = stmt == want and rec["statement_sha256"] == L.sha256(want)
|
|
97
|
+
h, k = L.entry_hash_and_key(entry)
|
|
98
|
+
ok &= h == L.sha256(want) and k is not None and L.key_id(L.load_pub(k)) == L.key_id(anchor_pub)
|
|
99
|
+
ok &= L.verify_sig(anchor_pub, base64.b64decode(rec["signature_b64"]), want)
|
|
100
|
+
ok &= entry.get("logID") == L.REKOR_LOG_ID and L.verify_set(entry, rekor_pub)
|
|
101
|
+
inc, why = L.verify_inclusion(entry, rekor_pub); ok &= inc
|
|
102
|
+
live = ""
|
|
103
|
+
if refetch and ok:
|
|
104
|
+
try:
|
|
105
|
+
e2 = L.rekor_get(rec["rekor"]["uuid"]); ok &= e2["body"] == entry["body"] and e2["integratedTime"] == entry["integratedTime"]; live = ", re-fetched live"
|
|
106
|
+
except Exception as ex: R.say(True, f"pulse {s:04d}: Rekor refetch failed ({ex}); offline proof stands", "WARN")
|
|
107
|
+
when = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(entry["integratedTime"]))
|
|
108
|
+
R.say(ok, f"pulse {s:04d}: Rekor anchor logIndex {entry['logIndex']} @ {when} — statement, signature, SET, inclusion proof + checkpoint verified offline against pinned keys{live}; OTS {rec.get('ots', {}).get('status')}")
|
|
109
|
+
status.append("ok" if ok else "FAIL")
|
|
110
|
+
if all(x == "ok" for x in status):
|
|
111
|
+
rel = float((rev_core.get("derived") or {}).get("round_release_unix_s") or 0); rec_c, _ = src.anchor(seqs[0])
|
|
112
|
+
it = rec_c["rekor"]["entry"]["integratedTime"]
|
|
113
|
+
if it < rel: R.say(True, f"commit {seqs[0]:04d}: Rekor time precedes the drand release by {rel-it:.0f} s (third independent clock on the commit)")
|
|
114
|
+
else: R.say(True, f"commit {seqs[0]:04d}: Rekor time is {it-rel:.0f} s AFTER the drand release — a retroactive anchor (pulses ≤ 0041 were anchored 2026-09-12 12:47 UTC); the RFC 3161 tokens are the commit-time proof", "WARN")
|
|
115
|
+
return "/".join(status)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""notbefore — CLI (NOTBEFORE.md §8).
|
|
2
|
+
|
|
3
|
+
notbefore verify <seq>
|
|
4
|
+
notbefore value <seq>
|
|
5
|
+
notbefore seed <seq> --purpose <P>
|
|
6
|
+
notbefore shuffle <seq> --purpose <P> <file>
|
|
7
|
+
notbefore split <seq> --purpose <P> --frac 0.8 <file>
|
|
8
|
+
|
|
9
|
+
`value`/`seed`/`shuffle`/`split` print nothing usable unless `verify` would pass; exit 1 otherwise.
|
|
10
|
+
The verifier and every key are vendored in this package; the log is read as data. Requires `git` and `openssl` on PATH.
|
|
11
|
+
"""
|
|
12
|
+
import sys, os, json, argparse, hashlib
|
|
13
|
+
from . import __version__, SPEC, DEFAULT_REPO
|
|
14
|
+
from .log import LogSource, LogError
|
|
15
|
+
from .check import check_pair, vendored_meta
|
|
16
|
+
from . import derive as D
|
|
17
|
+
|
|
18
|
+
def _err(msg): sys.stderr.write("notbefore: " + msg + "\n")
|
|
19
|
+
|
|
20
|
+
def build_parser():
|
|
21
|
+
ap = argparse.ArgumentParser(prog="notbefore", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
22
|
+
ap.add_argument("--version", action="version", version=f"notbefore {__version__} ({SPEC}; verifier vendored from {vendored_meta().get('git_sha','?')[:12]})")
|
|
23
|
+
g = ap.add_argument_group("log source")
|
|
24
|
+
g.add_argument("--repo", default=DEFAULT_REPO, help="git URL of the log (default: the public qrng-beacon-log)")
|
|
25
|
+
g.add_argument("--log-dir", help="use this local checkout/directory instead of a cached clone")
|
|
26
|
+
g.add_argument("--log-ref", help="git ref/sha of the log to read (default origin/main; with --log-dir default: working tree)")
|
|
27
|
+
g.add_argument("--cache", help="cache directory for the clone (default ~/.cache/notbefore/qrng-beacon-log)")
|
|
28
|
+
g.add_argument("--offline", action="store_true", help="no network: no fetch, no drand/Rekor refetch (BLS + proofs still verify offline)")
|
|
29
|
+
g.add_argument("--no-anchors", action="store_true", help="skip the Rekor/OpenTimestamps anchor check")
|
|
30
|
+
ap.add_argument("-v", "--verbose", action="store_true", help="print the vendored verifier's full output")
|
|
31
|
+
ap.add_argument("--json", action="store_true", help="machine-readable result on stdout")
|
|
32
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
33
|
+
def common(p, purpose=False, file=False):
|
|
34
|
+
p.add_argument("seq", type=int, help="reveal seq N (its commit is N-1)")
|
|
35
|
+
for flags, kw in ((("-v", "--verbose"), {}), (("--json",), {}), (("--offline",), {}), (("--no-anchors",), {})):
|
|
36
|
+
p.add_argument(*flags, action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS, **kw)
|
|
37
|
+
if purpose:
|
|
38
|
+
p.add_argument("--purpose", required=True, help="non-secret label, ^[A-Za-z0-9._:/=@+-]+$, ≤ 256 bytes")
|
|
39
|
+
p.add_argument("--transcript", help="write the transcript JSON here (default: ./notbefore-<seq>-<purpose>.json); '-' for stdout, 'none' to skip")
|
|
40
|
+
if file: p.add_argument("file", help="input file, one record per line (UTF-8)")
|
|
41
|
+
common(sub.add_parser("verify", help="verify the pair (commit N-1, reveal N); exit 0/1"))
|
|
42
|
+
common(sub.add_parser("value", help="print the attested value V (hex) if the pair verifies"))
|
|
43
|
+
common(sub.add_parser("seed", help="print the derived seed S = SHA256(D_derive || V || purpose)"), purpose=True)
|
|
44
|
+
common(sub.add_parser("shuffle", help="deterministically shuffle the lines of FILE with S"), purpose=True, file=True)
|
|
45
|
+
sp = sub.add_parser("split", help="shuffle, then split FILE into A (first floor(frac·k)) and B"); common(sp, purpose=True, file=True)
|
|
46
|
+
sp.add_argument("--frac", type=float, required=True); sp.add_argument("--out-a"); sp.add_argument("--out-b")
|
|
47
|
+
return ap
|
|
48
|
+
|
|
49
|
+
def _open_source(a):
|
|
50
|
+
return LogSource(repo=a.repo, log_dir=a.log_dir, cache=a.cache, ref=a.log_ref, offline=a.offline)
|
|
51
|
+
|
|
52
|
+
def _print_check(R, a):
|
|
53
|
+
if a.json: return
|
|
54
|
+
for l in R.lines: sys.stderr.write(l + "\n")
|
|
55
|
+
if a.verbose:
|
|
56
|
+
for v in R.verbose: sys.stderr.write("\n--- verifier output ---\n" + v)
|
|
57
|
+
sys.stderr.write(("VERIFIED" if R.ok else "NOT VERIFIED") + f" — NotBefore {R.seq} (commit {R.commit_seq}), log {str(R.log_git_sha)[:12]}\n")
|
|
58
|
+
|
|
59
|
+
def _read_records(path):
|
|
60
|
+
raw = open(path, "rb").read()
|
|
61
|
+
text = raw.decode("utf-8")
|
|
62
|
+
recs = text.split("\n")
|
|
63
|
+
if recs and recs[-1] == "": recs.pop()
|
|
64
|
+
return [r.rstrip("\r") for r in recs], hashlib.sha256(raw).hexdigest()
|
|
65
|
+
|
|
66
|
+
def _write_transcript(a, t, slug):
|
|
67
|
+
dest = a.transcript
|
|
68
|
+
if dest == "none": return None
|
|
69
|
+
if dest == "-": print(json.dumps(t, indent=1, sort_keys=True)); return "-"
|
|
70
|
+
dest = dest or f"notbefore-{a.seq}-{slug}.json"
|
|
71
|
+
json.dump(t, open(dest, "w"), indent=1, sort_keys=True); _err(f"transcript written: {dest}"); return dest
|
|
72
|
+
|
|
73
|
+
def main(argv=None):
|
|
74
|
+
a = build_parser().parse_args(argv)
|
|
75
|
+
try:
|
|
76
|
+
src = _open_source(a)
|
|
77
|
+
except LogError as e:
|
|
78
|
+
_err(str(e)); return 1
|
|
79
|
+
try:
|
|
80
|
+
R = check_pair(a.seq, src, refetch=not a.offline, anchors=not a.no_anchors, verbose=a.verbose)
|
|
81
|
+
_print_check(R, a)
|
|
82
|
+
if a.cmd == "verify":
|
|
83
|
+
if a.json: print(json.dumps(R.summary() | {"seq": R.seq, "commit_seq": R.commit_seq, "attested_value": R.attested_value if R.ok else None, "log_git_sha": R.log_git_sha}, indent=1))
|
|
84
|
+
return 0 if R.ok else 1
|
|
85
|
+
if not R.ok:
|
|
86
|
+
_err("pair did not verify; refusing to emit a value"); return 1
|
|
87
|
+
if a.cmd == "value":
|
|
88
|
+
print(json.dumps({"seq": R.seq, "attested_value": R.attested_value}) if a.json else R.attested_value); return 0
|
|
89
|
+
try: P = D.normalize_purpose(a.purpose).decode()
|
|
90
|
+
except D.PurposeError as e: _err(f"bad purpose: {e}"); return 2
|
|
91
|
+
S = D.seed(R.attested_value, P); slug = P.replace("/", "_").replace(":", "_")
|
|
92
|
+
if a.cmd == "seed":
|
|
93
|
+
t = D.transcript(R, P, S); _write_transcript(a, t, slug)
|
|
94
|
+
print(json.dumps({"seq": R.seq, "purpose": P, "derived_seed": S.hex()}) if a.json else S.hex()); return 0
|
|
95
|
+
recs, in_sha = _read_records(a.file)
|
|
96
|
+
if a.cmd == "shuffle":
|
|
97
|
+
out = D.shuffle(recs, S); body = "\n".join(out) + ("\n" if out else "")
|
|
98
|
+
t = D.transcript(R, P, S, {"operation": "shuffle", "input_file": os.path.basename(a.file), "input_sha256": in_sha, "record_count": len(recs), "output_sha256": D.sha256_hex(body.encode())})
|
|
99
|
+
_write_transcript(a, t, slug); sys.stdout.write(body); return 0
|
|
100
|
+
if a.cmd == "split":
|
|
101
|
+
try: A_, B_ = D.split(recs, S, a.frac)
|
|
102
|
+
except ValueError as e: _err(str(e)); return 2
|
|
103
|
+
oa, ob = a.out_a or a.file + ".A", a.out_b or a.file + ".B"
|
|
104
|
+
ba, bb = "\n".join(A_) + ("\n" if A_ else ""), "\n".join(B_) + ("\n" if B_ else "")
|
|
105
|
+
open(oa, "w").write(ba); open(ob, "w").write(bb)
|
|
106
|
+
t = D.transcript(R, P, S, {"operation": "split", "frac": a.frac, "input_file": os.path.basename(a.file), "input_sha256": in_sha, "record_count": len(recs),
|
|
107
|
+
"A": {"file": os.path.basename(oa), "count": len(A_), "sha256": D.sha256_hex(ba.encode())}, "B": {"file": os.path.basename(ob), "count": len(B_), "sha256": D.sha256_hex(bb.encode())}})
|
|
108
|
+
_write_transcript(a, t, slug); _err(f"A: {len(A_)} -> {oa} B: {len(B_)} -> {ob}"); return 0
|
|
109
|
+
finally:
|
|
110
|
+
src.close()
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__": sys.exit(main())
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""The derive layer (NOTBEFORE.md §7): labeled seed, shuffle, split, transcript. Pure functions; no I/O."""
|
|
2
|
+
import hashlib, re, unicodedata, json, time
|
|
3
|
+
from . import D_DERIVE, D_SHUFFLE, SPEC, __version__
|
|
4
|
+
|
|
5
|
+
PURPOSE_RE = re.compile(r"^[A-Za-z0-9._:/=@+-]+$")
|
|
6
|
+
|
|
7
|
+
class PurposeError(ValueError): pass
|
|
8
|
+
|
|
9
|
+
def normalize_purpose(purpose: str) -> bytes:
|
|
10
|
+
"""UTF-8 NFC, 1–256 bytes, CLI-safe charset, no newline. Returns the exact bytes that enter the hash."""
|
|
11
|
+
if not isinstance(purpose, str) or purpose == "": raise PurposeError("purpose must be a non-empty string")
|
|
12
|
+
if "\n" in purpose or "\r" in purpose: raise PurposeError("purpose must not contain a newline")
|
|
13
|
+
p = unicodedata.normalize("NFC", purpose).encode("utf-8")
|
|
14
|
+
if len(p) > 256: raise PurposeError("purpose must be at most 256 bytes of UTF-8")
|
|
15
|
+
if not PURPOSE_RE.match(p.decode("utf-8")): raise PurposeError("purpose must match ^[A-Za-z0-9._:/=@+-]+$ (no spaces; use - or _)")
|
|
16
|
+
return p
|
|
17
|
+
|
|
18
|
+
def seed(attested_value_hex: str, purpose: str) -> bytes:
|
|
19
|
+
"""S = SHA256( 'notbefore/derive/v1' || V || purpose ). V is the 32 raw bytes of the attested value."""
|
|
20
|
+
V = bytes.fromhex(attested_value_hex)
|
|
21
|
+
if len(V) != 32: raise ValueError("attested_value must be 32 bytes")
|
|
22
|
+
return hashlib.sha256(D_DERIVE + V + normalize_purpose(purpose)).digest()
|
|
23
|
+
|
|
24
|
+
def rank(key: bytes, i: int, record: str) -> bytes:
|
|
25
|
+
return hashlib.sha256(D_SHUFFLE + key + i.to_bytes(8, "big") + record.encode("utf-8")).digest()
|
|
26
|
+
|
|
27
|
+
def shuffle(records, key: bytes):
|
|
28
|
+
"""Stable sort by rank ascending; tie-break on the hex of the record bytes. Input order is part of the transcript."""
|
|
29
|
+
if len(key) != 32: raise ValueError("key must be the 32-byte derived seed")
|
|
30
|
+
ranked = [(rank(key, i, x), x.encode("utf-8").hex(), x) for i, x in enumerate(records)]
|
|
31
|
+
ranked.sort(key=lambda t: (t[0], t[1]))
|
|
32
|
+
return [t[2] for t in ranked]
|
|
33
|
+
|
|
34
|
+
def split(records, key: bytes, frac: float):
|
|
35
|
+
if not (0.0 < frac < 1.0): raise ValueError("frac must be in (0, 1)")
|
|
36
|
+
s = shuffle(records, key); k = int(frac * len(s)) # floor(f * k); do not re-draw
|
|
37
|
+
return s[:k], s[k:]
|
|
38
|
+
|
|
39
|
+
def sha256_hex(b: bytes) -> str: return hashlib.sha256(b).hexdigest()
|
|
40
|
+
|
|
41
|
+
def transcript(check, purpose: str, derived: bytes, extra=None):
|
|
42
|
+
"""The engineering artifact (§7.5). `check` is a CheckResult from notbefore.check."""
|
|
43
|
+
t = {"spec": SPEC, "derive_domain": D_DERIVE.decode(), "seq": check.seq, "commit_seq": check.commit_seq,
|
|
44
|
+
"pulse_hash_reveal": check.pulse_hash_reveal, "pulse_hash_commit": check.pulse_hash_commit,
|
|
45
|
+
"attested_value": check.attested_value, "drand_round": check.drand_round,
|
|
46
|
+
"purpose": normalize_purpose(purpose).decode("utf-8"), "derived_seed": derived.hex(),
|
|
47
|
+
"verifier_git_sha": check.verifier_git_sha, "log_git_sha": check.log_git_sha, "log_ref": check.log_ref,
|
|
48
|
+
"cli_version": __version__, "verified_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
49
|
+
"checks": check.summary()}
|
|
50
|
+
if extra: t.update(extra)
|
|
51
|
+
return t
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Read the public log as DATA. Nothing from the log is ever executed: the verifier and keys are vendored in this package."""
|
|
2
|
+
import os, subprocess, tempfile, json, shutil
|
|
3
|
+
from . import DEFAULT_REPO
|
|
4
|
+
|
|
5
|
+
SIDE = ("", ".tsa.json", ".freetsa.tsr", ".digicert.tsr")
|
|
6
|
+
|
|
7
|
+
class LogError(Exception): pass
|
|
8
|
+
|
|
9
|
+
def _git(*a, cwd=None, check=True):
|
|
10
|
+
r = subprocess.run(["git", *a], cwd=cwd, capture_output=True, text=True)
|
|
11
|
+
if check and r.returncode: raise LogError(f"git {' '.join(a)}: {(r.stderr or r.stdout).strip()[:300]}")
|
|
12
|
+
return r
|
|
13
|
+
|
|
14
|
+
class LogSource:
|
|
15
|
+
"""Either a remote clone kept in a cache directory (default) or a local directory (--log-dir)."""
|
|
16
|
+
def __init__(self, repo=DEFAULT_REPO, log_dir=None, cache=None, ref=None, offline=False):
|
|
17
|
+
self.repo, self.offline = repo, offline
|
|
18
|
+
self.tmp = tempfile.mkdtemp(prefix="notbefore-")
|
|
19
|
+
if log_dir:
|
|
20
|
+
self.dir = os.path.abspath(log_dir); self.mode = "dir"
|
|
21
|
+
self.is_git = _git("rev-parse", "--is-inside-work-tree", cwd=self.dir, check=False).returncode == 0
|
|
22
|
+
self.ref = ref # None -> working tree
|
|
23
|
+
else:
|
|
24
|
+
self.dir = cache or os.path.join(os.environ.get("XDG_CACHE_HOME") or os.path.expanduser("~/.cache"), "notbefore", "qrng-beacon-log")
|
|
25
|
+
self.mode = "cache"; self.is_git = True
|
|
26
|
+
if not os.path.isdir(os.path.join(self.dir, ".git")):
|
|
27
|
+
if offline: raise LogError(f"no cached clone at {self.dir} and --offline given")
|
|
28
|
+
os.makedirs(os.path.dirname(self.dir), exist_ok=True); _git("clone", "--quiet", repo, self.dir)
|
|
29
|
+
elif not offline:
|
|
30
|
+
_git("fetch", "--quiet", "--prune", "origin", cwd=self.dir)
|
|
31
|
+
self.ref = ref or "origin/main"
|
|
32
|
+
self.log_git_sha = self._sha()
|
|
33
|
+
|
|
34
|
+
def _sha(self):
|
|
35
|
+
if not self.is_git: return "unknown (not a git checkout)"
|
|
36
|
+
return _git("rev-parse", self.ref or "HEAD", cwd=self.dir).stdout.strip()
|
|
37
|
+
|
|
38
|
+
def close(self): shutil.rmtree(self.tmp, ignore_errors=True)
|
|
39
|
+
|
|
40
|
+
# ---- files
|
|
41
|
+
def _read(self, relpath):
|
|
42
|
+
"""Bytes of a file at the pinned ref (or the working tree for a plain --log-dir). None if absent."""
|
|
43
|
+
if self.mode == "dir" and self.ref is None:
|
|
44
|
+
p = os.path.join(self.dir, relpath); return open(p, "rb").read() if os.path.exists(p) else None
|
|
45
|
+
r = _git("show", f"{self.ref}:{relpath}", cwd=self.dir, check=False)
|
|
46
|
+
return r.stdout.encode("utf-8", "surrogateescape") if r.returncode == 0 else None
|
|
47
|
+
|
|
48
|
+
def _read_bytes(self, relpath):
|
|
49
|
+
# binary-safe variant for .tsr (git show via text mode would mangle bytes)
|
|
50
|
+
if self.mode == "dir" and self.ref is None:
|
|
51
|
+
p = os.path.join(self.dir, relpath); return open(p, "rb").read() if os.path.exists(p) else None
|
|
52
|
+
r = subprocess.run(["git", "show", f"{self.ref}:{relpath}"], cwd=self.dir, capture_output=True)
|
|
53
|
+
return r.stdout if r.returncode == 0 else None
|
|
54
|
+
|
|
55
|
+
def has_pulse(self, seq): return self._read_bytes(f"chain/pulse-{seq:04d}.json") is not None
|
|
56
|
+
|
|
57
|
+
def materialize(self, seq):
|
|
58
|
+
"""Write chain/pulse-NNNN.json and its TSA sidecars into the temp dir; return the pulse path (or None)."""
|
|
59
|
+
out = os.path.join(self.tmp, "chain"); os.makedirs(out, exist_ok=True)
|
|
60
|
+
base = f"chain/pulse-{seq:04d}.json"; main = None
|
|
61
|
+
for s in SIDE:
|
|
62
|
+
b = self._read_bytes(base + s)
|
|
63
|
+
if b is None:
|
|
64
|
+
if s == "": return None
|
|
65
|
+
continue
|
|
66
|
+
p = os.path.join(out, os.path.basename(base + s)); open(p, "wb").write(b)
|
|
67
|
+
if s == "": main = p
|
|
68
|
+
return main
|
|
69
|
+
|
|
70
|
+
def pulse(self, seq):
|
|
71
|
+
b = self._read_bytes(f"chain/pulse-{seq:04d}.json"); return json.loads(b) if b else None
|
|
72
|
+
|
|
73
|
+
# ---- anchors branch
|
|
74
|
+
def anchors_available(self):
|
|
75
|
+
if not self.is_git: return os.path.isdir(os.path.join(self.dir, "anchors"))
|
|
76
|
+
return _git("rev-parse", "--verify", "--quiet", "origin/anchors", cwd=self.dir, check=False).returncode == 0 \
|
|
77
|
+
or os.path.isdir(os.path.join(self.dir, "anchors"))
|
|
78
|
+
|
|
79
|
+
def anchor(self, seq):
|
|
80
|
+
"""(record dict, statement bytes) for a pulse from origin/anchors (or an anchors/ worktree), or (None, None)."""
|
|
81
|
+
rec = stmt = None
|
|
82
|
+
if self.is_git:
|
|
83
|
+
r = subprocess.run(["git", "show", f"origin/anchors:pulse-{seq:04d}.anchor.json"], cwd=self.dir, capture_output=True)
|
|
84
|
+
s = subprocess.run(["git", "show", f"origin/anchors:pulse-{seq:04d}.stmt.json"], cwd=self.dir, capture_output=True)
|
|
85
|
+
if r.returncode == 0 and s.returncode == 0: rec, stmt = json.loads(r.stdout), s.stdout
|
|
86
|
+
if rec is None:
|
|
87
|
+
d = os.path.join(self.dir, "anchors")
|
|
88
|
+
rp, sp = os.path.join(d, f"pulse-{seq:04d}.anchor.json"), os.path.join(d, f"pulse-{seq:04d}.stmt.json")
|
|
89
|
+
if os.path.exists(rp) and os.path.exists(sp): rec, stmt = json.load(open(rp)), open(sp, "rb").read()
|
|
90
|
+
return rec, stmt
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files": {
|
|
3
|
+
"bls_drand.py": "b909dd175b1f1c2dff0e78b77d83f2446afca73222a9946e35c2233732228ed9",
|
|
4
|
+
"ci/KNOWN_NONCOMPLIANT.json": "403268695cbbb93f5e4f757cdb3568056b40fbb31a14d0a0a1cb8b4ec953abd6",
|
|
5
|
+
"ci/anchor_lib.py": "7b65da28c3ff750808df2bc6e76ee7c8cb7273308cead542da0766c42f2edc71",
|
|
6
|
+
"drand_anchor.py": "8c1d16bd8376218697626e5614644713f52f83b31aeb836cd7dd302b500eac85",
|
|
7
|
+
"hosts/EXPECTED.json": "4fa57dfff93b7227db41fbd5c9001c1687e9ffb9f5e568b84ac8029be92c6e35",
|
|
8
|
+
"hosts/attest_host.py": "036cc9ca5e978fe9b3fb749a0ab7627fe1b18991ccfb05bbdeb3d1d4eec99a6a",
|
|
9
|
+
"hosts/attest_lib.py": "02ffc2a881d15daba1c1e1dda512d7f5efcf2adb9917a3748a7521954eea506f",
|
|
10
|
+
"hosts/entropy_host.py": "d2eaa923b7ac817f63b04a171abc667b0d56229c90b3e832ac787940710cd254",
|
|
11
|
+
"hosts/gnss_probe.py": "e5ccf3e8e400b69be669113f6fb1ce2439225e6635054150b64fcd65f0d12cdb",
|
|
12
|
+
"hosts/stamp_probe.py": "785763bd7d75cf792732387a7c439a78545ebe8414309ce738d3417c2d16fe48",
|
|
13
|
+
"keys/KEYS.json": "9de75424793bcb2b00b42efc271465fa1a6656194507b1dd3ffdb7e28eef9b3f",
|
|
14
|
+
"keys/anchor.pub": "5afc8aef90f65ae47aae97790aca4181c92b4cb37d23c7ab936c3a2b07de7b24",
|
|
15
|
+
"keys/drand-quicknet.json": "11ff6ba94bac65555551f3e097f4a498d5859cbf8a269127d569dedeaa1b8bf7",
|
|
16
|
+
"keys/entropy_signer.pub": "bbd318423b18b0a107382edf4f6f7be4f4a6186be6a0f73b17051e9f6f9713ab",
|
|
17
|
+
"keys/rekor.pub": "dce5ef715502ec9f3cdfd11f8cc384b31a6141023d3e7595e9908a81cb6241bd",
|
|
18
|
+
"keys/time_attester.f9t.RETIRED.pub": "fd6077eb26b8603c8ef8f445edfa3d5d13fa8d5298dcd7f3df20bafb9bd0565d",
|
|
19
|
+
"keys/time_attester.pub": "20b72663dddc2ce0ff73f9af4a4ad70761fe0af8c009a9bce520724d9c4ef424",
|
|
20
|
+
"keys/time_witness.pub": "2af608da26b0140ba3b2327357ebfd2a72167e983895aed1113a8ce19acc8267",
|
|
21
|
+
"schema.py": "d9f4bb521bb40020230ab14506c03b192467c7a6ddc07f5877d0cd56d178f8f1",
|
|
22
|
+
"tsa-certs/freetsa-ca.pem": "2151b61137ffa86bf664691ba67e7da0b19f98c758e3d228d5d8ebf27e044438",
|
|
23
|
+
"tsa-certs/freetsa-tsa.crt": "8bfb0305bb64e2571ca507552ef3245cb1c2fee8728e0ff8689225081ea13467",
|
|
24
|
+
"tsa.py": "dccdd56c6e087ee86e30c6d9130c2c9aef10eac10e6c24c2c39f492f44ed565a",
|
|
25
|
+
"verify.py": "898a3b343225b8ad5f42065c3f8d1690476edde5659bae2acc00d81c70200605"
|
|
26
|
+
},
|
|
27
|
+
"git_sha": "19fed1b1d291db54469fec682960ee544598da61",
|
|
28
|
+
"log_repo": "https://github.com/docdailey/qrng-beacon-log",
|
|
29
|
+
"note": "Verifier, keys and expected-config pinned at this commit. The CLI executes ONLY these files; the log is read as data.",
|
|
30
|
+
"vendored_utc": "2026-09-12T13:26:52Z"
|
|
31
|
+
}
|
|
File without changes
|