Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,17 @@ When debug access is disabled, misconfigured, or unauthorized, the debug routes

### CORS

CORS is hardcoded in the current server:
CORS defaults are:

- `Access-Control-Allow-Origin: *`
- `Access-Control-Allow-Headers: Content-Type, Authorization, X-Debug-Token`
- `Access-Control-Allow-Methods: GET,POST,OPTIONS`

There is no environment-based CORS configuration in the implementation today.
Environment overrides are supported via:

- `CORS_ALLOW_ORIGIN`
- `CORS_ALLOW_HEADERS`
- `CORS_ALLOW_METHODS`

### SSRF guard for the `fetch` verb

Expand Down
50 changes: 17 additions & 33 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,47 +44,27 @@ The server reads the first non-empty value from these lists.

#### Signer identifier

1. `CL_RECEIPT_SIGNER_ID`
2. `RECEIPT_SIGNER_ID`
3. `CL_RECEIPT_SIGNER`

`CL_RECEIPT_SIGNER` is still accepted, including by `scripts/dev.sh`, but it is not the primary name in `.env.example`.
1. `RECEIPT_SIGNER_ID`
2. `CL_RECEIPT_SIGNER_ID` (legacy alias; logs a deprecation warning)

#### Private key

1. `CL_RECEIPT_SIGNING_PRIVATE_KEY_PEM`
2. `RECEIPT_SIGNING_PRIVATE_KEY_PEM`
3. `CL_RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64`
4. `RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64`
5. `CL_RECEIPT_SIGNING_PRIVATE_KEY_B64`
6. `RECEIPT_SIGNING_PRIVATE_KEY_B64`
7. `CL_RECEIPT_SIGNING_PRIVATE_KEY_PEM_FILE`
8. `CL_PRIVATE_KEY_PEM`
9. `CL_PRIVATE_KEY_PEM_B64`
1. `RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64`
2. `CL_RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64` (legacy alias; logs a deprecation warning)

Behavior:

- `*_PEM` values are parsed as PEM text.
- `*_PEM_FILE` is read from disk.
- the remaining `*_B64` forms are base64-decoded and then parsed as PEM text.
- `*_B64` forms are base64-decoded and then parsed as PEM text.
- the private key must be PKCS#8 Ed25519 PEM.

#### Public key

1. `CL_RECEIPT_SIGNING_PUBLIC_KEY_B64`
2. `RECEIPT_SIGNING_PUBLIC_KEY_B64`
3. `CL_RECEIPT_SIGNING_PUBLIC_KEY_PEM`
4. `RECEIPT_SIGNING_PUBLIC_KEY_PEM`
5. `CL_RECEIPT_SIGNING_PUBLIC_KEY_PEM_B64`
6. `RECEIPT_SIGNING_PUBLIC_KEY_PEM_B64`
7. `CL_RECEIPT_SIGNING_PUBLIC_KEY_PEM_FILE`
8. `CL_PUBLIC_KEY_B64`
9. `RECEIPT_SIGNING_PUBLIC_KEY_RAW32_B64`
1. `RECEIPT_SIGNING_PUBLIC_KEY_B64`
2. `CL_RECEIPT_SIGNING_PUBLIC_KEY_B64` (legacy alias; logs a deprecation warning)

Behavior:

- `*_PUBLIC_KEY_B64` and `*_RAW32_B64` are treated as raw 32-byte Ed25519 public keys.
- `*_PEM`, `*_PEM_FILE`, and `*_PEM_B64` are treated as PEM public keys.
- `*_PUBLIC_KEY_B64` values are treated as raw 32-byte Ed25519 public keys.
- the server converts accepted public-key inputs to SPKI PEM internally for verification.


Expand Down Expand Up @@ -141,6 +121,8 @@ Important current behavior:

`SCHEMA_HOST` is used to construct `v1.1.0` receipt schema URLs during `/verify?schema=1`.

Implementation note: `server.mjs` intentionally keeps a minimal inline `BUILTIN_SHARED_SCHEMAS` map for `_shared/*` and generated commons receipt schemas as an offline/bootstrapping fallback for verification paths.

`REQUEST_SCHEMA_VALIDATION` is not implemented by `server.mjs`.

## Schema and validator caches
Expand Down Expand Up @@ -187,15 +169,19 @@ Current SSRF guard behavior blocks:

## CORS

The current server does not expose environment-based CORS configuration.
The server exposes environment-based CORS configuration:

- `CORS_ALLOW_ORIGIN` (default `*`)
- `CORS_ALLOW_HEADERS` (default `Content-Type, Authorization, X-Debug-Token`)
- `CORS_ALLOW_METHODS` (default `GET,POST,OPTIONS`)

It always returns:
Defaults are:

- `Access-Control-Allow-Origin: *`
- `Access-Control-Allow-Headers: Content-Type, Authorization, X-Debug-Token`
- `Access-Control-Allow-Methods: GET,POST,OPTIONS`

`CORS_ALLOW_ORIGINS`, `CORS_ALLOW_HEADERS`, and `CORS_ALLOW_METHODS` are not implemented.
`CORS_ALLOW_ORIGINS` (plural) is not implemented.

## Not implemented in `server.mjs`

Expand All @@ -206,8 +192,6 @@ These names appear in older docs or conventions but are not read by the live ser
- `ENS_SIG_PUB_TEXT_KEY`
- `ENS_SIG_KID_TEXT_KEY`
- `CORS_ALLOW_ORIGINS`
- `CORS_ALLOW_HEADERS`
- `CORS_ALLOW_METHODS`
- `RATE_LIMIT_ENABLED`
- `RATE_LIMIT_MAX`
- `RATE_LIMIT_WINDOW_MS`
Expand Down
4 changes: 1 addition & 3 deletions scripts/dev.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,5 @@ source ./keys.env
ENABLE_DEBUG=1 DEBUG_TOKEN=smoke \
HOST="$HOST" PORT="$PORT" \
ETH_RPC_URL="${ETH_RPC_URL:-}" \
CL_RECEIPT_SIGNER="runtime.commandlayer.eth" \
CL_KEY_ID="v1" \
CL_CANONICAL_ID="json.sorted_keys.v1" \
RECEIPT_SIGNER_ID="runtime.commandlayer.eth" \
node server.mjs
77 changes: 38 additions & 39 deletions server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,13 @@ import {
// env helpers (allow legacy aliases)
// -----------------------
function envAny(...names) {
for (const n of names) {
for (let i = 0; i < names.length; i += 1) {
const n = names[i];
const v = process.env[n];
if (v && String(v).trim()) return String(v).trim();
if (v && String(v).trim()) {
if (i > 0) console.warn(`[deprecation] Env alias "${n}" is deprecated; use "${names[0]}"`);
return String(v).trim();
}
}
return null;
}
Expand Down Expand Up @@ -71,11 +75,15 @@ app.use((err, req, res, next) => {
next();
});

const CORS_ALLOW_ORIGIN = String(process.env.CORS_ALLOW_ORIGIN || "*").trim() || "*";
const CORS_ALLOW_HEADERS = String(process.env.CORS_ALLOW_HEADERS || "Content-Type, Authorization, X-Debug-Token").trim();
const CORS_ALLOW_METHODS = String(process.env.CORS_ALLOW_METHODS || "GET,POST,OPTIONS").trim();

// basic CORS (no deps)
app.use((req, res, next) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Debug-Token");
res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
res.setHeader("Access-Control-Allow-Origin", CORS_ALLOW_ORIGIN);
res.setHeader("Access-Control-Allow-Headers", CORS_ALLOW_HEADERS);
res.setHeader("Access-Control-Allow-Methods", CORS_ALLOW_METHODS);
if (req.method === "OPTIONS") return res.status(204).end();
next();
});
Expand All @@ -97,36 +105,26 @@ const ENABLED_VERBS = (
const DEV_AUTO_KEYS = envFlag("DEV_AUTO_KEYS");

function envAnySource(...names) {
for (const n of names) {
for (let i = 0; i < names.length; i += 1) {
const n = names[i];
const v = process.env[n];
if (v && String(v).trim()) return { name: n, value: String(v).trim() };
if (v && String(v).trim()) {
if (i > 0) console.warn(`[deprecation] Env alias "${n}" is deprecated; use "${names[0]}"`);
return { name: n, value: String(v).trim() };
}
}
return null;
}

const runtimeConfig = {
signerId: String(envAny("CL_RECEIPT_SIGNER_ID", "RECEIPT_SIGNER_ID", "CL_RECEIPT_SIGNER") || "").trim(),
signerId: String(envAny("RECEIPT_SIGNER_ID", "CL_RECEIPT_SIGNER_ID") || "").trim(),
privateKeySource: envAnySource(
"CL_RECEIPT_SIGNING_PRIVATE_KEY_PEM",
"RECEIPT_SIGNING_PRIVATE_KEY_PEM",
"CL_RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64",
"RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64",
"CL_RECEIPT_SIGNING_PRIVATE_KEY_B64",
"RECEIPT_SIGNING_PRIVATE_KEY_B64",
"CL_RECEIPT_SIGNING_PRIVATE_KEY_PEM_FILE",
"CL_PRIVATE_KEY_PEM",
"CL_PRIVATE_KEY_PEM_B64"
"CL_RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64"
),
publicKeySource: envAnySource(
"CL_RECEIPT_SIGNING_PUBLIC_KEY_B64",
"RECEIPT_SIGNING_PUBLIC_KEY_B64",
"CL_RECEIPT_SIGNING_PUBLIC_KEY_PEM",
"RECEIPT_SIGNING_PUBLIC_KEY_PEM",
"CL_RECEIPT_SIGNING_PUBLIC_KEY_PEM_B64",
"RECEIPT_SIGNING_PUBLIC_KEY_PEM_B64",
"CL_RECEIPT_SIGNING_PUBLIC_KEY_PEM_FILE",
"CL_PUBLIC_KEY_B64",
"RECEIPT_SIGNING_PUBLIC_KEY_RAW32_B64"
"CL_RECEIPT_SIGNING_PUBLIC_KEY_B64"
),
canonicalId: CANONICAL_ID_SORTED_KEYS_V1,
kid: "",
Expand Down Expand Up @@ -1432,18 +1430,19 @@ function doExplain(body) {
}

function doAnalyze(body) {
const input = String(body?.input ?? "");
if (!input.trim()) throw new Error("analyze.input required (string)");
const input = body?.input || {};
const content = String(input?.content ?? "");
if (!content.trim()) throw new Error("analyze.input.content required");

const goal = String(body?.goal ?? "").trim();
const hints = Array.isArray(body?.hints) ? body.hints.map(String) : [];
const lines = input.split(/\r?\n/).filter((l) => l.trim() !== "");
const words = input.trim().split(/\s+/).filter(Boolean);
const lines = content.split(/\r?\n/).filter((l) => l.trim() !== "");
const words = content.trim().split(/\s+/).filter(Boolean);

const containsUrls = /\bhttps?:\/\/[^\s]+/i.test(input);
const containsEmails = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i.test(input);
const containsJsonMarkers = /[{[\]}]/.test(input);
const containsNumbers = /\b\d+(\.\d+)?\b/.test(input);
const containsUrls = /\bhttps?:\/\/[^\s]+/i.test(content);
const containsEmails = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i.test(content);
const containsJsonMarkers = /[{[\]}]/.test(content);
const containsNumbers = /\b\d+(\.\d+)?\b/.test(content);

const labels = [];
if (containsJsonMarkers) labels.push("structured");
Expand All @@ -1459,7 +1458,7 @@ function doAnalyze(body) {

const summary = `Deterministic analysis: ${labels.join(",") || "plain_text"}. Goal="${goal || "n/a"}". Score=${score}.`;
const insights = [
`Input length: ${input.length} chars; ~${words.length} words; ${lines.length} non-empty lines.`,
`Input length: ${content.length} chars; ~${words.length} words; ${lines.length} non-empty lines.`,
goal ? `Goal: ${goal}` : "Goal: (none)",
`Hints provided: ${hints.length}.`,
];
Expand Down Expand Up @@ -1663,12 +1662,12 @@ app.get("/debug/env", requireDebug, (req, res) => {
verifier_ok: !!pubPem || hasRpc(),
signer_source: activeSigner.source,
env_presence: {
CL_RECEIPT_SIGNER: !!process.env.CL_RECEIPT_SIGNER,
CL_KEY_ID: !!process.env.CL_KEY_ID,
CL_CANONICAL_ID: !!process.env.CL_CANONICAL_ID,
CL_PRIVATE_KEY_PEM: !!process.env.CL_PRIVATE_KEY_PEM,
CL_PRIVATE_KEY_PEM_B64: !!process.env.CL_PRIVATE_KEY_PEM_B64,
CL_PUBLIC_KEY_B64: !!process.env.CL_PUBLIC_KEY_B64,
RECEIPT_SIGNER_ID: !!process.env.RECEIPT_SIGNER_ID,
CL_RECEIPT_SIGNER_ID: !!process.env.CL_RECEIPT_SIGNER_ID,
RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64: !!process.env.RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64,
CL_RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64: !!process.env.CL_RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64,
RECEIPT_SIGNING_PUBLIC_KEY_B64: !!process.env.RECEIPT_SIGNING_PUBLIC_KEY_B64,
CL_RECEIPT_SIGNING_PUBLIC_KEY_B64: !!process.env.CL_RECEIPT_SIGNING_PUBLIC_KEY_B64,
ETH_RPC_URL: !!process.env.ETH_RPC_URL,
},
key_loading_mode: {
Expand Down
15 changes: 5 additions & 10 deletions tests/smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,7 @@ function generateTestKeyPair() {
const b64 = pubDer.toString("base64");
const wrapped = (b64.match(/.{1,64}/g) || [b64]).join("\n");
const pubPem = "-----BEGIN PUBLIC KEY-----\n" + wrapped + "\n-----END PUBLIC KEY-----\n";
const pubPemB64 = Buffer.from(pubPem, "utf8").toString("base64");

return { privatePemEscaped, publicKeyB64, pubPemB64 };
return { privatePemEscaped, publicKeyB64 };
}

function spawnRuntime({ port, env }) {
Expand Down Expand Up @@ -158,18 +156,15 @@ async function main() {
const baseUrl = `http://127.0.0.1:${port}`;
const SMOKE_ENS = String(process.env.SMOKE_ENS || "0") === "1";

const { privatePemEscaped, publicKeyB64, pubPemB64 } = generateTestKeyPair();
const { privatePemEscaped, publicKeyB64 } = generateTestKeyPair();

const env = {
ENABLE_DEBUG: "1",
DEBUG_TOKEN: "smoke",
...(SMOKE_ENS && process.env.ETH_RPC_URL ? { ETH_RPC_URL: String(process.env.ETH_RPC_URL) } : {}),
CL_RECEIPT_SIGNER: "runtime.commandlayer.eth",
CL_PRIVATE_KEY_PEM: privatePemEscaped,
RECEIPT_SIGNING_PRIVATE_KEY_PEM: privatePemEscaped,
CL_PUBLIC_KEY_B64: publicKeyB64,
RECEIPT_SIGNING_PUBLIC_KEY_RAW32_B64: publicKeyB64,
RECEIPT_SIGNING_PUBLIC_KEY_PEM_B64: pubPemB64,
RECEIPT_SIGNER_ID: "runtime.commandlayer.eth",
RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64: Buffer.from(privatePemEscaped.replace(/\\n/g, "\n"), "utf8").toString("base64"),
RECEIPT_SIGNING_PUBLIC_KEY_B64: publicKeyB64,
};

const proc = spawnRuntime({ port, env });
Expand Down
Loading