5 Commits
Author SHA1 Message Date
zemion 029406e9ee Release v0.1.16
Module Package Release / publish-packages (push) Successful in 12s
2026-08-05 19:52:06 +02:00
zemion e1d0742c46 Release v0.1.15
Module Package Release / publish-packages (push) Successful in 12s
2026-08-04 15:10:19 +02:00
zemion 869f7bbdc4 Make package publication retries hash-safe 2026-08-04 14:32:19 +02:00
zemion 517dfb720e Record governed encryption lifecycle defaults 2026-08-04 14:01:44 +02:00
zemion 99a5fa52a5 Harden module package publication 2026-08-04 14:01:36 +02:00
6 changed files with 131 additions and 34 deletions
+83 -22
View File
@@ -14,6 +14,8 @@ on:
jobs: jobs:
publish-packages: publish-packages:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
GITEA_REPOSITORY: ${{ gitea.repository }}
steps: steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
with: with:
@@ -29,7 +31,6 @@ jobs:
env: env:
REQUESTED_TAG: ${{ inputs.release_tag }} REQUESTED_TAG: ${{ inputs.release_tag }}
TRIGGER_TAG: ${{ gitea.ref_name }} TRIGGER_TAG: ${{ gitea.ref_name }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: | run: |
set -euo pipefail set -euo pipefail
tag="${REQUESTED_TAG:-$TRIGGER_TAG}" tag="${REQUESTED_TAG:-$TRIGGER_TAG}"
@@ -43,24 +44,6 @@ jobs:
echo "Release tag is not contained in main" >&2 echo "Release tag is not contained in main" >&2
exit 1 exit 1
} }
python - "$tag" <<'PY'
import fnmatch
import json
import os
import sys
import urllib.request
tag = sys.argv[1]
repository = os.environ["GITEA_REPOSITORY"]
request = urllib.request.Request(
f"{os.environ['GITEA_API_URL']}/repos/{repository}/tag_protections",
headers={"Authorization": f"token {os.environ['GITEA_TOKEN']}"},
)
with urllib.request.urlopen(request, timeout=30) as response:
protections = json.load(response)
if not any(fnmatch.fnmatchcase(tag, item.get("name_pattern", "")) for item in protections):
raise SystemExit(f"Release tag {tag!r} is not covered by repository tag protection")
PY
git checkout --detach "$tag" git checkout --detach "$tag"
printf 'RELEASE_TAG=%s\n' "$tag" >> "$GITEA_ENV" printf 'RELEASE_TAG=%s\n' "$tag" >> "$GITEA_ENV"
printf 'SOURCE_DATE_EPOCH=%s\n' "$(git show -s --format=%ct HEAD)" >> "$GITEA_ENV" printf 'SOURCE_DATE_EPOCH=%s\n' "$(git show -s --format=%ct HEAD)" >> "$GITEA_ENV"
@@ -130,7 +113,7 @@ jobs:
const escapedRepository = repository.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const escapedRepository = repository.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const gitTag = specifier.match( const gitTag = specifier.match(
new RegExp( new RegExp(
`^git\\+(?:ssh://git@|https://)git\\.add-ideas\\.de/GovOPlaN/${escapedRepository}\\.git#v([0-9]+\\.[0-9]+\\.[0-9]+)$`, `^git\\+(?:ssh://git@|https://)git\\.add-ideas\\.de/(?:GovOPlaN|add-ideas)/${escapedRepository}\\.git#v([0-9]+\\.[0-9]+\\.[0-9]+)$`,
), ),
); );
if (gitTag) { if (gitTag) {
@@ -180,6 +163,78 @@ jobs:
with: with:
name: module-packages-${{ gitea.ref_name }} name: module-packages-${{ gitea.ref_name }}
path: dist/package-artifacts.json path: dist/package-artifacts.json
- name: Check immutable registry state
shell: bash
env:
PACKAGE_TOKEN: ${{ secrets.GOVOPLAN_PACKAGE_TOKEN }}
run: |
set -euo pipefail
test -n "$PACKAGE_TOKEN"
python - <<'PY'
import hashlib
import json
import os
from pathlib import Path
import tomllib
from urllib.error import HTTPError
from urllib.parse import quote
from urllib.request import Request, urlopen
api_root = "https://git.add-ideas.de/api/v1/packages/GovOPlaN"
token = os.environ["PACKAGE_TOKEN"]
def should_publish(kind, name, version, path):
package_url = "/".join(
(api_root, kind, quote(name, safe=""), quote(version, safe=""), "files")
)
request = Request(
package_url,
headers={"Accept": "application/json", "Authorization": f"token {token}"},
)
try:
with urlopen(request, timeout=30) as response:
files = json.load(response)
except HTTPError as exc:
if exc.code == 404:
print(f"{kind} package {name}=={version} is not published yet")
return True
raise
if not isinstance(files, list) or len(files) != 1:
raise SystemExit(
f"immutable {kind} package {name}=={version} has an unexpected file set"
)
expected_sha256 = hashlib.sha256(path.read_bytes()).hexdigest()
if files[0].get("sha256") != expected_sha256:
raise SystemExit(
f"immutable {kind} package {name}=={version} already exists with a different SHA-256"
)
print(f"verified existing {kind} package {name}=={version} ({expected_sha256})")
return False
project = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))["project"]
wheels = tuple(Path("dist").glob("*.whl"))
if len(wheels) != 1:
raise SystemExit("release build must contain exactly one wheel")
publish_pypi = should_publish(
"pypi", str(project["name"]), str(project["version"]), wheels[0]
)
tarballs = tuple(Path("dist").glob("*.tgz"))
if len(tarballs) > 1:
raise SystemExit("release build must contain at most one npm package")
publish_npm = False
if tarballs:
webui = json.loads(
Path(".package-webui/package.json").read_text(encoding="utf-8")
)
publish_npm = should_publish(
"npm", str(webui["name"]), str(webui["version"]), tarballs[0]
)
with Path(os.environ["GITEA_ENV"]).open("a", encoding="utf-8") as env_file:
env_file.write(f"PUBLISH_PYPI={int(publish_pypi)}\n")
env_file.write(f"PUBLISH_NPM={int(publish_npm)}\n")
PY
- name: Publish wheel and WebUI package - name: Publish wheel and WebUI package
shell: bash shell: bash
env: env:
@@ -189,13 +244,17 @@ jobs:
set -euo pipefail set -euo pipefail
test -n "$PACKAGE_USERNAME" test -n "$PACKAGE_USERNAME"
test -n "$PACKAGE_TOKEN" test -n "$PACKAGE_TOKEN"
if [[ "$PUBLISH_PYPI" == 1 ]]; then
TWINE_USERNAME="$PACKAGE_USERNAME" TWINE_PASSWORD="$PACKAGE_TOKEN" \ TWINE_USERNAME="$PACKAGE_USERNAME" TWINE_PASSWORD="$PACKAGE_TOKEN" \
python -m twine upload --non-interactive \ python -m twine upload --non-interactive \
--repository-url https://git.add-ideas.de/api/packages/GovOPlaN/pypi \ --repository-url https://git.add-ideas.de/api/packages/GovOPlaN/pypi \
dist/*.whl dist/*.whl
else
echo "Exact wheel is already present; skipping immutable retry."
fi
shopt -s nullglob shopt -s nullglob
webui_packages=(dist/*.tgz) webui_packages=(dist/*.tgz)
if (( ${#webui_packages[@]} )); then if (( ${#webui_packages[@]} )) && [[ "$PUBLISH_NPM" == 1 ]]; then
npmrc="$(mktemp)" npmrc="$(mktemp)"
trap 'rm -f "$npmrc"' EXIT trap 'rm -f "$npmrc"' EXIT
chmod 600 "$npmrc" chmod 600 "$npmrc"
@@ -203,7 +262,9 @@ jobs:
'@govoplan:registry=https://git.add-ideas.de/api/packages/GovOPlaN/npm/' \ '@govoplan:registry=https://git.add-ideas.de/api/packages/GovOPlaN/npm/' \
"//git.add-ideas.de/api/packages/GovOPlaN/npm/:_authToken=$PACKAGE_TOKEN" \ "//git.add-ideas.de/api/packages/GovOPlaN/npm/:_authToken=$PACKAGE_TOKEN" \
> "$npmrc" > "$npmrc"
NPM_CONFIG_USERCONFIG="$npmrc" npm publish "${webui_packages[0]}" \ NPM_CONFIG_USERCONFIG="$npmrc" npm publish "./${webui_packages[0]}" \
--ignore-scripts --access public \ --ignore-scripts --access public \
--registry https://git.add-ideas.de/api/packages/GovOPlaN/npm/ --registry https://git.add-ideas.de/api/packages/GovOPlaN/npm/
elif (( ${#webui_packages[@]} )); then
echo "Exact WebUI package is already present; skipping immutable retry."
fi fi
+31
View File
@@ -18,6 +18,37 @@ certification claim. Stronger claims require a named protection profile whose
provider, clients, algorithms, backup procedure, and failure tests have passed provider, clients, algorithms, backup procedure, and failure tests have passed
the profile's conformance and security review. the profile's conformance and security review.
## Approved Product Baseline
The product decision recorded on 2026-08-04 separates module activation from
content protection. Enabling Encryption makes its capabilities and
administration available but never encrypts existing or new content by itself.
- System definitions are reusable profile templates. Tenants explicitly
activate profiles for an owner-module collection or individual object.
Accounts and groups remain policy subjects rather than cryptographic scopes.
- New objects inherit protection only inside an explicitly activated scope.
Existing objects move through an explicit, checkpointed, copy-on-write
migration with verified cutover.
- The first production profile is managed server-envelope encryption backed by
a KMS/HSM provider. The bundled local provider remains a bounded reference
implementation. Tenant-held and client E2EE profiles require separate
provider approval and conformance evidence.
- Recovery defaults to a high-assurance, distinct-custodian 2-of-3 quorum with
requester separation. Ordinary platform administration does not bypass the
ceremony.
- Horizontally scaled nodes may receive only short-lived, in-memory unwrap
grants. They do not persist usable key material on node-local storage.
- Disable remains blocked until every protected object is verifiably decrypted,
migrated, exported, or destroyed. Externally opaque ciphertext must be
resolved through its provider before the owning profile can be removed.
After a migration has retired plaintext, reversal is another governed
migration rather than retention of an undisclosed plaintext rollback copy.
True E2EE is deliberately not part of the first production profile because it
changes search, server-side processing, inspection, reporting, legal-hold
export, and recovery guarantees.
## Ownership Boundary ## Ownership Boundary
Encryption owns: Encryption owns:
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@govoplan/encryption", "name": "@govoplan/encryption",
"version": "0.1.14", "version": "0.1.16",
"private": true, "private": true,
"description": "GovOPlaN encryption platform module scaffold.", "description": "GovOPlaN encryption platform module scaffold.",
"type": "module", "type": "module",
+2 -2
View File
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "govoplan-encryption" name = "govoplan-encryption"
version = "0.1.14" version = "0.1.16"
description = "Optional key-vault and content-protection capabilities for GovOPlaN." description = "Optional key-vault and content-protection capabilities for GovOPlaN."
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = [{ name = "GovOPlaN" }] authors = [{ name = "GovOPlaN" }]
dependencies = ["cryptography>=44", "govoplan-core>=0.1.14"] dependencies = ["cryptography>=44", "govoplan-core>=0.1.16"]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
where = ["src"] where = ["src"]
+1 -1
View File
@@ -41,7 +41,7 @@ from govoplan_encryption.backend.service import SqlEncryptionService
MODULE_ID = "encryption" MODULE_ID = "encryption"
MODULE_NAME = "Encryption" MODULE_NAME = "Encryption"
MODULE_VERSION = "0.1.14" MODULE_VERSION = "0.1.16"
USE_SCOPE = "encryption:vault:use" USE_SCOPE = "encryption:vault:use"
ADMIN_SCOPE = "encryption:vault:admin" ADMIN_SCOPE = "encryption:vault:admin"
+9 -4
View File
@@ -1,23 +1,28 @@
{ {
"name": "@govoplan/encryption-webui", "name": "@govoplan/encryption-webui",
"version": "0.1.14", "version": "0.1.16",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"module": "src/index.ts", "module": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",
"exports": { "exports": {
".": { "types": "./src/index.ts", "import": "./src/index.ts" }, ".": {
"types": "./src/index.ts",
"import": "./src/index.ts"
},
"./styles/encryption.css": "./src/styles/encryption.css" "./styles/encryption.css": "./src/styles/encryption.css"
}, },
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.14", "@govoplan/core-webui": "^0.1.16",
"lucide-react": "^1.23.0", "lucide-react": "^1.23.0",
"react": ">=19.2.7 <20", "react": ">=19.2.7 <20",
"react-dom": ">=19.2.7 <20" "react-dom": ">=19.2.7 <20"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@govoplan/core-webui": { "optional": true } "@govoplan/core-webui": {
"optional": true
}
}, },
"scripts": { "scripts": {
"test:encryption-ui": "node tests/encryption-ui-structure.test.mjs" "test:encryption-ui": "node tests/encryption-ui-structure.test.mjs"