diff --git a/src/extractTBS.ts b/src/extractTBS.ts new file mode 100644 index 0000000..610a861 --- /dev/null +++ b/src/extractTBS.ts @@ -0,0 +1,18 @@ +import cbor from "./cbor" +import { EMPTY_BUFFER } from './lib/common' + +export const extractTBS = (message: Uint8Array) => { + const { tag, value } = cbor.decode(message) + if (tag !== 18) { + throw new Error('only cose sign 1 is supported') + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [protectedHeaderBytes, unprotectedHeaderMap, payloadBuffer] = value + const tbs = cbor.encode([ + 'Signature1', + protectedHeaderBytes, + EMPTY_BUFFER, + payloadBuffer + ]); + return tbs +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 1ec7399..6412bad 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,8 +23,11 @@ import * as lib from './lib' import * as scitt from './scitt' +import { extractTBS } from './extractTBS' + const cose = { key, + extractTBS, scitt, lib, utils, diff --git a/src/scitt/identifiers/index.ts b/src/scitt/identifiers/index.ts new file mode 100644 index 0000000..6afa0f7 --- /dev/null +++ b/src/scitt/identifiers/index.ts @@ -0,0 +1 @@ +export * from './urn' \ No newline at end of file diff --git a/src/scitt/identifiers/scitt-identifiers.test.ts b/src/scitt/identifiers/scitt-identifiers.test.ts index 964aca8..3398286 100644 --- a/src/scitt/identifiers/scitt-identifiers.test.ts +++ b/src/scitt/identifiers/scitt-identifiers.test.ts @@ -1,71 +1,51 @@ -import { base64url } from "jose"; -import { createHash } from 'crypto' -import * as cbor from 'cbor-web' -const urnPrefix = `urn:ietf:params:scitt` -const nodeCryptoHashFunction = 'sha256' -const mandatoryBaseEncoding = `base64url` // no pad . +import cose from '../../../src' -// https://www.iana.org/assignments/named-information/named-information.xhtml -const nodeCryptoToIanaNamedHashFunctions = { - [nodeCryptoHashFunction]: 'sha-256' -} +let signedStatement: Buffer; +let receipt: Buffer; -// TODO: -// Update to align with the TBS requirement in -// https://github.com/ietf-wg-scitt/draft-ietf-scitt-architecture/pull/145 +beforeAll(async () => { + const protectedHeader = new Map(); + protectedHeader.set(1, -7) + const unprotectedHeader = new Map(); + const signer = cose.lib.signer({ + secretKeyJwk: { + kty: 'EC', + crv: 'P-256', + alg: 'ES256', + d: 'o_95vWSheg19YU7viU3PmW_kRIWk14HiVLXDXiZjEL0', + x: 'LYdh0ITBGLOUpywy0adFxXyaIaQapIEOLgfw7933TRE', + y: 'I6R3hgQZf2topOWa0VBjEugRgHISJ39LvOlfVX29P0w', + } + }); + signedStatement = await signer.sign({ protectedHeader, unprotectedHeader, payload: Buffer.from('fake signed statement') }); + receipt = await signer.sign({ protectedHeader, unprotectedHeader, payload: Buffer.from('fake receipt') }); +}) describe('should produce a SCITT URN for SCITT Messages', () => { + it('should produce a statement identifier', () => { // in SCITT, statements are opaque bytes of a known content type // for example some bytes of type application/json - const messageType = 'statement'; const statement = JSON.stringify({ hello: ['world'] }) - const statementHashBase64 = base64url.encode(createHash(nodeCryptoHashFunction).update(statement).digest()); - const statementId = `${urnPrefix}:${messageType}:${nodeCryptoToIanaNamedHashFunctions[nodeCryptoHashFunction]}:${mandatoryBaseEncoding}:${statementHashBase64}` + const statementId = cose.scitt.identifiers.urn('statement', Buffer.from(statement)) expect(statementId).toBe('urn:ietf:params:scitt:statement:sha-256:base64url:5i6UeRzg1SSUUjEUp73DdHUmHnh5uX0g97fHqnGmr1o') }) it('should produce a signed statement identifier', () => { // in SCITT, signed statements are cose sign 1 bytes of type application/cose - // for the sake of this example, we substitute a simple cbor encoding for a cose sign 1. - const messageType = 'signed-statement'; - const signedStatement = cbor.encode({ hello: ['world'] }) // normally this would be a valid cose sign 1 - // (including whatever mutable values are present in the unprotected header) - const signedStatementHashBase64 = base64url.encode(createHash(nodeCryptoHashFunction).update(signedStatement).digest()); - const signedStatementId = `${urnPrefix}:${messageType}:${nodeCryptoToIanaNamedHashFunctions[nodeCryptoHashFunction]}:${mandatoryBaseEncoding}:${signedStatementHashBase64}` - expect(signedStatementId).toBe('urn:ietf:params:scitt:signed-statement:sha-256:base64url:h2drlVUvxYy5v9urLj7KGqBhGaaXS3Mf7K2P2us9d0U') - // note that when receipts are added to the unprotected header, the content identifier automatically changes to reflect their presence - // this also applies to all additional values added to the unprotected header before the identifier is computed. - // in this way, we may say that this identifier scheme is for a "transparent statement" of "unbounded transparency" - // we cannot know from the identifier itself, how many receipts will be present in the dereferenced content, but we do know - // the content type for the dereferenced bytes will always be application/cose. + const signedStatementId = cose.scitt.identifiers.urn('signed-statement', signedStatement) + // urn:ietf:params:scitt:signed-statement:sha-256:base64url:ysBmsRG2DagHYCgQuGHsG9alWWIiRwVRUhAz8j8cMxM + expect(signedStatementId.startsWith('urn:ietf:params:scitt:signed-statement:sha-256:base64url:')).toBe(true) }) it('should produce a receipt identifier', () => { // in SCITT, receipts are cose sign 1 bytes of type application/cose - // for the sake of this example, we substitute a simple cbor encoding for a cose sign 1. - const messageType = 'receipt'; - const receipt = cbor.encode({ hello: ['world'], other_identifiers: ['a', 'b'] }) // normally this would be a valid cose sign 1 - // (including whatever mutable values are present in the unprotected header) - const receiptHashBase64 = base64url.encode(createHash(nodeCryptoHashFunction).update(receipt).digest()); - const receiptId = `${urnPrefix}:${messageType}:${nodeCryptoToIanaNamedHashFunctions[nodeCryptoHashFunction]}:${mandatoryBaseEncoding}:${receiptHashBase64}` - expect(receiptId).toBe('urn:ietf:params:scitt:receipt:sha-256:base64url:S10jY1p6CRl8Vu8tr_S5z4tpKdvhLf0AfkbA3c2o790') - // note that when additional proofs are added to the unprotected header, the content identifier automatically changes to reflect their presence - // this also applies to all additional values added to the unprotected header before the identifier is computed. - - // this identifier is committing to the protected and unprotected header parameters, in addition to the signature - // this means if the receipt expires, the identifier expires - // this also means that if the receipt contains references to other identifiers, changing them will change its identifier. - // a common scenario we assume is that a receipt will refer to identifiers for other receipts - // this will build a DAG (directed acyclic graph), that is walkable assuming the following interface is implemented (regardless of API implementation details) - - // receipt = dereference(receiptId) - // nodes = [receiptId] for each dereferencable receipt - // edges = [receiptId, nestedReceiptId] for each nested receipt - - // because content identifiers are always computed from content, the content can never contain a reference to itself. + // in SCITT, signed statements are cose sign 1 bytes of type application/cose + const receiptId = cose.scitt.identifiers.urn('receipt', receipt) + // urn:ietf:params:scitt:receipt:sha-256:base64url:ysBmsRG2DagHYCgQuGHsG9alWWIiRwVRUhAz8j8cMxM + expect(receiptId.startsWith('urn:ietf:params:scitt:receipt:sha-256:base64url:')).toBe(true) }) }) @@ -76,11 +56,10 @@ describe('should produce a URL from a SCITT URN', () => { // given some bytes and a content type // the SCITT data URL is the trivial data URL of the form const contentType = `application/cose` - const receipt = cbor.encode({ hello: ['world'], other_identifiers: ['a', 'b'] }) // normally this would be a valid cose sign 1 const baseEncodedReceipt = Buffer.from(receipt).toString('base64') const dataURL = `data:${contentType};base64,${baseEncodedReceipt}`; // note that base64 is not the same as base64url no pad. - expect(dataURL).toBe('data:application/cose;base64,omVoZWxsb4Fld29ybGRxb3RoZXJfaWRlbnRpZmllcnOCYWFhYg==') + expect(dataURL.startsWith('data:application/cose;base64,')).toBe(true) }) it('SCITT SCRAPI URLs', () => { // SCRAPI provides an optional to implement HTTP API that supports the required dereference operation necessary to compute the dag @@ -102,7 +81,3 @@ describe('should produce a URL from a SCITT URN', () => { }) }) -// SCITT does not require Text Encoded Identifiers (URLs or URNs) -// Binary Encoded Identifiers for URLs or URNs -// MAY be constructed according to __RFC__. -// SCRAPI does not define http interfaces for working with binary identifiers. diff --git a/src/scitt/identifiers/urn.ts b/src/scitt/identifiers/urn.ts new file mode 100644 index 0000000..360473f --- /dev/null +++ b/src/scitt/identifiers/urn.ts @@ -0,0 +1,27 @@ + +import { base64url } from "jose"; +import { createHash } from 'crypto' + +import cose from '../../../src' + +const urnPrefix = `urn:ietf:params:scitt` +const nodeCryptoHashFunction = 'sha256' +const mandatoryBaseEncoding = `base64url` // no pad . + +// https://www.iana.org/assignments/named-information/named-information.xhtml +const nodeCryptoToIanaNamedHashFunctions = { + [nodeCryptoHashFunction]: 'sha-256' +} + +export const urn = (type: string, message: Buffer) => { + if (['statement', 'transparent-statement'].includes(type)) { + const messageHashBase64 = base64url.encode(createHash(nodeCryptoHashFunction).update(message).digest()); + const scittUrn = `${urnPrefix}:${type}:${nodeCryptoToIanaNamedHashFunctions[nodeCryptoHashFunction]}:${mandatoryBaseEncoding}:${messageHashBase64}` + return scittUrn; + } else { + const tbs = cose.extractTBS(message) + const messageHashBase64 = base64url.encode(createHash(nodeCryptoHashFunction).update(tbs).digest()); + const scittUrn = `${urnPrefix}:${type}:${nodeCryptoToIanaNamedHashFunctions[nodeCryptoHashFunction]}:${mandatoryBaseEncoding}:${messageHashBase64}` + return scittUrn + } +} \ No newline at end of file diff --git a/src/scitt/index.ts b/src/scitt/index.ts index 0c0b983..bc7bbd2 100644 --- a/src/scitt/index.ts +++ b/src/scitt/index.ts @@ -1,4 +1,4 @@ import * as statement from './statement' import * as receipt from './receipt' - -export { statement, receipt } \ No newline at end of file +import * as identifiers from './identifiers' +export { statement, receipt, identifiers } \ No newline at end of file