Skip to content

Commit

Permalink
update identifiers for scitt messages
Browse files Browse the repository at this point in the history
  • Loading branch information
OR13 committed Jan 13, 2024
1 parent ec24985 commit 7fb5111
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 57 deletions.
18 changes: 18 additions & 0 deletions src/extractTBS.ts
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/scitt/identifiers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './urn'
85 changes: 30 additions & 55 deletions src/scitt/identifiers/scitt-identifiers.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})

Expand All @@ -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
Expand All @@ -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.
27 changes: 27 additions & 0 deletions src/scitt/identifiers/urn.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
4 changes: 2 additions & 2 deletions src/scitt/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as statement from './statement'
import * as receipt from './receipt'

export { statement, receipt }
import * as identifiers from './identifiers'
export { statement, receipt, identifiers }

0 comments on commit 7fb5111

Please sign in to comment.