Skip to content

Commit 196eb89

Browse files
committed
feat: support encrypted REC delivery artifacts
1 parent 3c9a4a9 commit 196eb89

12 files changed

Lines changed: 300 additions & 66 deletions

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ Every compliant module produced here should satisfy all of the following:
7979
- Declared capability IDs come from this repo's vocabulary.
8080
- Single-file delivery appends one `REC` trailer carrying `MBL` and any
8181
publication metadata after the wasm payload.
82+
- Same-file signing or encrypted binary delivery appends an SDS `REC` trailer
83+
carrying standards-backed `PNM` and optional `ENC` publication records.
8284
- Sync guest hostcalls use the `sdn_host` import module and the bridge in
8385
`src/host` for sync-safe operations only.
8486
- The raw `sdn_host` bridge remains fail-closed and sync-only.

README.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -410,11 +410,22 @@ for resolved protocol installations and producer input bindings.
410410
## Publication Protection Extensions
411411

412412
Digital signature and encrypted-delivery metadata are publication-layer
413-
extensions, not a second module format. The canonical module is still:
413+
extensions. The canonical runtime module is still:
414414

415415
- valid `.wasm`
416416
- optionally carrying one appended SDS `REC` trailer after the wasm bytes
417417

418+
The same-file protected delivery layout is:
419+
420+
```text
421+
protected-payload-bytes || REC-flatbuffer-bytes || uint32le(REC length) || "$REC"
422+
```
423+
424+
For signed-only delivery, the protected payload bytes are the wasm bytes. For
425+
encrypted binary delivery, the protected payload bytes are ciphertext and the
426+
appended `REC` trailer carries `ENC` so a loader can decrypt them back to the
427+
canonical wasm before runtime startup.
428+
418429
That trailing `REC` FlatBuffer is the standards-backed container for
419430
publication records:
420431

@@ -435,7 +446,7 @@ The loader contract is always:
435446
FlatBuffers from `spacedatastandards.org`.
436447
4. Resolve `PNM` for signature/publication metadata.
437448
5. Resolve `ENC` if the delivery was encrypted.
438-
6. Strip the trailer or decrypt the protected payload.
449+
6. Strip the trailer or decrypt the protected payload bytes.
439450
7. Hand the remaining raw wasm bytes to the runtime.
440451

441452
Aligned-binary payloads are a separate invoke-ABI optimization. They do not
@@ -547,9 +558,12 @@ npx space-data-module check --manifest ./manifest.json --wasm ./dist/isomorphic/
547558
# Compile C/C++ source and embed the manifest
548559
npx space-data-module compile --manifest ./manifest.json --source ./src/module.c --out ./dist/isomorphic/module.wasm
549560

550-
# Sign and encrypt a deployment payload
561+
# Sign a deployment payload and print JSON metadata
551562
npx space-data-module protect --manifest ./manifest.json --wasm ./dist/isomorphic/module.wasm --json
552563

564+
# Emit an encrypted binary with an appended REC trailer
565+
npx space-data-module protect --manifest ./manifest.json --wasm ./dist/isomorphic/module.wasm --recipient-public-key <hex> --out ./dist/module.wasm.enc
566+
553567
# Emit a single-file bundled wasm
554568
npx space-data-module protect --manifest ./manifest.json --wasm ./dist/isomorphic/module.wasm --single-file-bundle --out ./dist/module.bundle.wasm
555569
```

bin/space-data-module.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ function printUsage() {
106106
space-data-module check --manifest ./manifest.json --wasm ./dist/module.wasm
107107
space-data-module compile --manifest ./manifest.json --source ./src/module.c --out ./dist/module.wasm
108108
space-data-module protect --manifest ./manifest.json --wasm ./dist/module.wasm --json
109+
space-data-module protect --manifest ./manifest.json --wasm ./dist/module.wasm --recipient-public-key <hex> --out ./dist/module.wasm.enc
109110
space-data-module protect --manifest ./manifest.json --wasm ./dist/module.wasm --single-file-bundle --out ./dist/module.bundle.wasm
110111
`);
111112
}
@@ -213,9 +214,6 @@ async function runProtect(argv) {
213214
if (!options.manifestPath || !options.wasmPath) {
214215
throw new Error("protect requires --manifest and --wasm.");
215216
}
216-
if (options.outputPath && options.singleFileBundle !== true) {
217-
throw new Error("protect only supports --out together with --single-file-bundle.");
218-
}
219217
const manifest = await loadManifestFromFile(options.manifestPath);
220218
const wasmBytes = new Uint8Array(await readFile(options.wasmPath));
221219
const result = await protectModuleArtifact({
@@ -225,8 +223,11 @@ async function runProtect(argv) {
225223
mnemonic: options.mnemonic,
226224
singleFileBundle: options.singleFileBundle,
227225
});
228-
if (options.singleFileBundle && options.outputPath && result.bundledWasmBytes) {
229-
await writeFile(options.outputPath, result.bundledWasmBytes);
226+
if (options.outputPath) {
227+
await writeFile(
228+
options.outputPath,
229+
result.bundledWasmBytes ?? result.protectedArtifactBytes,
230+
);
230231
}
231232
if (options.json) {
232233
console.log(JSON.stringify(result, null, 2));
@@ -235,7 +236,8 @@ async function runProtect(argv) {
235236
console.log(`signingPublicKeyHex=${result.signingPublicKeyHex}`);
236237
console.log(`encrypted=${result.encrypted}`);
237238
console.log(`wasmBase64Length=${bytesToBase64(wasmBytes).length}`);
238-
if (result.bundledWasmBytes) {
239+
console.log(`protectedArtifactBytes=${result.protectedArtifactBytes.length}`);
240+
if (options.singleFileBundle && result.bundledWasmBytes) {
239241
console.log(`singleFileBundle=true`);
240242
console.log(`bundledWasmBytes=${result.bundledWasmBytes.length}`);
241243
}

docs/module-publication-standard.md

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ language package ecosystems used around the SDK runtime surface:
1313

1414
The goal is simple: a loader should be able to inspect a package, locate the
1515
module artifact, and determine whether signatures or encrypted transport
16-
metadata are appended as SDS publication records after the module bytes or
16+
metadata are appended as SDS publication records in the module delivery file or
1717
shipped as sidecar FlatBuffers.
1818

1919
## Scope
@@ -28,7 +28,8 @@ In both cases the module artifact remains the same canonical format already
2828
defined by this repo:
2929

3030
- a runtime payload that is valid WebAssembly bytes once any SDS publication
31-
trailer has been stripped
31+
trailer has been stripped and any encrypted delivery payload has been
32+
decrypted
3233
- embedded `PluginManifest.fbs`
3334
- manifest accessors
3435
- `plugin_get_manifest_flatbuffer`
@@ -37,16 +38,20 @@ defined by this repo:
3738

3839
## Core Rules
3940

40-
1. The runtime payload before any publication trailer MUST remain valid `.wasm`.
41-
2. If signatures or encrypted-delivery metadata are carried in the same file,
42-
they MUST be appended after the wasm bytes as an SDS `REC` trailer.
43-
3. `REC` trailers MUST carry standards-sourced `MBL` metadata plus `PNM` and
41+
1. A signed-only or unencrypted artifact payload before any publication trailer
42+
MUST remain valid `.wasm`.
43+
2. An encrypted binary delivery file MUST be encoded as encrypted payload bytes
44+
followed by an appended SDS `REC` trailer. The bytes before the trailer are
45+
ciphertext and are not required to validate as wasm until decrypted.
46+
3. If bundle, signature, or encrypted-delivery metadata are carried in the same
47+
file, they MUST be appended as an SDS `REC` trailer at the end of that file.
48+
4. `REC` trailers MUST carry standards-sourced `MBL` metadata plus `PNM` and
4449
optional `ENC` records where applicable.
45-
4. Single-file bundle metadata MUST be read from the appended `REC` trailer,
50+
5. Single-file bundle metadata MUST be read from the appended `REC` trailer,
4651
not from an in-wasm custom section.
47-
5. Sidecar FlatBuffers are allowed when a package chooses not to append those
52+
6. Sidecar FlatBuffers are allowed when a package chooses not to append those
4853
metadata payloads to the module artifact.
49-
6. Paths in publication metadata are package-relative, never absolute.
54+
7. Paths in publication metadata are package-relative, never absolute.
5055

5156
## Publication Record Extensions
5257

@@ -76,6 +81,25 @@ The runtime-facing rule stays strict:
7681
`MBL`, `PNM`, and `ENC` extend publication and transport handling only. They do
7782
not change the canonical module ABI or manifest exports.
7883

84+
## Protected Binary Layout
85+
86+
The official same-file protection layout is:
87+
88+
```text
89+
protected-payload-bytes || REC-flatbuffer-bytes || uint32le(REC length) || "$REC"
90+
```
91+
92+
For signed-only delivery, `protected-payload-bytes` are the wasm bytes. For
93+
encrypted delivery, `protected-payload-bytes` are ciphertext and the appended
94+
`REC` MUST contain an `ENC` record with the decryption parameters. Loaders MUST
95+
decrypt those ciphertext bytes before attempting wasm validation, manifest
96+
inspection, or bundle metadata parsing.
97+
98+
The `PNM` content identity applies to the protected payload bytes as stored in
99+
the file. For encrypted delivery this means the `PNM.CID` identifies the
100+
ciphertext payload, while the decrypted bytes remain the canonical wasm module
101+
that is passed to the runtime.
102+
79103
### `PNM` digital-signature extension
80104

81105
`PNM` carries the publication notice for the module:
@@ -437,15 +461,16 @@ A loader consuming this standard SHOULD:
437461
2. read `module.path`
438462
3. scan the artifact from the end for an appended SDS `REC` trailer
439463
4. resolve `PNM` / `ENC` from that trailer before runtime startup
440-
5. if `ENC` is present, decrypt and strip the trailer before passing bytes to
441-
WasmEdge or any other runtime
442-
6. inspect the stripped artifact's `REC` trailer for `MBL`
443-
7. resolve any `package-file` metadata through relative paths
444-
8. validate manifest exports and any declared integrity hashes
445-
446-
If `module.packaging` is `sds-bundled-wasm`, loaders SHOULD treat the stripped
447-
wasm payload as the runtime artifact and the appended `REC` trailer as the
448-
single-file bundle/publication metadata container.
464+
5. if `ENC` is present, decrypt the protected payload bytes before passing bytes
465+
to WasmEdge or any other runtime
466+
6. if `ENC` is absent, strip the trailer and use the remaining wasm payload
467+
7. inspect the parsed `REC` trailer for `MBL`
468+
8. resolve any `package-file` metadata through relative paths
469+
9. validate manifest exports and any declared integrity hashes
470+
471+
If `module.packaging` is `sds-bundled-wasm`, loaders SHOULD treat the decrypted
472+
or stripped wasm payload as the runtime artifact and the appended `REC` trailer
473+
as the single-file bundle/publication metadata container.
449474

450475
## Relationship To Existing Bundle Format
451476

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "space-data-module-sdk",
3-
"version": "0.8.1",
3+
"version": "0.8.2",
44
"description": "Module SDK for building, validating, signing, and deploying WebAssembly modules on the Space Data Network.",
55
"type": "module",
66
"types": "./src/index.d.ts",

src/manifest/codec.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import { normalizeInvokeSurfaceName } from "../invoke/codec.js";
1212
import { toUint8Array } from "../runtime/bufferLike.js";
1313
import { toEmbeddedPluginManifest } from "./normalize.js";
14+
import { decodePlgManifest, isPlgManifestBuffer } from "./plgCodec.js";
1415

1516
function toByteBuffer(data) {
1617
if (data instanceof flatbuffers.ByteBuffer) {
@@ -107,7 +108,79 @@ function normalizeDecodedMethod(method = {}) {
107108
};
108109
}
109110

111+
function typeRefFromSchemaName(schemaName) {
112+
return typeof schemaName === "string" && schemaName.length > 0
113+
? { schemaName }
114+
: null;
115+
}
116+
117+
function portFromSchemaName(schemaName, index, direction) {
118+
const typeRef = typeRefFromSchemaName(schemaName);
119+
if (!typeRef) {
120+
return null;
121+
}
122+
return {
123+
portId: `${direction}-${index + 1}`,
124+
acceptedTypeSets: [
125+
{
126+
setId: schemaName,
127+
allowedTypes: [typeRef],
128+
},
129+
],
130+
minStreams: 0,
131+
maxStreams: 1,
132+
required: false,
133+
};
134+
}
135+
136+
function methodFromPlgEntry(entry = {}) {
137+
const methodId = entry.name;
138+
if (typeof methodId !== "string" || methodId.length === 0) {
139+
return null;
140+
}
141+
const inputPorts = Array.isArray(entry.inputSchemas)
142+
? entry.inputSchemas
143+
.map((schemaName, index) => portFromSchemaName(schemaName, index, "input"))
144+
.filter(Boolean)
145+
: [];
146+
const outputPort = portFromSchemaName(entry.outputSchema, 0, "output");
147+
return {
148+
methodId,
149+
displayName: methodId,
150+
description: entry.description,
151+
inputPorts,
152+
outputPorts: outputPort ? [outputPort] : [],
153+
maxBatch: 1,
154+
drainPolicy: "single-shot",
155+
};
156+
}
157+
158+
function normalizeDecodedPlgManifest(manifest = {}) {
159+
const methods = Array.isArray(manifest.entryFunctions)
160+
? manifest.entryFunctions.map((entry) => methodFromPlgEntry(entry)).filter(Boolean)
161+
: [];
162+
return {
163+
...manifest,
164+
methods,
165+
capabilities: Array.isArray(manifest.capabilities)
166+
? manifest.capabilities
167+
: [],
168+
invokeSurfaces: Array.isArray(manifest.invokeSurfaces)
169+
? manifest.invokeSurfaces
170+
.map((value) => normalizeInvokeSurfaceName(value))
171+
.filter(Boolean)
172+
: [],
173+
runtimeTargets: Array.isArray(manifest.runtimeTargets)
174+
? manifest.runtimeTargets
175+
: [],
176+
};
177+
}
178+
110179
export function decodePluginManifest(data) {
180+
const bytes = toUint8Array(data);
181+
if (bytes && isPlgManifestBuffer(bytes)) {
182+
return normalizeDecodedPlgManifest(decodePlgManifest(bytes));
183+
}
111184
const bb = toByteBuffer(data);
112185
if (!PluginManifest.bufferHasIdentifier(bb)) {
113186
throw new Error("Plugin manifest buffer identifier mismatch.");

src/testing/native/wasmedge_emscripten_pthread_runner.c

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,14 @@ static void register_env_host_functions(
682682
WasmEdge_ValTypeGenI32(),
683683
WasmEdge_ValTypeGenI32(),
684684
WasmEdge_ValTypeGenI32()};
685+
const WasmEdge_ValType seven_i32_params[7] = {
686+
WasmEdge_ValTypeGenI32(),
687+
WasmEdge_ValTypeGenI32(),
688+
WasmEdge_ValTypeGenI32(),
689+
WasmEdge_ValTypeGenI32(),
690+
WasmEdge_ValTypeGenI32(),
691+
WasmEdge_ValTypeGenI32(),
692+
WasmEdge_ValTypeGenI32()};
685693
const WasmEdge_ValType f64_return[1] = {WasmEdge_ValTypeGenF64()};
686694
const WasmEdge_ValType i32_return[1] = {WasmEdge_ValTypeGenI32()};
687695

@@ -735,8 +743,14 @@ static void register_env_host_functions(
735743
"_emscripten_receive_on_main_thread_js",
736744
stub_receive_on_main_thread,
737745
runner,
738-
receive_on_main_thread_param_len == 4 ? four_i32_params : five_i32_params,
739-
receive_on_main_thread_param_len == 4 ? 4 : 5,
746+
receive_on_main_thread_param_len == 4
747+
? four_i32_params
748+
: receive_on_main_thread_param_len == 7
749+
? seven_i32_params
750+
: five_i32_params,
751+
receive_on_main_thread_param_len == 4
752+
? 4
753+
: receive_on_main_thread_param_len == 7 ? 7 : 5,
740754
f64_return,
741755
1);
742756
add_host_func(
@@ -3072,7 +3086,7 @@ int main(int argc, char **argv) {
30723086
&runner,
30733087
import_config.receive_on_main_thread_param_len == 4
30743088
? 4
3075-
: 5,
3089+
: import_config.receive_on_main_thread_param_len == 7 ? 7 : 5,
30763090
import_config.notify_mailbox_postmessage_param_len == 3
30773091
? 3
30783092
: 2);

test/isomorphic-plugin-loading.test.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,15 +127,26 @@ test("real sibling standalone plugin artifacts load through the browser harness"
127127
new Set(inspection.imports.map((entry) => entry.module)),
128128
).sort();
129129

130-
assert.equal(inspection.profile, "standalone");
131-
assert.deepEqual(importedModuleNames, ["wasi_snapshot_preview1"]);
130+
assert.equal(
131+
inspection.profile,
132+
spec.expectedIsomorphicProfile ?? "standalone",
133+
);
134+
assert.deepEqual(
135+
importedModuleNames,
136+
spec.expectedImportModules ?? ["wasi_snapshot_preview1"],
137+
);
132138

133139
const report = await validatePluginArtifact({
134140
manifest,
135141
wasmPath: spec.standaloneArtifactPath,
136142
});
137143
assert.equal(report.ok, true, JSON.stringify(report.issues, null, 2));
138144

145+
if (spec.skipBrowserHarness) {
146+
t.skip(spec.skipBrowserHarness);
147+
return;
148+
}
149+
139150
const harness = await createBrowserModuleHarness({
140151
wasmSource: standaloneBytes,
141152
surface: "command",

0 commit comments

Comments
 (0)