
Research
/Security News
77 Firefox Extensions Linked to Crypto Wallet and Credential Theft
Socket uncovered 77 linked Firefox extensions, including 40 that steal wallet secrets or credentials and 37 deceptive sports-score shells.
@hellocoop/httpsig
Advanced tools
HTTP Message Signatures (RFC 9421) with Signature-Key header support
HTTP Message Signatures (RFC 9421) implementation with Signature-Key header support for Node.js and browsers.
This package implements RFC 9421 HTTP Message Signatures with support for the Signature-Key header proposal, enabling cryptographic signing and verification of HTTP requests.
| Package line | Implements | npm dist-tag |
|---|---|---|
2.x | draft-hardt-httpbis-signature-key-08 | latest |
1.x | draft-hardt-httpbis-signature-key-05 | — |
The draft is not yet adopted and -08 is not backward compatible with -05.
npm install @hellocoop/httpsig gives you 2.x. 1.x continues on the
1.x branch. See MIGRATING-2.0.md for what changed.
Key Features:
hwk, jwt, and jwks_urifetch() wrapper and verify() middleware helpernpm install @hellocoop/httpsig
import { fetch } from '@hellocoop/httpsig'
// Make a signed GET request with inline public key (hwk)
const response = await fetch('https://api.example.com/data', {
signingKey: privateKeyJwk, // JsonWebKey with private key
signatureKey: { type: 'hwk' },
})
// Make a signed POST request
const response = await fetch('https://api.example.com/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ foo: 'bar' }),
signingKey: privateKeyJwk,
signatureKey: { type: 'hwk' },
})
import { verify } from '@hellocoop/httpsig'
// In Express middleware
app.use(async (req, res, next) => {
try {
// Parse URL to extract path and query
const urlObj = new URL(
req.originalUrl,
`${req.protocol}://${req.hostname}`,
)
const result = await verify({
method: req.method,
authority: req.hostname,
path: urlObj.pathname,
query: urlObj.search ? urlObj.search.substring(1) : undefined,
headers: req.headers,
body: req.body,
})
if (result.verified) {
req.signature = result
next()
} else {
res.status(401).json({ error: 'Invalid signature' })
}
} catch (error) {
res.status(401).json({ error: error.message })
}
})
fetch(url, options)A drop-in replacement for the standard fetch() that automatically signs requests.
Parameters:
url (string | URL): The URL to fetchoptions (HttpSigFetchOptions): Standard fetch options plus signing parametersHttpSigFetchOptions extends RequestInit:
interface HttpSigFetchOptions extends RequestInit {
// Required: Private key as JWK
signingKey: JsonWebKey
// Required: Signature-Key header configuration
signatureKey:
| { type: 'hwk' }
| { type: 'jwt'; jwt: string }
| { type: 'jwks_uri'; id: string; kid: string; wellKnown: string }
// Optional parameters
label?: string // Signature label (default: 'sig')
components?: string[] // Override default components
// Content-Digest coverage for requests with a body (default: 'auto')
// 'auto' - cover content-digest when the body's exact bytes are
// available to hash (string, Uint8Array, ArrayBuffer, Buffer);
// streaming bodies (ReadableStream, FormData, Blob) are
// signed without it
// 'require' - like 'auto', but throw on a body that cannot be digested
// 'omit' - never auto-append content-digest
contentDigest?: 'auto' | 'require' | 'omit'
// Testing mode
dryRun?: boolean // Return headers without fetching (still returns Promise)
}
Returns:
Promise<Response> - Standard fetch Response objectdryRun: true, returns Promise<{ headers: Headers }> with the headers that would be sentExample with hwk:
const response = await fetch('https://api.example.com/data', {
signingKey: privateKeyJwk,
signatureKey: { type: 'hwk' },
})
Example with JWT:
const response = await fetch('https://api.example.com/data', {
signingKey: privateKeyJwk,
signatureKey: {
type: 'jwt',
jwt: 'eyJhbGciOiJFZERTQSIsInR5cCI6ImFnZW50K2p3dCJ9...',
},
})
Example with JWKS:
const response = await fetch('https://api.example.com/data', {
signingKey: privateKeyJwk,
signatureKey: {
type: 'jwks_uri',
id: 'https://agent.example',
kid: 'key-1',
wellKnown: 'agent-server', // Optional
},
})
Testing mode (dry run):
const { headers } = await fetch('https://api.example.com/data', {
signingKey: privateKeyJwk,
signatureKey: { type: 'hwk' },
dryRun: true,
})
console.log(headers.get('Signature'))
console.log(headers.get('Signature-Input'))
console.log(headers.get('Signature-Key'))
Overriding default components:
import {
fetch,
DEFAULT_COMPONENTS_GET,
DEFAULT_COMPONENTS_BODY,
} from '@hellocoop/httpsig'
// Default components for requests without body (GET, DELETE):
// ['@method', '@authority', '@path', 'signature-key']
// Default components for requests with body (POST, PUT, PATCH):
// ['@method', '@authority', '@path', 'content-type', 'signature-key']
// Override defaults for RFC 9421 interoperability
const response = await fetch('https://api.example.com/data', {
method: 'POST',
headers: {
date: new Date().toUTCString(),
'content-type': 'application/json',
},
body: JSON.stringify({ foo: 'bar' }),
signingKey: privateKeyJwk,
signatureKey: { type: 'hwk' },
// Override with different components
components: [
'date',
'@method',
'@path',
'@authority',
'content-type',
'signature-key',
],
})
// To extend defaults, add new components
const components = [
...DEFAULT_COMPONENTS_BODY,
'date', // Add date header to signature
'authorization', // Add authorization header
]
// Note: Duplicates are automatically removed
verify(request, options?)Verifies HTTP Message Signatures on incoming requests.
Parameters:
request (VerifyRequest): The request to verifyoptions? (VerifyOptions): Optional verification configurationVerifyRequest:
interface VerifyRequest {
method: string
authority: string // Canonical authority (e.g., 'api.example.com')
path: string // Request path (e.g., '/api/data')
query?: string // Optional query string without leading '?' (e.g., 'foo=bar')
headers: Headers | Record<string, string | string[]>
body?: string | Buffer | Uint8Array
}
Note: The body must be raw bytes (string, Buffer, or Uint8Array), NOT a parsed object. If you include content-digest in your components, signature verification will fail with parsed JSON because the content-digest is computed over the exact bytes.
VerifyOptions:
interface VerifyOptions {
// Timestamp validation
maxClockSkew?: number // Max clock skew in seconds (default: 60)
// JWKS caching
jwksCacheTtl?: number // JWKS cache TTL in ms (default: 3600000)
// Algorithms this verifier accepts (default: SUPPORTED_ALGORITHMS)
supportedAlgorithms?: SignatureAlgorithm[]
// AAuth HTTPSig profile (Section 10.3) enforcement: when true, a request
// with a body fails verification unless the signature covers
// content-digest and the digest validates against the body
requireContentDigest?: boolean
}
Returns: Promise<VerificationResult>
interface VerificationResult {
verified: boolean // Overall verification status
label: string // Signature label used
keyType: 'hwk' | 'jwt' | 'jwks_uri'
publicKey: JsonWebKey // Extracted public key
thumbprint: string // JWK thumbprint (RFC 7638) - stable key identifier
created: number // Signature timestamp
// JWT-specific fields (if keyType === 'jwt')
// Note: JWT is NOT validated - caller must validate issuer, expiration, etc.
jwt?: {
header: object
payload: object
raw: string // Raw JWT for caller to validate
}
// JWKS-specific fields (if keyType === 'jwks_uri')
jwks_uri?: {
id: string
kid: string
wellKnown: string
}
// Error information
error?: string
}
Example with Express:
import express from 'express'
import { expressVerify } from '@hellocoop/httpsig'
const app = express()
// IMPORTANT: Use express.raw() NOT express.json()!
app.use(express.raw({ type: 'application/json' }))
app.use(async (req, res, next) => {
const result = await expressVerify(req)
if (result.verified) {
req.signature = result
next()
} else {
res.status(401).json({ error: result.error })
}
})
Example with Fastify:
import Fastify from 'fastify'
import { fastifyVerify } from '@hellocoop/httpsig'
const fastify = Fastify({
// Preserve raw body for signature verification
preParsing: async (request, reply, payload) => {
const chunks: Buffer[] = []
for await (const chunk of payload) {
chunks.push(chunk)
}
request.rawBody = Buffer.concat(chunks)
return Buffer.concat(chunks)
},
})
fastify.addHook('preHandler', async (request, reply) => {
const result = await fastifyVerify(request)
if (!result.verified) {
reply.code(401).send({ error: result.error })
return
}
request.signature = result
})
Example with Next.js App Router:
import { nextJsVerify } from '@hellocoop/httpsig'
export async function POST(request: Request) {
// IMPORTANT: Consume body BEFORE verification!
const body = await request.text()
const result = await nextJsVerify(request, body)
if (!result.verified) {
return Response.json({ error: result.error }, { status: 401 })
}
// Parse body after verification
const data = JSON.parse(body)
// ... handle request
}
Example with JWT validation:
const result = await verify(request)
if (result.verified && result.keyType === 'jwt') {
// Caller is responsible for validating the JWT
const jwt = result.jwt
// Decode and validate JWT claims
const isValid = await validateJWT(jwt.raw, {
trustedIssuers: ['https://auth.example.com'],
// ... other validation logic
})
if (!isValid) {
throw new Error('Invalid JWT')
}
}
Example using thumbprint for authorization:
// Store allowed public key thumbprints (e.g., from registration)
const ALLOWED_THUMBPRINTS = new Set([
'NZQltk3VvFCjGIx8-UtxKBwkjRZ6O8kPKYNa3mRYFX8',
'kOzFrbnFA0SWOSKmY76ok0Ke-soe9Ja41xzhlK9v8Yo',
])
app.use(async (req, res, next) => {
const result = await expressVerify(req)
if (!result.verified) {
return res.status(401).json({ error: result.error })
}
// Use thumbprint as stable identifier for rate limiting, access control, etc.
if (!ALLOWED_THUMBPRINTS.has(result.thumbprint)) {
return res.status(403).json({
error: 'Public key not authorized',
thumbprint: result.thumbprint,
})
}
// Store thumbprint for logging/auditing
req.callerThumbprint = result.thumbprint
next()
})
verify()When verifying HTTP Message Signatures, you MUST provide:
// ❌ WRONG - body is parsed object
app.use(express.json())
app.use((req, res) => {
verify({
body: req.body, // This is { foo: "bar" }, not raw bytes!
})
})
// ❌ WRONG - url is just the path
verify({
url: req.url, // This is "/api/data", not "https://example.com/api/data"
})
Use the framework-specific verify functions which handle these requirements automatically:
import { expressVerify } from '@hellocoop/httpsig'
app.use(express.raw({ type: 'application/json' }))
app.use(async (req, res) => {
const result = await expressVerify(req)
})
Raw Body: If you use the content-digest component, it is computed over the exact bytes of the body. If you parse JSON and re-serialize it:
{"foo":"bar"} vs {"foo": "bar"}Authority and Path: The signature covers @authority and @path. Providing incorrect values will produce a different signature base → verification fails.
The package provides framework-specific functions that handle URL construction and body handling automatically:
expressVerify(req, options?) - Express.jsfastifyVerify(request, options?) - FastifynextJsVerify(request, body?, options?) - Next.js App RouternextJsPagesVerify(req, body?, host?, options?) - Next.js Pages RouterThese functions call verify() internally after correctly transforming the request.
See examples in the verify() documentation above.
By default, requests are signed with these components:
Requests without a body (GET, DELETE):
@method - HTTP method@authority - Host authority@path - Request pathsignature-key - The Signature-Key headerSignature-Input: sig=("@method" "@authority" "@path" "signature-key");created=1730217600
Requests with a body (POST, PUT, PATCH):
@method - HTTP method@authority - Host authority@path - Request pathcontent-type - Content-Type headersignature-key - The Signature-Key headerSignature-Input: sig=("@method" "@authority" "@path" "content-type" "signature-key");created=1730217600
Content-Digest (automatic since 2.2.0)
Per the AAuth HTTPSig profile (Section 10.3), a request carrying a body to a
PS or AS endpoint MUST also cover content-digest (RFC 9530). fetch()
appends content-digest to the covered components automatically whenever the
body's exact bytes are available to hash — a string, Uint8Array, ArrayBuffer,
or Buffer. A body serialized by the fetch implementation (ReadableStream,
FormData, Blob) is signed without it; pass contentDigest: 'require' to
throw instead, or contentDigest: 'omit' to never auto-append. The header is
computed as:
Content-Digest: sha-256=:BASE64(SHA256(body)):
A verifier enforces coverage with requireContentDigest: true.
You can override the default components using the components parameter. The library exports helpful constants:
Exported Constants:
import {
VALID_DERIVED_COMPONENTS, // All valid RFC 9421 derived components
DEFAULT_COMPONENTS_GET, // Default for GET requests
DEFAULT_COMPONENTS_BODY, // Default for requests with body
} from '@hellocoop/httpsig'
// VALID_DERIVED_COMPONENTS contains:
// ['@method', '@target-uri', '@authority', '@scheme',
// '@request-target', '@path', '@query', '@query-param', '@status']
// DEFAULT_COMPONENTS_GET contains:
// ['@method', '@authority', '@path', 'signature-key']
// DEFAULT_COMPONENTS_BODY contains:
// ['@method', '@authority', '@path', 'content-type', 'signature-key']
Example - Custom components:
// Add the date header to the covered components
await fetch('https://api.example.com/data', {
method: 'POST',
headers: {
date: new Date().toUTCString(),
'content-type': 'application/json',
},
body: JSON.stringify({ data: 'value' }),
signingKey: privateKeyJwk,
signatureKey: { type: 'hwk' },
components: [
'@method',
'@authority',
'@path',
'date', // Include date header
'content-type',
'signature-key',
// content-digest is appended automatically for a digestible body
],
})
Component Validation:
@) must be in VALID_DERIVED_COMPONENTSThe Signature-Key header uses RFC 8941 Structured Fields Dictionary format with exactly one dictionary member. The member key (label) is used to correlate the three signature headers: Signature-Key, Signature-Input, and Signature.
Format: label=scheme;param1="value1";param2="value2"
Label Discovery: During verification, the label is automatically discovered from the Signature-Key header (per AAuth spec). The same label must appear in both Signature-Input and Signature headers.
AAuth Profile Requirement: When strictAAuth: true (default), the signature-key component must be included in the covered components list.
Inline public key in the header for pseudonymous verification.
const response = await fetch(url, {
signingKey: privateKeyJwk,
signatureKey: { type: 'hwk' },
})
Generated headers (RFC 8941 Dictionary format):
Signature-Key: sig=hwk;kty="OKP";crv="Ed25519";x="JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs"
Use cases:
Public key embedded in a signed JWT using the cnf.jwk claim.
const response = await fetch(url, {
signingKey: privateKeyJwk,
signatureKey: {
type: 'jwt',
jwt: agentToken, // JWT with cnf.jwk claim
},
})
Generated headers (RFC 8941 Dictionary format):
Signature-Key: sig=jwt;jwt="eyJhbGciOiJFZERTQSIsInR5cCI6ImFnZW50K2p3dCJ9..."
The JWT must contain:
{
"iss": "https://issuer.example",
"sub": "instance-123",
"exp": 1732210000,
"cnf": {
"jwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs"
}
}
}
Use cases:
Key discovery via HTTPS URLs with automatic caching.
const response = await fetch(url, {
signingKey: privateKeyJwk,
signatureKey: {
type: 'jwks_uri',
id: 'https://agent.example',
kid: 'key-1',
},
})
Generated headers (RFC 8941 Dictionary format):
Signature-Key: sig=jwks_uri;id="https://agent.example";kid="key-1"
With well-known metadata:
const response = await fetch(url, {
signingKey: privateKeyJwk,
signatureKey: {
type: 'jwks_uri',
id: 'https://agent.example',
kid: 'key-1',
wellKnown: 'agent-server',
},
})
Generated headers (RFC 8941 Dictionary format):
Signature-Key: sig=jwks_uri;id="https://agent.example";kid="key-1";well-known="agent-server"
Discovery process:
well-known present: fetch {id}/.well-known/{well-known}, extract jwks_uri, fetch JWKSwell-known absent: fetch {id} directly as JWKSkidUse cases:
We support the two most widely recommended algorithms from the IANA HTTP Message Signatures registry:
Ed25519 (ed25519) - EdDSA with Curve25519 - Recommended
ES256 (ecdsa-p256-sha256) - ECDSA with P-256 and SHA-256
Every header this package reads or writes is an RFC 8941 Structured Field, and
the parser and serializer that handle them are exported. Use them for
neighbouring fields rather than writing another parser — AAuth-Requirement is
a Dictionary, AAuth-Capabilities a List of Tokens, and hand-rolled 8941 fails
on the same three things every time: quoting, escaping, and byte sequences.
import {
parseDictionary,
serializeDictionary,
Token,
bareItemToString,
} from '@hellocoop/httpsig'
// A `;` inside a quoted string does not end the parameter list.
const dict = parseDictionary(
'requirement=interaction; url="https://resource.example/i?a=1;b=2"; code="A1B2-C3D4"',
)
const [value, params] = dict.get('requirement') as Item
value instanceof Token // true — `interaction` is a Token, not a String
params.get('url') // 'https://resource.example/i?a=1;b=2'
params.get('code') // 'A1B2-C3D4'
serializeDictionary(
new Map([
[
'requirement',
[new Token('auth-token'), new Map([['resource-token', jwt]])],
],
]),
)
Exported
| Parsing | parseDictionary, parseList, parseItem, ParseError |
| Serializing | serializeDictionary, serializeList, serializeItem, serializeInnerList, serializeBareItem, serializeParameters, SerializeError |
| Values | Token, ByteSequence |
| Guards | isInnerList, isByteSequence, isValidTokenStr, isValidKeyStr |
| Helper | bareItemToString — reads a String or a Token, refuses anything else |
| Types | Dictionary, List, Item, InnerList, Parameters, BareItem |
Shapes:
Dictionary Map<string, Item | InnerList>
List (Item | InnerList)[]
Item [BareItem, Parameters]
InnerList [Item[], Parameters]
Parameters Map<string, BareItem>
BareItem number | string | Token | ByteSequence | boolean
A Token is a bare word (hwk, Ed25519); a string is a quoted sf-string.
The distinction is load-bearing — @signature-params is covered by the
signature, so a parameter that arrives as a Token must go back out as a Token.
The implementation is vendored from
structured-headers v1.0.1 (MIT)
rather than taken as a dependency, because this package has zero runtime
dependencies by design. See src/vendor/structured-headers/README.md.
created timestampmaxClockSkew)When verifying jwt signature-key types:
cnf.jwk claim is extractedCache-Control headersThe package includes a comprehensive test suite:
npm test
To run tests with coverage:
npm run test:coverage
See the examples/ directory for complete examples:
examples/basic-fetch.ts - Simple GET and POST requestsexamples/express-middleware.ts - Express integrationexamples/fastify-middleware.ts - Fastify integrationexamples/all-key-types.ts - Using hwk, jwt, and jwksThis implementation follows:
MIT
Contributions are welcome! Please see CONTRIBUTING.md for details.
FAQs
HTTP Message Signatures (RFC 9421) with Signature-Key header support
The npm package @hellocoop/httpsig receives a total of 1,408 weekly downloads. As such, @hellocoop/httpsig popularity was classified as popular.
We found that @hellocoop/httpsig demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
/Security News
Socket uncovered 77 linked Firefox extensions, including 40 that steal wallet secrets or credentials and 37 deceptive sports-score shells.

Security News
NIST disclosed an unreleased AI tool called V-etalon and opened a broad inquiry into NVD modernization after years of automation plans produced no public enrichment system.

Security News
In his AI Council 2026 talk, Feross Aboukhadijeh covers recent package compromises, vulnerability discovery, and a more automated security model.