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
8 changes: 4 additions & 4 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ jobs:
run: python -m pip install -e . pytest
- name: Run validation
run: make validate
- name: Validate M2 demo fixture through CLI
- name: Validate M2 recovery fixture through CLI
run: |
nlboot-plan \
--manifest examples/m2-demo/manifest.recovery.json \
--token examples/m2-demo/enrollment-token.recovery.json \
--trusted-keys examples/m2-demo/trusted-keys.json \
--manifest examples/signed_boot_manifest.recovery.json \
--token examples/enrollment_token.recovery.json \
--trusted-keys examples/trusted_keys.recovery.json \
--require-fips \
--now 2026-04-26T14:35:00Z
33 changes: 26 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ This repository implements the safe planning core for the SourceOS / SociOS boot
- validates one-time enrollment token intent, expiry, audience, and release/boot-release binding
- produces a boot plan as JSON
- records `execute=false` in produced plans
- emits SourceOS control-plane metadata in boot plans:
- `policy_ref`
- `allowed_operations`
- `proof_requirements`
- `offline_fallback`
- never downloads artifacts, writes disks, calls `kexec`, or mutates a host in this reference slice

## Protocol objects
Expand Down Expand Up @@ -39,22 +44,36 @@ This repository implements the safe planning core for the SourceOS / SociOS boot
- matching `boot_release_set_ref`
- purpose compatible with the boot mode

`BootPlan` is emitted only after manifest verification and token validation. It includes:

- the selected plan action
- boot and release-set references
- artifact references
- signature and crypto profile metadata
- policy reference for the boot mode
- safe planning operations allowed by that boot mode
- proof requirements the eventual executor must satisfy
- offline fallback posture
- `execute=false`

The planner is intentionally conservative. It creates an authorized plan record, not a host-mutating execution path.

## M2 demo fixture

The repository carries a side-effect-free M2 recovery fixture under `examples/m2-demo/`:
The repository carries a side-effect-free M2 recovery fixture under `examples/`:

- `manifest.recovery.json`
- `enrollment-token.recovery.json`
- `trusted-keys.json`
- `signed_boot_manifest.recovery.json`
- `enrollment_token.recovery.json`
- `trusted_keys.recovery.json`

Run it through the planner:

```bash
python3 -m pip install -e .
nlboot-plan \
--manifest examples/m2-demo/manifest.recovery.json \
--token examples/m2-demo/enrollment-token.recovery.json \
--trusted-keys examples/m2-demo/trusted-keys.json \
--manifest examples/signed_boot_manifest.recovery.json \
--token examples/enrollment_token.recovery.json \
--trusted-keys examples/trusted_keys.recovery.json \
--require-fips \
--now 2026-04-26T14:35:00Z
```
Expand Down
47 changes: 47 additions & 0 deletions src/nlboot/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ class BootPlan:
authorized_by: str
signature_algorithm: str
crypto_profile: str
policy_ref: str
allowed_operations: list[str]
proof_requirements: list[str]
offline_fallback: dict[str, Any]
execute: bool = False

def to_dict(self) -> dict[str, Any]:
Expand All @@ -185,10 +189,49 @@ def to_dict(self) -> dict[str, Any]:
"authorized_by": self.authorized_by,
"signature_algorithm": self.signature_algorithm,
"crypto_profile": self.crypto_profile,
"policy_ref": self.policy_ref,
"allowed_operations": self.allowed_operations,
"proof_requirements": self.proof_requirements,
"offline_fallback": self.offline_fallback,
"execute": self.execute,
}


def _policy_ref_for_mode(mode: BootMode) -> str:
return f"policy://sourceos/nlboot/{mode}/safe-plan-v1"


def _allowed_operations_for_mode(mode: BootMode) -> list[str]:
operations_by_mode: dict[BootMode, list[str]] = {
"recovery": ["present-menu", "verify-artifacts", "plan-recovery", "plan-rollback"],
"installer": ["present-menu", "verify-artifacts", "plan-install"],
"ephemeral": ["present-menu", "verify-artifacts", "plan-ephemeral-boot"],
"bootstrap": ["present-menu", "verify-artifacts", "plan-bootstrap"],
}
return operations_by_mode[mode]


def _proof_requirements_for_mode(mode: BootMode) -> list[str]:
common = [
"verified_manifest_signature",
"validated_one_time_token",
"artifact_ref_manifest",
"boot_plan_record",
]
if mode in {"recovery", "installer"}:
return [*common, "device_claim_record", "post_action_fingerprint"]
return [*common, "session_fingerprint"]


def _offline_fallback_for_mode(mode: BootMode) -> dict[str, Any]:
return {
"enabled": mode in {"recovery", "ephemeral"},
"strategy": "last-known-good-signed-boot-release-set" if mode in {"recovery", "ephemeral"} else "none",
"requires_signature_verification": True,
"allows_unsigned_artifacts": False,
}


def build_boot_plan(manifest: SignedBootManifest, token: EnrollmentToken, *, now: datetime | None = None) -> BootPlan:
token.validate_for_manifest(manifest, now=now)
action_by_mode: dict[BootMode, PlanAction] = {
Expand All @@ -206,5 +249,9 @@ def build_boot_plan(manifest: SignedBootManifest, token: EnrollmentToken, *, now
authorized_by=token.token_id,
signature_algorithm=manifest.signature_algorithm,
crypto_profile=manifest.crypto_profile,
policy_ref=_policy_ref_for_mode(manifest.boot_mode),
allowed_operations=_allowed_operations_for_mode(manifest.boot_mode),
proof_requirements=_proof_requirements_for_mode(manifest.boot_mode),
offline_fallback=_offline_fallback_for_mode(manifest.boot_mode),
execute=False,
)
21 changes: 21 additions & 0 deletions tests/test_cli_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,24 @@ def test_cli_examples_emit_safe_recovery_plan():
assert plan["release_set_ref"] == "urn:srcos:release-set:m2-demo-2026-04-26"
assert plan["signature_algorithm"] == "rsa-pss-sha256"
assert plan["crypto_profile"] == "fips-140-3-compatible"
assert plan["policy_ref"] == "policy://sourceos/nlboot/recovery/safe-plan-v1"
assert plan["allowed_operations"] == [
"present-menu",
"verify-artifacts",
"plan-recovery",
"plan-rollback",
]
assert plan["proof_requirements"] == [
"verified_manifest_signature",
"validated_one_time_token",
"artifact_ref_manifest",
"boot_plan_record",
"device_claim_record",
"post_action_fingerprint",
]
assert plan["offline_fallback"] == {
"enabled": True,
"strategy": "last-known-good-signed-boot-release-set",
"requires_signature_verification": True,
"allows_unsigned_artifacts": False,
}
8 changes: 6 additions & 2 deletions tests/test_verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,14 @@ def test_signed_trust_bundle_rejects_non_fips_algorithm():
load_verified_trust_bundle(trust_bundle(signature_algorithm="ed25519"), root_keys=root_keys, now=now())


def test_signed_trust_bundle_rejects_revoked_root():
def test_signed_trust_bundle_rejects_revoked_root(monkeypatch: pytest.MonkeyPatch):
def fake_verify(*, payload: bytes, signature_hex: str, trusted_key: object) -> None:
assert payload == verify.canonical_trust_bundle_payload(bundle)
assert signature_hex == "00"

root_keys = load_trusted_keys(key_doc(), now=now())
revoked_root = key_doc(status="revoked", revoked_at="2026-04-20T00:00:00Z")
root_keys["urn:srcos:key:sourceos-release-root"] = load_trusted_keys(key_doc(), now=now())["urn:srcos:key:sourceos-release-root"]
bundle = trust_bundle(keys=revoked_root["keys"])
monkeypatch.setattr(verify, "verify_rsa_pss_sha256", fake_verify)
with pytest.raises(VerificationError, match="revoked"):
load_verified_trust_bundle(bundle, root_keys=root_keys, now=now())
Loading