From d979e7ff8fe40cc1e02ae403585332a5d1f1a33e Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Sun, 26 Apr 2026 17:26:21 -0400 Subject: [PATCH 1/3] Map nlboot manifest and token fields into SourceOS adapter --- src/sourceos_boot/adapter.py | 165 +++++++++++++++++++++++++++++++++-- 1 file changed, 159 insertions(+), 6 deletions(-) diff --git a/src/sourceos_boot/adapter.py b/src/sourceos_boot/adapter.py index 37cddcb..d1c7b3c 100644 --- a/src/sourceos_boot/adapter.py +++ b/src/sourceos_boot/adapter.py @@ -1,9 +1,9 @@ -"""nlboot-compatible SourceOS boot adapter skeleton. +"""nlboot-compatible SourceOS boot adapter. -This module defines the first executable boundary between the original nlboot -shape and SourceOS BootReleaseSet v1. It deliberately does not perform network -or kexec actions yet; it normalizes request/response objects and produces an -evidence record that the boot client and Prophet Platform can agree on. +This module defines the executable boundary between the nlboot safe planner +shape and SourceOS BootReleaseSet v1. It deliberately avoids network, disk, and +kexec side effects. It maps nlboot manifest/token/plan-shaped dictionaries into +SourceOS control-plane payloads and evidence envelopes. """ from __future__ import annotations @@ -13,6 +13,21 @@ from typing import Any +BOOT_MODE_TO_CHANNEL = { + "installer": "installer", + "recovery": "recovery", + "ephemeral": "live", + "bootstrap": "live", +} + +BOOT_MODE_TO_ACTION = { + "installer": "install", + "recovery": "repair", + "ephemeral": "kexec", + "bootstrap": "enroll", +} + + @dataclass(frozen=True) class DeviceClaim: """Minimal self-registration claim emitted by a boot environment.""" @@ -75,8 +90,81 @@ def to_dict(self) -> dict[str, Any]: } +@dataclass(frozen=True) +class NlbootManifestView: + """Normalized subset of nlboot SignedBootManifest fields.""" + + manifest_id: str + boot_release_set_id: str + base_release_set_ref: str + boot_mode: str + artifacts: dict[str, str] + signature_ref: str + signer_ref: str + signature_algorithm: str + crypto_profile: str + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "NlbootManifestView": + return cls( + manifest_id=_required_str(data, "manifest_id"), + boot_release_set_id=_required_str(data, "boot_release_set_id"), + base_release_set_ref=_required_str(data, "base_release_set_ref"), + boot_mode=_required_str(data, "boot_mode"), + artifacts=_required_dict_of_str(data, "artifacts"), + signature_ref=_required_str(data, "signature_ref"), + signer_ref=_required_str(data, "signer_ref"), + signature_algorithm=_required_str(data, "signature_algorithm"), + crypto_profile=_required_str(data, "crypto_profile"), + ) + + +@dataclass(frozen=True) +class NlbootTokenView: + """Normalized subset of nlboot EnrollmentToken fields.""" + + token_id: str + purpose: str + expires_at: str + release_set_ref: str | None + boot_release_set_ref: str | None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "NlbootTokenView": + return cls( + token_id=_required_str(data, "token_id"), + purpose=_required_str(data, "purpose"), + expires_at=_required_str(data, "expires_at"), + release_set_ref=_optional_str(data, "release_set_ref"), + boot_release_set_ref=_optional_str(data, "boot_release_set_ref"), + ) + + +def _required_str(data: dict[str, Any], key: str) -> str: + value = data.get(key) + if not isinstance(value, str) or not value: + raise ValueError(f"{key} must be a non-empty string") + return value + + +def _optional_str(data: dict[str, Any], key: str) -> str | None: + value = data.get(key) + if value is None: + return None + if not isinstance(value, str): + raise ValueError(f"{key} must be a string or null") + return value + + +def _required_dict_of_str(data: dict[str, Any], key: str) -> dict[str, str]: + value = data.get(key) + if not isinstance(value, dict): + raise ValueError(f"{key} must be an object") + return {str(k): _required_str(value, str(k)) for k in value} + + class SourceOSBootAdapter: - """Pure adapter for the nlboot-like control-plane handshake. + """Pure adapter for nlboot-like control-plane handshakes. The runtime flow this class models is: @@ -86,6 +174,17 @@ class SourceOSBootAdapter: def build_announce_payload(self, claim: DeviceClaim) -> dict[str, Any]: return {"kind": "SourceOSBootAnnounce", "apiVersion": "sourceos.dev/v1", "claim": claim.to_dict()} + def authorization_from_nlboot_token(self, token_doc: dict[str, Any], *, correlation_id: str) -> BootAuthorization: + token = NlbootTokenView.from_dict(token_doc) + if token.boot_release_set_ref is None: + raise ValueError("nlboot token must include boot_release_set_ref") + return BootAuthorization( + correlation_id=correlation_id, + boot_release_set_ref=token.boot_release_set_ref, + token_id=token.token_id, + expires_at=token.expires_at, + ) + def build_fetch_request(self, authorization: BootAuthorization) -> dict[str, Any]: return { "kind": "SourceOSBootFetchRequest", @@ -93,6 +192,34 @@ def build_fetch_request(self, authorization: BootAuthorization) -> dict[str, Any "authorization": authorization.to_dict(), } + def boot_release_set_patch_from_nlboot_manifest(self, manifest_doc: dict[str, Any]) -> dict[str, Any]: + manifest = NlbootManifestView.from_dict(manifest_doc) + selected_channel = BOOT_MODE_TO_CHANNEL.get(manifest.boot_mode) + if selected_channel is None: + raise ValueError(f"unsupported nlboot boot_mode={manifest.boot_mode!r}") + return { + "releaseSetRef": manifest.base_release_set_ref, + "channels": [selected_channel], + "artifacts": [ + {"name": "kernel", "role": "kernel", "uri": manifest.artifacts["kernel_ref"], "sha256": _unknown_sha256()}, + {"name": "initrd", "role": "initrd", "uri": manifest.artifacts["initrd_ref"], "sha256": _unknown_sha256()}, + {"name": "rootfs", "role": "rootfs", "uri": manifest.artifacts["rootfs_ref"], "sha256": _unknown_sha256()}, + ], + "signature": { + "type": "x509" if manifest.signature_algorithm == "rsa-pss-sha256" else "other", + "bundleRef": manifest.signature_ref, + "digest": "sha256:" + _unknown_sha256(), + }, + "provenance": { + "builderId": manifest.signer_ref, + "sourceRefs": [manifest.manifest_id], + "attestations": ["slsa", "in-toto"], + }, + "policy": { + "allowedActions": ["announce", "enroll", "fetch", "verify", BOOT_MODE_TO_ACTION[manifest.boot_mode], "attest"] + }, + } + def build_evidence( self, *, @@ -119,3 +246,29 @@ def build_evidence( verification_result=verification_result, reports=reports, ) + + def build_evidence_from_nlboot_manifest( + self, + *, + claim: DeviceClaim, + authorization: BootAuthorization, + manifest_doc: dict[str, Any], + manifest_hash: str, + verification_result: str, + ) -> BootEvidence: + manifest = NlbootManifestView.from_dict(manifest_doc) + channel = BOOT_MODE_TO_CHANNEL.get(manifest.boot_mode) + if channel is None: + raise ValueError(f"unsupported nlboot boot_mode={manifest.boot_mode!r}") + return self.build_evidence( + claim=claim, + authorization=authorization, + selected_channel=channel, + boot_mode=manifest.boot_mode, + manifest_hash=manifest_hash, + verification_result=verification_result, + ) + + +def _unknown_sha256() -> str: + return "0" * 64 From edce89ef0b897014c5741bcae4587ee37d46e233 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Sun, 26 Apr 2026 17:26:48 -0400 Subject: [PATCH 2/3] Cover nlboot manifest and token mapping --- tests/test_adapter.py | 46 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/test_adapter.py b/tests/test_adapter.py index 479541a..e19e3c9 100644 --- a/tests/test_adapter.py +++ b/tests/test_adapter.py @@ -37,3 +37,49 @@ def test_adapter_builds_announce_fetch_and_evidence() -> None: "selected-channel", "boot-mode", ] + + +def test_adapter_maps_nlboot_manifest_and_token() -> None: + adapter = SourceOSBootAdapter() + token_doc = { + "token_id": "token-1", + "purpose": "recovery", + "expires_at": "2026-04-26T01:00:00Z", + "release_set_ref": "release/demo", + "boot_release_set_ref": "boot/demo", + } + manifest_doc = { + "manifest_id": "manifest-1", + "boot_release_set_id": "boot/demo", + "base_release_set_ref": "release/demo", + "boot_mode": "recovery", + "artifacts": { + "kernel_ref": "https://example.invalid/kernel", + "initrd_ref": "https://example.invalid/initrd", + "rootfs_ref": "https://example.invalid/rootfs", + }, + "signature_ref": "urn:srcos:signature:demo", + "signer_ref": "trusted-key-1", + "signature_algorithm": "rsa-pss-sha256", + "crypto_profile": "fips-140-3-compatible", + } + + authorization = adapter.authorization_from_nlboot_token(token_doc, correlation_id="corr-2") + patch = adapter.boot_release_set_patch_from_nlboot_manifest(manifest_doc) + evidence = adapter.build_evidence_from_nlboot_manifest( + claim=DeviceClaim("device-1", "sha256:demo", "apple-silicon", "nonce"), + authorization=authorization, + manifest_doc=manifest_doc, + manifest_hash="sha256:manifest", + verification_result="pass", + ) + + assert authorization.boot_release_set_ref == "boot/demo" + assert patch["releaseSetRef"] == "release/demo" + assert patch["channels"] == ["recovery"] + assert patch["artifacts"][0]["role"] == "kernel" + assert patch["signature"]["bundleRef"] == "urn:srcos:signature:demo" + assert patch["provenance"]["builderId"] == "trusted-key-1" + assert "repair" in patch["policy"]["allowedActions"] + assert evidence.selected_channel == "recovery" + assert evidence.boot_mode == "recovery" From c021389d8a04f4abefc7725d31c70b1be9179af3 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Sun, 26 Apr 2026 17:31:57 -0400 Subject: [PATCH 3/3] Document nlboot compatibility mapping --- docs/NLBOOT_COMPATIBILITY.md | 67 ++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 docs/NLBOOT_COMPATIBILITY.md diff --git a/docs/NLBOOT_COMPATIBILITY.md b/docs/NLBOOT_COMPATIBILITY.md new file mode 100644 index 0000000..d30c0cf --- /dev/null +++ b/docs/NLBOOT_COMPATIBILITY.md @@ -0,0 +1,67 @@ +# nlboot Compatibility Mapping + +`SociOS-Linux/nlboot` is no longer just a conceptual precursor. It already implements a safe planning core for SourceOS/SociOS boot and recovery. + +## Upstream nlboot facts + +The current nlboot reference implementation provides: + +- `SignedBootManifest` +- `EnrollmentToken` +- `BootPlan` +- `nlboot-plan` CLI +- RSA-PSS/SHA-256 manifest verification +- FIPS-compatible crypto profile marker +- one-time enrollment token validation +- side-effect-free planning with `execute=false` + +## SourceOS Boot integration stance + +`sourceos-boot` should not fork the protocol vocabulary unnecessarily. + +Instead: + +- nlboot remains the safe planner/reference protocol lane. +- SourceOS Boot adapts nlboot manifest/token/plan concepts into BootReleaseSet v1 and Prophet Lattice evidence contracts. +- BootReleaseSet remains the platform handoff object for Prophet Platform, SourceOS, and Lattice. + +## Field mapping + +| nlboot field | SourceOS BootReleaseSet / adapter field | +|---|---| +| `manifest_id` | `provenance.sourceRefs[]` / evidence manifest identity | +| `boot_release_set_id` | `BootAuthorization.boot_release_set_ref` | +| `base_release_set_ref` | `spec.releaseSetRef` | +| `boot_mode` | `spec.channels[]` and boot evidence `bootMode` | +| `artifacts.kernel_ref` | artifact role `kernel` | +| `artifacts.initrd_ref` | artifact role `initrd` | +| `artifacts.rootfs_ref` | artifact role `rootfs` | +| `signature_ref` | `signature.bundleRef` | +| `signer_ref` | `provenance.builderId` for current safe-planner bridge | +| `signature_algorithm` | `signature.type` mapping and trust policy note | +| `crypto_profile` | policy/trust evidence note | +| `EnrollmentToken.token_id` | `BootAuthorization.token_id` | +| `EnrollmentToken.expires_at` | `BootAuthorization.expires_at` | +| `EnrollmentToken.boot_release_set_ref` | `BootAuthorization.boot_release_set_ref` | + +## Current implementation + +`src/sourceos_boot/adapter.py` includes: + +- `NlbootManifestView` +- `NlbootTokenView` +- `authorization_from_nlboot_token` +- `boot_release_set_patch_from_nlboot_manifest` +- `build_evidence_from_nlboot_manifest` + +These are pure, side-effect-free mappings suitable for CI and contract testing. + +## Next implementation step + +Wire the adapter to verified nlboot planner output: + +1. Accept nlboot verified manifest document. +2. Accept nlboot enrollment token document. +3. Run nlboot verification/planning out-of-process or via library import. +4. Convert resulting manifest/token/plan into BootReleaseSet evidence. +5. Keep host mutation disabled until signed policy explicitly permits install/recovery actions.