import createError from "http-errors" ; import crypto from "crypto" ; import { BeforeRequestHook } from "got" ; import { Context } from "koa" ; import { TripleStore } from "./jsonld" ; import { PublicKey , getPublicKey } from "../util/rdfModel" ; import { ensureArray } from "../util/misc" ; export interface SigningService { verify ( ctx , body , store ) : Promise < PublicKey > ; signHook ( keyId , privateKeyPem ) : BeforeRequestHook ; } type HeaderMap = Map < string , string [ ] > ; export default async ( { domain , } ) : Promise < SigningService > => { const verify = async ( ctx , body , store ) => { const { headers , rawHeaders } = ctx . req ; if ( headers . host !== domain ) { throw createError ( 400 , "Host header mismatch" ) ; } const sha256 = ensureArray ( headers . digest || [ ] ) . join ( "," ) . split ( "," ) . find ( ( x ) => x . slice ( 0 , 8 ) === "SHA-256=" ) ; if ( ! sha256 ) { throw createError ( 400 , "Expected a Digest header with SHA-256" ) ; } const expected = crypto . createHash ( "sha256" ) . update ( body ) . digest ( "base64" ) ; if ( sha256 . slice ( 8 ) !== expected ) { throw createError ( 400 , "Digest mismatch" ) ; } const numRawHeaders = rawHeaders . length ; const headerMap = new Map ( ) ; for ( let i = 0 ; i < numRawHeaders ; i += 2 ) { const name = rawHeaders [ i ] . toLowerCase ( ) ; let arr = headerMap . get ( name ) ; if ( ! arr ) { arr = [ ] ; headerMap . set ( name , arr ) ; } arr . push ( rawHeaders [ i + 1 ] ) ; } const signatureHeaders = headerMap . get ( "signature" ) ; if ( ! signatureHeaders ) { throw createError ( 400 , "Expected a Signature header" ) ; } for ( const signatureHeader of signatureHeaders . slice ( 0 , 2 ) ) { const publicKey = await verifyOne ( ctx , signatureHeader , headerMap , store ) ; if ( publicKey ) { return publicKey ; } } throw createError ( 400 , "RSA-SHA256 signature mismatch" ) ; } ; const verifyOne = async ( ctx , signatureHeader , headerMap , store ) : Promise < PublicKey | undefined > => { const params = signatureHeader . split ( /\s*,\s*/g ) . reduce < { [ param ] : string ; } > ( ( obj , param ) => { const sepIdx = param . indexOf ( "=" ) ; if ( sepIdx === - 1 ) { obj [ param ] = "" ; } else { const key = param . slice ( 0 , sepIdx ) ; const value = param . slice ( sepIdx + 2 , - 1 ) ; obj [ key ] = value ; } return obj ; } , { } ) ; const { keyId , signature } = params ; if ( ! keyId || ! signature ) { throw createError ( 400 , "Missing required signature parameters" ) ; } const signedHeaders = params . headers ? params . headers . split ( " " ) : [ "date" ] ; if ( ! signedHeaders . includes ( "host" ) || ! signedHeaders . includes ( "digest" ) ) { throw createError ( 400 , "Not all required headers included in signature" ) ; } if ( new Set ( signedHeaders ) . size !== signedHeaders . length ) { throw createError ( 400 , "Duplicate headers in signature" ) ; } for ( const name of signedHeaders ) { if ( name !== "(request-target)" && ! headerMap . has ( name ) ) { throw createError ( 400 , "Signature contains headers not in the request" ) ; } } try { await store . load ( keyId ) ; } catch ( err ) { throw createError ( 400 , `Signature public key document could not be loaded: ${ err . mesage }` ) ; } const publicKey = getPublicKey ( store , keyId ) ; if ( ! publicKey . publicKeyPem ) { return undefined ; } const signedData = signedHeaders . map ( ( name ) => { const value = name === "(request-target)" ? `${ ctx . method . toLowerCase ( ) } ${ ctx . path }` : headerMap . get ( name ) ! . join ( ", " ) ; return `${ name }: ${ value }` ; } ) . join ( "\n" ) ; const valid = crypto . createVerify ( "sha256" ) . update ( signedData ) . verify ( publicKey . publicKeyPem , signature , "base64" ) ; if ( ! valid ) { return undefined ; } return publicKey ; } ; const signHook = ( keyId , privateKeyPem ) => ( options ) => { if ( ! options . body && options . json ) { options . body = JSON . stringify ( options . json ) ; } let body = options . body ; if ( typeof body !== "string" && ! Buffer . isBuffer ( body ) ) { throw Error ( "Cannot sign streaming body" ) ; } const sha256 = crypto . createHash ( "sha256" ) . update ( body ) . digest ( "base64" ) ; options . headers . digest = `SHA-256=${ sha256 }` ; options . headers . date = new Date ( ) . toUTCString ( ) ; const { method , url } = options ; const signedHeaders = Object . keys ( options . headers ) ; const signedData = [ `(request-target): ${ method . toLowerCase ( ) } ${ url . pathname }` , ... signedHeaders . map ( ( name ) => `${ name . toLowerCase ( ) }: ${ options . headers [ name ] }` ) , ] . join ( "\n" ) ; const signature = crypto . createSign ( "sha256" ) . update ( signedData ) . sign ( privateKeyPem , "base64" ) ; const signatureHeaders = [ "(request-target)" , ... signedHeaders . map ( ( name ) => name . toLowerCase ( ) ) , ] . join ( " " ) ; options . headers . signature = [ `keyId="${ keyId }"` , `headers="${ signatureHeaders }"` , `signature="${ signature }"` , ] . join ( "," ) ; } ; return { verify , signHook } ; } ;