Context
@aauth/local-keys today exposes signAgentToken() / createAgentToken(), which produce signatureKey: { type: 'jwt', jwt } — the agent-token / JWT keyType flow. There's no equivalent for the jwks_uri keyType, which is what server-side identities (a producer's domain, an operator's dickhardt.github.io, etc.) use to sign HTTP requests where the receiver fetches the signer's published JWKS to verify.
The underlying primitive is already in place: driver.signHash(keyId, hash) is exactly what HTTP signatures need. It's used by signAgentToken() for the hardware path. We just don't expose the equivalent for HTTP signatures.
Proposal
Add a high-level helper that mirrors signAgentToken() but for HTTP requests:
export interface SignHttpRequestOptions {
/** Agent URL to use as the signature-key `id` (e.g. \"https://you.github.io\"). */
agentUrl: string
/** The request to sign. */
request: {
method: string
url: string | URL // path + optional query; authority overridden below
authority: string // canonical authority the verifier will check
headers?: Record<string, string | string[]>
body?: string | Buffer | Uint8Array // required if content-digest is in covered components
}
/** Defaults to AAuth profile body components. */
components?: string[]
/** dwk value for the signature-key header (defaults to 'aauth-agent.json'). */
dwk?: string
/** Override key resolution — useful for testing. */
kid?: string
/** Signature label, defaults to 'sig'. */
label?: string
}
export interface SignedRequest {
headers: {
'Signature-Input': string
'Signature': string
'Signature-Key': string
'Content-Digest'?: string
}
/** Which key was actually used. Useful for logging. */
resolved: ResolvedKey
}
export declare function signHttpRequest(
options: SignHttpRequestOptions,
): Promise<SignedRequest>
Implementation sketch
async function signHttpRequest(opts: SignHttpRequestOptions): Promise<SignedRequest> {
const resolved = await resolveKey(opts.agentUrl)
const driver = getBackend(resolved.backend)
// Software backend: extract private JWK from keychain, use httpsig directly
if (resolved.backend === 'software') {
const data = readKeychain(opts.agentUrl)!
const privateJwk = data.keys[resolved.kid]
return signRequestWithKey(opts, resolved, privateJwk)
}
// Hardware: hash-and-sign via driver
return signRequest(opts, { algorithm: resolved.algorithm, hash: 'SHA-256' }, async (data) => {
const hash = createHash('sha256').update(data).digest()
const { signature } = await driver.signHash(resolved.keyId, hash)
return signature
})
}
(Where signRequest is the proposed primitive in companion issue hellocoop/packages-js#63.)
Why this is the right shape
- Mirrors
signAgentToken(). Same resolveKey-then-sign flow, same backend abstraction, same hardware-vs-software dispatch. Just produces HTTP signature headers instead of a JWT.
- No new backend surface. Reuses the existing
driver.signHash() primitive that already exists for the JWT path.
- Symmetry with the producer story. Cloudflare Workers and other software-only producers sign via
@hellocoop/httpsig fetch() today. Hardware-backed identities should have the same ergonomics.
Edge cases
- ES256 hardware drivers (PIV, SE) return signatures in different encodings. The
secure-enclave driver returns base64url-decoded raw bytes (per the code); YubiKey PIV typically returns DER. The helper needs to normalize to raw r||s for the HTTP signature Signature header.
content-digest: when included in covered components, the helper computes it from the body and adds the Content-Digest header to the returned set.
- Key resolution: defer to
resolveKey() as-is — hardware preferred, software fallback, kid override for tests.
Companion issue
@hellocoop/httpsig issue for the underlying signer-agnostic primitive: hellocoop/packages-js#63
Motivation
Came up while building the AAuth ingest endpoint for Hellō Freezer. Operator-side smoke testing against real production identities (e.g. dickhardt.github.io with hardware keys configured in ~/.aauth/config.json) needs this helper to produce signed test requests without each caller having to reproduce HTTP signature canonicalization by hand.
Context
@aauth/local-keystoday exposessignAgentToken()/createAgentToken(), which producesignatureKey: { type: 'jwt', jwt }— the agent-token / JWT keyType flow. There's no equivalent for thejwks_urikeyType, which is what server-side identities (a producer's domain, an operator'sdickhardt.github.io, etc.) use to sign HTTP requests where the receiver fetches the signer's published JWKS to verify.The underlying primitive is already in place:
driver.signHash(keyId, hash)is exactly what HTTP signatures need. It's used bysignAgentToken()for the hardware path. We just don't expose the equivalent for HTTP signatures.Proposal
Add a high-level helper that mirrors
signAgentToken()but for HTTP requests:Implementation sketch
(Where
signRequestis the proposed primitive in companion issue hellocoop/packages-js#63.)Why this is the right shape
signAgentToken(). Same resolveKey-then-sign flow, same backend abstraction, same hardware-vs-software dispatch. Just produces HTTP signature headers instead of a JWT.driver.signHash()primitive that already exists for the JWT path.@hellocoop/httpsigfetch()today. Hardware-backed identities should have the same ergonomics.Edge cases
secure-enclavedriver returns base64url-decoded raw bytes (per the code); YubiKey PIV typically returns DER. The helper needs to normalize to raw r||s for the HTTP signatureSignatureheader.content-digest: when included in covered components, the helper computes it from the body and adds theContent-Digestheader to the returned set.resolveKey()as-is — hardware preferred, software fallback, kid override for tests.Companion issue
@hellocoop/httpsigissue for the underlying signer-agnostic primitive: hellocoop/packages-js#63Motivation
Came up while building the AAuth ingest endpoint for Hellō Freezer. Operator-side smoke testing against real production identities (e.g.
dickhardt.github.iowith hardware keys configured in~/.aauth/config.json) needs this helper to produce signed test requests without each caller having to reproduce HTTP signature canonicalization by hand.