Skip to content

Commit 79447a3

Browse files
authored
Map nlboot manifest and token fields into SourceOS Boot adapter
Adds nlboot manifest/token normalized views, mapping into BootReleaseSet patch-shaped data, evidence conversion, tests, and compatibility documentation.
1 parent a260c21 commit 79447a3

3 files changed

Lines changed: 272 additions & 6 deletions

File tree

docs/NLBOOT_COMPATIBILITY.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# nlboot Compatibility Mapping
2+
3+
`SociOS-Linux/nlboot` is no longer just a conceptual precursor. It already implements a safe planning core for SourceOS/SociOS boot and recovery.
4+
5+
## Upstream nlboot facts
6+
7+
The current nlboot reference implementation provides:
8+
9+
- `SignedBootManifest`
10+
- `EnrollmentToken`
11+
- `BootPlan`
12+
- `nlboot-plan` CLI
13+
- RSA-PSS/SHA-256 manifest verification
14+
- FIPS-compatible crypto profile marker
15+
- one-time enrollment token validation
16+
- side-effect-free planning with `execute=false`
17+
18+
## SourceOS Boot integration stance
19+
20+
`sourceos-boot` should not fork the protocol vocabulary unnecessarily.
21+
22+
Instead:
23+
24+
- nlboot remains the safe planner/reference protocol lane.
25+
- SourceOS Boot adapts nlboot manifest/token/plan concepts into BootReleaseSet v1 and Prophet Lattice evidence contracts.
26+
- BootReleaseSet remains the platform handoff object for Prophet Platform, SourceOS, and Lattice.
27+
28+
## Field mapping
29+
30+
| nlboot field | SourceOS BootReleaseSet / adapter field |
31+
|---|---|
32+
| `manifest_id` | `provenance.sourceRefs[]` / evidence manifest identity |
33+
| `boot_release_set_id` | `BootAuthorization.boot_release_set_ref` |
34+
| `base_release_set_ref` | `spec.releaseSetRef` |
35+
| `boot_mode` | `spec.channels[]` and boot evidence `bootMode` |
36+
| `artifacts.kernel_ref` | artifact role `kernel` |
37+
| `artifacts.initrd_ref` | artifact role `initrd` |
38+
| `artifacts.rootfs_ref` | artifact role `rootfs` |
39+
| `signature_ref` | `signature.bundleRef` |
40+
| `signer_ref` | `provenance.builderId` for current safe-planner bridge |
41+
| `signature_algorithm` | `signature.type` mapping and trust policy note |
42+
| `crypto_profile` | policy/trust evidence note |
43+
| `EnrollmentToken.token_id` | `BootAuthorization.token_id` |
44+
| `EnrollmentToken.expires_at` | `BootAuthorization.expires_at` |
45+
| `EnrollmentToken.boot_release_set_ref` | `BootAuthorization.boot_release_set_ref` |
46+
47+
## Current implementation
48+
49+
`src/sourceos_boot/adapter.py` includes:
50+
51+
- `NlbootManifestView`
52+
- `NlbootTokenView`
53+
- `authorization_from_nlboot_token`
54+
- `boot_release_set_patch_from_nlboot_manifest`
55+
- `build_evidence_from_nlboot_manifest`
56+
57+
These are pure, side-effect-free mappings suitable for CI and contract testing.
58+
59+
## Next implementation step
60+
61+
Wire the adapter to verified nlboot planner output:
62+
63+
1. Accept nlboot verified manifest document.
64+
2. Accept nlboot enrollment token document.
65+
3. Run nlboot verification/planning out-of-process or via library import.
66+
4. Convert resulting manifest/token/plan into BootReleaseSet evidence.
67+
5. Keep host mutation disabled until signed policy explicitly permits install/recovery actions.

src/sourceos_boot/adapter.py

Lines changed: 159 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
"""nlboot-compatible SourceOS boot adapter skeleton.
1+
"""nlboot-compatible SourceOS boot adapter.
22
3-
This module defines the first executable boundary between the original nlboot
4-
shape and SourceOS BootReleaseSet v1. It deliberately does not perform network
5-
or kexec actions yet; it normalizes request/response objects and produces an
6-
evidence record that the boot client and Prophet Platform can agree on.
3+
This module defines the executable boundary between the nlboot safe planner
4+
shape and SourceOS BootReleaseSet v1. It deliberately avoids network, disk, and
5+
kexec side effects. It maps nlboot manifest/token/plan-shaped dictionaries into
6+
SourceOS control-plane payloads and evidence envelopes.
77
"""
88

99
from __future__ import annotations
@@ -13,6 +13,21 @@
1313
from typing import Any
1414

1515

16+
BOOT_MODE_TO_CHANNEL = {
17+
"installer": "installer",
18+
"recovery": "recovery",
19+
"ephemeral": "live",
20+
"bootstrap": "live",
21+
}
22+
23+
BOOT_MODE_TO_ACTION = {
24+
"installer": "install",
25+
"recovery": "repair",
26+
"ephemeral": "kexec",
27+
"bootstrap": "enroll",
28+
}
29+
30+
1631
@dataclass(frozen=True)
1732
class DeviceClaim:
1833
"""Minimal self-registration claim emitted by a boot environment."""
@@ -75,8 +90,81 @@ def to_dict(self) -> dict[str, Any]:
7590
}
7691

7792

93+
@dataclass(frozen=True)
94+
class NlbootManifestView:
95+
"""Normalized subset of nlboot SignedBootManifest fields."""
96+
97+
manifest_id: str
98+
boot_release_set_id: str
99+
base_release_set_ref: str
100+
boot_mode: str
101+
artifacts: dict[str, str]
102+
signature_ref: str
103+
signer_ref: str
104+
signature_algorithm: str
105+
crypto_profile: str
106+
107+
@classmethod
108+
def from_dict(cls, data: dict[str, Any]) -> "NlbootManifestView":
109+
return cls(
110+
manifest_id=_required_str(data, "manifest_id"),
111+
boot_release_set_id=_required_str(data, "boot_release_set_id"),
112+
base_release_set_ref=_required_str(data, "base_release_set_ref"),
113+
boot_mode=_required_str(data, "boot_mode"),
114+
artifacts=_required_dict_of_str(data, "artifacts"),
115+
signature_ref=_required_str(data, "signature_ref"),
116+
signer_ref=_required_str(data, "signer_ref"),
117+
signature_algorithm=_required_str(data, "signature_algorithm"),
118+
crypto_profile=_required_str(data, "crypto_profile"),
119+
)
120+
121+
122+
@dataclass(frozen=True)
123+
class NlbootTokenView:
124+
"""Normalized subset of nlboot EnrollmentToken fields."""
125+
126+
token_id: str
127+
purpose: str
128+
expires_at: str
129+
release_set_ref: str | None
130+
boot_release_set_ref: str | None
131+
132+
@classmethod
133+
def from_dict(cls, data: dict[str, Any]) -> "NlbootTokenView":
134+
return cls(
135+
token_id=_required_str(data, "token_id"),
136+
purpose=_required_str(data, "purpose"),
137+
expires_at=_required_str(data, "expires_at"),
138+
release_set_ref=_optional_str(data, "release_set_ref"),
139+
boot_release_set_ref=_optional_str(data, "boot_release_set_ref"),
140+
)
141+
142+
143+
def _required_str(data: dict[str, Any], key: str) -> str:
144+
value = data.get(key)
145+
if not isinstance(value, str) or not value:
146+
raise ValueError(f"{key} must be a non-empty string")
147+
return value
148+
149+
150+
def _optional_str(data: dict[str, Any], key: str) -> str | None:
151+
value = data.get(key)
152+
if value is None:
153+
return None
154+
if not isinstance(value, str):
155+
raise ValueError(f"{key} must be a string or null")
156+
return value
157+
158+
159+
def _required_dict_of_str(data: dict[str, Any], key: str) -> dict[str, str]:
160+
value = data.get(key)
161+
if not isinstance(value, dict):
162+
raise ValueError(f"{key} must be an object")
163+
return {str(k): _required_str(value, str(k)) for k in value}
164+
165+
78166
class SourceOSBootAdapter:
79-
"""Pure adapter for the nlboot-like control-plane handshake.
167+
"""Pure adapter for nlboot-like control-plane handshakes.
80168
81169
The runtime flow this class models is:
82170
@@ -86,13 +174,52 @@ class SourceOSBootAdapter:
86174
def build_announce_payload(self, claim: DeviceClaim) -> dict[str, Any]:
87175
return {"kind": "SourceOSBootAnnounce", "apiVersion": "sourceos.dev/v1", "claim": claim.to_dict()}
88176

177+
def authorization_from_nlboot_token(self, token_doc: dict[str, Any], *, correlation_id: str) -> BootAuthorization:
178+
token = NlbootTokenView.from_dict(token_doc)
179+
if token.boot_release_set_ref is None:
180+
raise ValueError("nlboot token must include boot_release_set_ref")
181+
return BootAuthorization(
182+
correlation_id=correlation_id,
183+
boot_release_set_ref=token.boot_release_set_ref,
184+
token_id=token.token_id,
185+
expires_at=token.expires_at,
186+
)
187+
89188
def build_fetch_request(self, authorization: BootAuthorization) -> dict[str, Any]:
90189
return {
91190
"kind": "SourceOSBootFetchRequest",
92191
"apiVersion": "sourceos.dev/v1",
93192
"authorization": authorization.to_dict(),
94193
}
95194

195+
def boot_release_set_patch_from_nlboot_manifest(self, manifest_doc: dict[str, Any]) -> dict[str, Any]:
196+
manifest = NlbootManifestView.from_dict(manifest_doc)
197+
selected_channel = BOOT_MODE_TO_CHANNEL.get(manifest.boot_mode)
198+
if selected_channel is None:
199+
raise ValueError(f"unsupported nlboot boot_mode={manifest.boot_mode!r}")
200+
return {
201+
"releaseSetRef": manifest.base_release_set_ref,
202+
"channels": [selected_channel],
203+
"artifacts": [
204+
{"name": "kernel", "role": "kernel", "uri": manifest.artifacts["kernel_ref"], "sha256": _unknown_sha256()},
205+
{"name": "initrd", "role": "initrd", "uri": manifest.artifacts["initrd_ref"], "sha256": _unknown_sha256()},
206+
{"name": "rootfs", "role": "rootfs", "uri": manifest.artifacts["rootfs_ref"], "sha256": _unknown_sha256()},
207+
],
208+
"signature": {
209+
"type": "x509" if manifest.signature_algorithm == "rsa-pss-sha256" else "other",
210+
"bundleRef": manifest.signature_ref,
211+
"digest": "sha256:" + _unknown_sha256(),
212+
},
213+
"provenance": {
214+
"builderId": manifest.signer_ref,
215+
"sourceRefs": [manifest.manifest_id],
216+
"attestations": ["slsa", "in-toto"],
217+
},
218+
"policy": {
219+
"allowedActions": ["announce", "enroll", "fetch", "verify", BOOT_MODE_TO_ACTION[manifest.boot_mode], "attest"]
220+
},
221+
}
222+
96223
def build_evidence(
97224
self,
98225
*,
@@ -119,3 +246,29 @@ def build_evidence(
119246
verification_result=verification_result,
120247
reports=reports,
121248
)
249+
250+
def build_evidence_from_nlboot_manifest(
251+
self,
252+
*,
253+
claim: DeviceClaim,
254+
authorization: BootAuthorization,
255+
manifest_doc: dict[str, Any],
256+
manifest_hash: str,
257+
verification_result: str,
258+
) -> BootEvidence:
259+
manifest = NlbootManifestView.from_dict(manifest_doc)
260+
channel = BOOT_MODE_TO_CHANNEL.get(manifest.boot_mode)
261+
if channel is None:
262+
raise ValueError(f"unsupported nlboot boot_mode={manifest.boot_mode!r}")
263+
return self.build_evidence(
264+
claim=claim,
265+
authorization=authorization,
266+
selected_channel=channel,
267+
boot_mode=manifest.boot_mode,
268+
manifest_hash=manifest_hash,
269+
verification_result=verification_result,
270+
)
271+
272+
273+
def _unknown_sha256() -> str:
274+
return "0" * 64

tests/test_adapter.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,49 @@ def test_adapter_builds_announce_fetch_and_evidence() -> None:
3737
"selected-channel",
3838
"boot-mode",
3939
]
40+
41+
42+
def test_adapter_maps_nlboot_manifest_and_token() -> None:
43+
adapter = SourceOSBootAdapter()
44+
token_doc = {
45+
"token_id": "token-1",
46+
"purpose": "recovery",
47+
"expires_at": "2026-04-26T01:00:00Z",
48+
"release_set_ref": "release/demo",
49+
"boot_release_set_ref": "boot/demo",
50+
}
51+
manifest_doc = {
52+
"manifest_id": "manifest-1",
53+
"boot_release_set_id": "boot/demo",
54+
"base_release_set_ref": "release/demo",
55+
"boot_mode": "recovery",
56+
"artifacts": {
57+
"kernel_ref": "https://example.invalid/kernel",
58+
"initrd_ref": "https://example.invalid/initrd",
59+
"rootfs_ref": "https://example.invalid/rootfs",
60+
},
61+
"signature_ref": "urn:srcos:signature:demo",
62+
"signer_ref": "trusted-key-1",
63+
"signature_algorithm": "rsa-pss-sha256",
64+
"crypto_profile": "fips-140-3-compatible",
65+
}
66+
67+
authorization = adapter.authorization_from_nlboot_token(token_doc, correlation_id="corr-2")
68+
patch = adapter.boot_release_set_patch_from_nlboot_manifest(manifest_doc)
69+
evidence = adapter.build_evidence_from_nlboot_manifest(
70+
claim=DeviceClaim("device-1", "sha256:demo", "apple-silicon", "nonce"),
71+
authorization=authorization,
72+
manifest_doc=manifest_doc,
73+
manifest_hash="sha256:manifest",
74+
verification_result="pass",
75+
)
76+
77+
assert authorization.boot_release_set_ref == "boot/demo"
78+
assert patch["releaseSetRef"] == "release/demo"
79+
assert patch["channels"] == ["recovery"]
80+
assert patch["artifacts"][0]["role"] == "kernel"
81+
assert patch["signature"]["bundleRef"] == "urn:srcos:signature:demo"
82+
assert patch["provenance"]["builderId"] == "trusted-key-1"
83+
assert "repair" in patch["policy"]["allowedActions"]
84+
assert evidence.selected_channel == "recovery"
85+
assert evidence.boot_mode == "recovery"

0 commit comments

Comments
 (0)