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
67 changes: 67 additions & 0 deletions docs/NLBOOT_COMPATIBILITY.md
Original file line number Diff line number Diff line change
@@ -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.
165 changes: 159 additions & 6 deletions src/sourceos_boot/adapter.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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."""
Expand Down Expand Up @@ -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:

Expand All @@ -86,13 +174,52 @@ 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",
"apiVersion": "sourceos.dev/v1",
"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,
*,
Expand All @@ -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
46 changes: 46 additions & 0 deletions tests/test_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Loading