diff --git a/src/keri/core/authing.ts b/src/keri/core/authing.ts index 5f816f0e..a54c49d6 100644 --- a/src/keri/core/authing.ts +++ b/src/keri/core/authing.ts @@ -1,11 +1,14 @@ import { Signer } from './signer'; import { Verfer } from './verfer'; -import { desiginput, normalize, siginput } from './httping'; -import { Signage, signature, designature } from '../end/ending'; +import { desiginput, sigbase, siginput } from './httping'; +import { designature, Signage, signature } from '../end/ending'; import { Cigar } from './cigar'; import { Siger } from './siger'; +import { nowUTC } from './utils'; +import { b } from './core'; + export class Authenticater { - static DefaultFields = [ + static readonly DefaultFields = [ '@method', '@path', 'signify-resource', @@ -19,64 +22,38 @@ export class Authenticater { this._verfer = verfer; } - verify(headers: Headers, method: string, path: string): boolean { - const siginput = headers.get('Signature-Input'); - if (siginput == null) { + verify( + headers: Headers, + method: string, + path: string, + authority?: string + ): boolean { + const siginputHeader = headers.get('Signature-Input'); + if (siginputHeader == null) { return false; } const signature = headers.get('Signature'); if (signature == null) { return false; } - let inputs = desiginput(siginput); - inputs = inputs.filter((input) => input.name == 'signify'); - if (inputs.length == 0) { + const inputs = desiginput(siginputHeader); + const input = inputs.get('signify'); + if (!input) { return false; } - inputs.forEach((input) => { - const items = new Array(); - input.fields!.forEach((field: string) => { - if (field.startsWith('@')) { - if (field == '@method') { - items.push(`"${field}": ${method}`); - } else if (field == '@path') { - items.push(`"${field}": ${path}`); - } - } else { - if (headers.has(field)) { - const value = normalize(headers.get(field) as string); - items.push(`"${field}": ${value}`); - } - } - }); - const values = new Array(); - values.push(`(${input.fields!.join(' ')})`); - values.push(`created=${input.created}`); - if (input.expires != undefined) { - values.push(`expires=${input.expires}`); - } - if (input.nonce != undefined) { - values.push(`nonce=${input.nonce}`); - } - if (input.keyid != undefined) { - values.push(`keyid=${input.keyid}`); - } - if (input.context != undefined) { - values.push(`context=${input.context}`); - } - if (input.alg != undefined) { - values.push(`alg=${input.alg}`); - } - const params = values.join(';'); - items.push(`"@signature-params: ${params}"`); - const ser = items.join('\n'); - const signage = designature(signature!); - const cig = signage[0].markers.get(input.name); - if (!this._verfer.verify(cig.raw, ser)) { - throw new Error(`Signature for ${input.keyid} invalid.`); - } - }); - + const ser = sigbase( + input.fields, + siginput(input), + headers, + method, + path, + authority + ); + const signage = designature(signature); + const cig = signage[0].markers.get('signify'); + if (!this._verfer.verify(cig.raw, ser)) { + throw new Error(`Signature for ${input.keyid} invalid.`); + } return true; } @@ -84,26 +61,31 @@ export class Authenticater { headers: Headers, method: string, path: string, + authority?: string, fields?: Array ): Headers { if (fields == undefined) { fields = Authenticater.DefaultFields; } - - const [header, sig] = siginput(this._csig, { - name: 'signify', - method, - path, - headers, + const input = { fields, + created: Math.floor(nowUTC().getTime() / 1000), alg: 'ed25519', keyid: this._csig.verfer.qb64, - }); - - header.forEach((value, key) => { - headers.append(key, value); - }); + }; + const signatureParams = siginput(input); + const signatureBase = sigbase( + fields, + signatureParams, + headers, + method, + path, + authority + ); + const sid = `signify=${signatureParams}`; + headers.append('Signature-Input', sid); + const sig = this._csig.sign(b(signatureBase)); const markers = new Map(); markers.set('signify', sig); const signage = new Signage(markers, false); diff --git a/src/keri/core/httping.ts b/src/keri/core/httping.ts index 70b59dd5..b9d9a606 100644 --- a/src/keri/core/httping.ts +++ b/src/keri/core/httping.ts @@ -1,15 +1,10 @@ import { - serializeDictionary, - Dictionary, - parseDictionary, Item, Parameters, + parseDictionary, + serializeInnerList, } from 'structured-headers'; -import { Signer } from './signer'; import { b } from './core'; -import { Cigar } from './cigar'; -import { nowUTC } from './utils'; -import { Siger } from './siger'; import Base64 from 'urlsafe-base64'; import { Buffer } from 'buffer'; @@ -18,99 +13,80 @@ export function normalize(header: string) { } export interface SiginputArgs { - name: string; method: string; path: string; + authority?: string; headers: Headers; - fields: Array; - expires?: number; - nonce?: string; - alg?: string; - keyid?: string; - context?: string; } -export function siginput( - signer: Signer, - { - name, - method, - path, - headers, - fields, - expires, - nonce, - alg, - keyid, - context, - }: SiginputArgs -): [Map, Siger | Cigar] { +/** + * Prepare signature-parameters (https://datatracker.ietf.org/doc/html/rfc9421#section-2.3) + * and signature-base (https://datatracker.ietf.org/doc/html/rfc9421#section-2.5) strings based on the following input + * @param fields - signature fields names in a signature order + * @param signatureParams - signature params string + * @param headers - request headers to derive signature input components from + * @param method - request method + * @param path - request path + * @param authority - request authority + */ +export function sigbase( + fields: Array, + signatureParams: string, + headers: Headers, + method: string, + path: string, + authority?: string +): string { const items = new Array(); - const ifields = new Array<[string, Map]>(); - - fields.forEach((field) => { + fields.forEach((field: string) => { if (field.startsWith('@')) { switch (field) { case '@method': items.push(`"${field}": ${method}`); - ifields.push([field, new Map()]); break; case '@path': items.push(`"${field}": ${path}`); - ifields.push([field, new Map()]); + break; + case '@authority': + items.push(`"${field}": ${authority}`); break; } - } else { - if (!headers.has(field)) return; - - ifields.push([field, new Map()]); - const value = normalize(headers.get(field)!); + } else if (headers.has(field)) { + const value = normalize(headers.get(field) as string); items.push(`"${field}": ${value}`); } }); + items.push(`"@signature-params": ${signatureParams}`); + return items.join('\n'); +} +/** + * Build a signature-params string based on the {@link Inputage} values + * @param input - the input values for signature-params + */ +export function siginput(input: Inputage): string { + const ifields = new Array<[string, Map]>(); + input.fields.forEach((field: string) => { + ifields.push([field, new Map()]); + }); const nameParams = new Map(); - const now = Math.floor(nowUTC().getTime() / 1000); - nameParams.set('created', now); - - const values = [ - `(${ifields.map((field) => field[0]).join(' ')})`, - `created=${now}`, - ]; - if (expires != undefined) { - values.push(`expires=${expires}`); - nameParams.set('expires', expires); + nameParams.set('created', input.created); + if (input.expires != undefined) { + nameParams.set('expires', input.expires); } - if (nonce != undefined) { - values.push(`nonce=${nonce}`); - nameParams.set('nonce', nonce); + if (input.nonce != undefined) { + nameParams.set('nonce', input.nonce); } - if (keyid != undefined) { - values.push(`keyid=${keyid}`); - nameParams.set('keyid', keyid); + if (input.keyid != undefined) { + nameParams.set('keyid', input.keyid); } - if (context != undefined) { - values.push(`context=${context}`); - nameParams.set('context', context); + if (input.context != undefined) { + nameParams.set('context', input.context); } - if (alg != undefined) { - values.push(`alg=${alg}`); - nameParams.set('alg', alg); + if (input.alg != undefined) { + nameParams.set('alg', input.alg); } - const sid = new Map([[name, [ifields, nameParams]]]); - - const params = values.join(';'); - items.push(`"@signature-params: ${params}"`); - - const ser = items.join('\n'); - const sig = signer.sign(b(ser)); - - return [ - new Map([ - ['Signature-Input', `${serializeDictionary(sid as Dictionary)}`], - ]), - sig, - ]; + return serializeInnerList([ifields, nameParams]); } export class Unqualified { @@ -130,23 +106,25 @@ export class Unqualified { } export class Inputage { - public name: any; public fields: any; public created: any; - public expires: any; - public nonce: any; - public alg: any; - public keyid: any; - public context: any; + public expires?: any; + public nonce?: any; + public alg?: any; + public keyid?: any; + public context?: any; } -export function desiginput(value: string): Array { +/** + * Parse a Signature-Input value into an {@link Inputage} by label map + * @param value - Signature-Input string + */ +export function desiginput(value: string): Map { const sid = parseDictionary(value); - const siginputs = new Array(); + const siginputs = new Map(); sid.forEach((value, key) => { const siginput = new Inputage(); - siginput.name = key; let list: Item[]; let params; [list, params] = value as [Item[], Parameters]; @@ -179,11 +157,12 @@ export function desiginput(value: string): Array { siginput.context = params.get('context'); } - siginputs.push(siginput); + siginputs.set(key, siginput); }); return siginputs; } + /** Parse start, end and total from HTTP Content-Range header value * @param {string|null} header - HTTP Range header value * @param {string} typ - type of range, e.g. "aids" diff --git a/test/core/authing.test.ts b/test/core/authing.test.ts index 27466437..1926c623 100644 --- a/test/core/authing.test.ts +++ b/test/core/authing.test.ts @@ -1,10 +1,19 @@ import { strict as assert } from 'assert'; import libsodium from 'libsodium-wrappers-sumo'; -import { Salter } from '../../src/keri/core/salter'; -import { b } from '../../src/keri/core/core'; -import { Authenticater } from '../../src/keri/core/authing'; +import { + Authenticater, + b, + Inputage, + Matter, + MtrDex, + Salter, + Signer, + Verfer, +} from '../../src'; import * as utilApi from '../../src/keri/core/utils'; -import { Verfer } from '../../src/keri/core/verfer'; +import * as httping from '../../src/keri/core/httping'; +import { mock } from 'ts-mockito'; +import Base64 from 'urlsafe-base64'; describe('Authenticater.verify', () => { it('verify signature on Response', async () => { @@ -12,7 +21,7 @@ describe('Authenticater.verify', () => { const salt = '0123456789abcdef'; const salter = new Salter({ raw: b(salt) }); const signer = salter.signer(); - const aaid = 'DMZh_y-H5C3cSbZZST-fqnsmdNTReZxIh0t2xSTOJQ8a'; + const aaid = 'DN54yRad_BTqgZYUSi_NthRBQrxSnqQdJXWI5UHcGOQt'; const verfer = new Verfer({ qb64: aaid }); const headers = new Headers([ @@ -20,11 +29,11 @@ describe('Authenticater.verify', () => { ['Content-Type', 'application/json'], [ 'Signature', - 'indexed="?0";signify="0BDLh8QCytVBx1YMam4Vt8s4b9HAW1dwfE4yU5H_w1V6gUvPBoVGWQlIMdC16T3WFWHDHCbMcuceQzrr6n9OULsK"', + 'indexed="?0";signify="0BBRr2GjhqbkjEUSYHLNyu0w4ORZw2IU9AOYikZfIBKESdrY_O1E_ePGYzzK_4I7LLkqZOiulq7P527t2zU5vKoH"', ], [ 'Signature-Input', - 'signify=("signify-resource" "@method" "@path" "signify-timestamp");created=1684715820;keyid="EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei";alg="ed25519"', + 'signify=("signify-resource" "@method" "@path" "signify-timestamp");created=1684715820;keyid="DN54yRad_BTqgZYUSi_NthRBQrxSnqQdJXWI5UHcGOQt";alg="ed25519"', ], [ 'Signify-Resource', @@ -32,14 +41,145 @@ describe('Authenticater.verify', () => { ], ['Signify-Timestamp', '2023-05-22T00:37:00.248708+00:00'], ]); + const desiginputMock = jest.fn(); + const input = { + fields: [ + 'signify-resource', + '@method', + '@path', + 'signify-timestamp', + ], + created: 1684715820, + keyid: 'DN54yRad_BTqgZYUSi_NthRBQrxSnqQdJXWI5UHcGOQt', + alg: 'ed25519', + } as Inputage; + desiginputMock.mockReturnValue(new Map([['signify', input]])); + const sigbaseMock = jest.fn(); + sigbaseMock.mockReturnValue( + '"signify-resource": EEXekkGu9IAzav6pZVJhkLnjtjM5v3AcyA-pdKUcaGei\n' + + '"@method": GET\n' + + '"@path": /identifiers/aid1\n' + + '"signify-timestamp": 2023-05-22T00:37:00.248708+00:00\n' + + '"@signature-params": ("signify-resource" "@method" "@path" "signify-timestamp");created=1684715820;keyid="DN54yRad_BTqgZYUSi_NthRBQrxSnqQdJXWI5UHcGOQt";alg="ed25519"' + ); + const siginputMock = jest.fn(); + siginputMock.mockReturnValue( + '("signify-resource" "@method" "@path" "signify-timestamp");created=1684715820;keyid="DN54yRad_BTqgZYUSi_NthRBQrxSnqQdJXWI5UHcGOQt";alg="ed25519"' + ); + jest.spyOn(httping, 'sigbase').mockImplementation(sigbaseMock); + jest.spyOn(httping, 'siginput').mockImplementation(siginputMock); + jest.spyOn(httping, 'desiginput').mockImplementation(desiginputMock); const authn = new Authenticater(signer, verfer); assert.notEqual(authn, undefined); + assert.equal(authn.verify(headers, 'GET', '/identifiers/aid1'), true); + expect(desiginputMock).toHaveBeenCalledTimes(1); + expect(desiginputMock).toHaveBeenCalledWith( + 'signify=("signify-resource" "@method" "@path" "signify-timestamp");created=1684715820;keyid="DN54yRad_BTqgZYUSi_NthRBQrxSnqQdJXWI5UHcGOQt";alg="ed25519"' + ); + expect(siginputMock).toHaveBeenCalledTimes(1); + expect(siginputMock).toHaveBeenCalledWith(input); + expect(sigbaseMock).toHaveBeenCalledTimes(1); + expect(sigbaseMock).toHaveBeenCalledWith( + input.fields, + '("signify-resource" "@method" "@path" "signify-timestamp");created=1684715820;keyid="DN54yRad_BTqgZYUSi_NthRBQrxSnqQdJXWI5UHcGOQt";alg="ed25519"', + headers, + 'GET', + '/identifiers/aid1', + undefined + ); + }); + it('verify test request https://datatracker.ietf.org/doc/html/rfc9421#appendix-B.2.6', async () => { + await libsodium.ready; + + const publicKey = new Uint8Array([ + 38, 180, 11, 143, 147, 255, 243, 216, 151, 17, 47, 126, 188, 88, 43, + 35, 45, 189, 114, 81, 125, 8, 47, 232, 60, 251, 48, 221, 206, 67, + 209, 187, + ]); + const signerMock = mock(Signer); + const verfer = new Verfer({ raw: publicKey }); + const expectedSignatureB64 = + 'wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgwUPiu4A0w6vuQv5lIp5WPpBKRCw=='; + const expectedSignatureRaw = new Uint8Array( + Base64.decode(expectedSignatureB64) + ); + const expectedSignatureCESR = new Matter({ + raw: expectedSignatureRaw, + code: MtrDex.Ed25519_Sig, + }); + + const headers = new Headers([ + [ + 'content-digest', + 'sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:', + ], + ['content-length', '18'], + ['content-type', 'application/json'], + ['date', 'Tue, 20 Apr 2021 02:07:55 GMT'], + [ + 'Signature', + `indexed="?0";signify="${expectedSignatureCESR.qb64}"`, + ], + [ + 'Signature-Input', + 'signify=("date" "@method" "@path" "@authority" "content-type" "content-length");created=1618884473;keyid="test-key-ed25519"', + ], + ]); + const desiginputMock = jest.fn(); + const input = { + fields: [ + 'date', + '@method', + '@path', + '@authority', + 'content-type', + 'content-length', + ], + created: 1618884473, + keyid: 'test-key-ed25519', + } as Inputage; + desiginputMock.mockReturnValue(new Map([['signify', input]])); + const sigbaseMock = jest.fn(); + sigbaseMock.mockReturnValue( + '"date": Tue, 20 Apr 2021 02:07:55 GMT\n' + + '"@method": POST\n' + + '"@path": /foo\n' + + '"@authority": example.com\n' + + '"content-type": application/json\n' + + '"content-length": 18\n' + + '"@signature-params": ("date" "@method" "@path" "@authority" "content-type" "content-length");created=1618884473;keyid="test-key-ed25519"' + ); + const siginputMock = jest.fn(); + siginputMock.mockReturnValue( + '("date" "@method" "@path" "@authority" "content-type" "content-length");created=1618884473;keyid="test-key-ed25519"' + ); + jest.spyOn(httping, 'sigbase').mockImplementation(sigbaseMock); + jest.spyOn(httping, 'siginput').mockImplementation(siginputMock); + jest.spyOn(httping, 'desiginput').mockImplementation(desiginputMock); + + const authn = new Authenticater(signerMock, verfer); + assert.notEqual(authn, undefined); assert.equal( - authn.verify(new Headers(headers), 'GET', '/identifiers/aid1'), + authn.verify(new Headers(headers), 'POST', '/foo', 'example.com'), true ); + expect(desiginputMock).toHaveBeenCalledTimes(1); + expect(desiginputMock).toHaveBeenCalledWith( + 'signify=("date" "@method" "@path" "@authority" "content-type" "content-length");created=1618884473;keyid="test-key-ed25519"' + ); + expect(siginputMock).toHaveBeenCalledTimes(1); + expect(siginputMock).toHaveBeenCalledWith(input); + expect(sigbaseMock).toHaveBeenCalledTimes(1); + expect(sigbaseMock).toHaveBeenCalledWith( + input.fields, + '("date" "@method" "@path" "@authority" "content-type" "content-length");created=1618884473;keyid="test-key-ed25519"', + headers, + 'POST', + '/foo', + 'example.com' + ); }); }); @@ -66,6 +206,21 @@ describe('Authenticater.sign', () => { new Date('2021-01-01T00:00:00.000000+00:00') ); + const sigbaseMock = jest.fn(); + sigbaseMock.mockReturnValue( + '"@method": POST\n' + + '"@path": /boot\n' + + '"signify-resource": EWJkQCFvKuyxZi582yJPb0wcwuW3VXmFNuvbQuBpgmIs\n' + + '"signify-timestamp": 2022-09-24T00:05:48.196795+00:00\n' + + '"@signature-params": ("@method" "@path" "signify-resource" "signify-timestamp");created=1609459200;keyid="DN54yRad_BTqgZYUSi_NthRBQrxSnqQdJXWI5UHcGOQt";alg="ed25519"' + ); + const siginputMock = jest.fn(); + siginputMock.mockReturnValue( + '("@method" "@path" "signify-resource" "signify-timestamp");created=1609459200;keyid="DN54yRad_BTqgZYUSi_NthRBQrxSnqQdJXWI5UHcGOQt";alg="ed25519"' + ); + jest.spyOn(httping, 'sigbase').mockImplementation(sigbaseMock); + jest.spyOn(httping, 'siginput').mockImplementation(siginputMock); + const authn = new Authenticater(signer, verfer); headers = authn.sign(headers, 'POST', '/boot'); @@ -77,7 +232,30 @@ describe('Authenticater.sign', () => { ); assert.equal( headers.get('Signature'), - 'indexed="?0";signify="0BChvN_BWAf-mgEuTnWfNnktgHdWOuOh9cWc4o0GFWuZOwra3DyJT5dJ_6BX7AANDOTnIlAKh5Sg_9qGQXHjj5oJ"' + 'indexed="?0";signify="0BDcjKTbpvmcF9oIeCI-95enQRd3_PfAgzOWi9vVf811lWGlTTOsKtFzpdkwr90ksvpvB_GhvsbV2l29wFN_QW0K"' + ); + + const expectedInput = { + fields: [ + '@method', + '@path', + 'signify-resource', + 'signify-timestamp', + ], + created: 1609459200, + alg: 'ed25519', + keyid: 'DN54yRad_BTqgZYUSi_NthRBQrxSnqQdJXWI5UHcGOQt', + } as Inputage; + expect(siginputMock).toHaveBeenCalledTimes(1); + expect(siginputMock).toHaveBeenCalledWith(expectedInput); + expect(sigbaseMock).toHaveBeenCalledTimes(1); + expect(sigbaseMock).toHaveBeenCalledWith( + expectedInput.fields, + '("@method" "@path" "signify-resource" "signify-timestamp");created=1609459200;keyid="DN54yRad_BTqgZYUSi_NthRBQrxSnqQdJXWI5UHcGOQt";alg="ed25519"', + headers, + 'POST', + '/boot', + undefined ); }); }); diff --git a/test/core/httping.test.ts b/test/core/httping.test.ts index c482fc25..cfaa24b2 100644 --- a/test/core/httping.test.ts +++ b/test/core/httping.test.ts @@ -1,87 +1,231 @@ import { strict as assert } from 'assert'; -import libsodium from 'libsodium-wrappers-sumo'; -import { Salter } from '../../src/keri/core/salter'; -import { b } from '../../src/keri/core/core'; -import { - siginput, - desiginput, - SiginputArgs, -} from '../../src/keri/core/httping'; -import * as utilApi from '../../src/keri/core/utils'; +import { desiginput, Inputage, sigbase, siginput } from '../../src'; describe('siginput', () => { - it('create valid Signature-Input header with signature', async () => { - await libsodium.ready; - const salt = '0123456789abcdef'; - const salter = new Salter({ raw: b(salt) }); - const signer = salter.signer(); - - const headers: Headers = new Headers([ - ['Content-Type', 'application/json'], - ['Content-Length', '256'], - ['Connection', 'close'], - [ - 'Signify-Resource', - 'EWJkQCFvKuyxZi582yJPb0wcwuW3VXmFNuvbQuBpgmIs', - ], - ['Signify-Timestamp', '2022-09-24T00:05:48.196795+00:00'], - ]); - jest.spyOn(utilApi, 'nowUTC').mockReturnValue( - new Date('2021-01-01T00:00:00.000000+00:00') - ); - - const [header, sig] = siginput(signer, { - name: 'sig0', - method: 'POST', - path: '/signify', - headers, + it('create valid signature parameters string from Inputage', async () => { + const input = { fields: [ - 'Signify-Resource', + 'signify-resource', '@method', '@path', - 'Signify-Timestamp', + 'signify-timestamp', ], + created: 1609459200, alg: 'ed25519', - keyid: signer.verfer.qb64, - } as SiginputArgs); - - assert.equal(header.size, 1); - assert.equal(header.has('Signature-Input'), true); - const sigipt = header.get('Signature-Input'); - assert.equal( - sigipt, - 'sig0=("Signify-Resource" "@method" "@path" "Signify-Timestamp");created=1609459200;keyid="DN54yRad_BTqgZYUSi_NthRBQrxSnqQdJXWI5UHcGOQt";alg="ed25519"' - ); + keyid: 'DN54yRad_BTqgZYUSi_NthRBQrxSnqQdJXWI5UHcGOQt', + } as Inputage; + const signiputString = siginput(input); assert.equal( - sig.qb64, - '0BAJWoDvZXYKnq_9rFTy_mucctxk3rVK6szopNi1rq5WQcJSNIw-_PocSQNoQGD1Ow_s2mDI5-Qqm34Y56gUKQcF' + signiputString, + '("signify-resource" "@method" "@path" "signify-timestamp");created=1609459200;keyid="DN54yRad_BTqgZYUSi_NthRBQrxSnqQdJXWI5UHcGOQt";alg="ed25519"' ); }); + it('RFC Test https://datatracker.ietf.org/doc/html/rfc9421#section-2.5', async () => { + const expectedParameters = + '("@method" "@authority" "@path" "content-digest" "content-length" "content-type");created=1618884473;keyid="test-key-rsa-pss"'; + const input = { + fields: [ + '@method', + '@authority', + '@path', + 'content-digest', + 'content-length', + 'content-type', + ], + created: 1618884473, + keyid: 'test-key-rsa-pss', + } as Inputage; + const result = siginput(input); + assert.equal(result, expectedParameters); + }); + it('RFC Test https://datatracker.ietf.org/doc/html/rfc9421#appendix-B.2.6', async () => { + const expectedParameters = + '("date" "@method" "@path" "@authority" "content-type" "content-length");created=1618884473;keyid="test-key-ed25519"'; + const input = { + fields: [ + 'date', + '@method', + '@path', + '@authority', + 'content-type', + 'content-length', + ], + created: 1618884473, + keyid: 'test-key-ed25519', + } as Inputage; + const result = siginput(input); + assert.equal(result, expectedParameters); + }); }); describe('desiginput', () => { - it('create valid Signature-Input header with signature', async () => { - await libsodium.ready; + it('parse signature input to a map of Inputage object by label', async () => { const siginput = - 'sig0=("signify-resource" "@method" "@path" "signify-timestamp");created=1609459200;keyid="EIaGMMWJFPmtXznY1IIiKDIrg-vIyge6mBl2QV8dDjI3";alg="ed25519"'; + 'sig0=("signify-resource" "@method" "@path" "signify-timestamp");created=1609459200;keyid="EIaGMMWJFPmtXznY1IIiKDIrg-vIyge6mBl2QV8dDjI3";alg="ed25519", ' + + 'sig1=("@method" "@path" "signify-resource" "signify-timestamp");created=1609459201;keyid="DN54yRad_BTqgZYUSi_NthRBQrxSnqQdJXWI5UHcGOQt";expires=1609459210;alg="ed25519"'; const inputs = desiginput(siginput); - assert.equal(inputs.length, 1); - const input = inputs[0]; - assert.deepStrictEqual(input.fields, [ + assert.equal(inputs.size, 2); + const sig0Input = inputs.get('sig0')!; + assert.deepStrictEqual(sig0Input.fields, [ 'signify-resource', '@method', '@path', 'signify-timestamp', ]); - assert.equal(input.created, 1609459200); - assert.equal(input.alg, 'ed25519'); + assert.equal(sig0Input.created, 1609459200); + assert.equal(sig0Input.alg, 'ed25519'); assert.equal( - input.keyid, + sig0Input.keyid, 'EIaGMMWJFPmtXznY1IIiKDIrg-vIyge6mBl2QV8dDjI3' ); + assert.equal(sig0Input.expires, undefined); + assert.equal(sig0Input.nonce, undefined); + assert.equal(sig0Input.context, undefined); + const sig1Input = inputs.get('sig1')!; + assert.deepStrictEqual(sig1Input.fields, [ + '@method', + '@path', + 'signify-resource', + 'signify-timestamp', + ]); + assert.equal(sig1Input.created, 1609459201); + assert.equal(sig1Input.alg, 'ed25519'); + assert.equal( + sig1Input.keyid, + 'DN54yRad_BTqgZYUSi_NthRBQrxSnqQdJXWI5UHcGOQt' + ); + assert.equal(sig1Input.expires, 1609459210); + assert.equal(sig1Input.nonce, undefined); + assert.equal(sig1Input.context, undefined); + }); + it('RFC Test https://datatracker.ietf.org/doc/html/rfc9421#appendix-B.2.6', async () => { + const siginputString = + 'sig-b26=("date" "@method" "@path" "@authority" "content-type" "content-length");created=1618884473;keyid="test-key-ed25519"'; + const inputs = desiginput(siginputString); + assert.equal(inputs.size, 1); + const input = inputs.get('sig-b26')!; + assert.deepStrictEqual(input.fields, [ + 'date', + '@method', + '@path', + '@authority', + 'content-type', + 'content-length', + ]); + assert.equal(input.created, 1618884473); + assert.equal(input.alg, undefined); + assert.equal(input.keyid, 'test-key-ed25519'); assert.equal(input.expires, undefined); assert.equal(input.nonce, undefined); assert.equal(input.context, undefined); }); }); + +describe('sigbase', () => { + it('RFC Test https://datatracker.ietf.org/doc/html/rfc9421#section-2.5', async () => { + const expectedSigbase = + '"@method": POST\n' + + '"@authority": example.com\n' + + '"@path": /foo\n' + + '"content-digest": sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:\n' + + '"content-length": 18\n' + + '"content-type": application/json\n' + + '"@signature-params": ("@method" "@authority" "@path" "content-digest" "content-length" "content-type");created=1618884473;keyid="test-key-rsa-pss"'; + const signatureParams = + '("@method" "@authority" "@path" "content-digest" "content-length" "content-type");created=1618884473;keyid="test-key-rsa-pss"'; + const fields = [ + '@method', + '@authority', + '@path', + 'content-digest', + 'content-length', + 'content-type', + ]; + const inputHeaders = new Headers([ + [ + 'content-digest', + 'sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:', + ], + ['content-length', '18'], + ['content-type', 'application/json'], + ]); + const result = sigbase( + fields, + signatureParams, + inputHeaders, + 'POST', + '/foo', + 'example.com' + ); + assert.equal(result, expectedSigbase); + }); + it('RFC Test https://datatracker.ietf.org/doc/html/rfc9421#appendix-B.2.6', async () => { + const expectedSigbase = + '"date": Tue, 20 Apr 2021 02:07:55 GMT\n' + + '"@method": POST\n' + + '"@path": /foo\n' + + '"@authority": example.com\n' + + '"content-type": application/json\n' + + '"content-length": 18\n' + + '"@signature-params": ("date" "@method" "@path" "@authority" "content-type" "content-length");created=1618884473;keyid="test-key-ed25519"'; + const signatureParams = + '("date" "@method" "@path" "@authority" "content-type" "content-length");created=1618884473;keyid="test-key-ed25519"'; + const fields = [ + 'date', + '@method', + '@path', + '@authority', + 'content-type', + 'content-length', + ]; + const inputHeaders = new Headers([ + ['content-length', '18'], + ['date', 'Tue, 20 Apr 2021 02:07:55 GMT'], + ['content-type', 'application/json'], + ]); + const result = sigbase( + fields, + signatureParams, + inputHeaders, + 'POST', + '/foo', + 'example.com' + ); + assert.equal(result, expectedSigbase); + }); + it('signify valid', async () => { + const expectedSigbase = + '"signify-resource": EWJkQCFvKuyxZi582yJPb0wcwuW3VXmFNuvbQuBpgmIs\n' + + '"@method": POST\n' + + '"@path": /signify\n' + + '"signify-timestamp": 2022-09-24T00:05:48.196795+00:00\n' + + '"@signature-params": ("signify-resource" "@method" "@path" "signify-timestamp");created=1609459200;keyid="DN54yRad_BTqgZYUSi_NthRBQrxSnqQdJXWI5UHcGOQt";alg="ed25519"'; + const signatureParams = + '("signify-resource" "@method" "@path" "signify-timestamp");created=1609459200;keyid="DN54yRad_BTqgZYUSi_NthRBQrxSnqQdJXWI5UHcGOQt";alg="ed25519"'; + const fields = [ + 'signify-resource', + '@method', + '@path', + 'signify-timestamp', + ]; + const inputHeaders: Headers = new Headers([ + ['Content-Type', 'application/json'], + ['Content-Length', '256'], + ['Connection', 'close'], + [ + 'Signify-Resource', + 'EWJkQCFvKuyxZi582yJPb0wcwuW3VXmFNuvbQuBpgmIs', + ], + ['Signify-Timestamp', '2022-09-24T00:05:48.196795+00:00'], + ]); + const result = sigbase( + fields, + signatureParams, + inputHeaders, + 'POST', + '/signify' + ); + assert.equal(result, expectedSigbase); + }); +}); diff --git a/test/core/signer.test.ts b/test/core/signer.test.ts index 439a08b7..74a227df 100644 --- a/test/core/signer.test.ts +++ b/test/core/signer.test.ts @@ -4,6 +4,7 @@ import libsodium from 'libsodium-wrappers-sumo'; import { Signer } from '../../src'; import { Matter, MtrDex } from '../../src'; import { b } from '../../src'; +import Base64 from 'urlsafe-base64'; describe('Signer', () => { it('should sign things', async () => { @@ -26,4 +27,30 @@ describe('Signer', () => { const result = signer.verfer.verify(cigar.raw, ser); assert.equal(result, true); }); + it('signs following RFC https://datatracker.ietf.org/doc/html/rfc9421#appendix-B.2.6', async () => { + await libsodium.ready; + + const content = + '"date": Tue, 20 Apr 2021 02:07:55 GMT\n' + + '"@method": POST\n' + + '"@path": /foo\n' + + '"@authority": example.com\n' + + '"content-type": application/json\n' + + '"content-length": 18\n' + + '"@signature-params": ("date" "@method" "@path" "@authority" "content-type" "content-length");created=1618884473;keyid="test-key-ed25519"'; + const privateKey = new Uint8Array([ + 159, 131, 98, 248, 122, 72, 74, 149, 78, 110, 116, 12, 91, 76, 14, + 132, 34, 145, 57, 162, 10, 168, 171, 86, 255, 102, 88, 111, 106, + 125, 41, 197, + ]); + const expectedSignature = + 'wqcAqbmYJ2ji2glfAMaRy4gruYYnx2nEFN2HN6jrnDnQCK1u02Gb04v9EDgwUPiu4A0w6vuQv5lIp5WPpBKRCw=='; + + const signer = new Signer({ raw: privateKey }); + const result = signer.sign(b(content)); + const expectedSignatureBytes = new Uint8Array( + Base64.decode(expectedSignature) + ); + assert.deepStrictEqual(result.raw, expectedSignatureBytes); + }); });