ed-sig-http
v0.2.0
Published
Ed25519 signatures for HTTP messages — a narrow profile of RFC 9421.
Readme
ed-sig-http (TypeScript)
Ed25519 signatures for HTTP messages — a narrow profile of RFC 9421.
This package is the TypeScript reference implementation of the signed-calls profile used by MoltWeb agents (spec §4). It is deliberately generic: nothing in the library mentions MoltWeb by name. The consuming application provides the identity header name, identity value, and the public-key resolver; the library handles signing and verification.
Runs in Node 20+, Deno (including Val Town), and modern browsers.
Status
Pre-release. Published under the dummy name ed-sig-http for
early development. The API is expected to stabilize through use inside
the MoltWeb registry and the tutorial suite; a rebrand and a 1.0
release will follow. Until then, pin to an exact version in production.
What it is
- An Ed25519-only HTTP Message Signatures library.
- Implements exactly the covered components the MoltWeb profile needs:
@method,@target-uri,@authority,content-digest(when body present), plus a caller-configured identity header. - Timestamp window, nonce replay protection, keyid pinning.
- Pluggable
PublicKeyResolverandNonceCacheinterfaces.
What it is not
- A general RFC 9421 implementation. No ECDSA, no RSA, no HMAC, no arbitrary covered-component selection.
Install
npm install ed-sig-httpIn Deno / Val Town, import from esm.sh:
import { sign, verify } from "https://esm.sh/[email protected]";Usage
Signing
import { sign } from "ed-sig-http";
const signed = await sign({
request: {
method: "POST",
url: "https://pricer.example.com/quote",
body: JSON.stringify({ product: "Sony WH-1000XM5" }),
headers: { "Content-Type": "application/json" },
},
privateKey: "z...", // multibase-encoded Ed25519 private key
publicKey: "z...", // multibase-encoded Ed25519 public key
identityHeader: "MoltWeb-Identity",
identity: "moltweb:shopper-bot",
});
// signed.headers now has Signature-Input, Signature, Content-Digest,
// and MoltWeb-Identity set. Pass them to your HTTP client.
await fetch(signed.url, {
method: signed.method,
headers: signed.headers,
body: signed.body,
});Verifying
import { verify, type PublicKeyResolver } from "ed-sig-http";
const resolver: PublicKeyResolver = {
async resolve(identity: string): Promise<string> {
// Hit a registry, read a cache, query a local table —
// whatever. Return the public key in multibase form.
const res = await fetch(`https://moltweb.app/.well-known/moltweb/id/${handleFrom(identity)}`);
return (await res.json()).public_key;
},
};
// In your request handler:
const headers: Record<string, string> = {};
for (const [k, v] of req.headers) headers[k] = v;
const result = await verify({
request: {
method: req.method,
url: req.url,
headers,
body: await req.text(),
},
resolver,
identityHeader: "MoltWeb-Identity",
});
if (!result.ok) {
return new Response(JSON.stringify({ reason: result.reason }), { status: 401 });
}
// result.identity is the verified caller.
// result.keyid is their public key, ready for per-caller policy.Failure reasons
result.reason is a stable, machine-readable string:
missing_headers · malformed_signature_input · signature_label_mismatch
missing_parameter · unsupported_algorithm · timestamp_outside_window
content_digest_not_covered · content_digest_covered_without_body
content_digest_mismatch · identity_not_covered · keyid_mismatch
nonce_replay · malformed_signature · signature_verify_error
signature_invalid · uncovered_component_missingNew reasons will only be added, never removed or renamed. Resolver
failures throw rather than returning a VerifyResult — they indicate
infrastructure problems, not caller errors.
Pluggable nonce cache
The default InMemoryNonceCache is suitable for single-process
deployments (Val Town, a single worker process, tests). For multi-worker
production setups, implement the NonceCache interface:
import type { NonceCache } from "ed-sig-http";
class RedisNonceCache implements NonceCache {
async checkAndStore(identity: string, nonce: string, timestamp: number): Promise<boolean> {
const key = `nonce:${identity}:${nonce}`;
const set = await redis.set(key, String(timestamp), { EX: 300, NX: true });
return set === "OK";
}
}
await verify({ request, resolver, identityHeader }, { nonceCache: new RedisNonceCache() });Testing
npm install
npm testTests validate against /packages/test-vectors/v0.1.json — the same
vectors the PHP package must satisfy. If these fail, either the
library has a bug or the vectors are stale.
License
Apache-2.0.
