Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/remove cbor package #64

Merged
merged 23 commits into from
Mar 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions build.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { build } from "esbuild";
import { nodeBuiltIns } from "esbuild-node-builtins";
async function main() {
const resultBrowser = await build({
plugins: [nodeBuiltIns()],
entryPoints: ["src/browser.ts"],
bundle: true,
outfile: "dist/esbuild/browser.js",
Expand All @@ -11,10 +9,6 @@ async function main() {
platform: "browser",
format: "cjs",
target: "es6",
define: {
"process.env.NODE_DEBUG": "false",
"global": "window"
},
});
console.log("resultBrowser", resultBrowser);

Expand Down
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
module.exports = {
testEnvironment: "node",
roots: ['<rootDir>/src'],
testMatch: [
"**/__tests__/**/*.+(ts|tsx|js)",
Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,11 @@
"@types/node-fetch": "^2.5.6",
"@typescript-eslint/eslint-plugin": "^4.19.0",
"@typescript-eslint/parser": "^4.19.0",
"cbor": "^8.1.0",
"base64-arraybuffer": "^1.0.2",
"did-resolver": "^3.1.3",
"dotenv": "^10.0.0",
"elliptic": "^6.5.4",
"esbuild": "^0.11.11",
"esbuild-node-builtins": "^0.1.0",
"eslint": "^7.22.0",
"jest": "^26.6.3",
"js-sha256": "^0.9.0",
Expand Down
17 changes: 2 additions & 15 deletions src/cbor.test.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,14 @@
import { Buffer } from "buffer";
import { base32 } from "rfc4648";
import { decodeCBOR, encodeOneCBOR } from "./cbor";
import { decodeCOSE } from "./cbor";

test("CBOR library decodes", async () => {
const res = base32.parse(
"2KCEVIQEIVVWK6JNGEASNICZAEP2KALYDZSGSZB2O5SWEOTOPJRXALTDN53GSZBRHEXGQZLBNR2GQLTOPICRUYMBTIFAIGTUKBAAUYTWMOSGQQDDN5XHIZLYOSBHQJTIOR2HA4Z2F4XXO53XFZ3TGLTPOJTS6MRQGE4C6Y3SMVSGK3TUNFQWY4ZPOYYXQKTIOR2HA4Z2F4XW46TDOAXGG33WNFSDCOJONBSWC3DUNAXG46RPMNXW45DFPB2HGL3WGFTXMZLSONUW63TFGEXDALRQMR2HS4DFQJ2FMZLSNFTGSYLCNRSUG4TFMRSW45DJMFWG6UDVMJWGSY2DN53GSZCQMFZXG4LDOJSWIZLOORUWC3CTOVRGUZLDOSRWSZ3JOZSW4TTBNVSWISTBMNVWUZTBNVUWY6KOMFWWKZ2TOBQXE4TPO5RWI33CNIYTSNRQFUYDILJRGYDVAYFE6VGU4MCDGK7DHLLYWHVPUS2YIDJOA6Y524TD3AZRM263WTY2BE4DPKIF27WKF3UDNNVSVWRDYIYVJ65IRJJJ6Z25M2DO4YZLBHWFQGVQR5ZLIWEQJOZTS3IQ7JTNCFDX"
);

const cborobj = decodeCBOR(res);
const cborobj = decodeCOSE(res);

expect(cborobj.err).toEqual(undefined);
expect(cborobj.tag).toEqual(18);
expect(cborobj.value.length).toEqual(4);
});

test("CBOR library encodes", async () => {
const res = Buffer.from(base32.parse(
"2KCEVIQEIVVWK6JNGEASNICZAEP2KALYDZSGSZB2O5SWEOTOPJRXALTDN53GSZBRHEXGQZLBNR2GQLTOPICRUYMBTIFAIGTUKBAAUYTWMOSGQQDDN5XHIZLYOSBHQJTIOR2HA4Z2F4XXO53XFZ3TGLTPOJTS6MRQGE4C6Y3SMVSGK3TUNFQWY4ZPOYYXQKTIOR2HA4Z2F4XW46TDOAXGG33WNFSDCOJONBSWC3DUNAXG46RPMNXW45DFPB2HGL3WGFTXMZLSONUW63TFGEXDALRQMR2HS4DFQJ2FMZLSNFTGSYLCNRSUG4TFMRSW45DJMFWG6UDVMJWGSY2DN53GSZCQMFZXG4LDOJSWIZLOORUWC3CTOVRGUZLDOSRWSZ3JOZSW4TTBNVSWISTBMNVWUZTBNVUWY6KOMFWWKZ2TOBQXE4TPO5RWI33CNIYTSNRQFUYDILJRGYDVAYFE6VGU4MCDGK7DHLLYWHVPUS2YIDJOA6Y524TD3AZRM263WTY2BE4DPKIF27WKF3UDNNVSVWRDYIYVJ65IRJJJ6Z25M2DO4YZLBHWFQGVQR5ZLIWEQJOZTS3IQ7JTNCFDX"
));

const cborobj = decodeCBOR(res);
const result = encodeOneCBOR(cborobj);

expect(res.toString("hex")).toEqual(result.toString("hex"));
});

216 changes: 203 additions & 13 deletions src/cbor.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,212 @@
// centralized place where cbor is included, in case we need to patch it
import { Data } from "./cborTypes";
import { DecodedCOSEStructure } from "./coseTypes";

import { Buffer } from "buffer";
// author: putara
// https://github.com/putara/nzcp/blob/master/verifier.js
class Stream {
data: Uint8Array;
ptr: number;
len: number;

import process from "process";
global.process = process;
constructor(data: Uint8Array) {
this.data = data;
this.ptr = 0;
this.len = data.length;
}
getc() {
if (this.ptr >= this.len) {
throw new Error("invalid data");
}
return this.data[this.ptr++];
}
ungetc() {
if (this.ptr <= 0) {
throw new Error("invalid data");
}
--this.ptr;
}
chop(len: number) {
if (len < 0) {
throw new Error("invalid length");
}
if (this.ptr + len > this.len) {
throw new Error("invalid data");
}
const out = this.data.subarray(this.ptr, this.ptr + len);
this.ptr += len;
return out;
}
}

import util from "util";
// @ts-ignore
global.TextDecoder = util.TextDecoder;
// @ts-ignore
global.TextEncoder = util.TextEncoder;
// RFC 7049
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function decodeCBORStream(stream: Stream) {
function decodeUint(stream: Stream, v: number) {
let x = v & 31;
if (x <= 23) {
// small
} else if (x === 24) {
// 8-bit
x = stream.getc();
} else if (x === 25) {
// 16-bit
x = stream.getc() << 8;
x |= stream.getc();
} else if (x === 26) {
// 32-bit
x = stream.getc() << 24;
x |= stream.getc() << 16;
x |= stream.getc() << 8;
x |= stream.getc();
} else if (x === 27) {
// 64-bit
x = stream.getc() << 56;
x |= stream.getc() << 48;
x |= stream.getc() << 40;
x |= stream.getc() << 32;
x |= stream.getc() << 24;
x |= stream.getc() << 16;
x |= stream.getc() << 8;
x |= stream.getc();
} else {
throw new Error("invalid data");
}
return x;
}
function decode(stream: Stream, isKeyString?: boolean): Data {
const v = stream.getc();
const type = v >> 5;
if (type === 0) {
// positive int
return decodeUint(stream, v);
} else if (type === 1) {
// negative int
return ~decodeUint(stream, v);
} else if (type === 2) {
// byte array
return stream.chop(decodeUint(stream, v));
} else if (type === 3) {
// utf-8 string
return new TextDecoder("utf-8").decode(
stream.chop(decodeUint(stream, v))
);
} else if (type === 4) {
// array
const d = new Array(decodeUint(stream, v))
.fill(undefined)
.map(() => decode(stream));
return d;
} else if (type === 5) {
// object
const dMap: Map<Data, Data> = new Map();
const dObj: { [key: string]: Data } = {};
const len = decodeUint(stream, v);
for (let i = 0; i < len; ++i) {
const key = decode(stream);
const value = decode(stream, typeof key === "string");
dMap.set(key, value);
dObj[`${key}`] = value;
}
return isKeyString ? dObj : dMap;
}
return null
}
return decode(stream);
}

import cbor from "cbor";
const encodeBytes = (data: Uint8Array | never[]) => {
const x = data.length;
if (x === 0) {
return [0x40];
} else if (x <= 23) {
// small
return [0x40 + x, ...data];
} else if (x < 256) {
// 8-bit
return [0x40 + 24, x, ...data];
} else if (x < 65536) {
// 16-bit
return [0x40 + 25, x >> 8, x & 0xff, ...data];
} // leave 32-bit and 64-bit unimplemented
throw new Error("Too big data");
};

export function encodeToBeSigned(bodyProtected: Uint8Array, payload: Uint8Array): Uint8Array {
const sig_structure = new Uint8Array([
// array w/ 4 items
0x84,
// #1: context: "Signature1"
0x6a,
0x53,
0x69,
0x67,
0x6e,
0x61,
0x74,
0x75,
0x72,
0x65,
0x31,
// #2: body_protected: CWT headers
...encodeBytes(bodyProtected),
// #3: external_aad: empty
...encodeBytes([]),
// #4: payload: CWT claims
...encodeBytes(payload),
]);
const ToBeSigned = sig_structure;
return ToBeSigned;
}

function decodeCOSEStream(stream: Stream) {
const vtag = stream.getc();
const tag = vtag & 31;

try {
if (vtag !== 0xD2) {
throw new Error('invalid data');
}
const data = decodeCBORStream(stream);
if (!(data instanceof Array)) {
throw new Error('invalid data');
}

const data1 = data[1];
if (!(data1 instanceof Map)) {
throw new Error('invalid data');
}

if (!(data instanceof Array) || data.length !== 4 || !(data[0] instanceof Uint8Array) || typeof data1 !== 'object' || Object.keys(data1).length !== 0 || !(data[2] instanceof Uint8Array) || !(data[3] instanceof Uint8Array)) {
throw new Error('invalid data');
}

return {
tag,
value: [
data[0],
data[1],
data[2],
data[3],
],
err: undefined,
};
}
catch (err) {
return {
tag,
value: [],
err,
}
}
}

export const encodeOneCBOR = (obj: any): Buffer => {
return cbor.encodeOne(obj, { genTypes: [Buffer, cbor.Encoder._pushBuffer] });
export const decodeCBOR = (buf: Uint8Array): Data => {
const data = decodeCBORStream(new Stream(buf))
return data
};

export const decodeCBOR = (buf: Buffer | Uint8Array): any => {
return cbor.decode(buf);
export const decodeCOSE = (buf: Uint8Array): DecodedCOSEStructure => {
const data = decodeCOSEStream(new Stream(buf))
return data
};
1 change: 1 addition & 0 deletions src/cborTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type Data = string | number | Uint8Array | Data[] | Map<Data, Data> | { [key: string]: Data } | null;
20 changes: 15 additions & 5 deletions src/coseTypes.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
type DecodedCOSEValue = (Buffer | Record<string, never>)[];
import { Data } from "./cborTypes";

export interface DecodedCOSEStructure {
tag: number;
value: DecodedCOSEValue;
err?: Error;
type DecodedCOSEValue = Data[];

interface DecodedCOSEStructureSuccess {
tag: number
value: DecodedCOSEValue
err: Error
}

interface DecodedCOSEStructureError {
tag: number
value: DecodedCOSEValue
err: undefined
}

export type DecodedCOSEStructure = DecodedCOSEStructureSuccess | DecodedCOSEStructureError
26 changes: 9 additions & 17 deletions src/crypto.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { sha256 } from "js-sha256";
import elliptic from "elliptic";
import { DecodedCOSEStructure } from "./coseTypes";
import { encodeOneCBOR } from "./cbor";
import { Buffer } from "buffer";
import { encodeToBeSigned } from "./cbor";
import { decode } from 'base64-arraybuffer'
import { toHex } from "./util";

const EC = elliptic.ec;
const ec = new EC("p256");
Expand All @@ -19,11 +20,11 @@ export function validateCOSESignature(
return false;
}

const xBuf = Buffer.from(publicKeyJwt.x, "base64");
const yBuf = Buffer.from(publicKeyJwt.y, "base64");
const xBuf = new Uint8Array(decode(publicKeyJwt.x.replace(/-/g, '+').replace(/_/g, '/')))
const yBuf = new Uint8Array(decode(publicKeyJwt.y.replace(/-/g, '+').replace(/_/g, '/')))

// 1) '04' + hex string of x + hex string of y
const publicKeyHex = `04${xBuf.toString("hex")}${yBuf.toString("hex")}`;
const publicKeyHex = `04${toHex(xBuf)}${toHex(yBuf)}`;
const key = ec.keyFromPublic(publicKeyHex, "hex");
// Sig_structure = [
// context : "Signature" / "Signature1" / "CounterSignature",
Expand All @@ -32,21 +33,12 @@ export function validateCOSESignature(
// external_aad : bstr,
// payload : bstr
// ]
const bufferProtected_ = Buffer.from(protected_ as Buffer);
const buffer0 = Buffer.alloc(0);
const bufferPayload_ = Buffer.from(payload_ as Buffer);
const SigStructure = [
"Signature1",
bufferProtected_,
buffer0,
bufferPayload_,
];

const ToBeSigned = encodeOneCBOR(SigStructure);
const ToBeSigned = encodeToBeSigned(protected_ as Uint8Array, payload_ as Uint8Array);
const messageHash = sha256.digest(ToBeSigned);
const signature = {
r: signature_.slice(0, signature_.length / 2),
s: signature_.slice(signature_.length / 2),
r: (signature_ as Uint8Array).slice(0, (signature_ as Uint8Array).length / 2),
s: (signature_ as Uint8Array).slice((signature_ as Uint8Array).length / 2),
};
const result = key.verify(messageHash, signature);
return result;
Expand Down
4 changes: 2 additions & 2 deletions src/cwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function parseCWTClaims(
// Section 2.1.1.2
// CWT Token ID claim MUST be a valid UUID in the form of a URI as specified by [RFC4122]
try {
const jtiResult = decodeCtiToJti(ctiClaimRaw as Buffer);
const jtiResult = decodeCtiToJti(ctiClaimRaw as Uint8Array);
jti = jtiResult;
} catch (error) {
// continue parsing, but don't set jti
Expand Down Expand Up @@ -275,7 +275,7 @@ export function parseCWTHeaders(
const CWTHeaderAlg = rawCWTHeaders.get(1);
// Section 2.2.1
// `kid` value MUST be encoded as a Major Type 3
const kid = CWTHeaderKid ? CWTHeaderKid.toString() : undefined;
const kid = CWTHeaderKid ? new TextDecoder("utf-8").decode(CWTHeaderKid as Uint8Array) : undefined;
// Section 2.2.2
// `alg` claim value MUST be set to the value corresponding to ES256 algorithm registration, which is the numeric value of -7
const alg = CWTHeaderAlg === -7 ? "ES256" : undefined;
Expand Down
Loading