Keep demo runtime profile changes out of signed billing state

The helper could repersist legacy entitlement secrets and clear billing integrity while granting demo fixtures. Require the existing demo capability before mutation, leave all billing/key material untouched, and keep verification and encrypted legacy migration with the canonical application. Preserve full-state recovery, service ownership and the existing native route; seed only non-secret admitted fresh-runner fixtures. Add real canonical encrypted/legacy/HMAC connected controls and malformed or unprovisioned refusal-before-mutation coverage.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-03 20:25:40 +01:00
parent 091f987798
commit 7d73969882
6 changed files with 296 additions and 16 deletions
+53 -14
View File
@@ -349,17 +349,61 @@ class Host:
values["PULSE_MOCK_" + key.upper()] = value
return values
def profile_matches(self, request):
text = (PATHS["data"] / ".env").read_text().splitlines()
values = self.profile_values(request)
return (all(text.count(key + "=" + value) == 1 for key, value in values.items())
and "demo_fixtures" in json.loads((PATHS["data"] / "billing.json").read_text()).get("capabilities", []))
def profile_admission(self):
"""Read-only prerequisite; this transaction never grants entitlements.
def profile(self, request, unhealthy):
The running application owns billing HMAC verification, encrypted
secrets and legacy migration. Do not decrypt, clear integrity, re-sign
or rewrite billing/key material to enable a demo profile.
"""
env_file = PATHS["data"] / ".env"
billing_file = PATHS["data"] / "billing.json"
if env_file.is_symlink() or billing_file.is_symlink():
raise Failure("configuration-symlink")
if not env_file.is_file():
raise Failure("configuration-shape")
def unique_fields(pairs):
result = {}
for key, value in pairs:
if key in result:
raise Failure("billing-shape")
result[key] = value
return result
def invalid_constant(_value):
raise Failure("billing-shape")
try:
fd = os.open(billing_file, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
with os.fdopen(fd, "rb") as stream:
if not stat.S_ISREG(os.fstat(stream.fileno()).st_mode):
raise Failure("billing-shape")
raw = stream.read(1024 * 1024 + 1)
if len(raw) > 1024 * 1024:
raise Failure("billing-shape")
billing = json.loads(raw, object_pairs_hook=unique_fields,
parse_constant=invalid_constant)
except (OSError, ValueError, UnicodeError, RecursionError) as exc:
raise Failure("billing-observation") from exc
if (not isinstance(billing, dict)
or not isinstance(billing.get("capabilities"), list)
or not all(isinstance(capability, str) for capability in billing["capabilities"])):
raise Failure("billing-shape")
if "demo_fixtures" not in billing["capabilities"]:
raise Failure("billing-demo-capability-required")
def profile_matches(self, request):
self.profile_admission()
text = (PATHS["data"] / ".env").read_text().splitlines()
values = self.profile_values(request)
return all(text.count(key + "=" + value) == 1 for key, value in values.items())
def profile(self, request, unhealthy):
env_file = PATHS["data"] / ".env"
# Recheck after quiescent capture too; a failed prerequisite must not
# partially change the profile or bypass the application's licence gate.
self.profile_admission()
values = self.profile_values(request)
text = env_file.read_text()
for key, value in values.items():
@@ -369,14 +413,6 @@ class Host:
data_owner = PATHS["data"].stat()
os.chown(env_file, data_owner.st_uid, data_owner.st_gid)
os.chmod(env_file, 0o600)
billing = json.loads(billing_file.read_text())
if not isinstance(billing, dict) or not isinstance(billing.get("capabilities", []), list):
raise Failure("billing-shape")
billing["capabilities"] = sorted(set(billing.get("capabilities", []) + ["demo_fixtures"]))
billing.pop("integrity", None)
billing_file.write_text(json.dumps(billing))
os.chown(billing_file, data_owner.st_uid, data_owner.st_gid)
os.chmod(billing_file, 0o600)
if unhealthy:
for relative in DEMO_HISTORY:
path = PATHS["data"] / relative
@@ -546,6 +582,9 @@ class Transaction:
if (prior.get("status") not in TERMINAL
or (prior.get("child_result_required") and not self.host.prior_closed(entry, prior["status"]))):
raise Failure("prior-recovery-required")
# Refuse absent/malformed or unprovisioned billing before stopping
# a service, installing a binary or changing any estate file.
self.host.profile_admission()
states = self.host.state()
if not all(active(states[name]) for name in ("pulse-relay", "caddy")) or self.host.status(RELAY_HEALTH) != "200":
raise Failure("cohost-unhealthy")
+1 -1
View File
@@ -92,7 +92,7 @@ def main():
Path("/etc/pulse").mkdir(mode=0o755)
os.chown("/etc/pulse", owner.pw_uid, owner.pw_gid)
Path("/etc/pulse/.env").write_text("DEMO_MODE=true\n")
Path("/etc/pulse/billing.json").write_text('{"capabilities":[]}')
Path("/etc/pulse/billing.json").write_text('{"capabilities":["demo_fixtures"],"limits":{},"meters_enabled":[],"plan_version":"community","subscription_state":"active"}')
Path("/etc/pulse/persistent-marker").write_text("original data")
for path in Path("/etc/pulse").iterdir():
os.chown(path, owner.pw_uid, owner.pw_gid)
@@ -156,7 +156,7 @@ class TransactionTest(unittest.TestCase):
path.chmod(0o755 if name == "binary" else 0o644)
(self.paths["dropins"] / "limits.conf").write_bytes(b"old limits")
(self.paths["data"] / ".env").write_text("PRIVATE_TOKEN=not-logged\n")
(self.paths["data"] / "billing.json").write_text('{"capabilities":[],"integrity":"private"}')
(self.paths["data"] / "billing.json").write_text('{"capabilities":["demo_fixtures"],"integrity":"private"}')
(self.paths["data"] / "persisted-history").write_bytes(b"original history")
(self.paths["data"] / "alerts").mkdir()
(self.paths["data"] / "alerts/events.db").write_bytes(b"synthetic opaque persistent file")
@@ -188,6 +188,105 @@ class TransactionTest(unittest.TestCase):
self.assertEqual(tx.receipt, json.loads((self.attempt / "receipt.json").read_text()))
return result, tx.receipt
def test_profile_preserves_encrypted_and_legacy_billing_bytes_key_and_metadata(self):
billing_path = self.paths["data"] / "billing.json"
key_path = self.paths["data"] / ".encryption.key"
key_path.write_bytes(b"synthetic-original-key")
key_path.chmod(0o600)
for representation in ("synthetic-legacy-plaintext", "synthetic-encrypted-ciphertext"):
with self.subTest(representation=representation):
billing = {"capabilities": ["other-feature", "demo_fixtures"],
"integrity": "original-synthetic-signature",
"entitlement_jwt": representation + "-jwt",
"entitlement_refresh_token": representation + "-refresh",
"limits": {"resources": 100}, "subscription_state": "active",
"other_field": {"preserve": [1, 2]}}
billing_path.write_text(json.dumps(billing, indent=2) + "\n")
billing_path.chmod(0o640)
before = engine.estate_hash({"billing": billing_path, "key": key_path})
self.host.profile(request(), False)
self.host.profile(request(), False)
self.assertEqual(engine.estate_hash({"billing": billing_path, "key": key_path}), before)
self.assertTrue(self.host.profile_matches(request()))
def test_missing_demo_capability_refuses_before_any_service_or_estate_mutation(self):
(self.paths["data"] / "billing.json").write_text('{"capabilities":[],"integrity":"unchanged"}')
before = engine.estate_hash(self.paths)
code, receipt = self.run_transaction("update", "v6.4.6")
self.assertEqual((code, receipt["status"], receipt["failure"]),
(1, "refused", "billing-demo-capability-required"))
self.assertFalse(receipt["mutated"])
self.assertEqual((self.host.stops, self.host.starts, self.host.install_count), (0, 0, 0))
self.assertEqual(engine.estate_hash(self.paths), before)
self.assertFalse((self.attempt / "snapshot").exists())
def test_malformed_billing_refuses_before_mutation_with_fixed_diagnostics(self):
path = self.paths["data"] / "billing.json"
cases = (b"private-malformed-input", b"[]", b"null", b"{}",
b'{"capabilities":"demo_fixtures"}',
b'{"capabilities":["demo_fixtures",{}]}',
b'{"capabilities":[],"capabilities":["demo_fixtures"]}',
b'{"capabilities":["demo_fixtures"],"other":NaN}',
b'{"capabilities":["demo_fixtures"],"other":"\xff"}',
b" " * (1024 * 1024 + 1))
for value in cases:
with self.subTest(length=len(value)):
path.write_bytes(value)
before = engine.estate_hash(self.paths)
code, receipt = self.run_transaction()
self.assertEqual((code, receipt["status"]), (1, "refused"))
self.assertIn(receipt["failure"], {"billing-shape", "billing-observation"})
self.assertEqual(self.host.stops, 0)
self.assertFalse(receipt["mutated"])
self.assertEqual(engine.estate_hash(self.paths), before)
self.assertNotIn("private-malformed-input", json.dumps(receipt))
def test_missing_symlink_and_nonregular_billing_refuse_without_touching_target(self):
path = self.paths["data"] / "billing.json"
target = self.paths["data"] / "other-billing"
target.write_text('{"capabilities":["demo_fixtures"]}')
path.unlink()
for kind in ("missing", "symlink", "directory"):
with self.subTest(kind=kind):
if kind == "symlink":
path.symlink_to(target)
elif kind == "directory":
path.mkdir()
before = engine.estate_hash(self.paths)
code, receipt = self.run_transaction()
self.assertEqual((code, receipt["status"]), (1, "refused"))
self.assertEqual(self.host.stops, 0)
self.assertEqual(engine.estate_hash(self.paths), before)
if kind == "symlink":
path.unlink()
elif kind == "directory":
path.rmdir()
def test_profile_rechecks_capability_before_changing_environment(self):
path = self.paths["data"] / "billing.json"
self.host.profile_admission()
path.write_text('{"capabilities":[]}')
before = engine.estate_hash(self.paths)
with self.assertRaisesRegex(engine.Failure, "^billing-demo-capability-required$"):
self.host.profile(request(), True)
self.assertEqual(engine.estate_hash(self.paths), before)
def test_late_update_failure_preserves_full_billing_and_key_through_recovery(self):
path = self.paths["data"] / "billing.json"
path.write_text(json.dumps({"capabilities": ["demo_fixtures"],
"integrity": "synthetic-original-signature",
"entitlement_jwt": "synthetic-original-secret",
"other": {"keep": True}}))
(self.paths["data"] / ".encryption.key").write_text("synthetic-original-key")
before = engine.estate_hash(self.paths)
self.host.fail_at = 55
code, receipt = self.run_transaction("update", "v6.4.6")
self.assertEqual((code, receipt["status"]), (1, "rolled_back"))
self.assertEqual(engine.estate_hash(self.paths), before)
self.assertEqual(receipt["recovery"]["elapsed_seconds"], 300)
for secret in ("synthetic-original-signature", "synthetic-original-secret", "synthetic-original-key"):
self.assertNotIn(secret, json.dumps(receipt))
def test_healthy_profile_change_keeps_data_and_watches_complete_window(self):
code, receipt = self.run_transaction()
self.assertEqual(code, 0)
@@ -18,6 +18,17 @@ workflow queue does not exclude another repository's host deployment.
file lock through capture, atomic runtime/profile work, forward observation,
complete restoration and recovery observation. It does not restart Relay or
Caddy, upgrade a package, lift the Caddy hold or activate frozen commercial copy.
- Billing and `.encryption.key` are read-only to the profile helper. Before the
first service stop or estate mutation, it requires regular, bounded,
unambiguous billing JSON with an already provisioned `demo_fixtures`
capability. Missing capability or unavailable/malformed state refuses the
transaction with fixed diagnostics; it never grants the capability, drops
integrity or rewrites secret-bearing JSON. It rechecks after quiescent capture
before changing the environment. The application still owns HMAC/entitlement
verification, encryption and legacy plaintext migration; this shape check
does not authenticate a licence. Full-state snapshots/recovery preserve the
original private billing/key estate. Native fixtures seed non-secret demo
capability state only while provisioning their fresh empty disposable estate.
- The complete quiescent **chosen footprint**: Pulse executable, VERSION,
unit/drop-in identity and demo data estate is retained
privately before mutation. No backup or database is deleted for space. The
@@ -69,6 +80,16 @@ unchanged-parent control retains the adverse skipped restoration. Collection
controls wait for writer closure and reject favourable stale terminals.
The production window is not shortened. This is **not native acceptance**.
Connected billing controls also pass actual canonical encrypted and legacy
secret state through the real profile helper, retain every billing/key byte and
metadata, then use the canonical loader to verify encryption/migration and HMAC.
A tampered signature remains rejected by that loader, not laundered by the
helper. Malformed, missing, duplicate-key, nonregular and unprovisioned billing
refuse before service/binary/data mutation. Late failures retain full-state
recovery and sanitised evidence. These are synthetic source tests; they do not
establish production capability provisioning, a green CodeQL check or native
forward/reverse acceptance.
`demo-runtime-native.yml` now carries the next executable acceptance rather
than requiring a worker to obtain system mode: exact PR/main source, a fresh
secret-free public hosted runner, actual systemd/Caddy and disposable trusted
@@ -34,6 +34,20 @@ browser fixture exercise those states; neither proves a binary/data rollback.
### Demo transactions share the service host, not just a workflow queue
The demo runtime profile does not grant or rewrite billing entitlements. Before
the first stop/install/profile mutation, the transaction requires regular,
bounded, unambiguous existing billing state containing `demo_fixtures`; missing
capability or malformed/unavailable state refuses without estate mutation. The
helper rechecks before profile writes and leaves all billing/key bytes,
ownership and modes unchanged, including encrypted or legacy plaintext secrets
and integrity. Only the canonical application verifies HMAC/entitlements and
performs encrypted-at-rest migration; the read-only shape check cannot make a
tampered signature valid. Recovery retains the full original private estate.
Synthetic native fixtures provision their non-secret capability before starting
the transaction on their admitted fresh empty runner, not by granting it during
recovery. Connected Python and `TestBillingState_DemoProfilePreservesCanonicalState`
controls cover these boundaries without asserting installed acceptance.
Stable demo update and recovery keep their existing publication/activation,
exact-tag installer signing key, Tailscale, pinned SSH and environment boundaries.
The mutating paths now share the Relay host's existing
@@ -0,0 +1,107 @@
package config
import (
"bytes"
"encoding/json"
"os"
"os/exec"
"path/filepath"
"testing"
pkglicensing "github.com/rcourtman/pulse-go-rewrite/pkg/licensing"
"github.com/stretchr/testify/require"
)
// Use the real billing encryption/HMAC implementation and the real Python
// profile writer. This is a synthetic, private filesystem test, not a native
// service, production entitlement or deployment acceptance.
func TestBillingState_DemoProfilePreservesCanonicalState(t *testing.T) {
for _, mode := range []string{"encrypted", "legacy-plaintext", "tampered-signature"} {
t.Run(mode, func(t *testing.T) {
dir := t.TempDir()
t.Setenv("PULSE_LEGACY_KEY_PATH", filepath.Join(t.TempDir(), ".encryption.key"))
writeTestEncryptionKey(t, dir)
require.NoError(t, os.WriteFile(filepath.Join(dir, ".env"), []byte("DEMO_MODE=true\nPRIVATE_SYNTHETIC=unchanged\n"), 0o600))
store := NewFileBillingStore(dir)
state := &pkglicensing.BillingState{
Capabilities: []string{pkglicensing.FeatureDemoFixtures},
SubscriptionState: pkglicensing.SubStateActive,
EntitlementJWT: "synthetic-lease.jwt.value",
EntitlementRefreshToken: "synthetic-refresh-token",
}
require.NoError(t, store.SaveBillingState("default", state))
billingPath := filepath.Join(dir, "billing.json")
if mode != "encrypted" {
data, err := os.ReadFile(billingPath)
require.NoError(t, err)
var raw pkglicensing.BillingState
require.NoError(t, json.Unmarshal(data, &raw))
if mode == "legacy-plaintext" {
// The same valid signature covers the unencrypted historical values.
raw.EntitlementJWT = state.EntitlementJWT
raw.EntitlementRefreshToken = state.EntitlementRefreshToken
} else {
raw.Integrity = "invalid-synthetic-signature"
}
data, err = json.Marshal(&raw)
require.NoError(t, err)
require.NoError(t, os.WriteFile(billingPath, data, 0o600))
}
beforeBilling, err := os.ReadFile(billingPath)
require.NoError(t, err)
keyPath := filepath.Join(dir, ".encryption.key")
beforeKey, err := os.ReadFile(keyPath)
require.NoError(t, err)
info, err := os.Stat(billingPath)
require.NoError(t, err)
helper, err := filepath.Abs(filepath.Join("..", "..", ".github", "scripts", "demo-runtime-transaction.py"))
require.NoError(t, err)
script := `import importlib.util,pathlib,sys
s=importlib.util.spec_from_file_location("demo",sys.argv[1]); m=importlib.util.module_from_spec(s); s.loader.exec_module(m)
m.PATHS={**m.PATHS,"data":pathlib.Path(sys.argv[2])}
request={"profile":{**{name:"2" for name in m.COUNT_KEYS},"seed_duration":"2h","sample_interval":"5m","update_interval":"15s"}}
m.Host().profile(request,False)
assert m.Host().profile_matches(request)
`
cmd := exec.Command("python3", "-c", script, helper, dir)
output, err := cmd.CombinedOutput()
require.NoError(t, err, "synthetic demo profile failed: %s", output)
require.Empty(t, output, "helper must not disclose any billing or key material")
afterBilling, err := os.ReadFile(billingPath)
require.NoError(t, err)
require.Equal(t, beforeBilling, afterBilling, "billing bytes, including all fields and integrity, must be untouched")
afterKey, err := os.ReadFile(keyPath)
require.NoError(t, err)
require.Equal(t, beforeKey, afterKey, "profile must not rotate or rewrite the key")
afterInfo, err := os.Stat(billingPath)
require.NoError(t, err)
require.Equal(t, info.Mode(), afterInfo.Mode())
require.Equal(t, info.ModTime(), afterInfo.ModTime())
// The canonical loader still owns both migration and tamper detection.
loaded, err := store.GetBillingState("default")
require.NoError(t, err)
if mode == "tampered-signature" {
require.Nil(t, loaded, "profile must not launder invalid signed billing into accepted state")
return
}
require.NotNil(t, loaded)
require.Equal(t, state.EntitlementJWT, loaded.EntitlementJWT)
require.Equal(t, state.EntitlementRefreshToken, loaded.EntitlementRefreshToken)
afterLoad, err := os.ReadFile(billingPath)
require.NoError(t, err)
require.False(t, bytes.Contains(afterLoad, []byte(state.EntitlementJWT)))
require.False(t, bytes.Contains(afterLoad, []byte(state.EntitlementRefreshToken)))
var persisted pkglicensing.BillingState
require.NoError(t, json.Unmarshal(afterLoad, &persisted))
manager, err := store.billingCryptoManager()
require.NoError(t, err)
persisted.EntitlementJWT, err = manager.DecryptString(persisted.EntitlementJWT)
require.NoError(t, err)
persisted.EntitlementRefreshToken, err = manager.DecryptString(persisted.EntitlementRefreshToken)
require.NoError(t, err)
hmacKey, err := store.loadHMACKey()
require.NoError(t, err)
require.True(t, verifyBillingIntegrity(&persisted, hmacKey), "preserved state/migration must retain a valid canonical HMAC")
})
}
}