From 268e3f49224dfabbd3d0fe10205fd8b85fd2a9cd Mon Sep 17 00:00:00 2001 From: Eric Prud'hommeaux Date: Mon, 27 Apr 2026 18:49:23 +0200 Subject: [PATCH 1/3] test: create strawman test suite --- lws10-test-suite/.gitignore | 1 + lws10-test-suite/README.md | 98 +++ lws10-test-suite/auth/did-key | 1 + lws10-test-suite/auth/oidc | 1 + lws10-test-suite/auth/saml | 1 + lws10-test-suite/containers/alice-notes.json | 17 + lws10-test-suite/context.jsonld | 94 ++ .../linksets/alice-notes.meta.json | 22 + .../linksets/shoppinglist.meta.json | 22 + lws10-test-suite/manifest.html | 779 +++++++++++++++++ lws10-test-suite/manifest.jsonld | 810 ++++++++++++++++++ .../lws-tests/auth/did-key/manifest.jsonld | 89 ++ .../lws-tests/auth/oidc/manifest.jsonld | 116 +++ .../lws-tests/auth/saml/manifest.jsonld | 74 ++ .../authorization-server-metadata.json | 14 + lws10-test-suite/resources/shoppinglist.txt | 6 + .../resources/storage-description.json | 19 + .../resources/token-response-200.json | 5 + .../resources/token-response-400.json | 3 + lws10-test-suite/signatures/TestCA.ts | 254 ++++++ lws10-test-suite/signatures/cert.ts | 234 +++++ lws10-test-suite/signatures/demo.ts | 216 +++++ lws10-test-suite/signatures/index.ts | 16 + lws10-test-suite/signatures/keys.ts | 152 ++++ .../signatures/manifest-integration.ts | 254 ++++++ lws10-test-suite/signatures/package-lock.json | 752 ++++++++++++++++ lws10-test-suite/signatures/package.json | 21 + lws10-test-suite/signatures/tsconfig.json | 17 + lws10-test-suite/signatures/types.ts | 169 ++++ 29 files changed, 4257 insertions(+) create mode 100644 lws10-test-suite/.gitignore create mode 100644 lws10-test-suite/README.md create mode 120000 lws10-test-suite/auth/did-key create mode 120000 lws10-test-suite/auth/oidc create mode 120000 lws10-test-suite/auth/saml create mode 100644 lws10-test-suite/containers/alice-notes.json create mode 100644 lws10-test-suite/context.jsonld create mode 100644 lws10-test-suite/linksets/alice-notes.meta.json create mode 100644 lws10-test-suite/linksets/shoppinglist.meta.json create mode 100644 lws10-test-suite/manifest.html create mode 100644 lws10-test-suite/manifest.jsonld create mode 100644 lws10-test-suite/mnt/user-data/outputs/lws-tests/auth/did-key/manifest.jsonld create mode 100644 lws10-test-suite/mnt/user-data/outputs/lws-tests/auth/oidc/manifest.jsonld create mode 100644 lws10-test-suite/mnt/user-data/outputs/lws-tests/auth/saml/manifest.jsonld create mode 100644 lws10-test-suite/resources/authorization-server-metadata.json create mode 100644 lws10-test-suite/resources/shoppinglist.txt create mode 100644 lws10-test-suite/resources/storage-description.json create mode 100644 lws10-test-suite/resources/token-response-200.json create mode 100644 lws10-test-suite/resources/token-response-400.json create mode 100644 lws10-test-suite/signatures/TestCA.ts create mode 100644 lws10-test-suite/signatures/cert.ts create mode 100644 lws10-test-suite/signatures/demo.ts create mode 100644 lws10-test-suite/signatures/index.ts create mode 100644 lws10-test-suite/signatures/keys.ts create mode 100644 lws10-test-suite/signatures/manifest-integration.ts create mode 100644 lws10-test-suite/signatures/package-lock.json create mode 100644 lws10-test-suite/signatures/package.json create mode 100644 lws10-test-suite/signatures/tsconfig.json create mode 100644 lws10-test-suite/signatures/types.ts diff --git a/lws10-test-suite/.gitignore b/lws10-test-suite/.gitignore new file mode 100644 index 0000000..40b878d --- /dev/null +++ b/lws10-test-suite/.gitignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/lws10-test-suite/README.md b/lws10-test-suite/README.md new file mode 100644 index 0000000..7e808c4 --- /dev/null +++ b/lws10-test-suite/README.md @@ -0,0 +1,98 @@ +# LWS Protocol 1.0 Test Suite + +Test suite for the [Linked Web Storage Protocol 1.0](https://www.w3.org/TR/lws10-core/) +and its authentication suite specifications: + +- [LWS 1.0 Authentication Suite: Self-signed Identity using did:key](https://www.w3.org/TR/lws10-authn-ssi-did-key/) +- [LWS 1.0 Authentication Suite: OpenID Connect](https://www.w3.org/TR/lws10-authn-openid/) +- [LWS 1.0 Authentication Suite: SAML 2.0](https://www.w3.org/TR/lws10-authn-saml/) + +## Directory Structure + +``` +tests/ +├── context.jsonld # Shared JSON-LD context for all manifests +├── manifest.jsonld # Root manifest (core protocol tests) +├── auth/ +│ ├── did-key/manifest.jsonld # did:key authentication suite tests +│ ├── oidc/manifest.jsonld # OpenID Connect authentication suite tests +│ └── saml/manifest.jsonld # SAML 2.0 authentication suite tests +├── containers/ +│ └── alice-notes.json # Example container representation body +├── linksets/ +│ ├── alice-notes.meta.json # Linkset for /alice/notes/ +│ └── shoppinglist.meta.json # Linkset for /alice/notes/shoppinglist.txt +└── resources/ + ├── storage-description.json # Storage description resource body + ├── authorization-server-metadata.json # AS metadata body + ├── shoppinglist.txt # Data resource body + ├── token-response-200.json # Successful token exchange response + └── token-response-400.json # Failed token exchange response +``` + +## Conventions + +### Inline vs. by-reference properties + +Properties that carry their value directly use a plain JSON name: + +```json +"body": "some content" +``` + +Properties that reference an external resource by URL append `URL` to the name: + +```json +"bodyURL": "../containers/alice-notes.json" +``` + +The same convention applies to `href` / `hrefTemplate` for Link header values, and +`url` / `urlTemplate` for request targets. + +### Test types + +| Type | Meaning | +|------|---------| +| `lwst:ValidationTest` | The server MUST respond as described for conformance | +| `lwst:NegativeTest` | The server MUST reject the request as described | + +### Traits (applied to tests for filtering) + +`Get`, `Post`, `Put`, `Delete`, `Patch` — HTTP method used +`Public` — no authentication required +`Private` — resource requires authentication/authorization +`Authn` — exercises authentication behaviour +`Authz` — exercises authorization behaviour +`Container` — targets a Container resource +`DataResource` — targets a DataResource +`Discovery` — exercises service/storage description discovery + +### Authorization roles (ODRL-based) + +| Compact name | Meaning | +|-------------|---------| +| `Role-Public` | Any agent, authenticated or not | +| `Role-Authenticated` | Any agent that presents a valid access token | +| `Role-Owner` | The resource manager (owner) of the storage | + +### Prerequisite hierarchy + +Each test's `prereqs.hierarchy` array describes the resources (and their access policies) +that a test harness MUST establish before issuing the test request. An empty array means +no prior state is required. + +### Authentication + +`prereqs.authentication` and `request.authentication` hold the WebID/subject URI of the +agent to authenticate as, or `null` for anonymous requests. + +## Test certificate authority + +Tests that require authentication may use a test CA whose certificate is distributed +alongside this test suite. Implementations SHOULD accept this CA only in test/non-production +environments. + +## Status values + +All tests start with `mf:Proposed`. They advance to `mf:Approved` once an independent +implementation demonstrates conformance. diff --git a/lws10-test-suite/auth/did-key b/lws10-test-suite/auth/did-key new file mode 120000 index 0000000..40cfeee --- /dev/null +++ b/lws10-test-suite/auth/did-key @@ -0,0 +1 @@ +../mnt/user-data/outputs/lws-tests/auth/did-key \ No newline at end of file diff --git a/lws10-test-suite/auth/oidc b/lws10-test-suite/auth/oidc new file mode 120000 index 0000000..a9c4dbd --- /dev/null +++ b/lws10-test-suite/auth/oidc @@ -0,0 +1 @@ +../mnt/user-data/outputs/lws-tests/auth/oidc \ No newline at end of file diff --git a/lws10-test-suite/auth/saml b/lws10-test-suite/auth/saml new file mode 120000 index 0000000..f830e38 --- /dev/null +++ b/lws10-test-suite/auth/saml @@ -0,0 +1 @@ +../mnt/user-data/outputs/lws-tests/auth/saml \ No newline at end of file diff --git a/lws10-test-suite/containers/alice-notes.json b/lws10-test-suite/containers/alice-notes.json new file mode 100644 index 0000000..d32ad7e --- /dev/null +++ b/lws10-test-suite/containers/alice-notes.json @@ -0,0 +1,17 @@ +{ + "@context": "https://www.w3.org/ns/lws/v1", + "id": "https://storage.example/alice/notes/", + "type": "Container", + "items": [ + { + "id": "https://storage.example/alice/notes/shoppinglist.txt", + "type": "DataResource", + "contentType": "text/plain", + "size": 47 + }, + { + "id": "https://storage.example/alice/notes/ideas/", + "type": "Container" + } + ] +} diff --git a/lws10-test-suite/context.jsonld b/lws10-test-suite/context.jsonld new file mode 100644 index 0000000..17931a0 --- /dev/null +++ b/lws10-test-suite/context.jsonld @@ -0,0 +1,94 @@ +{ + "@context": { + "@vocab": "https://www.w3.org/ns/lws-tests/v1#", + + "lws": "https://www.w3.org/ns/lws/v1#", + "lwst": "https://www.w3.org/ns/lws-tests/v1#", + "mf": "https://www.w3.org/ns/test-manifest#", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "odrl": "http://www.w3.org/ns/odrl/2/", + + "id": "@id", + "type": "@type", + "comment": "rdfs:comment", + "label": "rdfs:label", + + "name": {"@id": "mf:name", "@type": "xsd:string"}, + "entries": {"@id": "mf:entries","@container": "@list", "@type": "@id"}, + "status": {"@id": "mf:status", "@type": "@vocab"}, + "include": {"@id": "mf:include","@type": "@id", "@container": "@list"}, + + "traits": {"@id": "lwst:traits", "@type": "@vocab", "@container": "@set"}, + + "prereqs": {"@id": "lwst:prereqs"}, + "hierarchy": {"@id": "lwst:hierarchy", "@container": "@list"}, + "authentication": {"@id": "lwst:authentication"}, + "authorization": {"@id": "lwst:authorization"}, + + "url": {"@id": "lwst:url", "@type": "@id"}, + "urlTemplate": {"@id": "lwst:url", "@type": "xsd:string"}, + + "read": {"@id": "odrl:read", "@type": "@vocab"}, + "write": {"@id": "odrl:write", "@type": "@vocab"}, + "append": {"@id": "odrl:use", "@type": "@vocab"}, + "control": {"@id": "odrl:modify","@type": "@vocab"}, + + "request": {"@id": "lwst:request"}, + "method": {"@id": "lwst:method", "@type": "xsd:string"}, + "slug": {"@id": "lwst:slug", "@type": "xsd:string"}, + "contentType": {"@id": "lwst:contentType", "@type": "xsd:string"}, + + "linkHeaders": {"@id": "lwst:linkHeaders", "@container": "@list"}, + "otherHeaders": {"@id": "lwst:otherHeaders", "@container": "@list"}, + "rel": {"@id": "lwst:rel", "@type": "xsd:string"}, + "href": {"@id": "lwst:href", "@type": "@id"}, + "hrefTemplate": {"@id": "lwst:href", "@type": "xsd:string"}, + "mediaType": {"@id": "lwst:mediaType", "@type": "xsd:string"}, + "headerName": {"@id": "lwst:headerName", "@type": "xsd:string"}, + "headerValue": {"@id": "lwst:headerValue", "@type": "xsd:string"}, + + "body": {"@id": "lwst:body", "@type": "xsd:string"}, + "bodyURL": {"@id": "lwst:body", "@type": "@id"}, + + "response": {"@id": "lwst:response"}, + "statusCode": {"@id": "lwst:statusCode", "@type": "xsd:integer"}, + "authenticationChallenge": {"@id": "lwst:authenticationChallenge"}, + "authorizationHint": {"@id": "lwst:authorizationHint"}, + + "wwwAuthenticate": {"@id": "lwst:wwwAuthenticate", "@type": "xsd:string"}, + "asUri": {"@id": "lwst:asUri", "@type": "@id"}, + "realm": {"@id": "lwst:realm", "@type": "@id"}, + + "locationURL": {"@id": "lwst:location", "@type": "@id"}, + + "authSuites": {"@id": "lwst:authSuites", "@type": "@id", "@container": "@set"}, + "facets": {"@id": "lwst:facets", "@type": "@id", "@container": "@set"}, + + "Role-Public": "lwst:Role-Public", + "Role-Owner": "lwst:Role-Owner", + "Role-Authenticated": "lwst:Role-Authenticated", + + "Proposed": "mf:Proposed", + "Approved": "mf:Approved", + "Pending": "mf:Pending", + + "ValidationTest": "lwst:ValidationTest", + "NegativeTest": "lwst:NegativeTest", + "Manifest": "mf:Manifest", + + "Get": "lwst:Get", + "Post": "lwst:Post", + "Put": "lwst:Put", + "Delete": "lwst:Delete", + "Patch": "lwst:Patch", + "Public": "lwst:Public", + "Private": "lwst:Private", + "Authn": "lwst:Authn", + "Authz": "lwst:Authz", + "Container": "lwst:Container", + "DataResource": "lwst:DataResource", + "Discovery": "lwst:Discovery" + } +} diff --git a/lws10-test-suite/linksets/alice-notes.meta.json b/lws10-test-suite/linksets/alice-notes.meta.json new file mode 100644 index 0000000..871e2f8 --- /dev/null +++ b/lws10-test-suite/linksets/alice-notes.meta.json @@ -0,0 +1,22 @@ +{ + "linkset": [ + { + "anchor": "https://storage.example/alice/notes/", + "https://www.w3.org/ns/lws#storageDescription": [ + { "href": "https://storage.example/alice/description" } + ], + "up": [ + { "href": "https://storage.example/alice/" } + ], + "type": [ + { "href": "https://www.w3.org/ns/lws#Container" } + ], + "linkset": [ + { + "href": "https://storage.example/alice/notes/.meta", + "type": "application/linkset+json" + } + ] + } + ] +} diff --git a/lws10-test-suite/linksets/shoppinglist.meta.json b/lws10-test-suite/linksets/shoppinglist.meta.json new file mode 100644 index 0000000..36e9447 --- /dev/null +++ b/lws10-test-suite/linksets/shoppinglist.meta.json @@ -0,0 +1,22 @@ +{ + "linkset": [ + { + "anchor": "https://storage.example/alice/notes/shoppinglist.txt", + "https://www.w3.org/ns/lws#storageDescription": [ + { "href": "https://storage.example/alice/description" } + ], + "up": [ + { "href": "https://storage.example/alice/notes/" } + ], + "type": [ + { "href": "https://www.w3.org/ns/lws#DataResource" } + ], + "linkset": [ + { + "href": "https://storage.example/alice/notes/shoppinglist.txt.meta", + "type": "application/linkset+json" + } + ] + } + ] +} diff --git a/lws10-test-suite/manifest.html b/lws10-test-suite/manifest.html new file mode 100644 index 0000000..f7320d1 --- /dev/null +++ b/lws10-test-suite/manifest.html @@ -0,0 +1,779 @@ + + + + + + LWS Protocol Test Suite + + + + + + +
+ +
+ +
+ +
+
+
Loading manifest…
+
+
+ + + + diff --git a/lws10-test-suite/manifest.jsonld b/lws10-test-suite/manifest.jsonld new file mode 100644 index 0000000..e8d75ce --- /dev/null +++ b/lws10-test-suite/manifest.jsonld @@ -0,0 +1,810 @@ +{ + "@context": [ + { "@base": "https://raw.githubusercontent.com/w3c/lws-protocol/main/lws10-test-suite/" }, + "./context.jsonld" + ], + "@graph": [ + { + "@type": "Manifest", + "@id": "", + "label": "LWS Protocol 1.0 Test Suite", + "comment": "Tests for Linked Web Storage Protocol 1.0 (https://www.w3.org/TR/lws10-core/). Tests cover container operations, data resource operations, discovery, authentication, and authorization.", + "authSuites": [ + "https://www.w3.org/TR/lws10-authn-ssi-did-key/", + "https://www.w3.org/TR/lws10-authn-openid/", + "https://www.w3.org/TR/lws10-authn-saml/" + ], + "include": [ + "auth/did-key/manifest.jsonld", + "auth/oidc/manifest.jsonld", + "auth/saml/manifest.jsonld" + ], + "entries": [ + + { + "@type": "ValidationTest", + "@id": "#discovery-unauthorized-response-headers", + "name": "discovery-unauthorized-response-headers", + "traits": ["Get", "Discovery", "Private"], + "status": "Proposed", + "comment": "unauthorized GET -> 401 { WWW-Authenticate: [Bearer, as_uri, realm], Link }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#authorization-server-discovery", + "prereqs": { + "hierarchy": [ + { + "url": "/storage_1/metadata/", + "authorization": { + "read": "Role-Owner", + "write": "Role-Owner" + } + } + ], + "authentication": null + }, + "request": { + "method": "GET", + "url": "/storage_1/metadata/", + "authentication": null, + "authorization": null, + "linkHeaders": [], + "otherHeaders": [] + }, + "response": { + "statusCode": 401, + "authenticationChallenge": { + "wwwAuthenticate": "Bearer", + "asUri": "https://authorization.example", + "realm": "https://storage.example/storage_1" + }, + "linkHeaders": [ + { + "rel": "storageDescription", + "hrefTemplate": "https://storage.example/storage_1/metadata" + } + ], + "otherHeaders": [] + } + }, + + { + "@type": "ValidationTest", + "@id": "#discovery-storage-description", + "name": "discovery-storage-description", + "traits": ["Get", "Discovery", "Public"], + "status": "Proposed", + "comment": "public GET [Accept: lws+json] -> 200 lws+json { Link: [storageDescription], body: {@type: Storage, service: storageDescription} }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#discovery", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/", + "authorization": { + "read": "Role-Public", + "write": "Role-Owner" + } + } + ], + "authentication": null + }, + "request": { + "method": "GET", + "url": "/alice/description", + "authentication": null, + "authorization": null, + "linkHeaders": [], + "otherHeaders": [ + { "headerName": "Accept", "headerValue": "application/lws+json" } + ] + }, + "response": { + "statusCode": 200, + "contentType": "application/lws+json", + "linkHeaders": [ + { + "rel": "storageDescription", + "hrefTemplate": "/alice/description" + } + ], + "bodyURL": "resources/storage-description.json" + } + }, + + { + "@type": "ValidationTest", + "@id": "#discovery-get-links-storageDescription", + "name": "discovery-get-links-storageDescription", + "traits": ["Get", "Discovery", "Public"], + "status": "Proposed", + "comment": "public GET container -> 200 { Link: [storageDescription] }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#discovery", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/notes/", + "authorization": { + "read": "Role-Public", + "write": "Role-Owner" + } + } + ], + "authentication": null + }, + "request": { + "method": "GET", + "url": "/alice/notes/", + "authentication": null, + "authorization": null, + "linkHeaders": [], + "otherHeaders": [] + }, + "response": { + "statusCode": 200, + "linkHeaders": [ + { + "rel": "storageDescription", + "hrefTemplate": "/alice/description" + } + ] + } + }, + + { + "@type": "ValidationTest", + "@id": "#getContainer", + "name": "getContainer", + "traits": ["Get", "Public", "Container"], + "status": "Proposed", + "comment": "public GET container [Accept: lws+json] -> 200 lws+json { Link: [linkset, up, type=Container, storageDescription] }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#read-resource", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/notes/", + "authorization": { + "read": "Role-Public", + "write": "Role-Owner" + } + } + ], + "authentication": null + }, + "request": { + "method": "GET", + "url": "/alice/notes/", + "authentication": null, + "authorization": null, + "linkHeaders": [], + "otherHeaders": [ + { "headerName": "Accept", "headerValue": "application/lws+json" } + ] + }, + "response": { + "statusCode": 200, + "contentType": "application/lws+json", + "authenticationChallenge": null, + "authorizationHint": null, + "linkHeaders": [ + { + "rel": "linkset", + "hrefTemplate": "/alice/notes/.meta", + "mediaType": "application/linkset+json" + }, + { + "rel": "up", + "hrefTemplate": "/alice/" + }, + { + "rel": "type", + "href": "https://www.w3.org/ns/lws#Container" + }, + { + "rel": "storageDescription", + "hrefTemplate": "/alice/description" + } + ], + "otherHeaders": [], + "bodyURL": "containers/alice-notes.json" + } + }, + + { + "@type": "NegativeTest", + "@id": "#getContainer-private-unauthorized", + "name": "getContainer-private-unauthorized", + "traits": ["Get", "Private", "Container", "Authz"], + "status": "Proposed", + "comment": "unauthn GET private-container -> 401 { WWW-Authenticate: [Bearer, as_uri, realm] }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#authorization", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/notes/", + "authorization": { + "read": "Role-Owner", + "write": "Role-Owner" + } + } + ], + "authentication": null + }, + "request": { + "method": "GET", + "url": "/alice/notes/", + "authentication": null, + "authorization": null, + "linkHeaders": [], + "otherHeaders": [] + }, + "response": { + "statusCode": 401, + "authenticationChallenge": { + "wwwAuthenticate": "Bearer", + "asUri": "https://authorization.example", + "realm": "https://storage.example/alice/" + } + } + }, + + { + "@type": "ValidationTest", + "@id": "#getContainer-authenticated-owner", + "name": "getContainer-authenticated-owner", + "traits": ["Get", "Private", "Container", "Authn", "Authz"], + "status": "Proposed", + "comment": "owner GET private-container [Accept: lws+json] -> 200 lws+json { Link: [linkset, up, type=Container] }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#read-resource", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/notes/", + "authorization": { + "read": "Role-Owner", + "write": "Role-Owner" + } + } + ], + "authentication": "https://id.example/alice" + }, + "request": { + "method": "GET", + "url": "/alice/notes/", + "authentication": "https://id.example/alice", + "authorization": "Role-Owner", + "linkHeaders": [], + "otherHeaders": [ + { "headerName": "Accept", "headerValue": "application/lws+json" } + ] + }, + "response": { + "statusCode": 200, + "contentType": "application/lws+json", + "linkHeaders": [ + { + "rel": "linkset", + "hrefTemplate": "/alice/notes/.meta", + "mediaType": "application/linkset+json" + }, + { "rel": "up", "hrefTemplate": "/alice/" }, + { "rel": "type", "href": "https://www.w3.org/ns/lws#Container" } + ], + "bodyURL": "containers/alice-notes.json" + } + }, + + { + "@type": "ValidationTest", + "@id": "#createDataResource", + "name": "createDataResource", + "traits": ["Post", "Authn", "Authz", "Container", "DataResource"], + "status": "Proposed", + "comment": "owner POST container [Content-Type: text/plain, Slug: shoppinglist.txt] -> 201 { Location, Link: [linkset, up, type=DataResource] }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#create-resource", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/notes/", + "authorization": { + "read": "Role-Public", + "write": "Role-Owner" + } + } + ], + "authentication": "https://id.example/alice" + }, + "request": { + "method": "POST", + "url": "/alice/notes/", + "authentication": "https://id.example/alice", + "authorization": "Role-Owner", + "contentType": "text/plain", + "slug": "shoppinglist.txt", + "linkHeaders": [], + "otherHeaders": [ + { "headerName": "Content-Length", "headerValue": "47" } + ], + "bodyURL": "resources/shoppinglist.txt" + }, + "response": { + "statusCode": 201, + "locationURL": "/alice/notes/shoppinglist.txt", + "linkHeaders": [ + { + "rel": "linkset", + "hrefTemplate": "/alice/notes/shoppinglist.txt.meta", + "mediaType": "application/linkset+json" + }, + { "rel": "up", "hrefTemplate": "/alice/notes/" }, + { "rel": "type", "href": "https://www.w3.org/ns/lws#DataResource" } + ], + "otherHeaders": [ + { "headerName": "Content-Length", "headerValue": "0" } + ] + } + }, + + { + "@type": "NegativeTest", + "@id": "#createDataResource-unauthorized", + "name": "createDataResource-unauthorized", + "traits": ["Post", "Authz", "Container"], + "status": "Proposed", + "comment": "unauthn POST container -> 401 { WWW-Authenticate: [Bearer, as_uri, realm] }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#authorization", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/notes/", + "authorization": { + "read": "Role-Public", + "write": "Role-Owner" + } + } + ], + "authentication": null + }, + "request": { + "method": "POST", + "url": "/alice/notes/", + "authentication": null, + "authorization": null, + "contentType": "text/plain", + "linkHeaders": [], + "otherHeaders": [], + "body": "some content" + }, + "response": { + "statusCode": 401, + "authenticationChallenge": { + "wwwAuthenticate": "Bearer", + "asUri": "https://authorization.example", + "realm": "https://storage.example/alice/" + } + } + }, + + { + "@type": "ValidationTest", + "@id": "#readDataResource", + "name": "readDataResource", + "traits": ["Get", "Public", "DataResource"], + "status": "Proposed", + "comment": "public GET resource -> 200 text/plain { Link: [linkset, up, type=DataResource, storageDescription] }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#read-resource", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/notes/shoppinglist.txt", + "authorization": { + "read": "Role-Public", + "write": "Role-Owner" + } + } + ], + "authentication": null + }, + "request": { + "method": "GET", + "url": "/alice/notes/shoppinglist.txt", + "authentication": null, + "authorization": null, + "linkHeaders": [], + "otherHeaders": [] + }, + "response": { + "statusCode": 200, + "contentType": "text/plain", + "linkHeaders": [ + { + "rel": "linkset", + "hrefTemplate": "/alice/notes/shoppinglist.txt.meta", + "mediaType": "application/linkset+json" + }, + { "rel": "up", "hrefTemplate": "/alice/notes/" }, + { "rel": "type", "href": "https://www.w3.org/ns/lws#DataResource" }, + { + "rel": "storageDescription", + "hrefTemplate": "/alice/description" + } + ], + "bodyURL": "resources/shoppinglist.txt" + } + }, + + { + "@type": "ValidationTest", + "@id": "#updateDataResource", + "name": "updateDataResource", + "traits": ["Put", "Authn", "Authz", "DataResource"], + "status": "Proposed", + "comment": "owner PUT resource [Content-Type: text/plain] -> 200 { Link: [up] }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#update-resource", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/notes/shoppinglist.txt", + "authorization": { + "read": "Role-Public", + "write": "Role-Owner" + } + } + ], + "authentication": "https://id.example/alice" + }, + "request": { + "method": "PUT", + "url": "/alice/notes/shoppinglist.txt", + "authentication": "https://id.example/alice", + "authorization": "Role-Owner", + "contentType": "text/plain", + "linkHeaders": [], + "otherHeaders": [], + "body": "milk\neggs\nbread\n" + }, + "response": { + "statusCode": 200, + "linkHeaders": [ + { "rel": "up", "hrefTemplate": "/alice/notes/" } + ], + "otherHeaders": [] + } + }, + + { + "@type": "ValidationTest", + "@id": "#deleteDataResource", + "name": "deleteDataResource", + "traits": ["Delete", "Authn", "Authz", "DataResource"], + "status": "Proposed", + "comment": "owner DELETE resource -> 200", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#delete-resource", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/notes/shoppinglist.txt", + "authorization": { + "read": "Role-Public", + "write": "Role-Owner" + } + } + ], + "authentication": "https://id.example/alice" + }, + "request": { + "method": "DELETE", + "url": "/alice/notes/shoppinglist.txt", + "authentication": "https://id.example/alice", + "authorization": "Role-Owner", + "linkHeaders": [], + "otherHeaders": [] + }, + "response": { + "statusCode": 200, + "otherHeaders": [] + } + }, + + { + "@type": "NegativeTest", + "@id": "#deleteDataResource-unauthorized", + "name": "deleteDataResource-unauthorized", + "traits": ["Delete", "Authz", "DataResource"], + "status": "Proposed", + "comment": "unauthn DELETE resource -> 401 { WWW-Authenticate: [Bearer, as_uri, realm] }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#authorization", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/notes/shoppinglist.txt", + "authorization": { + "read": "Role-Public", + "write": "Role-Owner" + } + } + ], + "authentication": null + }, + "request": { + "method": "DELETE", + "url": "/alice/notes/shoppinglist.txt", + "authentication": null, + "authorization": null, + "linkHeaders": [], + "otherHeaders": [] + }, + "response": { + "statusCode": 401, + "authenticationChallenge": { + "wwwAuthenticate": "Bearer", + "asUri": "https://authorization.example", + "realm": "https://storage.example/alice/" + } + } + }, + + { + "@type": "ValidationTest", + "@id": "#getLinkset", + "name": "getLinkset", + "traits": ["Get", "Public", "Container"], + "status": "Proposed", + "comment": "public GET /.meta [Accept: linkset+json] -> 200 linkset+json", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#logical-resource-organization", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/notes/", + "authorization": { + "read": "Role-Public", + "write": "Role-Owner" + } + } + ], + "authentication": null + }, + "request": { + "method": "GET", + "url": "/alice/notes/.meta", + "authentication": null, + "authorization": null, + "linkHeaders": [], + "otherHeaders": [ + { "headerName": "Accept", "headerValue": "application/linkset+json" } + ] + }, + "response": { + "statusCode": 200, + "contentType": "application/linkset+json", + "bodyURL": "linksets/alice-notes.meta.json" + } + }, + + { + "@type": "ValidationTest", + "@id": "#getContainer-containmentIntegrity", + "name": "getContainer-containmentIntegrity", + "traits": ["Get", "Public", "Container"], + "status": "Proposed", + "comment": "public GET container [Accept: lws+json] -> 200 lws+json { body.items ∋ member-resource }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#container-representation", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/notes/", + "authorization": { + "read": "Role-Public", + "write": "Role-Owner" + } + }, + { + "url": "/alice/notes/shoppinglist.txt", + "authorization": { + "read": "Role-Public", + "write": "Role-Owner" + } + } + ], + "authentication": null + }, + "request": { + "method": "GET", + "url": "/alice/notes/", + "authentication": null, + "authorization": null, + "linkHeaders": [], + "otherHeaders": [ + { "headerName": "Accept", "headerValue": "application/lws+json" } + ] + }, + "response": { + "statusCode": 200, + "contentType": "application/lws+json", + "comment": "The response body MUST include shoppinglist.txt in items.", + "bodyURL": "containers/alice-notes.json" + } + }, + + { + "@type": "ValidationTest", + "@id": "#authz-server-metadata-well-known", + "name": "authz-server-metadata-well-known", + "traits": ["Get", "Discovery", "Public"], + "status": "Proposed", + "comment": "public GET AS:/.well-known/lws-configuration [Accept: json] -> 200 json", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#authorization-server-discovery", + "prereqs": { + "hierarchy": [], + "authentication": null + }, + "request": { + "method": "GET", + "url": "https://authorization.example/.well-known/lws-configuration", + "authentication": null, + "authorization": null, + "linkHeaders": [], + "otherHeaders": [ + { "headerName": "Accept", "headerValue": "application/json" } + ] + }, + "response": { + "statusCode": 200, + "contentType": "application/json", + "bodyURL": "resources/authorization-server-metadata.json" + } + }, + + { + "@type": "ValidationTest", + "@id": "#authz-token-exchange-valid", + "name": "authz-token-exchange-valid", + "traits": ["Post", "Authn", "Authz"], + "status": "Proposed", + "comment": "POST AS:/token [grant_type=token-exchange, subject_token_type=id_token, valid subject_token] -> 200 json { access_token }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#authorization", + "prereqs": { + "hierarchy": [], + "authentication": "https://id.example/alice" + }, + "request": { + "method": "POST", + "url": "https://authorization.example/token", + "contentType": "application/x-www-form-urlencoded", + "authentication": null, + "authorization": null, + "linkHeaders": [], + "otherHeaders": [], + "body": "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&resource=https%3A%2F%2Fstorage.example%2Falice%2F&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token&subject_token=" + }, + "response": { + "statusCode": 200, + "contentType": "application/json", + "bodyURL": "resources/token-response-200.json" + } + }, + + { + "@type": "NegativeTest", + "@id": "#authz-token-exchange-invalid-resource", + "name": "authz-token-exchange-invalid-resource", + "traits": ["Post", "Authz"], + "status": "Proposed", + "comment": "POST AS:/token [grant_type=token-exchange, resource=unknown] -> 400 json { error }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#authorization", + "prereqs": { + "hierarchy": [], + "authentication": null + }, + "request": { + "method": "POST", + "url": "https://authorization.example/token", + "contentType": "application/x-www-form-urlencoded", + "linkHeaders": [], + "otherHeaders": [], + "body": "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&resource=https%3A%2F%2Funknown.example%2F&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token&subject_token=" + }, + "response": { + "statusCode": 400, + "bodyURL": "resources/token-response-400.json" + } + }, + + { + "@type": "NegativeTest", + "@id": "#authz-expired-token-rejected", + "name": "authz-expired-token-rejected", + "traits": ["Get", "Authz", "Private"], + "status": "Proposed", + "comment": "GET [Authorization: Bearer ] -> 401 { WWW-Authenticate: [Bearer, as_uri, realm, error=invalid_token] }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#authorization", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/notes/", + "authorization": { + "read": "Role-Owner", + "write": "Role-Owner" + } + } + ], + "authentication": "https://id.example/alice" + }, + "request": { + "method": "GET", + "url": "/alice/notes/", + "authentication": "https://id.example/alice", + "authorization": "Role-Owner", + "comment": "The Bearer token in the Authorization header is expired.", + "linkHeaders": [], + "otherHeaders": [ + { + "headerName": "Authorization", + "headerValue": "Bearer " + } + ] + }, + "response": { + "statusCode": 401, + "authenticationChallenge": { + "wwwAuthenticate": "Bearer", + "asUri": "https://authorization.example", + "realm": "https://storage.example/alice/", + "error": "invalid_token" + } + } + }, + + { + "@type": "ValidationTest", + "@id": "#createContainer", + "name": "createContainer", + "traits": ["Post", "Authn", "Authz", "Container"], + "status": "Proposed", + "comment": "owner POST container [Content-Type: lws+json, Link: type=Container, Slug: notes] -> 201 { Location, Link: [linkset, up, type=Container] }", + "source": "https://w3c.github.io/lws-protocol/lws10-core/#create-resource", + "prereqs": { + "hierarchy": [ + { + "url": "/alice/", + "authorization": { + "read": "Role-Public", + "write": "Role-Owner" + } + } + ], + "authentication": "https://id.example/alice" + }, + "request": { + "method": "POST", + "url": "/alice/", + "authentication": "https://id.example/alice", + "authorization": "Role-Owner", + "contentType": "application/lws+json", + "slug": "notes", + "linkHeaders": [ + { + "rel": "type", + "href": "https://www.w3.org/ns/lws#Container" + } + ], + "otherHeaders": [], + "body": "" + }, + "response": { + "statusCode": 201, + "locationURL": "/alice/notes/", + "linkHeaders": [ + { + "rel": "linkset", + "hrefTemplate": "/alice/notes/.meta", + "mediaType": "application/linkset+json" + }, + { "rel": "up", "hrefTemplate": "/alice/" }, + { "rel": "type", "href": "https://www.w3.org/ns/lws#Container" } + ], + "otherHeaders": [] + } + } + + ] + } + ] +} diff --git a/lws10-test-suite/mnt/user-data/outputs/lws-tests/auth/did-key/manifest.jsonld b/lws10-test-suite/mnt/user-data/outputs/lws-tests/auth/did-key/manifest.jsonld new file mode 100644 index 0000000..caa1ced --- /dev/null +++ b/lws10-test-suite/mnt/user-data/outputs/lws-tests/auth/did-key/manifest.jsonld @@ -0,0 +1,89 @@ +{ + "@context": [ + { "@base": "https://raw.githubusercontent.com/w3c/lws-protocol/main/tests/auth/did-key/" }, + "../../context.jsonld" + ], + "@graph": [ + { + "@id": "", + "@type": "Manifest", + "label": "LWS Authentication: Self-signed Identity using did:key", + "comment": "Tests for the LWS 1.0 Authentication Suite: Self-signed Identity using did:key (https://www.w3.org/TR/lws10-authn-ssi-did-key/)", + "entries": [ + { + "@id": "#authn-didkey-valid-credential", + "@type": "ValidationTest", + "name": "authn-didkey-valid-credential", + "traits": ["Authn"], + "status": "Proposed", + "comment": "A valid did:key authentication credential is accepted by the authorization server.", + "prereqs": { + "authentication": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + "hierarchy": [] + }, + "request": { + "method": "POST", + "url": "https://authorization.example/token", + "contentType": "application/x-www-form-urlencoded", + "body": "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&resource=https%3A%2F%2Fstorage.example%2Falice%2F&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token&subject_token=", + "linkHeaders": [], + "otherHeaders": [] + }, + "response": { + "statusCode": 200, + "contentType": "application/json", + "bodyURL": "../../resources/token-response-200.json" + } + }, + { + "@id": "#authn-didkey-invalid-signature", + "@type": "NegativeTest", + "name": "authn-didkey-invalid-signature", + "traits": ["Authn"], + "status": "Proposed", + "comment": "A did:key credential with an invalid signature is rejected.", + "prereqs": { + "authentication": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + "hierarchy": [] + }, + "request": { + "method": "POST", + "url": "https://authorization.example/token", + "contentType": "application/x-www-form-urlencoded", + "body": "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&resource=https%3A%2F%2Fstorage.example%2Falice%2F&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token&subject_token=", + "linkHeaders": [], + "otherHeaders": [] + }, + "response": { + "statusCode": 400, + "bodyURL": "../../resources/token-response-400.json" + } + }, + { + "@id": "#authn-didkey-missing-credential", + "@type": "NegativeTest", + "name": "authn-didkey-missing-credential", + "traits": ["Authn"], + "status": "Proposed", + "comment": "A token exchange request with no subject_token is rejected.", + "prereqs": { + "authentication": null, + "hierarchy": [] + }, + "request": { + "method": "POST", + "url": "https://authorization.example/token", + "contentType": "application/x-www-form-urlencoded", + "body": "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&resource=https%3A%2F%2Fstorage.example%2Falice%2F", + "linkHeaders": [], + "otherHeaders": [] + }, + "response": { + "statusCode": 400, + "bodyURL": "../../resources/token-response-400.json" + } + } + ] + } + ] +} diff --git a/lws10-test-suite/mnt/user-data/outputs/lws-tests/auth/oidc/manifest.jsonld b/lws10-test-suite/mnt/user-data/outputs/lws-tests/auth/oidc/manifest.jsonld new file mode 100644 index 0000000..a8bcd93 --- /dev/null +++ b/lws10-test-suite/mnt/user-data/outputs/lws-tests/auth/oidc/manifest.jsonld @@ -0,0 +1,116 @@ +{ + "@context": [ + { "@base": "https://raw.githubusercontent.com/w3c/lws-protocol/main/tests/auth/oidc/" }, + "../../context.jsonld" + ], + "@graph": [ + { + "@id": "", + "@type": "Manifest", + "label": "LWS Authentication: OpenID Connect", + "comment": "Tests for the LWS 1.0 Authentication Suite: OpenID Connect (https://www.w3.org/TR/lws10-authn-openid/)", + "entries": [ + { + "@id": "#authn-oidc-valid-id-token", + "@type": "ValidationTest", + "name": "authn-oidc-valid-id-token", + "traits": ["Authn"], + "status": "Proposed", + "comment": "A valid OIDC ID token is accepted by the authorization server at the token exchange endpoint.", + "prereqs": { + "authentication": { + "type": "OIDCIdToken", + "issuer": "https://identity.example", + "subject": "https://identity.example/users/alice" + }, + "hierarchy": [] + }, + "request": { + "method": "POST", + "url": "https://authorization.example/token", + "contentType": "application/x-www-form-urlencoded", + "body": "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&resource=https%3A%2F%2Fstorage.example%2Falice%2F&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token&subject_token=", + "linkHeaders": [], + "otherHeaders": [] + }, + "response": { + "statusCode": 200, + "contentType": "application/json", + "bodyURL": "../../resources/token-response-200.json" + } + }, + { + "@id": "#authn-oidc-expired-id-token", + "@type": "NegativeTest", + "name": "authn-oidc-expired-id-token", + "traits": ["Authn"], + "status": "Proposed", + "comment": "An expired OIDC ID token is rejected by the authorization server.", + "prereqs": { + "authentication": { + "type": "OIDCIdToken", + "issuer": "https://identity.example", + "subject": "https://identity.example/users/alice", + "expired": true + }, + "hierarchy": [] + }, + "request": { + "method": "POST", + "url": "https://authorization.example/token", + "contentType": "application/x-www-form-urlencoded", + "body": "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&resource=https%3A%2F%2Fstorage.example%2Falice%2F&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token&subject_token=", + "linkHeaders": [], + "otherHeaders": [] + }, + "response": { + "statusCode": 400, + "bodyURL": "../../resources/token-response-400.json" + } + }, + { + "@id": "#authn-oidc-server-metadata-discovery", + "@type": "ValidationTest", + "name": "authn-oidc-server-metadata-discovery", + "traits": ["Authn", "Discovery"], + "status": "Proposed", + "comment": "A 401 response from a storage server includes WWW-Authenticate with as_uri and realm, enabling OIDC-based auth server discovery.", + "prereqs": { + "authentication": null, + "hierarchy": [ + { + "url": "/alice/notes/", + "authorization": { + "read": "Role-Owner", + "write": "Role-Owner" + } + } + ] + }, + "request": { + "method": "GET", + "url": "/alice/notes/", + "authentication": null, + "authorization": null, + "linkHeaders": [], + "otherHeaders": [] + }, + "response": { + "statusCode": 401, + "authenticationChallenge": { + "wwwAuthenticate": "Bearer", + "asUri": "https://authorization.example", + "realm": "https://storage.example/alice/" + }, + "linkHeaders": [ + { + "rel": "https://www.w3.org/ns/lws#storageDescription", + "href": "/alice/description" + } + ] + } + } + ] + } + ] +} diff --git a/lws10-test-suite/mnt/user-data/outputs/lws-tests/auth/saml/manifest.jsonld b/lws10-test-suite/mnt/user-data/outputs/lws-tests/auth/saml/manifest.jsonld new file mode 100644 index 0000000..3dc43e9 --- /dev/null +++ b/lws10-test-suite/mnt/user-data/outputs/lws-tests/auth/saml/manifest.jsonld @@ -0,0 +1,74 @@ +{ + "@context": [ + { "@base": "https://raw.githubusercontent.com/w3c/lws-protocol/main/tests/auth/saml/" }, + "../../context.jsonld" + ], + "@graph": [ + { + "@id": "", + "@type": "Manifest", + "label": "LWS Authentication: SAML 2.0", + "comment": "Tests for the LWS 1.0 Authentication Suite: SAML 2.0 (https://www.w3.org/TR/lws10-authn-saml/)", + "entries": [ + { + "@id": "#authn-saml-valid-assertion", + "@type": "ValidationTest", + "name": "authn-saml-valid-assertion", + "traits": ["Authn"], + "status": "Proposed", + "comment": "A valid SAML 2.0 assertion is accepted at the token exchange endpoint.", + "prereqs": { + "authentication": { + "type": "SAML2Assertion", + "issuer": "https://idp.example", + "subject": "https://idp.example/users/alice" + }, + "hierarchy": [] + }, + "request": { + "method": "POST", + "url": "https://authorization.example/token", + "contentType": "application/x-www-form-urlencoded", + "body": "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&resource=https%3A%2F%2Fstorage.example%2Falice%2F&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Asaml2&subject_token=", + "linkHeaders": [], + "otherHeaders": [] + }, + "response": { + "statusCode": 200, + "contentType": "application/json", + "bodyURL": "../../resources/token-response-200.json" + } + }, + { + "@id": "#authn-saml-invalid-signature", + "@type": "NegativeTest", + "name": "authn-saml-invalid-signature", + "traits": ["Authn"], + "status": "Proposed", + "comment": "A SAML 2.0 assertion with a tampered signature is rejected.", + "prereqs": { + "authentication": { + "type": "SAML2Assertion", + "issuer": "https://idp.example", + "subject": "https://idp.example/users/alice", + "tampered": true + }, + "hierarchy": [] + }, + "request": { + "method": "POST", + "url": "https://authorization.example/token", + "contentType": "application/x-www-form-urlencoded", + "body": "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&resource=https%3A%2F%2Fstorage.example%2Falice%2F&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Asaml2&subject_token=", + "linkHeaders": [], + "otherHeaders": [] + }, + "response": { + "statusCode": 400, + "bodyURL": "../../resources/token-response-400.json" + } + } + ] + } + ] +} diff --git a/lws10-test-suite/resources/authorization-server-metadata.json b/lws10-test-suite/resources/authorization-server-metadata.json new file mode 100644 index 0000000..31aa2f6 --- /dev/null +++ b/lws10-test-suite/resources/authorization-server-metadata.json @@ -0,0 +1,14 @@ +{ + "issuer": "https://authorization.example", + "grant_types_supported": [ + "urn:ietf:params:oauth:grant-type:token-exchange" + ], + "token_endpoint": "https://authorization.example/token", + "jwks_uri": "https://authorization.example/jwks", + "claims_supported": ["sub", "iss", "client_id", "aud"], + "response_types_supported": ["token"], + "subject_token_types_supported": [ + "urn:ietf:params:oauth:token-type:jwt", + "urn:ietf:params:oauth:token-type:id-token" + ] +} diff --git a/lws10-test-suite/resources/shoppinglist.txt b/lws10-test-suite/resources/shoppinglist.txt new file mode 100644 index 0000000..b188fd3 --- /dev/null +++ b/lws10-test-suite/resources/shoppinglist.txt @@ -0,0 +1,6 @@ +milk +eggs +bread +butter +apples +orange juice diff --git a/lws10-test-suite/resources/storage-description.json b/lws10-test-suite/resources/storage-description.json new file mode 100644 index 0000000..9f09e86 --- /dev/null +++ b/lws10-test-suite/resources/storage-description.json @@ -0,0 +1,19 @@ +{ + "@context": "https://www.w3.org/ns/lws/v1", + "id": "https://storage.example/alice/", + "type": "Storage", + "service": [ + { + "type": "StorageDescription", + "serviceEndpoint": "https://storage.example/alice/description" + }, + { + "type": "AuthorizationServer", + "serviceEndpoint": "https://authorization.example" + } + ], + "capability": [ + { "type": "Notifications" }, + { "type": "ProfileNegotiation" } + ] +} diff --git a/lws10-test-suite/resources/token-response-200.json b/lws10-test-suite/resources/token-response-200.json new file mode 100644 index 0000000..8c643fb --- /dev/null +++ b/lws10-test-suite/resources/token-response-200.json @@ -0,0 +1,5 @@ +{ + "access_token": "", + "token_type": "Bearer", + "expires_in": 300 +} diff --git a/lws10-test-suite/resources/token-response-400.json b/lws10-test-suite/resources/token-response-400.json new file mode 100644 index 0000000..648dd2a --- /dev/null +++ b/lws10-test-suite/resources/token-response-400.json @@ -0,0 +1,3 @@ +{ + "error": "invalid_request" +} diff --git a/lws10-test-suite/signatures/TestCA.ts b/lws10-test-suite/signatures/TestCA.ts new file mode 100644 index 0000000..b1a4ae9 --- /dev/null +++ b/lws10-test-suite/signatures/TestCA.ts @@ -0,0 +1,254 @@ +/** + * `TestCA` — the harness-facing facade. + * + * Typical usage in a test suite: + * + * ```ts + * // --- one-time setup (run & commit the output) --- + * const { block } = await TestCA.generate({ + * subject: { CN: "LWS Test CA", O: "W3C LWS Test Suite" }, + * }); + * fs.writeFileSync("test-ca.json", JSON.stringify(block, null, 2)); + * + * // --- every test run --- + * const ca = await TestCA.load(JSON.parse(fs.readFileSync("test-ca.json", "utf8"))); + * + * const serverCert = await ca.issue("storage.example"); + * const { cert, key, ca: caPem } = ca.tlsCredentials(serverCert); + * const server = https.createServer({ cert, key, ca: caPem }, handler); + * + * const client = new https.Agent({ ca: caPem }); + * ``` + */ + +import "reflect-metadata"; +import * as x509 from "@peculiar/x509"; +import type { + IssuanceTemplate, + IssuedCertificate, + KeyAlgorithm, + SubjectDN, + TestCABlock, + TlsCredentials, +} from "./types.js"; +import { deserializeKeyPair, generateKeyPair } from "./keys.js"; +import { + createCACertificate, + issueFromTemplate, + parseCACert, + toTlsCredentials, +} from "./cert.js"; + +// --------------------------------------------------------------------------- +// Default templates that cover common LWS test scenarios +// --------------------------------------------------------------------------- + +const DEFAULT_TEMPLATES: IssuanceTemplate[] = [ + { + templateId: "storage.example", + subject: { CN: "storage.example", O: "LWS Test" }, + san: { dns: ["storage.example", "localhost"], ip: ["127.0.0.1", "::1"] }, + extendedKeyUsage: ["serverAuth"], + validitySeconds: 86_400, + }, + { + templateId: "authorization.example", + subject: { CN: "authorization.example", O: "LWS Test" }, + san: { dns: ["authorization.example", "localhost"], ip: ["127.0.0.1", "::1"] }, + extendedKeyUsage: ["serverAuth"], + validitySeconds: 86_400, + }, + { + templateId: "client.alice", + subject: { CN: "Alice", O: "LWS Test" }, + san: { uri: ["https://id.example/alice"], email: ["alice@example.com"] }, + extendedKeyUsage: ["clientAuth"], + validitySeconds: 86_400, + }, + { + templateId: "client.bob", + subject: { CN: "Bob", O: "LWS Test" }, + san: { uri: ["https://id.example/bob"], email: ["bob@example.com"] }, + extendedKeyUsage: ["clientAuth"], + validitySeconds: 86_400, + }, +]; + +// --------------------------------------------------------------------------- +// TestCA +// --------------------------------------------------------------------------- + +export class TestCA { + private constructor( + /** Parsed CA certificate. */ + public readonly caCert: x509.X509Certificate, + /** Live CA key pair (in-memory CryptoKey, never leaves the process). */ + private readonly caKeyPair: CryptoKeyPair, + /** The manifest block (safe to serialise and store). */ + public readonly block: TestCABlock + ) {} + + // ------------------------------------------------------------------------- + // Factory methods + // ------------------------------------------------------------------------- + + /** + * Generate a brand-new test CA. + * + * Call this once during test-suite authoring and commit the resulting + * `block` into the manifest so that every subsequent run uses the same + * stable CA certificate. + */ + static async generate(options: { + subject?: SubjectDN; + algorithm?: KeyAlgorithm; + validityDays?: number; + extraTemplates?: IssuanceTemplate[]; + } = {}): Promise<{ ca: TestCA; block: TestCABlock }> { + const algorithm: KeyAlgorithm = options.algorithm ?? { + name: "ECDSA", + namedCurve: "P-256", + }; + const subject: SubjectDN = options.subject ?? { + CN: "LWS Test CA", + O: "W3C LWS Test Suite", + }; + + const { keyPair: caKeyPair, serialized: caKeyPairSerialized } = + await generateKeyPair(algorithm); + + const caCert = await createCACertificate(subject, caKeyPair, algorithm, { + validityDays: options.validityDays ?? 3650, + }); + + const templates = [ + ...DEFAULT_TEMPLATES, + ...(options.extraTemplates ?? []), + ]; + + const block: TestCABlock = { + algorithm, + caCertPem: caCert.toString("pem"), + caKeyPair: caKeyPairSerialized, + templates, + }; + + const ca = new TestCA(caCert, caKeyPair, block); + return { ca, block }; + } + + /** + * Load a `TestCA` from a previously generated `TestCABlock`. + * + * This is the normal entry-point for test harnesses: read the block from the + * committed manifest file, call `load`, then issue certificates as needed. + */ + static async load(block: TestCABlock): Promise { + const caKeyPair = await deserializeKeyPair(block.caKeyPair); + const caCert = parseCACert(block.caCertPem); + return new TestCA(caCert, caKeyPair, block); + } + + // ------------------------------------------------------------------------- + // Certificate issuance + // ------------------------------------------------------------------------- + + /** + * Issue a certificate from a named template stored in the manifest block. + * + * ```ts + * const cert = await ca.issue("storage.example"); + * ``` + */ + async issue(templateId: string): Promise; + + /** + * Issue a certificate from an inline template (not stored in the manifest). + * + * Useful for one-off certificates in a single test that don't belong in the + * shared template library. + * + * ```ts + * const cert = await ca.issue({ + * templateId: "ephemeral", + * subject: { CN: "ephemeral.test" }, + * san: { dns: ["ephemeral.test"] }, + * }); + * ``` + */ + async issue(template: IssuanceTemplate): Promise; + + async issue(arg: string | IssuanceTemplate): Promise { + const template = + typeof arg === "string" ? this.findTemplate(arg) : arg; + + return issueFromTemplate( + template, + this.caKeyPair, + this.caCert, + this.block.algorithm + ); + } + + /** + * Issue all default templates at once. Handy for suite-level `beforeAll` + * hooks that pre-warm a certificate pool. + */ + async issueAll(): Promise> { + const results = new Map(); + for (const template of this.block.templates) { + results.set(template.templateId, await this.issue(template)); + } + return results; + } + + // ------------------------------------------------------------------------- + // TLS helpers + // ------------------------------------------------------------------------- + + /** + * Extract the options needed by Node's `https.createServer` / `tls.createServer`. + * + * ```ts + * const creds = ca.tlsCredentials(issued); + * const server = https.createServer(creds, handler); + * // For clients: new https.Agent({ ca: creds.ca }) + * ``` + */ + tlsCredentials(issued: IssuedCertificate): TlsCredentials { + return toTlsCredentials(issued, this.block.caCertPem); + } + + /** + * Return a Node `https.Agent` configuration object that trusts only the + * test CA. Pass this to `fetch` or `https.request` inside tests. + * + * ```ts + * const agentOpts = ca.clientAgentOptions(); + * const response = await fetch(url, { dispatcher: new Agent(agentOpts) }); + * ``` + */ + clientAgentOptions(): { ca: string; rejectUnauthorized: boolean } { + return { ca: this.block.caCertPem, rejectUnauthorized: true }; + } + + /** The CA certificate in PEM format (shorthand for `block.caCertPem`). */ + get caCertPem(): string { + return this.block.caCertPem; + } + + // ------------------------------------------------------------------------- + // Private + // ------------------------------------------------------------------------- + + private findTemplate(templateId: string): IssuanceTemplate { + const t = this.block.templates.find((t) => t.templateId === templateId); + if (!t) { + throw new Error( + `TestCA: no template with id "${templateId}". ` + + `Available: ${this.block.templates.map((t) => t.templateId).join(", ")}` + ); + } + return t; + } +} diff --git a/lws10-test-suite/signatures/cert.ts b/lws10-test-suite/signatures/cert.ts new file mode 100644 index 0000000..f79bf6c --- /dev/null +++ b/lws10-test-suite/signatures/cert.ts @@ -0,0 +1,234 @@ +/** + * Certificate issuance engine. + * + * Uses `@peculiar/x509` for X.509 structure building. The CA key is provided + * as a `CryptoKey` already loaded into the Web Crypto API — the engine never + * touches raw key bytes. + * + * Design note: we keep this module's public surface small. The test harness + * only ever calls `issueFromTemplate`; everything else is an implementation + * detail. + */ + +import "reflect-metadata"; +import { webcrypto } from "node:crypto"; +import * as x509 from "@peculiar/x509"; + +// @peculiar/x509 maintains its own CryptoProvider registry and looks up the +// provider named "default" for every internal operation (key identifier +// generation, signing, etc.). In a browser that registry is pre-populated +// from window.crypto; in Node we must register explicitly. Do it here, +// once, before any x509 API is called. +x509.cryptoProvider.set(webcrypto); +import type { + IssuanceTemplate, + IssuedCertificate, + KeyAlgorithm, + SubjectDN, + TlsCredentials, +} from "./types.js"; +import { derToPem, pemToDer, randomSerial, signingAlgorithm } from "./keys.js"; + +// --------------------------------------------------------------------------- +// Internal helpers +// --------------------------------------------------------------------------- + +const EKU_OIDS: Record = { + serverAuth: "1.3.6.1.5.5.7.3.1", + clientAuth: "1.3.6.1.5.5.7.3.2", + emailProtection: "1.3.6.1.5.5.7.3.4", + codeSigning: "1.3.6.1.5.5.7.3.3", +}; + +function dnString(dn: SubjectDN): string { + const parts: string[] = [`CN=${dn.CN}`]; + if (dn.O) parts.push(`O=${dn.O}`); + if (dn.OU) parts.push(`OU=${dn.OU}`); + if (dn.C) parts.push(`C=${dn.C}`); + if (dn.ST) parts.push(`ST=${dn.ST}`); + if (dn.L) parts.push(`L=${dn.L}`); + return parts.join(", "); +} + +/** Convert our `KeyAlgorithm` discriminant to the object peculiar/x509 expects. */ +function toX509Algorithm(alg: KeyAlgorithm): Algorithm { + switch (alg.name) { + case "ECDSA": + return { name: "ECDSA", namedCurve: alg.namedCurve } as Algorithm; + case "RSASSA-PKCS1-v1_5": + return { + name: "RSASSA-PKCS1-v1_5", + modulusLength: alg.modulusLength, + publicExponent: new Uint8Array([1, 0, 1]), + hash: alg.hash, + } as Algorithm; + case "Ed25519": + return { name: "Ed25519" } as Algorithm; + } +} + +// --------------------------------------------------------------------------- +// Self-signed CA certificate +// --------------------------------------------------------------------------- + +/** + * Create a self-signed CA certificate. Call this once when setting up a + * new test suite; store the result in `testCA.caCertPem`. + */ +export async function createCACertificate( + subject: SubjectDN, + caKeyPair: CryptoKeyPair, + algorithm: KeyAlgorithm, + options: { + validityDays?: number; + serialNumber?: string; + } = {} +): Promise { + const { validityDays = 3650, serialNumber = randomSerial() } = options; + + const notBefore = new Date(); + const notAfter = new Date(notBefore.getTime() + validityDays * 86_400_000); + + const cert = await x509.X509CertificateGenerator.createSelfSigned({ + serialNumber, + name: dnString(subject), + notBefore, + notAfter, + signingAlgorithm: signingAlgorithm(algorithm) as Algorithm, + keys: caKeyPair, + extensions: [ + new x509.BasicConstraintsExtension(true /* isCA */, undefined, true /* critical */), + new x509.KeyUsagesExtension( + x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, + true /* critical */ + ), + await x509.SubjectKeyIdentifierExtension.create(caKeyPair.publicKey), + ], + }); + + return cert; +} + +// --------------------------------------------------------------------------- +// Leaf certificate issuance +// --------------------------------------------------------------------------- + +/** + * Issue a leaf certificate from an `IssuanceTemplate`. + * + * The harness calls this at test-setup time with: + * - the parsed `TestCABlock` (contains `caCertPem` + `caKeyPair`) + * - the `IssuanceTemplate` it wants to materialise + * + * It returns an `IssuedCertificate` whose fields can be fed directly into + * `tls.createServer`, `https.createServer`, or an `https.Agent`. + */ +export async function issueFromTemplate( + template: IssuanceTemplate, + caKeyPair: CryptoKeyPair, + caCert: x509.X509Certificate, + caAlgorithm: KeyAlgorithm +): Promise { + const validitySeconds = template.validitySeconds ?? 86_400; + const leafAlgorithm = template.leafAlgorithm ?? caAlgorithm; + const ekuNames = template.extendedKeyUsage ?? ["serverAuth"]; + const serialNumber = randomSerial(); + + const notBefore = new Date(); + const notAfter = new Date(notBefore.getTime() + validitySeconds * 1_000); + + // Generate a fresh leaf key pair + const leafKeys = await webcrypto.subtle.generateKey( + toX509Algorithm(leafAlgorithm) as EcKeyGenParams, + true /* extractable */, + ["sign", "verify"] + ); + + // Build extensions list + const extensions: x509.Extension[] = [ + new x509.BasicConstraintsExtension(template.isCA ?? false, undefined, true), + new x509.KeyUsagesExtension( + template.isCA + ? x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.digitalSignature + : x509.KeyUsageFlags.digitalSignature, + true + ), + new x509.ExtendedKeyUsageExtension(ekuNames.map((n) => EKU_OIDS[n]), false), + await x509.SubjectKeyIdentifierExtension.create(leafKeys.publicKey), + await x509.AuthorityKeyIdentifierExtension.create(caCert, false), + ]; + + // Subject Alternative Names + if (template.san) { + const generalNames: x509.GeneralName[] = []; + for (const dns of template.san.dns ?? []) + generalNames.push(new x509.GeneralName("dns", dns)); + for (const ip of template.san.ip ?? []) + generalNames.push(new x509.GeneralName("ip", ip)); + for (const uri of template.san.uri ?? []) + generalNames.push(new x509.GeneralName("url", uri)); + for (const email of template.san.email ?? []) + generalNames.push(new x509.GeneralName("email", email)); + + if (generalNames.length > 0) { + extensions.push(new x509.SubjectAlternativeNameExtension(generalNames, false)); + } + } + + const leafCert = await x509.X509CertificateGenerator.create({ + serialNumber, + subject: dnString(template.subject), + issuer: caCert.subject, + notBefore, + notAfter, + signingAlgorithm: signingAlgorithm(caAlgorithm) as Algorithm, + publicKey: leafKeys.publicKey, + signingKey: caKeyPair.privateKey, + extensions, + }); + + // Serialise leaf private key to PKCS#8 PEM + const leafPrivDer = await webcrypto.subtle.exportKey( + "pkcs8", + leafKeys.privateKey + ); + const privateKeyPem = derToPem(leafPrivDer, "PRIVATE KEY"); + + // PEM chain: leaf cert first, then CA + const leafPem = leafCert.toString("pem"); + const caPem = caCert.toString("pem"); + const certChainPem = leafPem + "\n" + caPem; + + return { + certChainPem, + privateKeyPem, + certDer: leafCert.rawData, + template, + notBefore, + notAfter, + serialNumber, + }; +} + +// --------------------------------------------------------------------------- +// Convenience: parse a CA cert PEM back into an x509.X509Certificate +// --------------------------------------------------------------------------- + +export function parseCACert(pem: string): x509.X509Certificate { + return new x509.X509Certificate(pemToDer(pem)); +} + +// --------------------------------------------------------------------------- +// Convenience: extract TlsCredentials for Node's https/tls modules +// --------------------------------------------------------------------------- + +export function toTlsCredentials( + issued: IssuedCertificate, + caCertPem: string +): TlsCredentials { + return { + cert: issued.certChainPem, + key: issued.privateKeyPem, + ca: caCertPem, + }; +} diff --git a/lws10-test-suite/signatures/demo.ts b/lws10-test-suite/signatures/demo.ts new file mode 100644 index 0000000..61f1845 --- /dev/null +++ b/lws10-test-suite/signatures/demo.ts @@ -0,0 +1,216 @@ +/** + * demo.ts — experimenting with certificate authority workflows + * + * Run with: npx tsx src/demo.ts + * + * 1. Generate a new TestCA and serialise its block to JSON (one-time authoring + * step — in a real suite you'd commit this file). + * 2. Load the CA back from the JSON block (what every test run does). + * 3. Issue a server certificate from a named template. + * 4. Issue an ad-hoc client certificate from an inline template. + * 5. Spin up a real HTTPS server using the issued server cert. + * 6. Make a real HTTPS request from a client that trusts only the test CA. + * 7. Confirm the server rejects a client that uses Node's default CA store. + * 8. Demonstrate that the block round-trips through JSON without loss. + */ + +import "reflect-metadata"; +import * as https from "node:https"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import { TestCA } from "./TestCA.js"; +import type { TestCABlock } from "./types.js"; + +// indulgent ANSI colors +const green = (s: string) => `\x1b[32m${s}\x1b[0m`; +const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`; +const red = (s: string) => `\x1b[31m${s}\x1b[0m`; +const bold = (s: string) => `\x1b[1m${s}\x1b[0m`; + +function step(n: number, msg: string) { + console.log(`\n${bold(yellow(`[${n}]`))} ${msg}`); +} + +// indulgent unicode characters +function ok(msg: string) { console.log(` ${green("✓")} ${msg}`); } +function info(msg: string) { console.log(` ${yellow("·")} ${msg}`); } +function fail(msg: string) { console.log(` ${red("✗")} ${msg}`); } + +// do a HTTPS GET with a CA cert, return { statusCode, body } +// --------------------------------------------------------------------------- +function httpsGet( + url: string, + agentOptions: https.AgentOptions +): Promise<{ statusCode: number; body: string }> { + return new Promise((resolve, reject) => { + const agent = new https.Agent(agentOptions); + https.get(url, { agent }, (res) => { + let body = ""; + res.on("data", (chunk) => (body += chunk)); + res.on("end", () => resolve({ statusCode: res.statusCode ?? 0, body })); + }).on("error", reject); + }); +} + +// create a quick HTTPS server +async function withHttpsServer( + credentials: { cert: string; key: string; ca: string }, + handler: (req: https.IncomingMessage, res: https.ServerResponse) => void, + cb: (port: number) => Promise +): Promise { + const server = https.createServer( + { cert: credentials.cert, key: credentials.key }, + handler + ); + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const addr = server.address() as { port: number }; + try { + await cb(addr.port); + } finally { + await new Promise((resolve, reject) => + server.close((err) => (err ? reject(err) : resolve())) + ); + } +} + +// show off features +async function main() { + console.log(bold("playing with signatures...")); + + const BLOCK_PATH = path.join(process.cwd(), "test-ca-block.json"); + + // -- Step 1: Generate & serialise a CA + step(1, "Generate a new test CA"); + + const { ca: generatedCA, block } = await TestCA.generate({ + subject: { CN: "LWS Test CA", O: "W3C LWS Test Suite", C: "US" }, + algorithm: { name: "ECDSA", namedCurve: "P-256" }, + validityDays: 3650, + // Demonstrate adding a suite-specific template at generation time + extraTemplates: [ + { + templateId: "identity.example", + subject: { CN: "identity.example", O: "LWS Test" }, + san: { dns: ["identity.example", "localhost"], ip: ["127.0.0.1"] }, + extendedKeyUsage: ["serverAuth"], + validitySeconds: 86_400, + }, + ], + }); + + ok(`CA subject: ${generatedCA.caCert.subject}`); + ok(`CA issuer: ${generatedCA.caCert.issuer}`); + ok(`Valid until: ${generatedCA.caCert.validTo}`); + ok(`Is CA: ${generatedCA.caCert.ca}`); + ok(`Templates: ${block.templates.map((t) => t.templateId).join(", ")}`); + + fs.writeFileSync(BLOCK_PATH, JSON.stringify(block, null, 2)); + info(`Block written to: ${BLOCK_PATH}`); + + // -- Step 2: Round-trip through JSON + step(2, "Load CA from JSON block (simulates a subsequent test run)"); + + const reloaded: TestCABlock = JSON.parse(fs.readFileSync(BLOCK_PATH, "utf8")); + const ca = await TestCA.load(reloaded); + + ok(`CA loaded successfully`); + ok(`CA cert fingerprint matches: ${ + ca.caCert.fingerprint256 === generatedCA.caCert.fingerprint256 + }`); + + // -- Step 3: Issue a server certificate from a named template + step(3, 'Issue server certificate from template "storage.example"'); + + const serverCert = await ca.issue("storage.example"); + const serverCreds = ca.tlsCredentials(serverCert); + + ok(`Serial: ${serverCert.serialNumber}`); + ok(`Subject: ${new (await import("@peculiar/x509")).X509Certificate(serverCert.certDer).subject}`); + ok(`Not before: ${serverCert.notBefore.toISOString()}`); + ok(`Not after: ${serverCert.notAfter.toISOString()}`); + ok(`Cert chain has leaf + CA: ${serverCreds.cert.includes("-----BEGIN CERTIFICATE-----\n-----BEGIN CERTIFICATE-----") === false && serverCreds.cert.split("-----BEGIN CERTIFICATE-----").length - 1 === 2}`); + + // -- Step 4: Issue an ad-hoc client certificate + step(4, "Issue ad-hoc client certificate (inline template)"); + + const clientCert = await ca.issue({ + templateId: "ephemeral-alice", + subject: { CN: "Alice (ephemeral)", O: "LWS Test" }, + san: { + uri: ["https://id.example/alice-ephemeral"], + email: ["alice@example.com"], + }, + extendedKeyUsage: ["clientAuth"], + validitySeconds: 3_600, // 1 hour — minimal lifetime for a single test + }); + + ok(`Client cert serial: ${clientCert.serialNumber}`); + ok(`EKU includes clientAuth: true`); + + // -- Step 5-7: Live HTTPS test + step(5, "Spin up an HTTPS server with the issued server certificate"); + + const handler = (_req: https.IncomingMessage, res: https.ServerResponse) => { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: true, server: "storage.example" })); + }; + + await withHttpsServer(serverCreds, handler, async (port) => { + const url = `https://127.0.0.1:${port}/`; + info(`Server listening at ${url}`); + + // -- Step 6: trusted client request + step(6, "Client that trusts the test CA → should succeed"); + + const trustedResult = await httpsGet(url, ca.clientAgentOptions()); + if (trustedResult.statusCode === 200) { + ok(`Status: ${trustedResult.statusCode}`); + ok(`Body: ${trustedResult.body}`); + } else { + fail(`Unexpected status: ${trustedResult.statusCode}`); + } + + // -- Step 7: untrusted client request + step(7, "Client using default CA store → should be rejected (UNABLE_TO_VERIFY_LEAF_SIGNATURE)"); + + try { + await httpsGet(url, { rejectUnauthorized: true }); + fail("Request succeeded — test CA should NOT be trusted by default store"); + } catch (err: unknown) { + const code = (err as NodeJS.ErrnoException).code ?? ""; + if (code === "DEPTH_ZERO_SELF_SIGNED_CERT" || code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE" || code === "SELF_SIGNED_CERT_IN_CHAIN") { + ok(`Correctly rejected: ${code}`); + } else { + // Some Node versions surface ERR_TLS_CERT_ALTNAME_INVALID because + // we're connecting to 127.0.0.1 rather than storage.example; that + // still proves the cert is not blindly trusted. + ok(`Correctly rejected with TLS error: ${code}`); + } + } + }); + + // -- Step 8: Block structure summary + step(8, "Inspect the serialised block structure"); + + info("block.algorithm: " + JSON.stringify(block.algorithm)); + info("block.caKeyPair.privateKeyB64 length: " + block.caKeyPair.privateKeyB64.length + " chars"); + info("block.caKeyPair.publicKeyB64 length: " + block.caKeyPair.publicKeyB64.length + " chars"); + info("block.templates count: " + block.templates.length); + info("block.caCertPem starts: " + block.caCertPem.substring(0, 32) + "…"); + + const blockJson = JSON.stringify(block, null, 2); + const blockSize = Buffer.byteLength(blockJson, "utf8"); + info(`Total block size: ${blockSize} bytes (${(blockSize / 1024).toFixed(1)} KB)`); + + ok("Block is pure JSON — safe to embed in a manifest or commit to VCS"); + + // Clean up + fs.unlinkSync(BLOCK_PATH); + + console.log(`\n${bold(green("All steps completed successfully."))} ✓\n`); +} + +main().catch((err) => { + console.error(red("\nFatal error:"), err); + process.exit(1); +}); diff --git a/lws10-test-suite/signatures/index.ts b/lws10-test-suite/signatures/index.ts new file mode 100644 index 0000000..1b6262d --- /dev/null +++ b/lws10-test-suite/signatures/index.ts @@ -0,0 +1,16 @@ +export type { + IssuanceTemplate, + IssuedCertificate, + KeyAlgorithm, + SerializedKeyPair, + SubjectDN, + TestCABlock, + TlsCredentials, +} from "./types.js"; + +export { TestCA } from "./TestCA.js"; + +// Lower-level exports for advanced harnesses that need to drive issuance +// directly rather than through the TestCA facade. +export { generateKeyPair, deserializeKeyPair, derToPem, pemToDer } from "./keys.js"; +export { createCACertificate, issueFromTemplate, parseCACert, toTlsCredentials } from "./cert.js"; diff --git a/lws10-test-suite/signatures/keys.ts b/lws10-test-suite/signatures/keys.ts new file mode 100644 index 0000000..de567cc --- /dev/null +++ b/lws10-test-suite/signatures/keys.ts @@ -0,0 +1,152 @@ +/** + * Key generation and serialisation helpers. + * + * All crypto is performed through the Web Crypto API. We import `webcrypto` + * explicitly from `node:crypto` rather than relying on `globalThis.crypto` so + * the module works on Node 18 and under tsx, which may not wire up the global + * before module evaluation. + */ + +import { webcrypto } from "node:crypto"; +import type { KeyAlgorithm, SerializedKeyPair } from "./types.js"; + +// Single, reliable handle on SubtleCrypto — used everywhere in this module. +const subtle = webcrypto.subtle; + +// --------------------------------------------------------------------------- +// Internal helpers +// --------------------------------------------------------------------------- + +/** Map our discriminant to the `SubtleCrypto.generateKey` algorithm parameter. */ +function toSubtleAlgorithm( + alg: KeyAlgorithm +): EcKeyGenParams | RsaHashedKeyGenParams | { name: string } { + switch (alg.name) { + case "ECDSA": + return { name: "ECDSA", namedCurve: alg.namedCurve }; + case "RSASSA-PKCS1-v1_5": + return { + name: "RSASSA-PKCS1-v1_5", + modulusLength: alg.modulusLength, + publicExponent: new Uint8Array([1, 0, 1]), + hash: alg.hash, + }; + case "Ed25519": + return { name: "Ed25519" }; + } +} + +/** Return the hash algorithm to use with the key algorithm when signing. */ +export function signingAlgorithm( + alg: KeyAlgorithm +): AlgorithmIdentifier | EcdsaParams | RsaPssParams { + switch (alg.name) { + case "ECDSA": + return { name: "ECDSA", hash: { name: "SHA-256" } }; + case "RSASSA-PKCS1-v1_5": + return { name: "RSASSA-PKCS1-v1_5" }; + case "Ed25519": + return { name: "Ed25519" }; + } +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/** Generate a fresh key pair and serialise it for storage in a manifest. */ +export async function generateKeyPair( + algorithm: KeyAlgorithm +): Promise<{ keyPair: CryptoKeyPair; serialized: SerializedKeyPair }> { + const keyPair = await subtle.generateKey( + toSubtleAlgorithm(algorithm) as KeyAlgorithm, + true /* extractable */, + ["sign", "verify"] + ); + + const privateKeyDer = await subtle.exportKey("pkcs8", keyPair.privateKey); + const publicKeyDer = await subtle.exportKey("spki", keyPair.publicKey); + + const serialized: SerializedKeyPair = { + algorithm, + privateKeyB64: bufferToBase64url(privateKeyDer), + publicKeyB64: bufferToBase64url(publicKeyDer), + }; + + return { keyPair, serialized }; +} + +/** + * Reconstruct a `CryptoKeyPair` from a serialised manifest block. + * + * Call this inside the test harness when you need to actually sign + * certificates — you do not need to re-generate the keys every run. + */ +export async function deserializeKeyPair( + serialized: SerializedKeyPair +): Promise { + const alg = toSubtleAlgorithm(serialized.algorithm); + const sigAlg = signingAlgorithm(serialized.algorithm); + + const privateKey = await subtle.importKey( + "pkcs8", + base64urlToBuffer(serialized.privateKeyB64), + alg, + true, + ["sign"] + ); + + const publicKey = await subtle.importKey( + "spki", + base64urlToBuffer(serialized.publicKeyB64), + alg, + true, + ["verify"] + ); + + // Suppress unused-variable warning – sigAlg is consumed by the caller. + void sigAlg; + + return { privateKey, publicKey }; +} + +// --------------------------------------------------------------------------- +// PEM ↔ DER utilities (public so callers can use them with external DER blobs) +// --------------------------------------------------------------------------- + +export function bufferToBase64url(buf: ArrayBuffer): string { + return Buffer.from(buf).toString("base64url"); +} + +export function base64urlToBuffer(b64url: string): ArrayBuffer { + const buf = Buffer.from(b64url, "base64url"); + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer; +} + +/** Convert raw DER bytes to a PEM string with the given label. */ +export function derToPem(der: ArrayBuffer, label: string): string { + const b64 = Buffer.from(der).toString("base64"); + const lines = b64.match(/.{1,64}/g) ?? []; + return `-----BEGIN ${label}-----\n${lines.join("\n")}\n-----END ${label}-----\n`; +} + +/** Strip PEM headers and decode to DER. */ +export function pemToDer(pem: string): ArrayBuffer { + const b64 = pem + .replace(/-----BEGIN [^-]+-----/g, "") + .replace(/-----END [^-]+-----/g, "") + .replace(/\s/g, ""); + const buf = Buffer.from(b64, "base64"); + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer; +} + +/** Encode an 8-byte random serial as a hex string (RFC 5280 §4.1.2.2). */ +export function randomSerial(): string { + const bytes = new Uint8Array(8); + webcrypto.getRandomValues(bytes); + // Clear the high bit to keep the integer positive in DER encoding. + bytes[0] &= 0x7f; + return Array.from(bytes) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); +} diff --git a/lws10-test-suite/signatures/manifest-integration.ts b/lws10-test-suite/signatures/manifest-integration.ts new file mode 100644 index 0000000..ea83734 --- /dev/null +++ b/lws10-test-suite/signatures/manifest-integration.ts @@ -0,0 +1,254 @@ +/** + * manifest-integration.ts + * + * Shows how a test harness reads the `lwst:testCA` block from the LWS test + * manifest and uses it to bootstrap TLS for every test in a suite. + * + * This is the glue layer between the JSON-LD manifest (produced by the test + * suite authors) and the live infrastructure (HTTPS servers, HTTP clients) + * that the tests run against. + * + * Run with: npx tsx src/manifest-integration.ts + */ + +import "reflect-metadata"; +import * as https from "node:https"; +import { TestCA } from "./TestCA.js"; +import type { TestCABlock, IssuedCertificate, TlsCredentials } from "./types.js"; + +// --------------------------------------------------------------------------- +// Simulated manifest fragment +// --------------------------------------------------------------------------- +// In a real suite this JSON would be parsed from manifest.jsonld. +// The `lwst:testCA` key holds a `TestCABlock` which is generated once and +// committed. We generate it inline here so this file is self-contained. + +async function buildSampleManifestFragment(): Promise<{ + "lwst:testCA": TestCABlock; + "mf:entries": Array<{ "@id": string; "lwst:name": string }>; +}> { + const { block } = await TestCA.generate({ + subject: { CN: "LWS Test CA", O: "W3C LWS Test Suite" }, + }); + + return { + "lwst:testCA": block, + "mf:entries": [ + { "@id": "#getContainer", "lwst:name": "getContainer" }, + { "@id": "#createDataResource","lwst:name": "createDataResource" }, + ], + }; +} + +// --------------------------------------------------------------------------- +// Harness bootstrap +// --------------------------------------------------------------------------- + +/** + * Everything the test harness exposes to individual tests. + * + * Built once in `beforeAll`, torn down in `afterAll`. + */ +interface HarnessContext { + /** TLS options for `https.createServer`. */ + serverCredentials: Record; + /** Issued cert objects (serial, dates, …). */ + issuedCerts: Map; + /** CA PEM for client-side trust. */ + caCertPem: string; + /** Convenience factory: returns an `https.Agent` that trusts the test CA. */ + trustedAgent(): https.Agent; +} + +async function bootstrapHarness(caBlock: TestCABlock): Promise { + const ca = await TestCA.load(caBlock); + + // Issue every named template defined in the block. + const issuedCerts = await ca.issueAll(); + + // Pre-build TLS credentials keyed by templateId. + const serverCredentials: Record = {}; + for (const [id, cert] of issuedCerts) { + serverCredentials[id] = ca.tlsCredentials(cert); + } + + return { + serverCredentials, + issuedCerts, + caCertPem: ca.caCertPem, + trustedAgent: () => new https.Agent(ca.clientAgentOptions()), + }; +} + +// --------------------------------------------------------------------------- +// Simulated test runner +// --------------------------------------------------------------------------- + +interface TestEntry { + id: string; + name: string; + run: (ctx: HarnessContext) => Promise; +} + +async function runSuite( + manifest: { "lwst:testCA": TestCABlock; "mf:entries": Array<{ "@id": string; "lwst:name": string }> }, + tests: TestEntry[] +): Promise { + console.log("\n─── Harness bootstrap ───"); + const ctx = await bootstrapHarness(manifest["lwst:testCA"]); + console.log(`✓ CA loaded, ${ctx.issuedCerts.size} certs pre-issued`); + console.log(` Issued templates: ${[...ctx.issuedCerts.keys()].join(", ")}`); + + let passed = 0; + let failed = 0; + + for (const test of tests) { + process.stdout.write(`\n Test ${test.id} (${test.name}) … `); + try { + await test.run(ctx); + console.log("✓ PASS"); + passed++; + } catch (err) { + console.log(`✗ FAIL\n ${err}`); + failed++; + } + } + + console.log(`\n─── Results: ${passed} passed, ${failed} failed ───\n`); +} + +// --------------------------------------------------------------------------- +// Two concrete (toy) tests that use the harness context +// --------------------------------------------------------------------------- + +const TESTS: TestEntry[] = [ + { + id: "#getContainer", + name: "getContainer", + async run(ctx) { + // Spin up a fake storage server using the pre-issued "storage.example" cert. + const creds = ctx.serverCredentials["storage.example"]; + if (!creds) throw new Error('No credentials for "storage.example"'); + + const server = https.createServer( + { cert: creds.cert, key: creds.key }, + (_req, res) => { + res.writeHead(200, { + "Content-Type": "application/lws+json", + Link: '; rel="linkset"; type="application/linkset+json"', + }); + res.end(JSON.stringify({ "@type": "Container", items: [] })); + } + ); + + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const { port } = server.address() as { port: number }; + + try { + const response = await new Promise<{ status: number; linkHeader: string | undefined }>( + (resolve, reject) => { + const req = https.get( + `https://127.0.0.1:${port}/alice/notes/`, + { agent: ctx.trustedAgent() }, + (res) => { + res.resume(); + resolve({ + status: res.statusCode ?? 0, + linkHeader: res.headers["link"], + }); + } + ); + req.on("error", reject); + } + ); + + if (response.status !== 200) throw new Error(`Expected 200, got ${response.status}`); + if (!response.linkHeader?.includes('rel="linkset"')) + throw new Error(`Missing linkset Link header; got: ${response.linkHeader}`); + } finally { + await new Promise((r) => server.close(() => r())); + } + }, + }, + + { + id: "#createDataResource", + name: "createDataResource", + async run(ctx) { + const creds = ctx.serverCredentials["storage.example"]; + if (!creds) throw new Error('No credentials for "storage.example"'); + + const server = https.createServer( + { cert: creds.cert, key: creds.key }, + (_req, res) => { + res.writeHead(201, { + Location: "/alice/notes/shoppinglist.txt", + Link: [ + '; rel="linkset"; type="application/linkset+json"', + '; rel="up"', + '; rel="type"', + ].join(", "), + "Content-Length": "0", + }); + res.end(); + } + ); + + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const { port } = server.address() as { port: number }; + + try { + const response = await new Promise<{ status: number; location: string | undefined }>( + (resolve, reject) => { + const req = https.request( + { + hostname: "127.0.0.1", + port, + path: "/alice/notes/", + method: "POST", + agent: ctx.trustedAgent(), + headers: { + "Content-Type": "text/plain", + "Content-Length": "47", + Slug: "shoppinglist.txt", + }, + }, + (res) => { + res.resume(); + resolve({ status: res.statusCode ?? 0, location: res.headers["location"] }); + } + ); + req.on("error", reject); + req.end("milk\neggs\nbread\nbutter\napples\norange juice\n"); + } + ); + + if (response.status !== 201) throw new Error(`Expected 201, got ${response.status}`); + if (!response.location?.includes("shoppinglist.txt")) + throw new Error(`Bad Location: ${response.location}`); + } finally { + await new Promise((r) => server.close(() => r())); + } + }, + }, +]; + +// --------------------------------------------------------------------------- +// Entry point +// --------------------------------------------------------------------------- +async function main() { + console.log("\n══════════════════════════════════════════"); + console.log(" LWS manifest → harness integration demo"); + console.log("══════════════════════════════════════════"); + + const manifest = await buildSampleManifestFragment(); + console.log(`\nManifest has ${manifest["mf:entries"].length} test entries`); + console.log(`testCA block size: ${JSON.stringify(manifest["lwst:testCA"]).length} chars`); + + await runSuite(manifest, TESTS); +} + +main().catch((err) => { + console.error("Fatal:", err); + process.exit(1); +}); diff --git a/lws10-test-suite/signatures/package-lock.json b/lws10-test-suite/signatures/package-lock.json new file mode 100644 index 0000000..6c1f158 --- /dev/null +++ b/lws10-test-suite/signatures/package-lock.json @@ -0,0 +1,752 @@ +{ + "name": "lws-ca", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lws-ca", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@peculiar/x509": "^2.0.0", + "reflect-metadata": "^0.2.2" + }, + "devDependencies": { + "@types/node": "^25.6.0", + "tsx": "^4.21.0", + "typescript": "^6.0.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz", + "integrity": "sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw==", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-x509-attr": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.1.tgz", + "integrity": "sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w==", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.1.tgz", + "integrity": "sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g==", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.1.tgz", + "integrity": "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.1", + "@peculiar/asn1-pkcs8": "^2.6.1", + "@peculiar/asn1-rsa": "^2.6.1", + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.1.tgz", + "integrity": "sha512-JB5iQ9Izn5yGMw3ZG4Nw3Xn/hb/G38GYF3lf7WmJb8JZUydhVGEjK/ZlFSWhnlB7K/4oqEs8HnfFIKklhR58Tw==", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.1.tgz", + "integrity": "sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw==", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.1", + "@peculiar/asn1-pfx": "^2.6.1", + "@peculiar/asn1-pkcs8": "^2.6.1", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-x509-attr": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.1.tgz", + "integrity": "sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA==", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", + "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", + "dependencies": { + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.1.tgz", + "integrity": "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.1.tgz", + "integrity": "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/x509": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-2.0.0.tgz", + "integrity": "sha512-r10lkuy6BNfRmyYdRAfgu6dq0HOmyIV2OLhXWE3gDEPBdX1b8miztJVyX/UxWhLwemNyDP3CLZHpDxDwSY0xaA==", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "dev": true, + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "dev": true + } + } +} diff --git a/lws10-test-suite/signatures/package.json b/lws10-test-suite/signatures/package.json new file mode 100644 index 0000000..ce8f40e --- /dev/null +++ b/lws10-test-suite/signatures/package.json @@ -0,0 +1,21 @@ +{ + "name": "lws-ca", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@types/node": "^25.6.0", + "tsx": "^4.21.0", + "typescript": "^6.0.3" + }, + "dependencies": { + "@peculiar/x509": "^2.0.0", + "reflect-metadata": "^0.2.2" + } +} diff --git a/lws10-test-suite/signatures/tsconfig.json b/lws10-test-suite/signatures/tsconfig.json new file mode 100644 index 0000000..8f30e6e --- /dev/null +++ b/lws10-test-suite/signatures/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "moduleResolution": "node", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "sourceMap": true + }, + "include": ["src/**/*"] +} diff --git a/lws10-test-suite/signatures/types.ts b/lws10-test-suite/signatures/types.ts new file mode 100644 index 0000000..dd53fee --- /dev/null +++ b/lws10-test-suite/signatures/types.ts @@ -0,0 +1,169 @@ +/** + * Types shared across the test-CA implementation. + * + * The manifest embeds a `testCA` block that supplies everything a test harness + * needs to bootstrap a self-contained PKI: + * + * - The CA's serialised key pair (PKCS#8 private key + SPKI public key, both + * PEM-encoded so they are copy-pasteable into JSON without escaping). + * - The self-signed CA certificate (PEM). + * - A set of named `IssuanceTemplate`s that the harness can instantiate into + * real leaf certificates on demand. + * + * All key material is EC/P-256 by default; RSA-2048 is also supported via the + * algorithm discriminant. + */ + +// --------------------------------------------------------------------------- +// Key material +// --------------------------------------------------------------------------- + +/** Algorithm discriminant stored in the manifest. */ +export type KeyAlgorithm = + | { name: "ECDSA"; namedCurve: "P-256" | "P-384" | "P-521" } + | { name: "RSASSA-PKCS1-v1_5"; modulusLength: 2048 | 4096; hash: "SHA-256" | "SHA-384" } + | { name: "Ed25519" }; + +/** A serialised key pair as it appears inside the test manifest. */ +export interface SerializedKeyPair { + /** Algorithm used to generate this key pair. */ + algorithm: KeyAlgorithm; + /** PKCS#8 DER → base64url (no PEM headers – cleaner inside JSON). */ + privateKeyB64: string; + /** SubjectPublicKeyInfo DER → base64url. */ + publicKeyB64: string; +} + +// --------------------------------------------------------------------------- +// Certificate issuance templates +// --------------------------------------------------------------------------- + +/** Subject distinguished name expressed as a flat map of RDN types → values. */ +export interface SubjectDN { + CN: string; + O?: string; + OU?: string; + C?: string; + ST?: string; + L?: string; +} + +/** + * A named template that the harness expands into a leaf certificate. + * + * Templates are intentionally underspecified: the harness fills in the gaps + * (serial number, validity window, actual key material) at runtime so every + * test run gets fresh, non-replayable certs. + */ +export interface IssuanceTemplate { + /** Human-readable label used to look up the template by tests. */ + templateId: string; + + subject: SubjectDN; + + /** + * Subject Alternative Names. At least one of `dns` or `ip` SHOULD be + * present for server certificates so TLS hostname verification passes. + */ + san?: { + dns?: string[]; + ip?: string[]; + uri?: string[]; + email?: string[]; + }; + + /** + * Key algorithm for the *leaf* key pair. Defaults to the CA algorithm when + * omitted, which keeps the happy path simple. + */ + leafAlgorithm?: KeyAlgorithm; + + /** + * Validity period in seconds from the moment of issuance. + * Defaults to 86 400 (1 day) – short enough to stay safe in CI, long + * enough that a slow test suite won't hit clock-skew problems. + */ + validitySeconds?: number; + + /** Extended Key Usage OIDs. Defaults to ["serverAuth"]. */ + extendedKeyUsage?: Array<"serverAuth" | "clientAuth" | "emailProtection" | "codeSigning">; + + /** + * When true the issued cert is marked as a CA (basicConstraints CA:TRUE). + * Use this to create intermediate CAs for chain-validation tests. + */ + isCA?: boolean; +} + +// --------------------------------------------------------------------------- +// Test-CA manifest block +// --------------------------------------------------------------------------- + +/** + * The `testCA` block that lives inside a LWS test manifest. + * + * Example embedding: + * + * ```json + * { + * "@context": ["./context.jsonld"], + * "lwst:testCA": { + * "algorithm": { "name": "ECDSA", "namedCurve": "P-256" }, + * "caCertPem": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n", + * "caKeyPair": { ... }, + * "templates": [ ... ] + * } + * } + * ``` + */ +export interface TestCABlock { + /** Algorithm used for the CA key pair (and default for leaf keys). */ + algorithm: KeyAlgorithm; + + /** PEM-encoded self-signed CA certificate. */ + caCertPem: string; + + /** Serialised CA key pair. */ + caKeyPair: SerializedKeyPair; + + /** Named issuance templates that tests can reference. */ + templates: IssuanceTemplate[]; +} + +// --------------------------------------------------------------------------- +// Runtime types (not stored in the manifest) +// --------------------------------------------------------------------------- + +/** A fully-materialised leaf certificate produced by the harness at runtime. */ +export interface IssuedCertificate { + /** PEM certificate chain: leaf first, then CA. */ + certChainPem: string; + + /** PEM-encoded leaf private key (PKCS#8). */ + privateKeyPem: string; + + /** DER buffer of the leaf certificate (handy for TLS options). */ + certDer: ArrayBuffer; + + /** The template that was used. */ + template: IssuanceTemplate; + + /** UTC timestamp when the certificate becomes valid. */ + notBefore: Date; + + /** UTC timestamp when the certificate expires. */ + notAfter: Date; + + /** Hex serial number assigned to this certificate. */ + serialNumber: string; +} + +/** Everything a Node `tls.createServer` / `https.createServer` call needs. */ +export interface TlsCredentials { + /** PEM cert chain (leaf + CA). */ + cert: string; + /** PEM private key for the leaf. */ + key: string; + /** PEM CA certificate (pass as `ca` to clients so they trust the chain). */ + ca: string; +} From 554cc7d26a3d6d79923128f4670238b26963a208 Mon Sep 17 00:00:00 2001 From: Eric Prud'hommeaux Date: Sat, 2 May 2026 17:30:37 +0200 Subject: [PATCH 2/3] vertically align comment delimiters Co-authored-by: Ted Thibodeau Jr --- lws10-test-suite/README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lws10-test-suite/README.md b/lws10-test-suite/README.md index 7e808c4..45eb0dd 100644 --- a/lws10-test-suite/README.md +++ b/lws10-test-suite/README.md @@ -11,23 +11,23 @@ and its authentication suite specifications: ``` tests/ -├── context.jsonld # Shared JSON-LD context for all manifests -├── manifest.jsonld # Root manifest (core protocol tests) +├── context.jsonld # Shared JSON-LD context for all manifests +├── manifest.jsonld # Root manifest (core protocol tests) ├── auth/ -│ ├── did-key/manifest.jsonld # did:key authentication suite tests -│ ├── oidc/manifest.jsonld # OpenID Connect authentication suite tests -│ └── saml/manifest.jsonld # SAML 2.0 authentication suite tests +│ ├── did-key/manifest.jsonld # did:key authentication suite tests +│ ├── oidc/manifest.jsonld # OpenID Connect authentication suite tests +│ └── saml/manifest.jsonld # SAML 2.0 authentication suite tests ├── containers/ -│ └── alice-notes.json # Example container representation body +│ └── alice-notes.json # Example container representation body ├── linksets/ -│ ├── alice-notes.meta.json # Linkset for /alice/notes/ -│ └── shoppinglist.meta.json # Linkset for /alice/notes/shoppinglist.txt +│ ├── alice-notes.meta.json # Linkset for /alice/notes/ +│ └── shoppinglist.meta.json # Linkset for /alice/notes/shoppinglist.txt └── resources/ - ├── storage-description.json # Storage description resource body + ├── storage-description.json # Storage description resource body ├── authorization-server-metadata.json # AS metadata body - ├── shoppinglist.txt # Data resource body - ├── token-response-200.json # Successful token exchange response - └── token-response-400.json # Failed token exchange response + ├── shoppinglist.txt # Data resource body + ├── token-response-200.json # Successful token exchange response + └── token-response-400.json # Failed token exchange response ``` ## Conventions From 724a7340f5b798330597180723b595984f2bafaa Mon Sep 17 00:00:00 2001 From: Eric Prud'hommeaux Date: Tue, 19 May 2026 14:39:49 +0200 Subject: [PATCH 3/3] ~ moved signatures to https://github.com/lws-contrib/signatures-in-typescript --- lws10-test-suite/signatures/TestCA.ts | 254 ------ lws10-test-suite/signatures/cert.ts | 234 ------ lws10-test-suite/signatures/demo.ts | 216 ----- lws10-test-suite/signatures/index.ts | 16 - lws10-test-suite/signatures/keys.ts | 152 ---- .../signatures/manifest-integration.ts | 254 ------ lws10-test-suite/signatures/package-lock.json | 752 ------------------ lws10-test-suite/signatures/package.json | 21 - lws10-test-suite/signatures/tsconfig.json | 17 - lws10-test-suite/signatures/types.ts | 169 ---- 10 files changed, 2085 deletions(-) delete mode 100644 lws10-test-suite/signatures/TestCA.ts delete mode 100644 lws10-test-suite/signatures/cert.ts delete mode 100644 lws10-test-suite/signatures/demo.ts delete mode 100644 lws10-test-suite/signatures/index.ts delete mode 100644 lws10-test-suite/signatures/keys.ts delete mode 100644 lws10-test-suite/signatures/manifest-integration.ts delete mode 100644 lws10-test-suite/signatures/package-lock.json delete mode 100644 lws10-test-suite/signatures/package.json delete mode 100644 lws10-test-suite/signatures/tsconfig.json delete mode 100644 lws10-test-suite/signatures/types.ts diff --git a/lws10-test-suite/signatures/TestCA.ts b/lws10-test-suite/signatures/TestCA.ts deleted file mode 100644 index b1a4ae9..0000000 --- a/lws10-test-suite/signatures/TestCA.ts +++ /dev/null @@ -1,254 +0,0 @@ -/** - * `TestCA` — the harness-facing facade. - * - * Typical usage in a test suite: - * - * ```ts - * // --- one-time setup (run & commit the output) --- - * const { block } = await TestCA.generate({ - * subject: { CN: "LWS Test CA", O: "W3C LWS Test Suite" }, - * }); - * fs.writeFileSync("test-ca.json", JSON.stringify(block, null, 2)); - * - * // --- every test run --- - * const ca = await TestCA.load(JSON.parse(fs.readFileSync("test-ca.json", "utf8"))); - * - * const serverCert = await ca.issue("storage.example"); - * const { cert, key, ca: caPem } = ca.tlsCredentials(serverCert); - * const server = https.createServer({ cert, key, ca: caPem }, handler); - * - * const client = new https.Agent({ ca: caPem }); - * ``` - */ - -import "reflect-metadata"; -import * as x509 from "@peculiar/x509"; -import type { - IssuanceTemplate, - IssuedCertificate, - KeyAlgorithm, - SubjectDN, - TestCABlock, - TlsCredentials, -} from "./types.js"; -import { deserializeKeyPair, generateKeyPair } from "./keys.js"; -import { - createCACertificate, - issueFromTemplate, - parseCACert, - toTlsCredentials, -} from "./cert.js"; - -// --------------------------------------------------------------------------- -// Default templates that cover common LWS test scenarios -// --------------------------------------------------------------------------- - -const DEFAULT_TEMPLATES: IssuanceTemplate[] = [ - { - templateId: "storage.example", - subject: { CN: "storage.example", O: "LWS Test" }, - san: { dns: ["storage.example", "localhost"], ip: ["127.0.0.1", "::1"] }, - extendedKeyUsage: ["serverAuth"], - validitySeconds: 86_400, - }, - { - templateId: "authorization.example", - subject: { CN: "authorization.example", O: "LWS Test" }, - san: { dns: ["authorization.example", "localhost"], ip: ["127.0.0.1", "::1"] }, - extendedKeyUsage: ["serverAuth"], - validitySeconds: 86_400, - }, - { - templateId: "client.alice", - subject: { CN: "Alice", O: "LWS Test" }, - san: { uri: ["https://id.example/alice"], email: ["alice@example.com"] }, - extendedKeyUsage: ["clientAuth"], - validitySeconds: 86_400, - }, - { - templateId: "client.bob", - subject: { CN: "Bob", O: "LWS Test" }, - san: { uri: ["https://id.example/bob"], email: ["bob@example.com"] }, - extendedKeyUsage: ["clientAuth"], - validitySeconds: 86_400, - }, -]; - -// --------------------------------------------------------------------------- -// TestCA -// --------------------------------------------------------------------------- - -export class TestCA { - private constructor( - /** Parsed CA certificate. */ - public readonly caCert: x509.X509Certificate, - /** Live CA key pair (in-memory CryptoKey, never leaves the process). */ - private readonly caKeyPair: CryptoKeyPair, - /** The manifest block (safe to serialise and store). */ - public readonly block: TestCABlock - ) {} - - // ------------------------------------------------------------------------- - // Factory methods - // ------------------------------------------------------------------------- - - /** - * Generate a brand-new test CA. - * - * Call this once during test-suite authoring and commit the resulting - * `block` into the manifest so that every subsequent run uses the same - * stable CA certificate. - */ - static async generate(options: { - subject?: SubjectDN; - algorithm?: KeyAlgorithm; - validityDays?: number; - extraTemplates?: IssuanceTemplate[]; - } = {}): Promise<{ ca: TestCA; block: TestCABlock }> { - const algorithm: KeyAlgorithm = options.algorithm ?? { - name: "ECDSA", - namedCurve: "P-256", - }; - const subject: SubjectDN = options.subject ?? { - CN: "LWS Test CA", - O: "W3C LWS Test Suite", - }; - - const { keyPair: caKeyPair, serialized: caKeyPairSerialized } = - await generateKeyPair(algorithm); - - const caCert = await createCACertificate(subject, caKeyPair, algorithm, { - validityDays: options.validityDays ?? 3650, - }); - - const templates = [ - ...DEFAULT_TEMPLATES, - ...(options.extraTemplates ?? []), - ]; - - const block: TestCABlock = { - algorithm, - caCertPem: caCert.toString("pem"), - caKeyPair: caKeyPairSerialized, - templates, - }; - - const ca = new TestCA(caCert, caKeyPair, block); - return { ca, block }; - } - - /** - * Load a `TestCA` from a previously generated `TestCABlock`. - * - * This is the normal entry-point for test harnesses: read the block from the - * committed manifest file, call `load`, then issue certificates as needed. - */ - static async load(block: TestCABlock): Promise { - const caKeyPair = await deserializeKeyPair(block.caKeyPair); - const caCert = parseCACert(block.caCertPem); - return new TestCA(caCert, caKeyPair, block); - } - - // ------------------------------------------------------------------------- - // Certificate issuance - // ------------------------------------------------------------------------- - - /** - * Issue a certificate from a named template stored in the manifest block. - * - * ```ts - * const cert = await ca.issue("storage.example"); - * ``` - */ - async issue(templateId: string): Promise; - - /** - * Issue a certificate from an inline template (not stored in the manifest). - * - * Useful for one-off certificates in a single test that don't belong in the - * shared template library. - * - * ```ts - * const cert = await ca.issue({ - * templateId: "ephemeral", - * subject: { CN: "ephemeral.test" }, - * san: { dns: ["ephemeral.test"] }, - * }); - * ``` - */ - async issue(template: IssuanceTemplate): Promise; - - async issue(arg: string | IssuanceTemplate): Promise { - const template = - typeof arg === "string" ? this.findTemplate(arg) : arg; - - return issueFromTemplate( - template, - this.caKeyPair, - this.caCert, - this.block.algorithm - ); - } - - /** - * Issue all default templates at once. Handy for suite-level `beforeAll` - * hooks that pre-warm a certificate pool. - */ - async issueAll(): Promise> { - const results = new Map(); - for (const template of this.block.templates) { - results.set(template.templateId, await this.issue(template)); - } - return results; - } - - // ------------------------------------------------------------------------- - // TLS helpers - // ------------------------------------------------------------------------- - - /** - * Extract the options needed by Node's `https.createServer` / `tls.createServer`. - * - * ```ts - * const creds = ca.tlsCredentials(issued); - * const server = https.createServer(creds, handler); - * // For clients: new https.Agent({ ca: creds.ca }) - * ``` - */ - tlsCredentials(issued: IssuedCertificate): TlsCredentials { - return toTlsCredentials(issued, this.block.caCertPem); - } - - /** - * Return a Node `https.Agent` configuration object that trusts only the - * test CA. Pass this to `fetch` or `https.request` inside tests. - * - * ```ts - * const agentOpts = ca.clientAgentOptions(); - * const response = await fetch(url, { dispatcher: new Agent(agentOpts) }); - * ``` - */ - clientAgentOptions(): { ca: string; rejectUnauthorized: boolean } { - return { ca: this.block.caCertPem, rejectUnauthorized: true }; - } - - /** The CA certificate in PEM format (shorthand for `block.caCertPem`). */ - get caCertPem(): string { - return this.block.caCertPem; - } - - // ------------------------------------------------------------------------- - // Private - // ------------------------------------------------------------------------- - - private findTemplate(templateId: string): IssuanceTemplate { - const t = this.block.templates.find((t) => t.templateId === templateId); - if (!t) { - throw new Error( - `TestCA: no template with id "${templateId}". ` + - `Available: ${this.block.templates.map((t) => t.templateId).join(", ")}` - ); - } - return t; - } -} diff --git a/lws10-test-suite/signatures/cert.ts b/lws10-test-suite/signatures/cert.ts deleted file mode 100644 index f79bf6c..0000000 --- a/lws10-test-suite/signatures/cert.ts +++ /dev/null @@ -1,234 +0,0 @@ -/** - * Certificate issuance engine. - * - * Uses `@peculiar/x509` for X.509 structure building. The CA key is provided - * as a `CryptoKey` already loaded into the Web Crypto API — the engine never - * touches raw key bytes. - * - * Design note: we keep this module's public surface small. The test harness - * only ever calls `issueFromTemplate`; everything else is an implementation - * detail. - */ - -import "reflect-metadata"; -import { webcrypto } from "node:crypto"; -import * as x509 from "@peculiar/x509"; - -// @peculiar/x509 maintains its own CryptoProvider registry and looks up the -// provider named "default" for every internal operation (key identifier -// generation, signing, etc.). In a browser that registry is pre-populated -// from window.crypto; in Node we must register explicitly. Do it here, -// once, before any x509 API is called. -x509.cryptoProvider.set(webcrypto); -import type { - IssuanceTemplate, - IssuedCertificate, - KeyAlgorithm, - SubjectDN, - TlsCredentials, -} from "./types.js"; -import { derToPem, pemToDer, randomSerial, signingAlgorithm } from "./keys.js"; - -// --------------------------------------------------------------------------- -// Internal helpers -// --------------------------------------------------------------------------- - -const EKU_OIDS: Record = { - serverAuth: "1.3.6.1.5.5.7.3.1", - clientAuth: "1.3.6.1.5.5.7.3.2", - emailProtection: "1.3.6.1.5.5.7.3.4", - codeSigning: "1.3.6.1.5.5.7.3.3", -}; - -function dnString(dn: SubjectDN): string { - const parts: string[] = [`CN=${dn.CN}`]; - if (dn.O) parts.push(`O=${dn.O}`); - if (dn.OU) parts.push(`OU=${dn.OU}`); - if (dn.C) parts.push(`C=${dn.C}`); - if (dn.ST) parts.push(`ST=${dn.ST}`); - if (dn.L) parts.push(`L=${dn.L}`); - return parts.join(", "); -} - -/** Convert our `KeyAlgorithm` discriminant to the object peculiar/x509 expects. */ -function toX509Algorithm(alg: KeyAlgorithm): Algorithm { - switch (alg.name) { - case "ECDSA": - return { name: "ECDSA", namedCurve: alg.namedCurve } as Algorithm; - case "RSASSA-PKCS1-v1_5": - return { - name: "RSASSA-PKCS1-v1_5", - modulusLength: alg.modulusLength, - publicExponent: new Uint8Array([1, 0, 1]), - hash: alg.hash, - } as Algorithm; - case "Ed25519": - return { name: "Ed25519" } as Algorithm; - } -} - -// --------------------------------------------------------------------------- -// Self-signed CA certificate -// --------------------------------------------------------------------------- - -/** - * Create a self-signed CA certificate. Call this once when setting up a - * new test suite; store the result in `testCA.caCertPem`. - */ -export async function createCACertificate( - subject: SubjectDN, - caKeyPair: CryptoKeyPair, - algorithm: KeyAlgorithm, - options: { - validityDays?: number; - serialNumber?: string; - } = {} -): Promise { - const { validityDays = 3650, serialNumber = randomSerial() } = options; - - const notBefore = new Date(); - const notAfter = new Date(notBefore.getTime() + validityDays * 86_400_000); - - const cert = await x509.X509CertificateGenerator.createSelfSigned({ - serialNumber, - name: dnString(subject), - notBefore, - notAfter, - signingAlgorithm: signingAlgorithm(algorithm) as Algorithm, - keys: caKeyPair, - extensions: [ - new x509.BasicConstraintsExtension(true /* isCA */, undefined, true /* critical */), - new x509.KeyUsagesExtension( - x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, - true /* critical */ - ), - await x509.SubjectKeyIdentifierExtension.create(caKeyPair.publicKey), - ], - }); - - return cert; -} - -// --------------------------------------------------------------------------- -// Leaf certificate issuance -// --------------------------------------------------------------------------- - -/** - * Issue a leaf certificate from an `IssuanceTemplate`. - * - * The harness calls this at test-setup time with: - * - the parsed `TestCABlock` (contains `caCertPem` + `caKeyPair`) - * - the `IssuanceTemplate` it wants to materialise - * - * It returns an `IssuedCertificate` whose fields can be fed directly into - * `tls.createServer`, `https.createServer`, or an `https.Agent`. - */ -export async function issueFromTemplate( - template: IssuanceTemplate, - caKeyPair: CryptoKeyPair, - caCert: x509.X509Certificate, - caAlgorithm: KeyAlgorithm -): Promise { - const validitySeconds = template.validitySeconds ?? 86_400; - const leafAlgorithm = template.leafAlgorithm ?? caAlgorithm; - const ekuNames = template.extendedKeyUsage ?? ["serverAuth"]; - const serialNumber = randomSerial(); - - const notBefore = new Date(); - const notAfter = new Date(notBefore.getTime() + validitySeconds * 1_000); - - // Generate a fresh leaf key pair - const leafKeys = await webcrypto.subtle.generateKey( - toX509Algorithm(leafAlgorithm) as EcKeyGenParams, - true /* extractable */, - ["sign", "verify"] - ); - - // Build extensions list - const extensions: x509.Extension[] = [ - new x509.BasicConstraintsExtension(template.isCA ?? false, undefined, true), - new x509.KeyUsagesExtension( - template.isCA - ? x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.digitalSignature - : x509.KeyUsageFlags.digitalSignature, - true - ), - new x509.ExtendedKeyUsageExtension(ekuNames.map((n) => EKU_OIDS[n]), false), - await x509.SubjectKeyIdentifierExtension.create(leafKeys.publicKey), - await x509.AuthorityKeyIdentifierExtension.create(caCert, false), - ]; - - // Subject Alternative Names - if (template.san) { - const generalNames: x509.GeneralName[] = []; - for (const dns of template.san.dns ?? []) - generalNames.push(new x509.GeneralName("dns", dns)); - for (const ip of template.san.ip ?? []) - generalNames.push(new x509.GeneralName("ip", ip)); - for (const uri of template.san.uri ?? []) - generalNames.push(new x509.GeneralName("url", uri)); - for (const email of template.san.email ?? []) - generalNames.push(new x509.GeneralName("email", email)); - - if (generalNames.length > 0) { - extensions.push(new x509.SubjectAlternativeNameExtension(generalNames, false)); - } - } - - const leafCert = await x509.X509CertificateGenerator.create({ - serialNumber, - subject: dnString(template.subject), - issuer: caCert.subject, - notBefore, - notAfter, - signingAlgorithm: signingAlgorithm(caAlgorithm) as Algorithm, - publicKey: leafKeys.publicKey, - signingKey: caKeyPair.privateKey, - extensions, - }); - - // Serialise leaf private key to PKCS#8 PEM - const leafPrivDer = await webcrypto.subtle.exportKey( - "pkcs8", - leafKeys.privateKey - ); - const privateKeyPem = derToPem(leafPrivDer, "PRIVATE KEY"); - - // PEM chain: leaf cert first, then CA - const leafPem = leafCert.toString("pem"); - const caPem = caCert.toString("pem"); - const certChainPem = leafPem + "\n" + caPem; - - return { - certChainPem, - privateKeyPem, - certDer: leafCert.rawData, - template, - notBefore, - notAfter, - serialNumber, - }; -} - -// --------------------------------------------------------------------------- -// Convenience: parse a CA cert PEM back into an x509.X509Certificate -// --------------------------------------------------------------------------- - -export function parseCACert(pem: string): x509.X509Certificate { - return new x509.X509Certificate(pemToDer(pem)); -} - -// --------------------------------------------------------------------------- -// Convenience: extract TlsCredentials for Node's https/tls modules -// --------------------------------------------------------------------------- - -export function toTlsCredentials( - issued: IssuedCertificate, - caCertPem: string -): TlsCredentials { - return { - cert: issued.certChainPem, - key: issued.privateKeyPem, - ca: caCertPem, - }; -} diff --git a/lws10-test-suite/signatures/demo.ts b/lws10-test-suite/signatures/demo.ts deleted file mode 100644 index 61f1845..0000000 --- a/lws10-test-suite/signatures/demo.ts +++ /dev/null @@ -1,216 +0,0 @@ -/** - * demo.ts — experimenting with certificate authority workflows - * - * Run with: npx tsx src/demo.ts - * - * 1. Generate a new TestCA and serialise its block to JSON (one-time authoring - * step — in a real suite you'd commit this file). - * 2. Load the CA back from the JSON block (what every test run does). - * 3. Issue a server certificate from a named template. - * 4. Issue an ad-hoc client certificate from an inline template. - * 5. Spin up a real HTTPS server using the issued server cert. - * 6. Make a real HTTPS request from a client that trusts only the test CA. - * 7. Confirm the server rejects a client that uses Node's default CA store. - * 8. Demonstrate that the block round-trips through JSON without loss. - */ - -import "reflect-metadata"; -import * as https from "node:https"; -import * as fs from "node:fs"; -import * as path from "node:path"; -import { TestCA } from "./TestCA.js"; -import type { TestCABlock } from "./types.js"; - -// indulgent ANSI colors -const green = (s: string) => `\x1b[32m${s}\x1b[0m`; -const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`; -const red = (s: string) => `\x1b[31m${s}\x1b[0m`; -const bold = (s: string) => `\x1b[1m${s}\x1b[0m`; - -function step(n: number, msg: string) { - console.log(`\n${bold(yellow(`[${n}]`))} ${msg}`); -} - -// indulgent unicode characters -function ok(msg: string) { console.log(` ${green("✓")} ${msg}`); } -function info(msg: string) { console.log(` ${yellow("·")} ${msg}`); } -function fail(msg: string) { console.log(` ${red("✗")} ${msg}`); } - -// do a HTTPS GET with a CA cert, return { statusCode, body } -// --------------------------------------------------------------------------- -function httpsGet( - url: string, - agentOptions: https.AgentOptions -): Promise<{ statusCode: number; body: string }> { - return new Promise((resolve, reject) => { - const agent = new https.Agent(agentOptions); - https.get(url, { agent }, (res) => { - let body = ""; - res.on("data", (chunk) => (body += chunk)); - res.on("end", () => resolve({ statusCode: res.statusCode ?? 0, body })); - }).on("error", reject); - }); -} - -// create a quick HTTPS server -async function withHttpsServer( - credentials: { cert: string; key: string; ca: string }, - handler: (req: https.IncomingMessage, res: https.ServerResponse) => void, - cb: (port: number) => Promise -): Promise { - const server = https.createServer( - { cert: credentials.cert, key: credentials.key }, - handler - ); - await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); - const addr = server.address() as { port: number }; - try { - await cb(addr.port); - } finally { - await new Promise((resolve, reject) => - server.close((err) => (err ? reject(err) : resolve())) - ); - } -} - -// show off features -async function main() { - console.log(bold("playing with signatures...")); - - const BLOCK_PATH = path.join(process.cwd(), "test-ca-block.json"); - - // -- Step 1: Generate & serialise a CA - step(1, "Generate a new test CA"); - - const { ca: generatedCA, block } = await TestCA.generate({ - subject: { CN: "LWS Test CA", O: "W3C LWS Test Suite", C: "US" }, - algorithm: { name: "ECDSA", namedCurve: "P-256" }, - validityDays: 3650, - // Demonstrate adding a suite-specific template at generation time - extraTemplates: [ - { - templateId: "identity.example", - subject: { CN: "identity.example", O: "LWS Test" }, - san: { dns: ["identity.example", "localhost"], ip: ["127.0.0.1"] }, - extendedKeyUsage: ["serverAuth"], - validitySeconds: 86_400, - }, - ], - }); - - ok(`CA subject: ${generatedCA.caCert.subject}`); - ok(`CA issuer: ${generatedCA.caCert.issuer}`); - ok(`Valid until: ${generatedCA.caCert.validTo}`); - ok(`Is CA: ${generatedCA.caCert.ca}`); - ok(`Templates: ${block.templates.map((t) => t.templateId).join(", ")}`); - - fs.writeFileSync(BLOCK_PATH, JSON.stringify(block, null, 2)); - info(`Block written to: ${BLOCK_PATH}`); - - // -- Step 2: Round-trip through JSON - step(2, "Load CA from JSON block (simulates a subsequent test run)"); - - const reloaded: TestCABlock = JSON.parse(fs.readFileSync(BLOCK_PATH, "utf8")); - const ca = await TestCA.load(reloaded); - - ok(`CA loaded successfully`); - ok(`CA cert fingerprint matches: ${ - ca.caCert.fingerprint256 === generatedCA.caCert.fingerprint256 - }`); - - // -- Step 3: Issue a server certificate from a named template - step(3, 'Issue server certificate from template "storage.example"'); - - const serverCert = await ca.issue("storage.example"); - const serverCreds = ca.tlsCredentials(serverCert); - - ok(`Serial: ${serverCert.serialNumber}`); - ok(`Subject: ${new (await import("@peculiar/x509")).X509Certificate(serverCert.certDer).subject}`); - ok(`Not before: ${serverCert.notBefore.toISOString()}`); - ok(`Not after: ${serverCert.notAfter.toISOString()}`); - ok(`Cert chain has leaf + CA: ${serverCreds.cert.includes("-----BEGIN CERTIFICATE-----\n-----BEGIN CERTIFICATE-----") === false && serverCreds.cert.split("-----BEGIN CERTIFICATE-----").length - 1 === 2}`); - - // -- Step 4: Issue an ad-hoc client certificate - step(4, "Issue ad-hoc client certificate (inline template)"); - - const clientCert = await ca.issue({ - templateId: "ephemeral-alice", - subject: { CN: "Alice (ephemeral)", O: "LWS Test" }, - san: { - uri: ["https://id.example/alice-ephemeral"], - email: ["alice@example.com"], - }, - extendedKeyUsage: ["clientAuth"], - validitySeconds: 3_600, // 1 hour — minimal lifetime for a single test - }); - - ok(`Client cert serial: ${clientCert.serialNumber}`); - ok(`EKU includes clientAuth: true`); - - // -- Step 5-7: Live HTTPS test - step(5, "Spin up an HTTPS server with the issued server certificate"); - - const handler = (_req: https.IncomingMessage, res: https.ServerResponse) => { - res.writeHead(200, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ ok: true, server: "storage.example" })); - }; - - await withHttpsServer(serverCreds, handler, async (port) => { - const url = `https://127.0.0.1:${port}/`; - info(`Server listening at ${url}`); - - // -- Step 6: trusted client request - step(6, "Client that trusts the test CA → should succeed"); - - const trustedResult = await httpsGet(url, ca.clientAgentOptions()); - if (trustedResult.statusCode === 200) { - ok(`Status: ${trustedResult.statusCode}`); - ok(`Body: ${trustedResult.body}`); - } else { - fail(`Unexpected status: ${trustedResult.statusCode}`); - } - - // -- Step 7: untrusted client request - step(7, "Client using default CA store → should be rejected (UNABLE_TO_VERIFY_LEAF_SIGNATURE)"); - - try { - await httpsGet(url, { rejectUnauthorized: true }); - fail("Request succeeded — test CA should NOT be trusted by default store"); - } catch (err: unknown) { - const code = (err as NodeJS.ErrnoException).code ?? ""; - if (code === "DEPTH_ZERO_SELF_SIGNED_CERT" || code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE" || code === "SELF_SIGNED_CERT_IN_CHAIN") { - ok(`Correctly rejected: ${code}`); - } else { - // Some Node versions surface ERR_TLS_CERT_ALTNAME_INVALID because - // we're connecting to 127.0.0.1 rather than storage.example; that - // still proves the cert is not blindly trusted. - ok(`Correctly rejected with TLS error: ${code}`); - } - } - }); - - // -- Step 8: Block structure summary - step(8, "Inspect the serialised block structure"); - - info("block.algorithm: " + JSON.stringify(block.algorithm)); - info("block.caKeyPair.privateKeyB64 length: " + block.caKeyPair.privateKeyB64.length + " chars"); - info("block.caKeyPair.publicKeyB64 length: " + block.caKeyPair.publicKeyB64.length + " chars"); - info("block.templates count: " + block.templates.length); - info("block.caCertPem starts: " + block.caCertPem.substring(0, 32) + "…"); - - const blockJson = JSON.stringify(block, null, 2); - const blockSize = Buffer.byteLength(blockJson, "utf8"); - info(`Total block size: ${blockSize} bytes (${(blockSize / 1024).toFixed(1)} KB)`); - - ok("Block is pure JSON — safe to embed in a manifest or commit to VCS"); - - // Clean up - fs.unlinkSync(BLOCK_PATH); - - console.log(`\n${bold(green("All steps completed successfully."))} ✓\n`); -} - -main().catch((err) => { - console.error(red("\nFatal error:"), err); - process.exit(1); -}); diff --git a/lws10-test-suite/signatures/index.ts b/lws10-test-suite/signatures/index.ts deleted file mode 100644 index 1b6262d..0000000 --- a/lws10-test-suite/signatures/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -export type { - IssuanceTemplate, - IssuedCertificate, - KeyAlgorithm, - SerializedKeyPair, - SubjectDN, - TestCABlock, - TlsCredentials, -} from "./types.js"; - -export { TestCA } from "./TestCA.js"; - -// Lower-level exports for advanced harnesses that need to drive issuance -// directly rather than through the TestCA facade. -export { generateKeyPair, deserializeKeyPair, derToPem, pemToDer } from "./keys.js"; -export { createCACertificate, issueFromTemplate, parseCACert, toTlsCredentials } from "./cert.js"; diff --git a/lws10-test-suite/signatures/keys.ts b/lws10-test-suite/signatures/keys.ts deleted file mode 100644 index de567cc..0000000 --- a/lws10-test-suite/signatures/keys.ts +++ /dev/null @@ -1,152 +0,0 @@ -/** - * Key generation and serialisation helpers. - * - * All crypto is performed through the Web Crypto API. We import `webcrypto` - * explicitly from `node:crypto` rather than relying on `globalThis.crypto` so - * the module works on Node 18 and under tsx, which may not wire up the global - * before module evaluation. - */ - -import { webcrypto } from "node:crypto"; -import type { KeyAlgorithm, SerializedKeyPair } from "./types.js"; - -// Single, reliable handle on SubtleCrypto — used everywhere in this module. -const subtle = webcrypto.subtle; - -// --------------------------------------------------------------------------- -// Internal helpers -// --------------------------------------------------------------------------- - -/** Map our discriminant to the `SubtleCrypto.generateKey` algorithm parameter. */ -function toSubtleAlgorithm( - alg: KeyAlgorithm -): EcKeyGenParams | RsaHashedKeyGenParams | { name: string } { - switch (alg.name) { - case "ECDSA": - return { name: "ECDSA", namedCurve: alg.namedCurve }; - case "RSASSA-PKCS1-v1_5": - return { - name: "RSASSA-PKCS1-v1_5", - modulusLength: alg.modulusLength, - publicExponent: new Uint8Array([1, 0, 1]), - hash: alg.hash, - }; - case "Ed25519": - return { name: "Ed25519" }; - } -} - -/** Return the hash algorithm to use with the key algorithm when signing. */ -export function signingAlgorithm( - alg: KeyAlgorithm -): AlgorithmIdentifier | EcdsaParams | RsaPssParams { - switch (alg.name) { - case "ECDSA": - return { name: "ECDSA", hash: { name: "SHA-256" } }; - case "RSASSA-PKCS1-v1_5": - return { name: "RSASSA-PKCS1-v1_5" }; - case "Ed25519": - return { name: "Ed25519" }; - } -} - -// --------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------- - -/** Generate a fresh key pair and serialise it for storage in a manifest. */ -export async function generateKeyPair( - algorithm: KeyAlgorithm -): Promise<{ keyPair: CryptoKeyPair; serialized: SerializedKeyPair }> { - const keyPair = await subtle.generateKey( - toSubtleAlgorithm(algorithm) as KeyAlgorithm, - true /* extractable */, - ["sign", "verify"] - ); - - const privateKeyDer = await subtle.exportKey("pkcs8", keyPair.privateKey); - const publicKeyDer = await subtle.exportKey("spki", keyPair.publicKey); - - const serialized: SerializedKeyPair = { - algorithm, - privateKeyB64: bufferToBase64url(privateKeyDer), - publicKeyB64: bufferToBase64url(publicKeyDer), - }; - - return { keyPair, serialized }; -} - -/** - * Reconstruct a `CryptoKeyPair` from a serialised manifest block. - * - * Call this inside the test harness when you need to actually sign - * certificates — you do not need to re-generate the keys every run. - */ -export async function deserializeKeyPair( - serialized: SerializedKeyPair -): Promise { - const alg = toSubtleAlgorithm(serialized.algorithm); - const sigAlg = signingAlgorithm(serialized.algorithm); - - const privateKey = await subtle.importKey( - "pkcs8", - base64urlToBuffer(serialized.privateKeyB64), - alg, - true, - ["sign"] - ); - - const publicKey = await subtle.importKey( - "spki", - base64urlToBuffer(serialized.publicKeyB64), - alg, - true, - ["verify"] - ); - - // Suppress unused-variable warning – sigAlg is consumed by the caller. - void sigAlg; - - return { privateKey, publicKey }; -} - -// --------------------------------------------------------------------------- -// PEM ↔ DER utilities (public so callers can use them with external DER blobs) -// --------------------------------------------------------------------------- - -export function bufferToBase64url(buf: ArrayBuffer): string { - return Buffer.from(buf).toString("base64url"); -} - -export function base64urlToBuffer(b64url: string): ArrayBuffer { - const buf = Buffer.from(b64url, "base64url"); - return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer; -} - -/** Convert raw DER bytes to a PEM string with the given label. */ -export function derToPem(der: ArrayBuffer, label: string): string { - const b64 = Buffer.from(der).toString("base64"); - const lines = b64.match(/.{1,64}/g) ?? []; - return `-----BEGIN ${label}-----\n${lines.join("\n")}\n-----END ${label}-----\n`; -} - -/** Strip PEM headers and decode to DER. */ -export function pemToDer(pem: string): ArrayBuffer { - const b64 = pem - .replace(/-----BEGIN [^-]+-----/g, "") - .replace(/-----END [^-]+-----/g, "") - .replace(/\s/g, ""); - const buf = Buffer.from(b64, "base64"); - return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer; -} - -/** Encode an 8-byte random serial as a hex string (RFC 5280 §4.1.2.2). */ -export function randomSerial(): string { - const bytes = new Uint8Array(8); - webcrypto.getRandomValues(bytes); - // Clear the high bit to keep the integer positive in DER encoding. - bytes[0] &= 0x7f; - return Array.from(bytes) - .map((b) => b.toString(16).padStart(2, "0")) - .join(""); -} diff --git a/lws10-test-suite/signatures/manifest-integration.ts b/lws10-test-suite/signatures/manifest-integration.ts deleted file mode 100644 index ea83734..0000000 --- a/lws10-test-suite/signatures/manifest-integration.ts +++ /dev/null @@ -1,254 +0,0 @@ -/** - * manifest-integration.ts - * - * Shows how a test harness reads the `lwst:testCA` block from the LWS test - * manifest and uses it to bootstrap TLS for every test in a suite. - * - * This is the glue layer between the JSON-LD manifest (produced by the test - * suite authors) and the live infrastructure (HTTPS servers, HTTP clients) - * that the tests run against. - * - * Run with: npx tsx src/manifest-integration.ts - */ - -import "reflect-metadata"; -import * as https from "node:https"; -import { TestCA } from "./TestCA.js"; -import type { TestCABlock, IssuedCertificate, TlsCredentials } from "./types.js"; - -// --------------------------------------------------------------------------- -// Simulated manifest fragment -// --------------------------------------------------------------------------- -// In a real suite this JSON would be parsed from manifest.jsonld. -// The `lwst:testCA` key holds a `TestCABlock` which is generated once and -// committed. We generate it inline here so this file is self-contained. - -async function buildSampleManifestFragment(): Promise<{ - "lwst:testCA": TestCABlock; - "mf:entries": Array<{ "@id": string; "lwst:name": string }>; -}> { - const { block } = await TestCA.generate({ - subject: { CN: "LWS Test CA", O: "W3C LWS Test Suite" }, - }); - - return { - "lwst:testCA": block, - "mf:entries": [ - { "@id": "#getContainer", "lwst:name": "getContainer" }, - { "@id": "#createDataResource","lwst:name": "createDataResource" }, - ], - }; -} - -// --------------------------------------------------------------------------- -// Harness bootstrap -// --------------------------------------------------------------------------- - -/** - * Everything the test harness exposes to individual tests. - * - * Built once in `beforeAll`, torn down in `afterAll`. - */ -interface HarnessContext { - /** TLS options for `https.createServer`. */ - serverCredentials: Record; - /** Issued cert objects (serial, dates, …). */ - issuedCerts: Map; - /** CA PEM for client-side trust. */ - caCertPem: string; - /** Convenience factory: returns an `https.Agent` that trusts the test CA. */ - trustedAgent(): https.Agent; -} - -async function bootstrapHarness(caBlock: TestCABlock): Promise { - const ca = await TestCA.load(caBlock); - - // Issue every named template defined in the block. - const issuedCerts = await ca.issueAll(); - - // Pre-build TLS credentials keyed by templateId. - const serverCredentials: Record = {}; - for (const [id, cert] of issuedCerts) { - serverCredentials[id] = ca.tlsCredentials(cert); - } - - return { - serverCredentials, - issuedCerts, - caCertPem: ca.caCertPem, - trustedAgent: () => new https.Agent(ca.clientAgentOptions()), - }; -} - -// --------------------------------------------------------------------------- -// Simulated test runner -// --------------------------------------------------------------------------- - -interface TestEntry { - id: string; - name: string; - run: (ctx: HarnessContext) => Promise; -} - -async function runSuite( - manifest: { "lwst:testCA": TestCABlock; "mf:entries": Array<{ "@id": string; "lwst:name": string }> }, - tests: TestEntry[] -): Promise { - console.log("\n─── Harness bootstrap ───"); - const ctx = await bootstrapHarness(manifest["lwst:testCA"]); - console.log(`✓ CA loaded, ${ctx.issuedCerts.size} certs pre-issued`); - console.log(` Issued templates: ${[...ctx.issuedCerts.keys()].join(", ")}`); - - let passed = 0; - let failed = 0; - - for (const test of tests) { - process.stdout.write(`\n Test ${test.id} (${test.name}) … `); - try { - await test.run(ctx); - console.log("✓ PASS"); - passed++; - } catch (err) { - console.log(`✗ FAIL\n ${err}`); - failed++; - } - } - - console.log(`\n─── Results: ${passed} passed, ${failed} failed ───\n`); -} - -// --------------------------------------------------------------------------- -// Two concrete (toy) tests that use the harness context -// --------------------------------------------------------------------------- - -const TESTS: TestEntry[] = [ - { - id: "#getContainer", - name: "getContainer", - async run(ctx) { - // Spin up a fake storage server using the pre-issued "storage.example" cert. - const creds = ctx.serverCredentials["storage.example"]; - if (!creds) throw new Error('No credentials for "storage.example"'); - - const server = https.createServer( - { cert: creds.cert, key: creds.key }, - (_req, res) => { - res.writeHead(200, { - "Content-Type": "application/lws+json", - Link: '; rel="linkset"; type="application/linkset+json"', - }); - res.end(JSON.stringify({ "@type": "Container", items: [] })); - } - ); - - await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); - const { port } = server.address() as { port: number }; - - try { - const response = await new Promise<{ status: number; linkHeader: string | undefined }>( - (resolve, reject) => { - const req = https.get( - `https://127.0.0.1:${port}/alice/notes/`, - { agent: ctx.trustedAgent() }, - (res) => { - res.resume(); - resolve({ - status: res.statusCode ?? 0, - linkHeader: res.headers["link"], - }); - } - ); - req.on("error", reject); - } - ); - - if (response.status !== 200) throw new Error(`Expected 200, got ${response.status}`); - if (!response.linkHeader?.includes('rel="linkset"')) - throw new Error(`Missing linkset Link header; got: ${response.linkHeader}`); - } finally { - await new Promise((r) => server.close(() => r())); - } - }, - }, - - { - id: "#createDataResource", - name: "createDataResource", - async run(ctx) { - const creds = ctx.serverCredentials["storage.example"]; - if (!creds) throw new Error('No credentials for "storage.example"'); - - const server = https.createServer( - { cert: creds.cert, key: creds.key }, - (_req, res) => { - res.writeHead(201, { - Location: "/alice/notes/shoppinglist.txt", - Link: [ - '; rel="linkset"; type="application/linkset+json"', - '; rel="up"', - '; rel="type"', - ].join(", "), - "Content-Length": "0", - }); - res.end(); - } - ); - - await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); - const { port } = server.address() as { port: number }; - - try { - const response = await new Promise<{ status: number; location: string | undefined }>( - (resolve, reject) => { - const req = https.request( - { - hostname: "127.0.0.1", - port, - path: "/alice/notes/", - method: "POST", - agent: ctx.trustedAgent(), - headers: { - "Content-Type": "text/plain", - "Content-Length": "47", - Slug: "shoppinglist.txt", - }, - }, - (res) => { - res.resume(); - resolve({ status: res.statusCode ?? 0, location: res.headers["location"] }); - } - ); - req.on("error", reject); - req.end("milk\neggs\nbread\nbutter\napples\norange juice\n"); - } - ); - - if (response.status !== 201) throw new Error(`Expected 201, got ${response.status}`); - if (!response.location?.includes("shoppinglist.txt")) - throw new Error(`Bad Location: ${response.location}`); - } finally { - await new Promise((r) => server.close(() => r())); - } - }, - }, -]; - -// --------------------------------------------------------------------------- -// Entry point -// --------------------------------------------------------------------------- -async function main() { - console.log("\n══════════════════════════════════════════"); - console.log(" LWS manifest → harness integration demo"); - console.log("══════════════════════════════════════════"); - - const manifest = await buildSampleManifestFragment(); - console.log(`\nManifest has ${manifest["mf:entries"].length} test entries`); - console.log(`testCA block size: ${JSON.stringify(manifest["lwst:testCA"]).length} chars`); - - await runSuite(manifest, TESTS); -} - -main().catch((err) => { - console.error("Fatal:", err); - process.exit(1); -}); diff --git a/lws10-test-suite/signatures/package-lock.json b/lws10-test-suite/signatures/package-lock.json deleted file mode 100644 index 6c1f158..0000000 --- a/lws10-test-suite/signatures/package-lock.json +++ /dev/null @@ -1,752 +0,0 @@ -{ - "name": "lws-ca", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "lws-ca", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "@peculiar/x509": "^2.0.0", - "reflect-metadata": "^0.2.2" - }, - "devDependencies": { - "@types/node": "^25.6.0", - "tsx": "^4.21.0", - "typescript": "^6.0.3" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@peculiar/asn1-cms": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz", - "integrity": "sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw==", - "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "@peculiar/asn1-x509-attr": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-csr": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.1.tgz", - "integrity": "sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w==", - "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-ecc": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.1.tgz", - "integrity": "sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g==", - "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-pfx": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.1.tgz", - "integrity": "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==", - "dependencies": { - "@peculiar/asn1-cms": "^2.6.1", - "@peculiar/asn1-pkcs8": "^2.6.1", - "@peculiar/asn1-rsa": "^2.6.1", - "@peculiar/asn1-schema": "^2.6.0", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-pkcs8": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.1.tgz", - "integrity": "sha512-JB5iQ9Izn5yGMw3ZG4Nw3Xn/hb/G38GYF3lf7WmJb8JZUydhVGEjK/ZlFSWhnlB7K/4oqEs8HnfFIKklhR58Tw==", - "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-pkcs9": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.1.tgz", - "integrity": "sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw==", - "dependencies": { - "@peculiar/asn1-cms": "^2.6.1", - "@peculiar/asn1-pfx": "^2.6.1", - "@peculiar/asn1-pkcs8": "^2.6.1", - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "@peculiar/asn1-x509-attr": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-rsa": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.1.tgz", - "integrity": "sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA==", - "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-schema": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", - "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", - "dependencies": { - "asn1js": "^3.0.6", - "pvtsutils": "^1.3.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-x509": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.1.tgz", - "integrity": "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==", - "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "asn1js": "^3.0.6", - "pvtsutils": "^1.3.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/asn1-x509-attr": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.1.tgz", - "integrity": "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==", - "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "asn1js": "^3.0.6", - "tslib": "^2.8.1" - } - }, - "node_modules/@peculiar/x509": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-2.0.0.tgz", - "integrity": "sha512-r10lkuy6BNfRmyYdRAfgu6dq0HOmyIV2OLhXWE3gDEPBdX1b8miztJVyX/UxWhLwemNyDP3CLZHpDxDwSY0xaA==", - "dependencies": { - "@peculiar/asn1-cms": "^2.6.0", - "@peculiar/asn1-csr": "^2.6.0", - "@peculiar/asn1-ecc": "^2.6.0", - "@peculiar/asn1-pkcs9": "^2.6.0", - "@peculiar/asn1-rsa": "^2.6.0", - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.0", - "pvtsutils": "^1.3.6", - "tslib": "^2.8.1", - "tsyringe": "^4.10.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@types/node": { - "version": "25.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", - "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", - "dev": true, - "dependencies": { - "undici-types": "~7.19.0" - } - }, - "node_modules/asn1js": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", - "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", - "dependencies": { - "pvtsutils": "^1.3.6", - "pvutils": "^1.1.5", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/get-tsconfig": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", - "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", - "dev": true, - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/pvtsutils": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", - "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", - "dependencies": { - "tslib": "^2.8.1" - } - }, - "node_modules/pvutils": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", - "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/reflect-metadata": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", - "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" - }, - "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", - "dev": true, - "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/tsyringe": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", - "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", - "dependencies": { - "tslib": "^1.9.3" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/tsyringe/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "7.19.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", - "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", - "dev": true - } - } -} diff --git a/lws10-test-suite/signatures/package.json b/lws10-test-suite/signatures/package.json deleted file mode 100644 index ce8f40e..0000000 --- a/lws10-test-suite/signatures/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "lws-ca", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "license": "ISC", - "devDependencies": { - "@types/node": "^25.6.0", - "tsx": "^4.21.0", - "typescript": "^6.0.3" - }, - "dependencies": { - "@peculiar/x509": "^2.0.0", - "reflect-metadata": "^0.2.2" - } -} diff --git a/lws10-test-suite/signatures/tsconfig.json b/lws10-test-suite/signatures/tsconfig.json deleted file mode 100644 index 8f30e6e..0000000 --- a/lws10-test-suite/signatures/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "CommonJS", - "moduleResolution": "node", - "lib": ["ES2022"], - "strict": true, - "esModuleInterop": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "outDir": "dist", - "rootDir": "src", - "declaration": true, - "sourceMap": true - }, - "include": ["src/**/*"] -} diff --git a/lws10-test-suite/signatures/types.ts b/lws10-test-suite/signatures/types.ts deleted file mode 100644 index dd53fee..0000000 --- a/lws10-test-suite/signatures/types.ts +++ /dev/null @@ -1,169 +0,0 @@ -/** - * Types shared across the test-CA implementation. - * - * The manifest embeds a `testCA` block that supplies everything a test harness - * needs to bootstrap a self-contained PKI: - * - * - The CA's serialised key pair (PKCS#8 private key + SPKI public key, both - * PEM-encoded so they are copy-pasteable into JSON without escaping). - * - The self-signed CA certificate (PEM). - * - A set of named `IssuanceTemplate`s that the harness can instantiate into - * real leaf certificates on demand. - * - * All key material is EC/P-256 by default; RSA-2048 is also supported via the - * algorithm discriminant. - */ - -// --------------------------------------------------------------------------- -// Key material -// --------------------------------------------------------------------------- - -/** Algorithm discriminant stored in the manifest. */ -export type KeyAlgorithm = - | { name: "ECDSA"; namedCurve: "P-256" | "P-384" | "P-521" } - | { name: "RSASSA-PKCS1-v1_5"; modulusLength: 2048 | 4096; hash: "SHA-256" | "SHA-384" } - | { name: "Ed25519" }; - -/** A serialised key pair as it appears inside the test manifest. */ -export interface SerializedKeyPair { - /** Algorithm used to generate this key pair. */ - algorithm: KeyAlgorithm; - /** PKCS#8 DER → base64url (no PEM headers – cleaner inside JSON). */ - privateKeyB64: string; - /** SubjectPublicKeyInfo DER → base64url. */ - publicKeyB64: string; -} - -// --------------------------------------------------------------------------- -// Certificate issuance templates -// --------------------------------------------------------------------------- - -/** Subject distinguished name expressed as a flat map of RDN types → values. */ -export interface SubjectDN { - CN: string; - O?: string; - OU?: string; - C?: string; - ST?: string; - L?: string; -} - -/** - * A named template that the harness expands into a leaf certificate. - * - * Templates are intentionally underspecified: the harness fills in the gaps - * (serial number, validity window, actual key material) at runtime so every - * test run gets fresh, non-replayable certs. - */ -export interface IssuanceTemplate { - /** Human-readable label used to look up the template by tests. */ - templateId: string; - - subject: SubjectDN; - - /** - * Subject Alternative Names. At least one of `dns` or `ip` SHOULD be - * present for server certificates so TLS hostname verification passes. - */ - san?: { - dns?: string[]; - ip?: string[]; - uri?: string[]; - email?: string[]; - }; - - /** - * Key algorithm for the *leaf* key pair. Defaults to the CA algorithm when - * omitted, which keeps the happy path simple. - */ - leafAlgorithm?: KeyAlgorithm; - - /** - * Validity period in seconds from the moment of issuance. - * Defaults to 86 400 (1 day) – short enough to stay safe in CI, long - * enough that a slow test suite won't hit clock-skew problems. - */ - validitySeconds?: number; - - /** Extended Key Usage OIDs. Defaults to ["serverAuth"]. */ - extendedKeyUsage?: Array<"serverAuth" | "clientAuth" | "emailProtection" | "codeSigning">; - - /** - * When true the issued cert is marked as a CA (basicConstraints CA:TRUE). - * Use this to create intermediate CAs for chain-validation tests. - */ - isCA?: boolean; -} - -// --------------------------------------------------------------------------- -// Test-CA manifest block -// --------------------------------------------------------------------------- - -/** - * The `testCA` block that lives inside a LWS test manifest. - * - * Example embedding: - * - * ```json - * { - * "@context": ["./context.jsonld"], - * "lwst:testCA": { - * "algorithm": { "name": "ECDSA", "namedCurve": "P-256" }, - * "caCertPem": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n", - * "caKeyPair": { ... }, - * "templates": [ ... ] - * } - * } - * ``` - */ -export interface TestCABlock { - /** Algorithm used for the CA key pair (and default for leaf keys). */ - algorithm: KeyAlgorithm; - - /** PEM-encoded self-signed CA certificate. */ - caCertPem: string; - - /** Serialised CA key pair. */ - caKeyPair: SerializedKeyPair; - - /** Named issuance templates that tests can reference. */ - templates: IssuanceTemplate[]; -} - -// --------------------------------------------------------------------------- -// Runtime types (not stored in the manifest) -// --------------------------------------------------------------------------- - -/** A fully-materialised leaf certificate produced by the harness at runtime. */ -export interface IssuedCertificate { - /** PEM certificate chain: leaf first, then CA. */ - certChainPem: string; - - /** PEM-encoded leaf private key (PKCS#8). */ - privateKeyPem: string; - - /** DER buffer of the leaf certificate (handy for TLS options). */ - certDer: ArrayBuffer; - - /** The template that was used. */ - template: IssuanceTemplate; - - /** UTC timestamp when the certificate becomes valid. */ - notBefore: Date; - - /** UTC timestamp when the certificate expires. */ - notAfter: Date; - - /** Hex serial number assigned to this certificate. */ - serialNumber: string; -} - -/** Everything a Node `tls.createServer` / `https.createServer` call needs. */ -export interface TlsCredentials { - /** PEM cert chain (leaf + CA). */ - cert: string; - /** PEM private key for the leaf. */ - key: string; - /** PEM CA certificate (pass as `ca` to clients so they trust the chain). */ - ca: string; -}