diff --git a/docs/verify.md b/docs/verify.md index 9b60b2c5..d6916a35 100644 --- a/docs/verify.md +++ b/docs/verify.md @@ -18,7 +18,19 @@ folder `release/simple-run/`. The terms (segment, boundary, chain entry, anchor) ## 1. Consistency check, without a GPU Needs Python 3.12 or newer, `git`, `openssl`, `sha256sum` and network access. On macOS, -`sha256sum` comes with `brew install coreutils` (as `gsha256sum`). Download `release/simple-run/` +`sha256sum` comes with `brew install coreutils` (as `gsha256sum`). + +### Windows + +On Windows, install Python 3.12 or newer, Git for Windows, and ensure OpenSSL is available. The verifier uses the `certifi` CA bundle when the platform does not provide one of the standard system CA-bundle paths. + +From PowerShell, download `release/simple-run/` and run the same verification command shown below. If OpenSSL is installed with Git for Windows but is not on `PATH`, add the Git OpenSSL directory to `PATH` for the current PowerShell session: + +```powershell +$env:Path = "C:\Program Files\Git\mingw64\bin;$env:Path" +``` + +The normal verification process checks the DigiCert and FreeTSA timestamps. Do not use `--no-timestamps` unless you intentionally want to skip timestamp verification. Download `release/simple-run/` without the `ckpt-*.pt` files, then from that folder: ```bash diff --git a/pyproject.toml b/pyproject.toml index 493f2f08..67862f6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "Train a small language model on English Wikipedia so that anyone readme = "README.md" requires-python = ">=3.12" # Pinned with hashes in requirements/preparation.lock, which CI installs. -dependencies = ["torch>=2.10.0,<3.0", "numpy>=2.4,<3.0", "tokenizers==0.23.2"] +dependencies = ["torch>=2.10.0,<3.0", "numpy>=2.4,<3.0", "tokenizers==0.23.2", "certifi>=2026.2.2"] [project.optional-dependencies] gguf = ["gguf"] diff --git a/simple/ovl.py b/simple/ovl.py index 005c76e7..74447a36 100644 --- a/simple/ovl.py +++ b/simple/ovl.py @@ -374,6 +374,8 @@ def run_phases(data, model, opt, on_boundary, start=None): def read_chain(run): path = Path(run) / "chain.jsonl" + if not path.is_file(): + sys.exit(f"{path} not found") chain = [] for n, line in enumerate(path.read_text().splitlines(), 1): try: diff --git a/simple/stamp.py b/simple/stamp.py index 4965c20d..a9950c02 100644 --- a/simple/stamp.py +++ b/simple/stamp.py @@ -19,6 +19,7 @@ import threading import time import urllib.request +import certifi TSA = {"freetsa": "https://freetsa.org/tsr", "digicert": "http://timestamp.digicert.com"} FREETSA_CERTS = ("https://freetsa.org/files/cacert.pem", "https://freetsa.org/files/tsa.crt") @@ -75,10 +76,13 @@ def stamp_async(run, entry): def system_ca(): - """The system CA bundle that holds DigiCert's root: Linux, then macOS.""" - return next((p for p in ("/etc/ssl/certs/ca-certificates.crt", "/etc/pki/tls/certs/ca-bundle.crt", - "/etc/ssl/cert.pem") if Path(p).exists()), None) - + """Return a CA bundle that can verify the DigiCert timestamp.""" + paths = ( + "/etc/ssl/certs/ca-certificates.crt", + "/etc/pki/tls/certs/ca-bundle.crt", + "/etc/ssl/cert.pem", + ) + return next((p for p in paths if Path(p).is_file()), certifi.where()) def freetsa_certs(d): ca, tsa = (Path(d) / "cacert.pem", Path(d) / "tsa.crt") diff --git a/tests/simple/test_ovl.py b/tests/simple/test_ovl.py index c2e4f9c5..dc9479fb 100644 --- a/tests/simple/test_ovl.py +++ b/tests/simple/test_ovl.py @@ -91,6 +91,10 @@ def test_empty_chain_file_is_reported(self): with self.assertRaisesRegex(SystemExit, "chain.jsonl has no chain entries"): ovl.read_chain(self.run) + def test_chain_path_that_is_directory_is_reported(self): + (self.run / "chain.jsonl").mkdir() + with self.assertRaisesRegex(SystemExit, r"chain\.jsonl not found"): + ovl.read_chain(self.run) def test_cut_off_last_line_is_reported_with_its_line_number(self): write_chain(self.run, self.chain[:2]) with (self.run / "chain.jsonl").open("a") as f: diff --git a/tests/simple/test_ovl_anchor.py b/tests/simple/test_ovl_anchor.py index 9538d2b7..0b27af8d 100644 --- a/tests/simple/test_ovl_anchor.py +++ b/tests/simple/test_ovl_anchor.py @@ -147,6 +147,11 @@ def test_anchor_result_is_recorded(self): self.assertTrue(record["checks"][0]["match"]) self.assertEqual(record["result"], "PASS") +class SystemCaTests(unittest.TestCase): + def test_falls_back_to_certifi_when_system_bundle_is_missing(self): + with mock.patch.object(ovl.stamp.Path, "is_file", return_value=False): + with mock.patch.object(ovl.stamp.certifi, "where", return_value="/certifi/cacert.pem"): + self.assertEqual(ovl.stamp.system_ca(), "/certifi/cacert.pem") class AnchorTimestamps(unittest.TestCase): """A rewritten anchor.json still matches a rewritten chain; only its timestamps catch that."""