floorvault 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.
Files changed (137) hide show
  1. floorvault-0.1.0/.clusterfuzzlite/Dockerfile +9 -0
  2. floorvault-0.1.0/.clusterfuzzlite/build.sh +15 -0
  3. floorvault-0.1.0/.clusterfuzzlite/project.yaml +1 -0
  4. floorvault-0.1.0/.dockerignore +5 -0
  5. floorvault-0.1.0/.gitattributes +18 -0
  6. floorvault-0.1.0/.github/copilot-instructions.md +67 -0
  7. floorvault-0.1.0/.github/workflows/README-cbom-workflow-removed-2026-09-15.md +27 -0
  8. floorvault-0.1.0/.github/workflows/cflite-prune.yml +43 -0
  9. floorvault-0.1.0/.github/workflows/cflite.yml +60 -0
  10. floorvault-0.1.0/.github/workflows/ci.yml +236 -0
  11. floorvault-0.1.0/.github/workflows/release.yml +139 -0
  12. floorvault-0.1.0/.github/workflows/scorecard.yml +59 -0
  13. floorvault-0.1.0/.github/workflows/verify.yml +52 -0
  14. floorvault-0.1.0/.gitignore +10 -0
  15. floorvault-0.1.0/.gitleaks.toml +30 -0
  16. floorvault-0.1.0/.gitleaksignore +29 -0
  17. floorvault-0.1.0/LICENSE +15 -0
  18. floorvault-0.1.0/LICENSE-APACHE +202 -0
  19. floorvault-0.1.0/LICENSE-MIT +19 -0
  20. floorvault-0.1.0/PKG-INFO +465 -0
  21. floorvault-0.1.0/README.md +418 -0
  22. floorvault-0.1.0/SECURITY.md +583 -0
  23. floorvault-0.1.0/docs/ARCHITECTURE-REVIEW-2026-10-02.md +190 -0
  24. floorvault-0.1.0/docs/COMPARATIVE-BENCHMARK-2026-09-15.md +86 -0
  25. floorvault-0.1.0/docs/CROSS-PLATFORM-CI-FINDINGS-2026-09-15.md +144 -0
  26. floorvault-0.1.0/docs/DEEP-DIVE-REVIEW-2026-10-01.md +156 -0
  27. floorvault-0.1.0/docs/FLOORVAULT-ARCHITECTURE-CRYPTO-REPORT-2026-09-16.md +126 -0
  28. floorvault-0.1.0/docs/HARDWARE-CUSTODY.md +119 -0
  29. floorvault-0.1.0/docs/PENTEST-2026-10-04.md +118 -0
  30. floorvault-0.1.0/docs/PERFORMANCE-HARDENING-COST-REVIEW-2026-09-15.md +42 -0
  31. floorvault-0.1.0/docs/RECORD-FORMAT-2026-09-15.md +127 -0
  32. floorvault-0.1.0/docs/RELEASING.md +58 -0
  33. floorvault-0.1.0/docs/REMEDIATION-REPORT-2026-09-16.md +263 -0
  34. floorvault-0.1.0/docs/SPEC.md +864 -0
  35. floorvault-0.1.0/docs/VAULT-TRANSIT.md +130 -0
  36. floorvault-0.1.0/docs/assets/context-binding.svg +48 -0
  37. floorvault-0.1.0/docs/assets/floorvault-hero.svg +8 -0
  38. floorvault-0.1.0/fuzz/fuzz_associated_data.py +74 -0
  39. floorvault-0.1.0/fuzz/fuzz_beacons.py +85 -0
  40. floorvault-0.1.0/fuzz/fuzz_envelope.py +89 -0
  41. floorvault-0.1.0/fuzz/fuzz_identifiers.py +82 -0
  42. floorvault-0.1.0/fuzz/fuzz_protected_store.py +96 -0
  43. floorvault-0.1.0/fuzz/write_seed_corpus.py +37 -0
  44. floorvault-0.1.0/pyproject.toml +128 -0
  45. floorvault-0.1.0/scripts/benchmark_compare.py +126 -0
  46. floorvault-0.1.0/scripts/gen_vectors.py +628 -0
  47. floorvault-0.1.0/scripts/memory_probe.py +61 -0
  48. floorvault-0.1.0/scripts/mutation_check.py +1471 -0
  49. floorvault-0.1.0/scripts/realworld_exercise.py +843 -0
  50. floorvault-0.1.0/scripts/security-check.sh +216 -0
  51. floorvault-0.1.0/scripts/verify_wheel.py +62 -0
  52. floorvault-0.1.0/src/floorvault/__init__.py +100 -0
  53. floorvault-0.1.0/src/floorvault/beacons.py +406 -0
  54. floorvault-0.1.0/src/floorvault/core.py +771 -0
  55. floorvault-0.1.0/src/floorvault/inspector.py +95 -0
  56. floorvault-0.1.0/src/floorvault/key_recovery.py +95 -0
  57. floorvault-0.1.0/src/floorvault/keyring.py +171 -0
  58. floorvault-0.1.0/src/floorvault/libsql_adapter.py +83 -0
  59. floorvault-0.1.0/src/floorvault/memory.py +304 -0
  60. floorvault-0.1.0/src/floorvault/migration.py +357 -0
  61. floorvault-0.1.0/src/floorvault/platform_support.py +244 -0
  62. floorvault-0.1.0/src/floorvault/providers/__init__.py +17 -0
  63. floorvault-0.1.0/src/floorvault/providers/adaptive.py +281 -0
  64. floorvault-0.1.0/src/floorvault/providers/base.py +42 -0
  65. floorvault-0.1.0/src/floorvault/providers/generation_store.py +413 -0
  66. floorvault-0.1.0/src/floorvault/providers/linux_keyring.py +301 -0
  67. floorvault-0.1.0/src/floorvault/providers/platform_custody.py +429 -0
  68. floorvault-0.1.0/src/floorvault/providers/vault_transit.py +559 -0
  69. floorvault-0.1.0/src/floorvault/providers/windows_dpapi.py +249 -0
  70. floorvault-0.1.0/src/floorvault/records.py +209 -0
  71. floorvault-0.1.0/src/floorvault/sqlalchemy_adapter.py +489 -0
  72. floorvault-0.1.0/src/floorvault/sqlite_adapter.py +324 -0
  73. floorvault-0.1.0/src/floorvault/sqlite_migration.py +241 -0
  74. floorvault-0.1.0/src/floorvault/vault_rotation.py +115 -0
  75. floorvault-0.1.0/src/floorvault/vaultkit/__init__.py +28 -0
  76. floorvault-0.1.0/src/floorvault/vaultkit/session_crypto.py +112 -0
  77. floorvault-0.1.0/src/floorvault/vaultkit/vault.py +1014 -0
  78. floorvault-0.1.0/tests/test_adaptive_provider.py +655 -0
  79. floorvault-0.1.0/tests/test_benchmark.py +57 -0
  80. floorvault-0.1.0/tests/test_caller_key_lifecycle.py +39 -0
  81. floorvault-0.1.0/tests/test_ci_workflow.py +273 -0
  82. floorvault-0.1.0/tests/test_crypto_core.py +343 -0
  83. floorvault-0.1.0/tests/test_encrypted_sqlite_table.py +135 -0
  84. floorvault-0.1.0/tests/test_envelope_versioning.py +221 -0
  85. floorvault-0.1.0/tests/test_fuzz.py +151 -0
  86. floorvault-0.1.0/tests/test_fuzz_targets.py +129 -0
  87. floorvault-0.1.0/tests/test_gate_script.py +149 -0
  88. floorvault-0.1.0/tests/test_generation_store.py +308 -0
  89. floorvault-0.1.0/tests/test_inspector_sql_injection.py +123 -0
  90. floorvault-0.1.0/tests/test_key_recovery.py +67 -0
  91. floorvault-0.1.0/tests/test_key_store_paths.py +244 -0
  92. floorvault-0.1.0/tests/test_keychain_live.py +70 -0
  93. floorvault-0.1.0/tests/test_keyring.py +199 -0
  94. floorvault-0.1.0/tests/test_latency_hotpath.py +274 -0
  95. floorvault-0.1.0/tests/test_libsql_adapter.py +284 -0
  96. floorvault-0.1.0/tests/test_linux_keyring.py +607 -0
  97. floorvault-0.1.0/tests/test_memory_custody.py +175 -0
  98. floorvault-0.1.0/tests/test_memory_platform_probe.py +63 -0
  99. floorvault-0.1.0/tests/test_migration.py +180 -0
  100. floorvault-0.1.0/tests/test_migration_retirement.py +216 -0
  101. floorvault-0.1.0/tests/test_mutation_expectations.py +157 -0
  102. floorvault-0.1.0/tests/test_platform_providers.py +125 -0
  103. floorvault-0.1.0/tests/test_platform_support.py +163 -0
  104. floorvault-0.1.0/tests/test_properties.py +96 -0
  105. floorvault-0.1.0/tests/test_protected_store_safety.py +928 -0
  106. floorvault-0.1.0/tests/test_public_api.py +267 -0
  107. floorvault-0.1.0/tests/test_realworld_exercise.py +62 -0
  108. floorvault-0.1.0/tests/test_release_artifacts.py +60 -0
  109. floorvault-0.1.0/tests/test_repository_hygiene.py +123 -0
  110. floorvault-0.1.0/tests/test_revision_binding.py +117 -0
  111. floorvault-0.1.0/tests/test_rfc5297_independent.py +409 -0
  112. floorvault-0.1.0/tests/test_rfc_vectors.py +73 -0
  113. floorvault-0.1.0/tests/test_rotation.py +237 -0
  114. floorvault-0.1.0/tests/test_scrub_secrets.py +66 -0
  115. floorvault-0.1.0/tests/test_searchable_beacons.py +687 -0
  116. floorvault-0.1.0/tests/test_security_scan.py +216 -0
  117. floorvault-0.1.0/tests/test_session_crypto_framing.py +135 -0
  118. floorvault-0.1.0/tests/test_sql_identifier_quoting.py +219 -0
  119. floorvault-0.1.0/tests/test_sqlalchemy_adapter.py +494 -0
  120. floorvault-0.1.0/tests/test_sqlite_integration.py +76 -0
  121. floorvault-0.1.0/tests/test_sqlite_migration.py +230 -0
  122. floorvault-0.1.0/tests/test_store_permissions.py +243 -0
  123. floorvault-0.1.0/tests/test_vault_rotation.py +307 -0
  124. floorvault-0.1.0/tests/test_vault_transit.py +601 -0
  125. floorvault-0.1.0/tests/test_vault_transit_live.py +69 -0
  126. floorvault-0.1.0/tests/test_vaultkit_adapter.py +365 -0
  127. floorvault-0.1.0/tests/test_vaultstore_permissions.py +34 -0
  128. floorvault-0.1.0/tests/test_verify_wheel.py +56 -0
  129. floorvault-0.1.0/tests/test_windows_dpapi_live.py +20 -0
  130. floorvault-0.1.0/tests/test_wire_vectors.py +371 -0
  131. floorvault-0.1.0/tests/test_wycheproof_vectors.py +170 -0
  132. floorvault-0.1.0/tests/vectors/floorvault_wire_vectors.json +934 -0
  133. floorvault-0.1.0/tests/vectors/independent_wire.py +569 -0
  134. floorvault-0.1.0/tests/vectors/wycheproof/LICENSE +202 -0
  135. floorvault-0.1.0/tests/vectors/wycheproof/aead_aes_siv_cmac_test.json +12809 -0
  136. floorvault-0.1.0/tests/vectors/wycheproof/aes_siv_cmac_test.json +4203 -0
  137. floorvault-0.1.0/uv.lock +1534 -0
@@ -0,0 +1,9 @@
1
+ # ClusterFuzzLite build image for FloorVault's Atheris fuzz targets (fuzz/).
2
+ #
3
+ # Pinned by digest: the base image carries the fuzzing toolchain (Atheris,
4
+ # libFuzzer, the sanitizer runtimes), so a moving tag would let the thing that
5
+ # decides "no crash found" change underneath an unchanged repository.
6
+ FROM gcr.io/oss-fuzz-base/base-builder-python@sha256:58f80c90a90944b68b8e70b98e38997ee9396225311cfb0f3aea78671446ab4e
7
+ COPY . $SRC/floorvault
8
+ WORKDIR $SRC/floorvault
9
+ COPY .clusterfuzzlite/build.sh $SRC/
@@ -0,0 +1,15 @@
1
+ #!/bin/bash -eu
2
+ # Build every Atheris fuzz target in fuzz/ for ClusterFuzzLite.
3
+ #
4
+ # Installs FloorVault from this checkout (not from PyPI), so the fuzzers exercise
5
+ # the code under review, then packages each target with the seed corpus it
6
+ # declares in seed_inputs(). A target added to fuzz/ is picked up automatically;
7
+ # tests/test_fuzz_targets.py pins that this glob covers them all.
8
+
9
+ pip3 install --no-cache-dir "$SRC/floorvault"
10
+
11
+ for fuzzer in "$SRC"/floorvault/fuzz/fuzz_*.py; do
12
+ name="$(basename -s .py "$fuzzer")"
13
+ compile_python_fuzzer "$fuzzer"
14
+ python3 "$SRC/floorvault/fuzz/write_seed_corpus.py" "$fuzzer" "$OUT/${name}_seed_corpus.zip"
15
+ done
@@ -0,0 +1 @@
1
+ language: python
@@ -0,0 +1,5 @@
1
+ .venv/
2
+ dist/
3
+ build/
4
+ **/__pycache__/
5
+ .pytest_cache/
@@ -0,0 +1,18 @@
1
+ # Normalise line endings for every text file in the repository.
2
+ #
3
+ # Without this the checked-out bytes depend on the platform: git's Windows
4
+ # default (core.autocrlf=true, which is also what the GitHub Windows runners use)
5
+ # rewrites text files to CRLF. The build then packs those CRLF bytes, so the
6
+ # wheel and sdist differ from a POSIX build of the same commit - two digests for
7
+ # one source tree, and no way to check a download against a build from source.
8
+ #
9
+ # eol=lf forces LF on checkout everywhere, so the artifact is at least a function
10
+ # of the source rather than of the machine that checked it out.
11
+ #
12
+ # What this does NOT fix: the archive records the file mode it sees on the
13
+ # filesystem (measured: a source file chmod 0755 produces a different wheel than
14
+ # the same file at 0644), and Windows cannot represent POSIX modes. Byte-identity
15
+ # *across* operating systems is therefore not claimed anywhere; the digest
16
+ # identifies the artifact for a given platform. See tests/test_release_artifacts.py
17
+ # and Appendix B of the third-party review report.
18
+ * text=auto eol=lf
@@ -0,0 +1,67 @@
1
+ # Copilot review instructions — FloorVault
2
+
3
+ FloorVault is contextual, misuse-resistant database encryption (AES-256-SIV
4
+ with coordinate-bound AAD). Reviews here are security reviews first, style
5
+ reviews second.
6
+
7
+ ## Hard rules — flag any violation
8
+
9
+ - **Never weaken authentication.** The AEAD associated-data vector is
10
+ `[aad, header, nonce]` for v2 envelopes and `[aad, nonce]` for v1. Every
11
+ cleartext envelope field must stay inside that vector. Removing an element,
12
+ reordering, or mutating cleartext fields is a critical finding.
13
+ - **Never remove or weaken validation.** Rejection rules in `docs/SPEC.md`
14
+ §4.3/§16 are normative, including their ORDER. Do not suggest relaxing a
15
+ check for convenience.
16
+ - **Nonce discipline.** Nonces are 16 bytes from `os.urandom`, fresh per
17
+ field. Counter-derived, reused, or truncated nonces are critical findings.
18
+ The dedup window is defence-in-depth, not a freshness mechanism.
19
+ - **No key/custody downgrades.** A broken or refused custody tier must raise,
20
+ never silently fall through to a weaker one. No-clobber store publication
21
+ (hard link) is deliberate; do not suggest `os.replace`.
22
+ - **SQL identifiers** must pass `_safe_identifier` and be interpolated via
23
+ `_quoted_identifier` (bracket-quoted). Any other interpolation of a name
24
+ into SQL is a finding.
25
+ - **On-disk formats are a contract** (`docs/SPEC.md`). Byte-level changes
26
+ require a version bump AND a reader for every documented version. Flag any
27
+ wire-format change without a spec + vector update.
28
+ - **Tombstones/retirement** are fail-closed: a migrated id is never served
29
+ from the legacy source again, even if the modern record is gone.
30
+
31
+ ## Conventions
32
+
33
+ - Errors are honest: no swallowed exceptions, no "cannot tell is fine", no
34
+ masking one check behind another (the mutation suite hunts these).
35
+ - New security guards get a curated mutant in `scripts/mutation_check.py`;
36
+ platform-equivalent survivors are declared, not silently skipped.
37
+ - Comments and docstrings explain *why*, and state limits plainly. Correct an
38
+ outdated claim rather than leave it — this codebase treats wrong docs as
39
+ bugs (see `docs/SPEC.md` §17).
40
+ - `json.dumps` for authenticated data uses `ensure_ascii=False`,
41
+ `sort_keys=True`, `separators=(",", ":")`; payload JSON uses default
42
+ separators. They are NOT interchangeable.
43
+ - Style: ruff clean, compact, no unnecessary try/except — error boundaries
44
+ follow the existing pattern.
45
+
46
+ ## Frozen quirks — flag these too, and weigh them in every review
47
+
48
+ These behaviors are deliberate *today* (documented and frozen in
49
+ `docs/SPEC.md`), but they are not above scrutiny. Flag them when they appear
50
+ in a diff or are relevant to a change, note that the design intent is known,
51
+ and assess whether the change under review makes the quirk riskier — e.g.
52
+ new code that relies on one of these edge cases is worth a comment even
53
+ though the behavior itself is intentional:
54
+
55
+ - `memoryview` plaintext input is refused; `bytearray` envelope input is
56
+ snapshotted once before parsing (§4.3). Intentional — snapshot safety.
57
+ - Negative or >64-bit `schema_version`/`revision` integers serialize into
58
+ the AAD (§5.2). Frozen quirk — any tightening must be write-side only so
59
+ existing records stay readable.
60
+ - `record_id=" r "` (whitespace-padded) is accepted verbatim; only
61
+ whitespace-only strings are refused (frozen).
62
+ - Integer subclasses in coordinate ints are serialized via `int.__repr__`,
63
+ not `str()` — the fast path exists specifically so a subclass cannot
64
+ inject bytes into the AAD. Any new serialization path must keep that.
65
+ - Nonce dedup is in-process only (deque+set window): it does not survive
66
+ restart and is not a freshness mechanism — `revision` bound into the AAD
67
+ is. Flag anything that treats the window as stronger.
@@ -0,0 +1,27 @@
1
+ # Crypto Bill of Materials workflow — removed 2026-09-15
2
+
3
+ The `.github/workflows/main.yml` "Create Crypto Bill of Materials" workflow,
4
+ added in commit `7ca3499`, was removed in `18de92f` (this file's parent).
5
+
6
+ **Reason.** The workflow consumed `advanced-security/cbom-action@v1`, which
7
+ internally uses the now-deprecated `actions/upload-artifact: v3`. GitHub
8
+ auto-blocks requests to that deprecated action, so every matrix-analysis job
9
+ (and therefore the whole workflow) failed on every run, including the first
10
+ run after the workflow was added and again on the push that included this
11
+ removal. The failure was in the third-party action's own machinery, not in
12
+ anything `vaultfloor/floorvault` ships.
13
+
14
+ **What the workflow did.** It used the GitHub Advanced Security CBOM action to
15
+ build a crypto bill of materials across a matrix of public Python ecosystem
16
+ repositories (`coveragepy`, `cryptography`, `ruff`, `packaging`, `pytest`,
17
+ `pyobjc`, etc.), not floorvault's own dependencies in any actionable sense.
18
+
19
+ **What was already covering the real gate.** FloorVault's secret scan is
20
+ gitleaks in `.github/workflows/ci.yml` (CI job `secret scan (history)`), which
21
+ runs on every push to `main` and must pass for the push to be considered
22
+ gate-clean. The CBOM workflow was informational only and was never part of the
23
+ pre-push gate.
24
+
25
+ **Status.** Removed. No replacement is in place; if a crypto bill of materials
26
+ is wanted in future it should use a maintained action whose dependencies do not
27
+ include a deprecated artifact-action version.
@@ -0,0 +1,43 @@
1
+ name: ClusterFuzzLite Corpus Prune
2
+
3
+ on:
4
+ workflow_run:
5
+ workflows: [ClusterFuzzLite]
6
+ types: [completed]
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+ actions: read
12
+
13
+ concurrency:
14
+ group: cflite-prune-main
15
+ cancel-in-progress: false
16
+
17
+ jobs:
18
+ # Corpus pruning is required alongside batch fuzzing: without it the artifact-
19
+ # persisted corpus grows unboundedly. Runs after the weekly batch or by manual dispatch.
20
+ prune:
21
+ name: corpus prune (weekly)
22
+ if: >-
23
+ github.event_name == 'workflow_dispatch' ||
24
+ (github.event.workflow_run.event == 'schedule' &&
25
+ github.event.workflow_run.conclusion == 'success' &&
26
+ github.event.workflow_run.head_branch == 'main' &&
27
+ github.event.workflow_run.head_repository.full_name == github.repository)
28
+ runs-on: ubuntu-latest
29
+ steps:
30
+ - name: Build fuzzers
31
+ uses: google/clusterfuzzlite/actions/build_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1
32
+ with:
33
+ language: python
34
+ github-token: ${{ secrets.GITHUB_TOKEN }}
35
+ sanitizer: address
36
+
37
+ - name: Prune corpus
38
+ uses: google/clusterfuzzlite/actions/run_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1
39
+ with:
40
+ github-token: ${{ secrets.GITHUB_TOKEN }}
41
+ sanitizer: address
42
+ mode: prune
43
+ fuzz-seconds: 600
@@ -0,0 +1,60 @@
1
+ name: ClusterFuzzLite
2
+
3
+ # Coverage-guided fuzzing of the Atheris targets in fuzz/ (envelope parsing,
4
+ # canonical AAD, the protected key-store reader, SQL identifier validation and
5
+ # beacon encoding). Build integration lives in .clusterfuzzlite/.
6
+ #
7
+ # Pull requests fuzz for 5 minutes in code-change mode, so a crash introduced by
8
+ # the PR fails it. Pushes to main and a weekly schedule run longer batch fuzzing.
9
+ # With no storage repo configured, ClusterFuzzLite persists the corpus as GitHub
10
+ # Actions artifacts: each batch run downloads the previous run's corpus-*
11
+ # artifacts (that is what the actions: read permission is for) and uploads the
12
+ # grown corpus. Weekly pruning runs in a separate follow-on workflow
13
+ # (cflite-prune.yml) after the completed batch, so the persisted corpus stays
14
+ # minimized instead of growing unboundedly.
15
+ #
16
+ # The private repository needs the token passed to both steps.
17
+
18
+ on:
19
+ pull_request:
20
+ push:
21
+ branches: [main]
22
+ schedule:
23
+ - cron: "41 2 * * 0"
24
+ workflow_dispatch:
25
+
26
+ permissions:
27
+ contents: read
28
+
29
+ jobs:
30
+ fuzz:
31
+ name: fuzz (${{ github.event_name == 'pull_request' && 'code-change' || 'batch' }})
32
+ runs-on: ubuntu-latest
33
+ permissions:
34
+ contents: read
35
+ # Lets run_fuzzers list previous runs' artifacts (corpus, builds); without
36
+ # it the download fails with 403 and every run starts from the seeds.
37
+ actions: read
38
+ concurrency:
39
+ group: cflite-${{ github.ref }}
40
+ cancel-in-progress: true
41
+ steps:
42
+ - name: Build fuzzers
43
+ uses: google/clusterfuzzlite/actions/build_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1
44
+ with:
45
+ language: python
46
+ github-token: ${{ secrets.GITHUB_TOKEN }}
47
+ sanitizer: address
48
+
49
+ - name: Run fuzzers
50
+ uses: google/clusterfuzzlite/actions/run_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1
51
+ with:
52
+ github-token: ${{ secrets.GITHUB_TOKEN }}
53
+ sanitizer: address
54
+ mode: ${{ github.event_name == 'pull_request' && 'code-change' || 'batch' }}
55
+ fuzz-seconds: ${{ github.event_name == 'pull_request' && 300 || 1800 }}
56
+ # Fail on a crash even when the reproduction step cannot confirm it.
57
+ # Python fuzzer start-up is slow enough that reproduction can time out;
58
+ # the first run of this workflow found a real crash, failed to
59
+ # reproduce it for exactly that reason, discarded it, and went green.
60
+ report-unreproducible-crashes: true
@@ -0,0 +1,236 @@
1
+ name: CI
2
+
3
+ # Runs the same security gates developers run before pushing
4
+ # (scripts/security-check.sh) across every supported OS and Python version, so
5
+ # the cross-platform and "zero C compilation" claims are exercised on every
6
+ # push rather than only on a maintainer's machine.
7
+
8
+ on:
9
+ push:
10
+ branches: [main]
11
+ pull_request:
12
+ workflow_dispatch:
13
+
14
+ concurrency:
15
+ group: ci-${{ github.ref }}
16
+ cancel-in-progress: true
17
+
18
+ permissions:
19
+ contents: read
20
+
21
+ jobs:
22
+ # Python-version sweep on Linux (regressions here are platform-independent),
23
+ # plus one macOS and one Windows runner for the OS-specific code paths.
24
+ gates:
25
+ name: ${{ matrix.os }} / py${{ matrix.python }}
26
+ runs-on: ${{ matrix.os }}
27
+ strategy:
28
+ fail-fast: false
29
+ matrix:
30
+ include:
31
+ # Linux: the full supported range.
32
+ - { os: ubuntu-latest, python: "3.10" }
33
+ - { os: ubuntu-latest, python: "3.11" }
34
+ - { os: ubuntu-latest, python: "3.12" }
35
+ - { os: ubuntu-latest, python: "3.13" }
36
+ - { os: ubuntu-latest, python: "3.14" }
37
+ # macOS and Windows: BOTH ends of the range, not just the default.
38
+ # A platform-specific code path can be fine on the newest interpreter
39
+ # and broken on the oldest (or the reverse), and every defect the
40
+ # matrix has found so far was platform-specific; see
41
+ # docs/CROSS-PLATFORM-CI-FINDINGS-2026-09-15.md. 3.10 is the floor
42
+ # declared in pyproject.toml, so it must be exercised on every OS.
43
+ - { os: macos-latest, python: "3.10" }
44
+ - { os: macos-latest, python: "3.13" }
45
+ - { os: windows-latest, python: "3.10" }
46
+ - { os: windows-latest, python: "3.13" }
47
+ steps:
48
+ # fetch-depth: 0 is required because gitleaks scans history, not just the
49
+ # tip; a shallow checkout would make the scan meaningless.
50
+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
51
+ with:
52
+ fetch-depth: 0
53
+
54
+ - name: Set up uv and Python ${{ matrix.python }}
55
+ # Pinned to a full version: setup-uv only maintains moving major tags up
56
+ # to v7; v8+ exist as full versions only (v10 does not resolve).
57
+ uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4
58
+ with:
59
+ enable-cache: true
60
+ python-version: ${{ matrix.python }}
61
+ # Pin the uv binary too: setup-uv otherwise resolves its latest
62
+ # release, so the tool building the artifacts would move even with
63
+ # the action pinned by SHA.
64
+ version: "0.12.5"
65
+
66
+ - name: Sync environment
67
+ shell: bash
68
+ run: |
69
+ if [ "${{ runner.os }}" = "macOS" ]; then
70
+ # Keychain (Tier 2 of the adaptive provider) needs the Security bindings.
71
+ uv sync --extra dev --extra macos
72
+ else
73
+ uv sync --extra dev
74
+ fi
75
+ # The gate audits dependencies with pip-audit; install it into the venv
76
+ # so require_tool finds it under FLOORVAULT_STRICT=1 on every OS.
77
+ uv pip install pip-audit
78
+
79
+ - name: Run security gates
80
+ shell: bash
81
+ env:
82
+ FLOORVAULT_STRICT: "1"
83
+ # History is identical on every runner, so the secret scan runs once,
84
+ # in the `secrets` job below.
85
+ FLOORVAULT_SKIP_SECRET_SCAN: "1"
86
+ # Same reasoning for Semgrep: it runs once, in the `sast` job below.
87
+ FLOORVAULT_SKIP_SEMGREP: "1"
88
+ run: bash scripts/security-check.sh
89
+
90
+ # Publish one leg's artifacts as the release identity. The gate prints the
91
+ # digests of what it built on EVERY leg (step 11), so the other platforms
92
+ # stay comparable from the logs; this step exists so that a released wheel
93
+ # can be checked against a CI-built artifact rather than only against a
94
+ # maintainer's machine. One leg only: ten legs publishing the same name
95
+ # would collide, and upload-artifact cannot merge them meaningfully.
96
+ - name: Publish the built artifacts
97
+ if: matrix.os == 'ubuntu-latest' && matrix.python == '3.13'
98
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
99
+ with:
100
+ name: floorvault-dist
101
+ path: |
102
+ dist/*.whl
103
+ dist/*.tar.gz
104
+ # Fail closed: an upload step that silently publishes nothing is worse
105
+ # than no step at all, because it looks like the artifact was captured.
106
+ if-no-files-found: error
107
+
108
+ # Secret scanning over full history, once, on Linux.
109
+ #
110
+ # This runs the gitleaks CLI rather than `gitleaks/gitleaks-action`: the Action
111
+ # requires a licence for ORGANIZATION-owned repositories, and this repository
112
+ # moved into the `vaultfloor` organization, at which point the job started
113
+ # failing with "License key is required" and - because the workflow cancels
114
+ # in-progress runs - cancelled five other legs with it. The CLI has no such
115
+ # gate, runs exactly the command the local gate runs, and removes one
116
+ # third-party action from the supply chain.
117
+ #
118
+ # The download is version-pinned and checksum-verified: a scanner that cannot
119
+ # be verified must not silently become the thing that certifies everything else.
120
+ secrets:
121
+ name: secret scan (history)
122
+ runs-on: ubuntu-latest
123
+ permissions:
124
+ contents: read
125
+ steps:
126
+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
127
+ with:
128
+ # A shallow checkout would make a history scan meaningless.
129
+ fetch-depth: 0
130
+ - name: Install gitleaks (pinned, checksum-verified)
131
+ shell: bash
132
+ run: |
133
+ set -euo pipefail
134
+ VERSION=8.30.1
135
+ ASSET="gitleaks_${VERSION}_linux_x64.tar.gz"
136
+ SHA256=551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb
137
+ curl -sSfL -o "$ASSET" \
138
+ "https://github.com/gitleaks/gitleaks/releases/download/v${VERSION}/${ASSET}"
139
+ echo "${SHA256} ${ASSET}" | sha256sum -c -
140
+ tar -xzf "$ASSET" gitleaks
141
+ sudo install -m 0755 gitleaks /usr/local/bin/gitleaks
142
+ gitleaks version
143
+ - name: Scan history for secrets
144
+ shell: bash
145
+ # Same command as step 1 of scripts/security-check.sh, so CI and a local
146
+ # run agree on what the scan is. Non-zero exit when a secret is found.
147
+ run: gitleaks git --verbose --redact
148
+
149
+ # Static application security testing, once, on Linux.
150
+ #
151
+ # Bandit also runs on every matrix leg inside the gate (it is a dev dependency);
152
+ # it is repeated here so this job is a complete SAST record on its own. Semgrep
153
+ # is pinned to an exact version, but the p/python registry ruleset is fetched
154
+ # at run time and is not versioned: a newly published rule can fail this job
155
+ # without a code change. That is deliberate - a new rule that fires is a new
156
+ # finding to review - and it is why Semgrep runs here rather than on every leg.
157
+ sast:
158
+ name: SAST (Bandit, Semgrep CE)
159
+ runs-on: ubuntu-latest
160
+ permissions:
161
+ contents: read
162
+ steps:
163
+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
164
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4
165
+ with:
166
+ python-version: "3.13"
167
+ version: "0.12.5"
168
+ - name: Sync environment
169
+ run: uv sync --extra dev
170
+ - name: Bandit
171
+ # Same command as step 3a of scripts/security-check.sh.
172
+ run: uv run bandit -q -r src/ scripts/ fuzz/
173
+ - name: Semgrep CE
174
+ # Same command as step 3b of scripts/security-check.sh.
175
+ run: |
176
+ SEMGREP_VERSION=1.178.0
177
+ uvx --from "semgrep==${SEMGREP_VERSION}" semgrep scan --config p/python --metrics=off --error src/ scripts/ fuzz/
178
+
179
+ # Live-Vault integration leg (roadmap 1f): the hermetic fake-Vault suite can
180
+ # encode a wrong wire assumption in lockstep with the implementation — a real
181
+ # TLS-terminated Vault cannot. This runs the provider end-to-end against a
182
+ # dev server with verified TLS (the provider refuses plain HTTP by design).
183
+ #
184
+ # The Vault binary is version-pinned and checksum-verified for the same
185
+ # reason gitleaks is: a tool certifying the contract must itself be verified.
186
+ # The root token is a throwaway CI literal for an in-memory dev server that
187
+ # dies with the job — it is not a credential.
188
+ vault-live:
189
+ name: vault transit (live dev server)
190
+ runs-on: ubuntu-latest
191
+ steps:
192
+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
193
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4
194
+ with:
195
+ python-version: "3.13"
196
+ version: "0.12.5"
197
+ - name: Sync environment
198
+ run: uv sync --extra dev
199
+ - name: Install Vault (pinned, checksum-verified)
200
+ run: |
201
+ set -euo pipefail
202
+ VERSION=2.1.1
203
+ ASSET="vault_${VERSION}_linux_amd64.zip"
204
+ SHA256=8aa90f9cea46f541fc7baa3d0ec692fc06afde9a248cc1f2dcac46a567c6f56b
205
+ curl -sSfL -o "$ASSET" \
206
+ "https://releases.hashicorp.com/vault/${VERSION}/${ASSET}"
207
+ echo "${SHA256} ${ASSET}" | sha256sum -c -
208
+ unzip -q "$ASSET" vault
209
+ sudo install -m 0755 vault /usr/local/bin/vault
210
+ vault version
211
+ - name: Start Vault dev server (TLS) and provision the transit key
212
+ run: |
213
+ set -euo pipefail
214
+ mkdir -p "$RUNNER_TEMP/vault-tls"
215
+ nohup vault server -dev -dev-tls \
216
+ -dev-tls-cert-dir="$RUNNER_TEMP/vault-tls" \
217
+ -dev-root-token-id=fv-ci-token \
218
+ -dev-listen-address=127.0.0.1:8200 \
219
+ >"$RUNNER_TEMP/vault-dev.log" 2>&1 &
220
+ export VAULT_ADDR="https://127.0.0.1:8200"
221
+ export VAULT_CACERT="$RUNNER_TEMP/vault-tls/vault-ca.pem"
222
+ export VAULT_TOKEN="fv-ci-token"
223
+ for i in $(seq 1 30); do
224
+ vault status -format=json >/dev/null 2>&1 && break
225
+ [ "$i" = 30 ] && { cat "$RUNNER_TEMP/vault-dev.log"; exit 1; }
226
+ sleep 1
227
+ done
228
+ vault secrets enable transit
229
+ vault write transit/keys/floorvault-ci type=aes256-gcm96 derived=true
230
+ - name: Run the live-Vault contract test
231
+ env:
232
+ FLOORVAULT_LIVE_VAULT: "1"
233
+ VAULT_ADDR: "https://127.0.0.1:8200"
234
+ VAULT_CACERT: ${{ runner.temp }}/vault-tls/vault-ca.pem
235
+ VAULT_TOKEN: fv-ci-token
236
+ run: uv run pytest -q tests/test_vault_transit_live.py
@@ -0,0 +1,139 @@
1
+ name: Release
2
+
3
+ # Drafted release/provenance controls (roadmap 0e). Two modes:
4
+ #
5
+ # workflow_dispatch - rehearsal: build, reproducible-build check, SBOM,
6
+ # artifact provenance attestation. Nothing is published.
7
+ # release: published - the same build, then PyPI publish via Trusted
8
+ # Publishing with PEP 740 (Sigstore) attestations.
9
+ #
10
+ # Publishing stays inert until the maintainer completes the owner-side setup in
11
+ # docs/RELEASING.md (PyPI project + Trusted Publisher entry for this workflow,
12
+ # the `pypi` GitHub environment). Until then a real release run fails loudly at
13
+ # the publish step rather than silently skipping it.
14
+
15
+ on:
16
+ release:
17
+ types: [published]
18
+ workflow_dispatch:
19
+
20
+ permissions:
21
+ contents: read
22
+
23
+ jobs:
24
+ dist:
25
+ name: Build, verify, attest
26
+ runs-on: ubuntu-latest
27
+ permissions:
28
+ contents: read
29
+ id-token: write # artifact attestation signs via Sigstore OIDC
30
+ attestations: write
31
+ steps:
32
+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
33
+ with:
34
+ fetch-depth: 0
35
+
36
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4
37
+ with:
38
+ python-version: "3.13"
39
+ version: "0.12.5"
40
+
41
+ - name: Build release artifacts twice
42
+ # The comparison copy MUST be built outside the checkout: the sdist is
43
+ # VCS-aware, so a second in-repo out-dir appears mid-build and packages
44
+ # itself into the archive it is producing (dist-second/.gitignore was
45
+ # the first live demonstration). A mktemp dir can never self-include.
46
+ run: |
47
+ set -euo pipefail
48
+ uv build --out-dir dist
49
+ SECOND="$(mktemp -d /tmp/floorvault-dist-second.XXXXXX)"
50
+ uv build --out-dir "$SECOND"
51
+ echo "SECOND=$SECOND" >> "$GITHUB_ENV"
52
+
53
+ - name: Reproducible-build check
54
+ # Same runner, same source, same toolchain: the digests must match
55
+ # byte-for-byte. Cross-PLATFORM identity is explicitly not claimed
56
+ # (SECURITY.md), but a single-environment build that cannot reproduce
57
+ # itself indicates the toolchain picked up nondeterminism.
58
+ run: |
59
+ set -euo pipefail
60
+ python - "$SECOND" <<'PY'
61
+ import hashlib
62
+ import pathlib
63
+ import sys
64
+
65
+ first = {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in pathlib.Path("dist").iterdir()}
66
+ second = {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in pathlib.Path(sys.argv[1]).iterdir()}
67
+ mismatched = sorted(n for n in set(first) | set(second) if first.get(n) != second.get(n))
68
+ if mismatched:
69
+ for name in mismatched:
70
+ print(f"mismatch {name}: {first.get(name)} != {second.get(name)}", file=sys.stderr)
71
+ sys.exit(1)
72
+ for name, digest in sorted(first.items()):
73
+ print(f"[REPRODUCIBLE] sha256 {digest} {name}")
74
+ PY
75
+
76
+ - name: Verify universal wheel shape
77
+ run: python scripts/verify_wheel.py dist
78
+
79
+ - name: Generate CycloneDX SBOM
80
+ # The SBOM describes what a consumer installs: a runtime-only sync, not
81
+ # the dev extras. cyclonedx-bom is version-pinned and >7 days old.
82
+ # It lives OUTSIDE dist/: the publish job uploads everything in dist/
83
+ # to PyPI, and a JSON file is not a valid distribution.
84
+ run: |
85
+ set -euo pipefail
86
+ uv sync
87
+ mkdir -p sbom
88
+ uvx --from "cyclonedx-bom==7.4.0" \
89
+ cyclonedx-py environment .venv -o sbom/floorvault-sbom.cdx.json --of JSON \
90
+ --mc-type library --pyproject pyproject.toml --output-reproducible
91
+
92
+ - name: Publish the built artifacts
93
+ # Two artifacts, not one: a multi-path upload keeps both directory
94
+ # names at the artifact root, so a single-artifact download would land
95
+ # sbom/ inside the publish job's dist/ - and the PyPI action uploads
96
+ # every file it finds there, which a JSON SBOM is not.
97
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
98
+ with:
99
+ name: floorvault-release-dist
100
+ path: dist/
101
+ if-no-files-found: error
102
+
103
+ - name: Publish the SBOM
104
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
105
+ with:
106
+ name: floorvault-release-sbom
107
+ path: sbom/
108
+ if-no-files-found: error
109
+
110
+ - name: Attest build provenance
111
+ uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8
112
+ with:
113
+ subject-path: dist/
114
+
115
+ publish:
116
+ name: Publish to PyPI (Trusted Publishing)
117
+ needs: dist
118
+ if: github.event_name == 'release'
119
+ runs-on: ubuntu-latest
120
+ environment: pypi
121
+ permissions:
122
+ id-token: write # Trusted Publishing exchanges this OIDC token for PyPI
123
+ steps:
124
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
125
+ with:
126
+ name: floorvault-release-dist
127
+ path: dist
128
+
129
+ - name: Drop uv's out-dir marker
130
+ # `uv build --out-dir` writes a `.gitignore` into the directory; it is
131
+ # not a distribution and the PyPI action rejects unknown files.
132
+ run: rm -f dist/.gitignore
133
+
134
+ - name: Publish with PEP 740 attestations
135
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33
136
+ with:
137
+ attestations: true
138
+ print-hash: true
139
+ verbose: true