Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ tmp_completao_*.yaml
*.key
*.pem
!core/licensing/license-pub-v1.pem
!core/licensing/license-mldsa-pub-v1.pem
!tests/data/licensing_hybrid/*.pem
core/licensing/_build_digest.txt
*.p12
Expand Down
43 changes: 43 additions & 0 deletions core/licensing/license-mldsa-pub-v1.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
-----BEGIN ML-DSA-65 PUBLIC KEY-----
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
-----END ML-DSA-65 PUBLIC KEY-----
2 changes: 2 additions & 0 deletions docs/LICENSING_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ Environment variables override YAML when set:

Enforced mode verifies the `.lic` against the Ed25519 **public** key shipped **inside the wheel** as `core/licensing/license-pub-v1.pem` (loaded via `importlib.resources`). A clean install with a valid machine-bound `.lic` does **not** need a pubkey path.

The ML-DSA-65 **rotation / hybrid** anchor ships as `core/licensing/license-mldsa-pub-v1.pem` (same packaging path as Ed25519). Canonical on-disk SHA-256 (LF bytes): `37faef8ff167cf01a0fc6a2bc8b7cfcac6bfe5651360051b1eb00a0a60fa85b8`. Hybrid `dbmldsa_sig` verify remains **optional** until the operator adopts a mandatory policy; tokens without that claim stay Ed25519-only.

Key rotation is a JSON attestation at `licensing.rotation_attestation_path`. It is accepted only when **both** embedded anchors (Ed25519 and ML-DSA-65) sign the same message and `epoch` is `>= MIN_ACCEPTED_KEY_EPOCH` in `core/licensing/trust_anchor.py`. The signed bytes are `data-boar/license-key-rotation/v1\\0` plus the epoch as an 8-byte big-endian integer plus the canonical JSON of the new public keys. The epoch is inside that message; changing it after signing fails the signature. One anchor, a raw PEM, or an older epoch fails closed (`rotation_rejected:…`). `missing_public_key` remains only when the packaged Ed25519 resource is absent and no accepted rotation is configured.

Other environment variables:
Expand Down
2 changes: 2 additions & 0 deletions docs/LICENSING_SPEC.pt_BR.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ Variáveis de ambiente sobrescrevem o YAML quando definidas:

O modo enforced verifica o `.lic` contra a chave **pública** Ed25519 que vai **dentro do wheel** em `core/licensing/license-pub-v1.pem` (carregada via `importlib.resources`). Uma instalação limpa com um `.lic` válido e vinculado à máquina **não** precisa de caminho de pubkey.

A âncora ML-DSA-65 de **rotação / híbrido** vai em `core/licensing/license-mldsa-pub-v1.pem` (mesmo empacotamento que a Ed25519). SHA-256 canônico do arquivo em disco (bytes LF): `37faef8ff167cf01a0fc6a2bc8b7cfcac6bfe5651360051b1eb00a0a60fa85b8`. A verificação híbrida `dbmldsa_sig` segue **opcional** até o operador adotar política obrigatória; tokens sem esse claim continuam só Ed25519.

A rotação de chave é uma atestação JSON em `licensing.rotation_attestation_path`. Ela só é aceita quando **as duas** âncoras embarcadas (Ed25519 e ML-DSA-65) assinam a mesma mensagem e `epoch` é `>= MIN_ACCEPTED_KEY_EPOCH` em `core/licensing/trust_anchor.py`. Os bytes assinados são `data-boar/license-key-rotation/v1\\0` mais a época como inteiro de 8 bytes big-endian mais o JSON canônico das chaves públicas novas. A época fica dentro dessa mensagem; mudá-la depois de assinar invalida a assinatura. Uma âncora só, um PEM cru ou uma época antiga falham fechado (`rotation_rejected:…`). `missing_public_key` só ocorre quando o recurso Ed25519 empacotado está ausente e não há rotação aceita.

Outras variáveis de ambiente:
Expand Down
2 changes: 1 addition & 1 deletion docs/ops/LICENSE_STUDIO_SPINOUT.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ The Go **license issuance** tool moved out of this public tree on **2026-06-25**
| What | Where |
| ---- | ----- |
| **Operator issuer (Go)** | Private repo **`DataBoar/license-studio`** — clone with `gh repo clone DataBoar/license-studio` |
| **Client enforcement** | Stays here: `core/licensing/`, `license-pub-v1.pem`, ADR-0063/0064 |
| **Client enforcement** | Stays here: `core/licensing/`, `license-pub-v1.pem`, `license-mldsa-pub-v1.pem`, ADR-0063/0064 |
| **Dev JWT helper (Python)** | Stays here: `scripts/issue_dev_license_jwt.py` + `docs/ops/QA_LOCAL_LICENSE.md` |

Never commit signing keys, audit DBs, or issuance blobs to **`origin`**.
Expand Down
4 changes: 4 additions & 0 deletions docs/ops/QA_LOCAL_LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ Send only the `.lic` file — never the private key.
## 4. Install and run enforced

The official verify key is **embedded** in the install (`license-pub-v1.pem`).
The ML-DSA-65 anchor (`license-mldsa-pub-v1.pem`, SHA-256
`37faef8ff167cf01a0fc6a2bc8b7cfcac6bfe5651360051b1eb00a0a60fa85b8`) ships in
the same wheel for rotation and optional hybrid verify; `dbmldsa_sig` is not
required yet.
A valid machine-bound `.lic` is enough — no pubkey env/config on a clean
install (#1331). A raw `DATA_BOAR_LICENSE_PUBLIC_KEY_*`, `DATA_BOAR_LICENSE_MLDSA_PUBLIC_KEY_*`,
`licensing.public_key_path`, or `licensing.mldsa_public_key_path` fails closed
Expand Down
5 changes: 4 additions & 1 deletion docs/ops/QA_LOCAL_LICENSE.pt_BR.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ Envie apenas o arquivo `.lic` — nunca a chave privada.
## 4. Instalar e rodar enforced

A chave pública oficial de verificação já vai **embarcada** na instalação
(`license-pub-v1.pem`). Um `.lic` válido e vinculado à máquina basta — sem
(`license-pub-v1.pem`). A âncora ML-DSA-65 (`license-mldsa-pub-v1.pem`, SHA-256
`37faef8ff167cf01a0fc6a2bc8b7cfcac6bfe5651360051b1eb00a0a60fa85b8`) vem no
mesmo wheel para rotação e verificação híbrida opcional; `dbmldsa_sig` ainda
não é obrigatório. Um `.lic` válido e vinculado à máquina basta — sem
env/config de pubkey numa instalação limpa (#1331). Um
`DATA_BOAR_LICENSE_PUBLIC_KEY_*`, `DATA_BOAR_LICENSE_MLDSA_PUBLIC_KEY_*`,
`licensing.public_key_path` ou `licensing.mldsa_public_key_path` falha
Expand Down
1 change: 1 addition & 0 deletions docs/plans/PLANS_TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ When **partners** or **buyers** anchor on a vertical (MSP, insurance, RPO, real
- **post4 trust-floor closure (2026-07-12):** merged hardening issues **[#1194](https://github.com/DataBoar/data-boar/issues/1194)**, **[#1195](https://github.com/DataBoar/data-boar/issues/1195)**, **[#1201](https://github.com/DataBoar/data-boar/issues/1201)**, **[#1202](https://github.com/DataBoar/data-boar/issues/1202)**, **[#1137](https://github.com/DataBoar/data-boar/issues/1137)**, **[#1198](https://github.com/DataBoar/data-boar/issues/1198)**, **[#1130](https://github.com/DataBoar/data-boar/issues/1130)** via PRs **[#1203](https://github.com/DataBoar/data-boar/pull/1203)**–**[#1209](https://github.com/DataBoar/data-boar/pull/1209)** and **[#1214](https://github.com/DataBoar/data-boar/pull/1214)**; trust-floor scope for post4 marked closed. New queued follow-ups: **[#1210](https://github.com/DataBoar/data-boar/issues/1210)** (merge-secrets), **[#1211](https://github.com/DataBoar/data-boar/issues/1211)** (integrity fail-open), **[#1212](https://github.com/DataBoar/data-boar/issues/1212)** (GRACE).
- **Canonical product facts / anti-invention (`#1470` slice):** [PLAN_CANONICAL_PRODUCT_FACTS.md](PLAN_CANONICAL_PRODUCT_FACTS.md) — `docs/CANONICAL_PRODUCT_FACTS.md` (+ pt-BR) + FACTS-anchored guard `tests/test_canonical_product_facts.py`. Does **not** close #1470 (README friendliness / QUICKSTART de-Docker / Audience remain). Org-path canonicalize [#1473](https://github.com/DataBoar/data-boar/issues/1473) merged on `main`.
- **License canonical path fallback ([#1940](https://github.com/DataBoar/data-boar/issues/1940)):** ✅ **Done** (local epic) — `LicenseGuard` reads `<user_config_dir>/databoar/license.jwt` when env and `licensing.license_path` are unset/blank and the file exists; explicit paths stay fail-closed. [PLAN_LICENSE_CANONICAL_PATH_FALLBACK.md](PLAN_LICENSE_CANONICAL_PATH_FALLBACK.md) (+ pt-BR).
- **Shell script hardening ([#1926](https://github.com/DataBoar/data-boar/issues/1926)):** ✅ **Done** — `shellcheck --severity=error` on all tracked `*.sh` in `scripts/check-all.sh` and `scripts/check-all.ps1`, and in CI (`.github/workflows/ci.yml`, step **Shellcheck (severity=error)**). [PLAN_SHELL_SCRIPT_HARDENING.md](PLAN_SHELL_SCRIPT_HARDENING.md) inventories **73** tracked `.sh` files with per-file logic/bats verdicts; optional local `check-all` runs `tests/bats/` (**3** files, **11** tests) when `bats` is on PATH (warn-and-skip otherwise). **No** bats job in CI (documented in plan).
- **Native packages on the product Release (`#1408`):** nfpm **deb/rpm/apk/pacman** from the same x86-64 glibc wheelhouse populate (#1437 / #1182) attach beside SBOMs on a `v*` GitHub Release (`native-packages.yml` `attach-release`). Packager filename convention + `SHA256SUMS` + `release-manifest.json` `native_packages[]`. Hand-built recipe-proof `.deb` is **not** publishable. Signed repo (#1405) consumes **the same files**. Air-gap: [NATIVE_PACKAGE_RELEASE.md](../ops/NATIVE_PACKAGE_RELEASE.md). Plan [PLAN_NATIVE_PACKAGES.md](PLAN_NATIVE_PACKAGES.md).
- **Distribution format matrix — Nuitka / `.pyc` (`#1922`, plan 2026-09-13):** Orthogonal axis **③** (artifact shape) vs wheelhouse/native **①** and ISA **②**. Evaluation-only; **do not** implement spikes D1–D5 in this slice. Independent of [#676](https://github.com/DataBoar/data-boar/issues/676). [PLAN_DISTRIBUTION_FORMAT_MATRIX_NUITKA.md](PLAN_DISTRIBUTION_FORMAT_MATRIX_NUITKA.md).
- **Wheelhouse pan-ABI matrix via GitHub Releases (`#1182`, seed 2026-07-12 / v1 2026-07-29):** Target = third-party compiled deps **`cp312` + `cp313` + `cp314`** × `(manylinux|musllinux)` × `(x86_64|arm64)` × **CPU baseline (`x86-64-v1` where needed)**; **`boar_fast_filter`** stays **ONE `cp38-abi3` wheel per `(libc×arch)`** ([#782](https://github.com/DataBoar/data-boar/issues/782) / GAP-001) — **not on PyPI**; wheelhouse is the only channel today. **Verified release:** [`wheelhouse-x86-64-v1-2026-07-29`](https://github.com/DataBoar/data-boar-site/releases/tag/wheelhouse-x86-64-v1-2026-07-29) on **`DataBoar/data-boar-site`** (two-step `pipx` + offline ML swap + `boar_fast_filter` inject; **58** `.whl` incl. `mariadb` both libcs × cp312/313/314 + **14× `cp314t`**: 10 ML + 4 SQL extras `linux_x86_64` for the **`-nogil` image only** — `mariadb` / `oracledb` / `psycopg2-binary` / `pymssql`; no PyPI free-threaded wheels; [#1398](https://github.com/DataBoar/data-boar/issues/1398)). **PEP 503 `simple/`:** live at `https://databoar.com.br/simple/` ([data-boar-site#21](https://github.com/DataBoar/data-boar-site/pull/21) / [#1445](https://github.com/DataBoar/data-boar/issues/1445); SQL extras index [data-boar-site#86](https://github.com/DataBoar/data-boar-site/pull/86)); `--extra-index-url` does **not** prefer wheelhouse over PyPI on version tie — keep `--force-reinstall`. **Docs rollout:** [#1365](https://github.com/DataBoar/data-boar/issues/1365) → `TROUBLESHOOTING` + `OS_COMPATIBILITY_TESTING_MATRIX` + [PLAN_WHEELHOUSE_DISTRIBUTION.md](PLAN_WHEELHOUSE_DISTRIBUTION.md). **`mariadb` glibc recipe pinned** ([#1367](https://github.com/DataBoar/data-boar/issues/1367)); **recipe CI** ([#1379](https://github.com/DataBoar/data-boar/issues/1379) — `scripts/wheelhouse/recipe-manifest.yaml` + canary/full workflow). aarch64 still [#1366](https://github.com/DataBoar/data-boar/issues/1366). **cp314 = signal-only** in CI gating. **SHA256SUMS (#1410 + #1398):** ✅ **58**/58 coverage incl. **14× cp314t**; gate `verify_release_sha256sums.sh`. **Remaining #1182:** arm64 slice + **`+x86v1`** local versions.
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ packages = [
[tool.hatch.build.targets.wheel.force-include]
"main.py" = "main.py"
"core/licensing/license-pub-v1.pem" = "core/licensing/license-pub-v1.pem"
"core/licensing/license-mldsa-pub-v1.pem" = "core/licensing/license-mldsa-pub-v1.pem"
"docs/sdk/metadata_manifest.schema.json" = "core/data/metadata_manifest.schema.json"
"docs/sdk/metadata_manifest.pin.json" = "core/data/metadata_manifest.pin.json"
"docs/sdk/transformed_rows.schema.json" = "core/data/transformed_rows.schema.json"
Expand Down
103 changes: 103 additions & 0 deletions tests/test_licensing_mldsa_anchor_embedded.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""Packaged ML-DSA-65 trust anchor (#2042 / #1462).

Hybrid ``dbmldsa_sig`` verification stays optional; this file only pins the
embedded public anchor bytes and loader behavior.
"""

from __future__ import annotations

import hashlib
import re
from pathlib import Path

import jwt
import pytest
from cryptography.hazmat.backends.openssl.backend import backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

from core.licensing import trust_anchor
from core.licensing.verify import (
CLAIM_MLDSA_SIG,
decode_license_jwt,
load_mldsa65_public_key_pem,
)

REPO_ROOT = Path(__file__).resolve().parents[1]
ANCHOR_PEM = REPO_ROOT / "core" / "licensing" / "license-mldsa-pub-v1.pem"

# Canonical SHA-256 is over UTF-8 bytes with LF line endings (2717 bytes on disk).
CANONICAL_ANCHOR_SHA256 = (
"37faef8ff167cf01a0fc6a2bc8b7cfcac6bfe5651360051b1eb00a0a60fa85b8"
)
MLDSA65_PUBLIC_KEY_RAW_BYTES = 1952

pytestmark = pytest.mark.skipif(
not backend.mldsa_supported(),
reason="OpenSSL backend has no ML-DSA support",
)


def _sha256_normalized_pem_text(text: str) -> str:
# Windows checkouts may use CRLF; the canonical digest uses LF-only bytes.
normalized = text.replace("\r\n", "\n").encode("utf-8")
return hashlib.sha256(normalized).hexdigest()


def test_committed_mldsa_anchor_pem_matches_canonical_sha256() -> None:
assert ANCHOR_PEM.is_file(), "missing packaged anchor PEM in source tree"
text = ANCHOR_PEM.read_text(encoding="utf-8")
assert text.strip().startswith("-----BEGIN ML-DSA-65 PUBLIC KEY-----")
assert _sha256_normalized_pem_text(text) == CANONICAL_ANCHOR_SHA256
assert "PRIVATE" not in text.upper()


def test_load_embedded_mldsa_anchor_pem_matches_committed_file() -> None:
loaded = trust_anchor.load_embedded_mldsa_anchor_pem()
assert loaded is not None
committed = ANCHOR_PEM.read_text(encoding="utf-8").strip()
assert loaded == committed
# ``load_embedded_mldsa_anchor_pem`` strips whitespace; canonical SHA-256 is
# over the on-disk file bytes (see ``test_committed_mldsa_anchor_pem_*``).


def test_load_embedded_mldsa_anchor_pem_none_when_resource_missing(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(
trust_anchor,
"EMBEDDED_MLDSA_ANCHOR_RESOURCE",
"license-mldsa-pub-v1-missing.pem",
)
assert trust_anchor.load_embedded_mldsa_anchor_pem() is None


def test_embedded_mldsa_anchor_loads_as_mldsa65_public_key() -> None:
pem = trust_anchor.load_embedded_mldsa_anchor_pem()
assert pem is not None
key = load_mldsa65_public_key_pem(pem)
raw = key.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw,
)
assert len(raw) == MLDSA65_PUBLIC_KEY_RAW_BYTES


def test_decode_license_jwt_without_dbmldsa_claim_is_ed25519_only() -> None:
"""Unchanged semantics: no hybrid claim → Ed25519 path only (no ML-DSA key)."""
priv = Ed25519PrivateKey.generate()
token = jwt.encode(
{"sub": "ed-only", "exp": 9_999_999_999},
priv,
algorithm="EdDSA",
)
assert CLAIM_MLDSA_SIG not in jwt.decode(token, options={"verify_signature": False})
claims = decode_license_jwt(token, priv.public_key())
assert claims["sub"] == "ed-only"
assert CLAIM_MLDSA_SIG not in claims


def test_embedded_mldsa_pem_has_no_private_block() -> None:
pem = trust_anchor.load_embedded_mldsa_anchor_pem()
assert pem is not None
assert not re.search(r"BEGIN\s+.*PRIVATE", pem, flags=re.IGNORECASE)
Loading