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
1 change: 1 addition & 0 deletions .tmp_test_trigger
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dummy
2 changes: 1 addition & 1 deletion docs/15-milestones.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ signed AEP evidence instead.
- [ ] `wasmagent-ops/federation/`: Control plane for multi-cluster agent mesh synchronization and cross-domain attestation (`wasmagent-mesh sync --peers mesh-peers.yaml`)
- [ ] `aep/zk-proofs/`: Zero-Knowledge (ZK-SNARK) attestation exporter for privacy-preserving AEP evidence verification (`passport prove --privacy-mode zk`)
- [ ] `agentbom/policy/`: Embed WebAssembly-native Open Policy Agent (OPA/Rego) evaluator for inline runtime guardrail enforcement
- [ ] `wasmagent-js/spiffe/`: SPIFFE/SPIRE cryptographic identity driver binding Wasm sandbox workloads to enterprise mTLS credentials
- [x] `wasmagent-js/spiffe/`: SPIFFE/SPIRE cryptographic identity driver binding Wasm sandbox workloads to enterprise mTLS credentials
- [ ] `trace-pipeline/stream/`: Real-time gRPC telemetry and event ingestion pipeline for instant posture drift detection and passport revocation
- [ ] `wasmagent/edge/`: Low-latency WasmAgent edge runtime supporting offline evidence buffering and eventual ledger synchronization
- [ ] `agent-golden-path/multi-agent/`: Multi-agent procurement federation workload (buyer copilot ↔ supplier copilot) under signed AEP contracts
Expand Down
2 changes: 1 addition & 1 deletion docs/project-index.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"status": "shipped",
"visibility": "public",
"in_profile": true,
"summary": "Embedded agent runtime v1.x: WASM kernels (QuickJS, Pyodide, Wasmtime, Remote), MCP gateway + attestation, AEP emitter, capability manifests; adapters for A2A, AG-UI, AI SDK, Claude Agent SDK.",
"summary": "Embedded agent runtime v1.x: WASM kernels (QuickJS, Pyodide, Wasmtime, Remote), MCP gateway + attestation, AEP emitter, capability manifests, SPIFFE/SPIRE cryptographic identity driver binding Wasm sandbox workloads to enterprise mTLS credentials; adapters for A2A, AG-UI, AI SDK, Claude Agent SDK.",
"url": "https://github.com/WasmAgent/wasmagent-js",
"latest_version": "3.1.1",
"focus": "core-spine"
Expand Down
85 changes: 85 additions & 0 deletions tests/e2e/wasmagent_js_spiffe_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package e2e

import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/WasmAgent/.github/pkg/docs"
)

// TestWasmagentJsSpiffeDriver validates the wasmagent-js/spiffe reference
// artifacts for the Milestone 6 bullet:
//
// wasmagent-js/spiffe/: SPIFFE/SPIRE cryptographic identity driver binding
// Wasm sandbox workloads to enterprise mTLS credentials
//
// It checks that:
// - wasmagent-js/spiffe/spiffe.ts ships a SpiffeIdentityDriver with
// X.509-SVID / JWT-SVID fetching, rotation watching, workload binding, and
// mTLS credential material (cert chain + private key) for Wasm sandbox
// workloads.
// - wasmagent-js/spiffe/spiffe.test.ts exercises SPIFFE ID parsing, SVID
// rotation, JWT issuance, and mTLS credential binding.
// - The project index advertises the SPIFFE/SPIRE identity driver on the
// wasmagent-js repository (the owning repo of the spiffe/ surface).
func TestWasmagentJsSpiffeDriver(t *testing.T) {
driverDir := filepath.Join("..", "..", "wasmagent-js", "spiffe")

// 1. Driver module must exist with the required identity capabilities.
driverPath := filepath.Join(driverDir, "spiffe.ts")
driverSource, err := os.ReadFile(driverPath)
if err != nil {
t.Fatalf("wasmagent-js/spiffe/spiffe.ts is missing: %v", err)
}
for _, fragment := range []string{
"export class SpiffeIdentityDriver",
"parseSpiffeId",
"fetchX509Svid",
"fetchJwtSvid",
"watchX509Svid",
"bindWorkload",
"createMtlsClientCredentials",
"createMtlsServerCredentials",
"spiffe://",
} {
if !strings.Contains(string(driverSource), fragment) {
t.Errorf("wasmagent-js/spiffe driver is missing required capability %q", fragment)
}
}

// 2. Driver tests must cover SPIFFE ID parsing, SVID rotation, JWT
// issuance, and mTLS credential binding.
testPath := filepath.Join(driverDir, "spiffe.test.ts")
testSource, err := os.ReadFile(testPath)
if err != nil {
t.Fatalf("wasmagent-js/spiffe coverage is missing: %v", err)
}
for _, scenario := range []string{
"parses and validates SPIFFE IDs",
"fetches and rotates X.509 SVIDs",
"issues JWT-SVIDs",
"binds a Wasm sandbox workload to mTLS credentials",
} {
if !strings.Contains(string(testSource), scenario) {
t.Errorf("wasmagent-js/spiffe test is missing scenario %q", scenario)
}
}

// 3. The project index must advertise the SPIFFE/SPIRE identity driver on
// the wasmagent-js repository (the owning repo of the spiffe/ surface).
projectIndex, err := docs.LoadProjectIndex()
if err != nil {
t.Fatalf("Failed to load project index: %v", err)
}
jsRepo, found := projectIndex.GetRepoByName("wasmagent-js")
if !found {
t.Fatal("wasmagent-js repository not found in project index")
}
if !strings.Contains(strings.ToLower(jsRepo.Summary), "spiffe") {
t.Errorf("wasmagent-js summary does not mention the SPIFFE/SPIRE identity driver: %s", jsRepo.Summary)
}

t.Log("SPIFFE/SPIRE cryptographic identity driver validated for wasmagent-js")
}
21 changes: 21 additions & 0 deletions wasmagent-js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,24 @@ const result = await runtime.verify("acme", {
});
// result.decision === "PROCEED"
```

## SPIFFE/SPIRE identity driver

`spiffe/spiffe.ts` provides a dependency-free `SpiffeIdentityDriver` that binds
Wasm sandbox workloads to enterprise mTLS credentials (Milestone 6). The driver
fetches X.509-SVIDs and JWT-SVIDs from the SPIRE Workload API, watches for SVID
rotation, and exposes the active certificate chain + private key as mTLS
client/server credential material. The Workload API transport is pluggable so
tests can simulate SVID issuance and rotation without a live SPIRE agent.

```ts
import { SpiffeIdentityDriver } from "./spiffe/spiffe";

const driver = new SpiffeIdentityDriver({
workloadApiAddress: "unix:///run/spire/sockets/agent.sock",
selectors: [{ type: "k8s", value: "ns:payroll" }],
});
const bundle = await driver.connect();
// bundle.x509Svid.certChain + bundle.x509Svid.privateKey
// feed the mTLS handshake for the sandbox workload's SPIFFE identity.
```
150 changes: 150 additions & 0 deletions wasmagent-js/spiffe/spiffe.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { describe, expect, it } from "bun:test";

import type { JwtSvid, SvidBundle, WorkloadApiTransport, X509Svid } from "./spiffe";
import {
InvalidSpiffeIdError,
SpiffeIdentityDriver,
parseSpiffeId,
} from "./spiffe";

const sampleSvid = (spiffeId: string, expiresAt?: string): X509Svid => ({
spiffeId,
certChain: [
"-----BEGIN CERTIFICATE-----\nMIIB leaf chain\n-----END CERTIFICATE-----",
],
privateKey:
"-----BEGIN PRIVATE KEY-----\nMIIE workload key\n-----END PRIVATE KEY-----",
bundle: [
"-----BEGIN CERTIFICATE-----\nMIIB trust root\n-----END CERTIFICATE-----",
],
expiresAt:
expiresAt ?? new Date(Date.now() + 60_000).toISOString(),
});

class FakeWorkloadApi implements WorkloadApiTransport {
private svids: X509Svid[];
private readonly listeners = new Set<(svid: X509Svid) => void>();

constructor(svid: X509Svid) {
this.svids = [svid];
}

fetchX509Svid(): Promise<X509Svid> {
return Promise.resolve(this.svids[0]);
}

fetchJwtSvid(audience: string): Promise<JwtSvid> {
const svid = this.svids[0];
return Promise.resolve({
spiffeId: svid.spiffeId,
token: "eyJhbGciOiJFUzI1NiJ9.signed-jwt",
audience,
expiresAt: new Date(Date.now() + 60_000).toISOString(),
});
}

watchX509Svid(onUpdate: (svid: X509Svid) => void): () => void {
this.listeners.add(onUpdate);
return () => this.listeners.delete(onUpdate);
}

push(svid: X509Svid): void {
this.svids = [svid];
for (const listener of this.listeners) {
listener(svid);
}
}
}

describe("SpiffeIdentityDriver", () => {
it("parses and validates SPIFFE IDs with trust domain and path", () => {
const parts = parseSpiffeId("spiffe://example.org/ns/payroll/sa/billing");
expect(parts.trustDomain).toBe("example.org");
expect(parts.path).toBe("/ns/payroll/sa/billing");

expect(() => parseSpiffeId("https://example.org/x")).toThrow(
InvalidSpiffeIdError,
);
expect(() => parseSpiffeId("spiffe://example.org")).toThrow(
InvalidSpiffeIdError,
);
expect(() => parseSpiffeId("spiffe:///ns/payroll")).toThrow(
InvalidSpiffeIdError,
);
});

it("fetches and rotates X.509 SVIDs through the Workload API transport", async () => {
const first = sampleSvid(
"spiffe://example.org/ns/payroll/sa/billing",
new Date(Date.now() + 60_000).toISOString(),
);
const second = sampleSvid(
"spiffe://example.org/ns/payroll/sa/billing",
new Date(Date.now() + 120_000).toISOString(),
);
const api = new FakeWorkloadApi(first);
const driver = new SpiffeIdentityDriver({
transport: api,
selectors: [{ type: "k8s", value: "ns:payroll" }],
});

expect(driver.getSelectors()).toEqual([{ type: "k8s", value: "ns:payroll" }]);

const bundle = await driver.connect();
expect(bundle.trustDomain).toBe("example.org");
expect(bundle.x509Svid.spiffeId).toBe("spiffe://example.org/ns/payroll/sa/billing");

let rotated: SvidBundle | undefined;
driver.onRotation((next) => {
rotated = next;
});
api.push(second);
expect(rotated?.x509Svid.expiresAt).toBe(second.expiresAt);
expect(driver.getActiveBundle().x509Svid.expiresAt).toBe(second.expiresAt);

const creds = driver.createMtlsClientCredentials();
expect(creds.certChain.length).toBeGreaterThan(0);
expect(creds.privateKey).toContain("BEGIN PRIVATE KEY");
expect(creds.trustBundle.length).toBeGreaterThan(0);
expect(creds.spiffeId).toBe("spiffe://example.org/ns/payroll/sa/billing");

driver.close();
});

it("issues JWT-SVIDs for a requested audience", async () => {
const api = new FakeWorkloadApi(
sampleSvid("spiffe://example.org/ns/payroll/sa/billing"),
);
const driver = new SpiffeIdentityDriver({ transport: api });
await driver.connect();

const jwt = await driver.fetchJwtSvid("spire-server");
expect(jwt.token).toBe("eyJhbGciOiJFUzI1NiJ9.signed-jwt");
expect(jwt.audience).toBe("spire-server");
expect(jwt.spiffeId).toBe("spiffe://example.org/ns/payroll/sa/billing");
expect(driver.getActiveJwtSvid().audience).toBe("spire-server");

driver.close();
});

it("binds a Wasm sandbox workload to mTLS credentials", async () => {
const api = new FakeWorkloadApi(
sampleSvid("spiffe://example.org/ns/payroll/sa/billing"),
);
const driver = new SpiffeIdentityDriver({
transport: api,
selectors: [{ type: "wasm", value: "sandbox:default" }],
});
await driver.connect();

driver.bindWorkload("spiffe://example.org/ns/payroll/sa/billing");
const serverCreds = driver.createMtlsServerCredentials();
expect(serverCreds.spiffeId).toBe("spiffe://example.org/ns/payroll/sa/billing");

expect(() => driver.bindWorkload("spiffe://other.org/ns/payroll/sa/billing")).toThrow(
/does not match required/,
);

driver.close();
});
});
Loading
Loading