diff --git a/CHANGELOG.md b/CHANGELOG.md index 16faf3a..d26d3d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Replacement of an existing file - Local resolver for did:ethr +- Validation of did:peer JWANT - Testing for file deletion ### Fixed - Incorrect check for others who recorded same image diff --git a/package.json b/package.json index ce3f9ad..4cb3ad9 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,12 @@ "dependencies": { "@aws-sdk/client-s3": "^3.614.0", "@aws-sdk/lib-storage": "^3.614.0", + "@peculiar/asn1-ecc": "^2.3.8", + "@peculiar/asn1-schema": "^2.3.8", + "base64url": "^3.0.1", + "cbor-x": "^1.5.9", "cors": "^2.8.5", - "did-jwt": "^8.0.4", + "did-jwt": "^7.4.7", "did-resolver": "^4.1.0", "dotenv": "^16.4.5", "express": "^4.19.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e51e3ba..1dfebb8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,12 +11,24 @@ dependencies: '@aws-sdk/lib-storage': specifier: ^3.614.0 version: 3.614.0(@aws-sdk/client-s3@3.614.0) + '@peculiar/asn1-ecc': + specifier: ^2.3.8 + version: 2.3.8 + '@peculiar/asn1-schema': + specifier: ^2.3.8 + version: 2.3.8 + base64url: + specifier: ^3.0.1 + version: 3.0.1 + cbor-x: + specifier: ^1.5.9 + version: 1.5.9 cors: specifier: ^2.8.5 version: 2.8.5 did-jwt: - specifier: ^8.0.4 - version: 8.0.4 + specifier: ^7.4.7 + version: 7.4.7 did-resolver: specifier: ^4.1.0 version: 4.1.0 @@ -689,6 +701,54 @@ packages: tslib: 2.6.3 dev: false + /@cbor-extract/cbor-extract-darwin-arm64@2.2.0: + resolution: {integrity: sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@cbor-extract/cbor-extract-darwin-x64@2.2.0: + resolution: {integrity: sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w==} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@cbor-extract/cbor-extract-linux-arm64@2.2.0: + resolution: {integrity: sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ==} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@cbor-extract/cbor-extract-linux-arm@2.2.0: + resolution: {integrity: sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q==} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@cbor-extract/cbor-extract-linux-x64@2.2.0: + resolution: {integrity: sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw==} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@cbor-extract/cbor-extract-win32-x64@2.2.0: + resolution: {integrity: sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w==} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@cspotcode/source-map-support@0.8.1: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -722,8 +782,8 @@ packages: resolution: {integrity: sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw==} dev: false - /@noble/ciphers@0.5.3: - resolution: {integrity: sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==} + /@noble/ciphers@0.4.1: + resolution: {integrity: sha512-QCOA9cgf3Rc33owG0AYBB9wszz+Ul2kramWN8tXG44Gyciud/tbkEqvxRF/IpqQaBpRBNi9f4jdNxqB2CQCIXg==} dev: false /@noble/curves@1.4.2: @@ -757,6 +817,33 @@ packages: dev: false optional: true + /@peculiar/asn1-ecc@2.3.8: + resolution: {integrity: sha512-Ah/Q15y3A/CtxbPibiLM/LKcMbnLTdUdLHUgdpB5f60sSvGkXzxJCu5ezGTFHogZXWNX3KSmYqilCrfdmBc6pQ==} + dependencies: + '@peculiar/asn1-schema': 2.3.8 + '@peculiar/asn1-x509': 2.3.8 + asn1js: 3.0.5 + tslib: 2.6.3 + dev: false + + /@peculiar/asn1-schema@2.3.8: + resolution: {integrity: sha512-ULB1XqHKx1WBU/tTFIA+uARuRoBVZ4pNdOA878RDrRbBfBGcSzi5HBkdScC6ZbHn8z7L8gmKCgPC1LHRrP46tA==} + dependencies: + asn1js: 3.0.5 + pvtsutils: 1.3.5 + tslib: 2.6.3 + dev: false + + /@peculiar/asn1-x509@2.3.8: + resolution: {integrity: sha512-voKxGfDU1c6r9mKiN5ZUsZWh3Dy1BABvTM3cimf0tztNwyMJPhiXY94eRTgsMQe6ViLfT6EoXxkWVzcm3mFAFw==} + dependencies: + '@peculiar/asn1-schema': 2.3.8 + asn1js: 3.0.5 + ipaddr.js: 2.2.0 + pvtsutils: 1.3.5 + tslib: 2.6.3 + dev: false + /@scure/base@1.1.7: resolution: {integrity: sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g==} dev: false @@ -1384,6 +1471,15 @@ packages: safer-buffer: 2.1.2 dev: true + /asn1js@3.0.5: + resolution: {integrity: sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==} + engines: {node: '>=12.0.0'} + dependencies: + pvtsutils: 1.3.5 + pvutils: 1.1.3 + tslib: 2.6.3 + dev: false + /assert-plus@1.0.0: resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} engines: {node: '>=0.8'} @@ -1409,6 +1505,11 @@ packages: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} dev: false + /base64url@3.0.1: + resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==} + engines: {node: '>=6.0.0'} + dev: false + /bcrypt-pbkdf@1.0.2: resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} dependencies: @@ -1556,6 +1657,28 @@ packages: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} dev: true + /cbor-extract@2.2.0: + resolution: {integrity: sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA==} + hasBin: true + requiresBuild: true + dependencies: + node-gyp-build-optional-packages: 5.1.1 + optionalDependencies: + '@cbor-extract/cbor-extract-darwin-arm64': 2.2.0 + '@cbor-extract/cbor-extract-darwin-x64': 2.2.0 + '@cbor-extract/cbor-extract-linux-arm': 2.2.0 + '@cbor-extract/cbor-extract-linux-arm64': 2.2.0 + '@cbor-extract/cbor-extract-linux-x64': 2.2.0 + '@cbor-extract/cbor-extract-win32-x64': 2.2.0 + dev: false + optional: true + + /cbor-x@1.5.9: + resolution: {integrity: sha512-OEI5rEu3MeR0WWNUXuIGkxmbXVhABP+VtgAXzm48c9ulkrsvxshjjk94XSOGphyAKeNGLPfAxxzEtgQ6rEVpYQ==} + optionalDependencies: + cbor-extract: 2.2.0 + dev: false + /chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -1736,10 +1859,10 @@ packages: engines: {node: '>=8'} dev: false - /did-jwt@8.0.4: - resolution: {integrity: sha512-KPtG7H+8GgKGMiDqFvOdNy5BBN3hpA+8xV7VygEnpst5oPIqjvcH3rTtnPF55a8bOxIzE2PudKGIXIQhekv7WA==} + /did-jwt@7.4.7: + resolution: {integrity: sha512-Apz7nIfIHSKWIMaEP5L/K8xkwByvjezjTG0xiqwKdnNj1x8M0+Yasury5Dm/KPltxi2PlGfRPf3IejRKZrT8mQ==} dependencies: - '@noble/ciphers': 0.5.3 + '@noble/ciphers': 0.4.1 '@noble/curves': 1.4.2 '@noble/hashes': 1.4.0 '@scure/base': 1.1.7 @@ -2279,6 +2402,11 @@ packages: engines: {node: '>= 0.10'} dev: false + /ipaddr.js@2.2.0: + resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} + engines: {node: '>= 10'} + dev: false + /is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -2605,6 +2733,15 @@ packages: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} dev: false + /node-gyp-build-optional-packages@5.1.1: + resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} + hasBin: true + requiresBuild: true + dependencies: + detect-libc: 2.0.3 + dev: false + optional: true + /node-gyp@8.4.1: resolution: {integrity: sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==} engines: {node: '>= 10.12.0'} @@ -2810,6 +2947,17 @@ packages: engines: {node: '>=6'} dev: true + /pvtsutils@1.3.5: + resolution: {integrity: sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==} + dependencies: + tslib: 2.6.3 + dev: false + + /pvutils@1.1.3: + resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==} + engines: {node: '>=6.0.0'} + dev: false + /qs@6.11.0: resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} engines: {node: '>=0.6'} diff --git a/src/server.ts b/src/server.ts index 1a24937..2472fb2 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,4 +1,5 @@ //@ts-nocheck + const { DeleteObjectCommand, PutObjectCommand, S3Client } = require('@aws-sdk/client-s3'); const cors = require('cors'); const crypto = require('crypto'); @@ -11,7 +12,7 @@ const multer = require('multer'); const path = require('path'); const sqlite3 = require('sqlite3').verbose(); -const { didEthLocalResolver } = require("./vc/did-eth-local-resolver"); +import { decodeAndVerifyJwt } from "./vc"; require('dotenv').config() @@ -24,8 +25,6 @@ const dbFile = process.env.SQLITE_FILE || './image-db.sqlite'; const bucketName = process.env.S3_BUCKET_NAME || 'gifts-image-test'; const imageServer = process.env.DOWNLOAD_IMAGE_SERVER || 'test-image.timesafari.app'; -const resolver = new Resolver({ 'ethr': didEthLocalResolver }); - // Open a connection to the SQLite database const db = new sqlite3.Database(dbFile, (err) => { if (err) { @@ -502,7 +501,7 @@ async function decodeJwt(req, res) { }; } const jwt = auth.substring('Bearer '.length); - const verified = await didJwt.verifyJWT(jwt, { resolver }); + const verified = await decodeAndVerifyJwt(jwt); if (!verified.verified) { const errorTime = new Date().toISOString(); console.error(errorTime, 'Got invalid JWT in Authorization header:', verified); diff --git a/src/vc/did-eth-local-resolver.ts b/src/vc/did-eth-local-resolver.ts new file mode 100644 index 0000000..ed6dcf8 --- /dev/null +++ b/src/vc/did-eth-local-resolver.ts @@ -0,0 +1,8 @@ +import {DIDResolutionResult} from "did-resolver"; + +declare module './did-eth-local-resolver.js' { + const value: { + didEthLocalResolver: (jwt: string) => Promise; + }; + export default value; +} diff --git a/src/vc/didPeer.ts b/src/vc/didPeer.ts new file mode 100644 index 0000000..ddbd925 --- /dev/null +++ b/src/vc/didPeer.ts @@ -0,0 +1,139 @@ +import { AsnParser } from "@peculiar/asn1-schema"; +import { ECDSASigValue } from "@peculiar/asn1-ecc"; +import crypto from "crypto"; +import { decode as cborDecode } from "cbor-x"; + +/** + * + * + * similar code is in crowd-funder-for-time-pwa libs/crypto/vc/passkeyDidPeer.ts verifyJwtWebCrypto + * + * @returns {Promise} + */ +export async function verifyPeerSignature( + payloadBytes: Uint8Array, + publicKeyBytes: Uint8Array, + signatureBytes: Uint8Array +) { + // this simple approach doesn't work + //const verify = crypto.createVerify('sha256') + //verify.update(preimage) + //const result = verify.verify(publicKey, signature) + + const finalSignatureBuffer = unwrapEC2Signature(signatureBytes); + const verifyAlgorithm = { + name: "ECDSA", + hash: { name: "SHA-256" }, + }; + const publicKeyJwk = cborToKeys(publicKeyBytes).publicKeyJwk; + const keyAlgorithm = { + name: "ECDSA", + namedCurve: publicKeyJwk.crv, + }; + const publicKeyCryptoKey = await crypto.subtle.importKey( + "jwk", + publicKeyJwk, + keyAlgorithm, + false, + ["verify"], + ); + const verified = await crypto.subtle.verify( + verifyAlgorithm, + publicKeyCryptoKey, + finalSignatureBuffer, + payloadBytes, + ); + return verified; +} + +function cborToKeys(publicKeyBytes: Uint8Array) { + const jwkObj = cborDecode(publicKeyBytes); + if ( + jwkObj[1] != 2 || // kty "EC" + jwkObj[3] != -7 || // alg "ES256" + jwkObj[-1] != 1 || // crv "P-256" + jwkObj[-2].length != 32 || // x + jwkObj[-3].length != 32 // y + ) { + throw new Error("Unable to extract key."); + } + const publicKeyJwk = { + alg: "ES256", + crv: "P-256", + kty: "EC", + x: arrayToBase64Url(jwkObj[-2]), + y: arrayToBase64Url(jwkObj[-3]), + }; + const publicKeyBuffer = Buffer.concat([ + Buffer.from(jwkObj[-2]), + Buffer.from(jwkObj[-3]), + ]); + return { publicKeyJwk, publicKeyBuffer }; +} + +function toBase64Url(anythingB64: string) { + return anythingB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +} + +function arrayToBase64Url(anything: Uint8Array) { + return toBase64Url(Buffer.from(anything).toString("base64")); +} + +/** + * In WebAuthn, EC2 signatures are wrapped in ASN.1 structure so we need to peel r and s apart. + * + * See https://www.w3.org/TR/webauthn-2/#sctn-signature-attestation-types + * + * @return Uint8Array of the signature inside + */ +function unwrapEC2Signature(signature: Uint8Array) { + const parsedSignature = AsnParser.parse(signature, ECDSASigValue); + let rBytes = new Uint8Array(parsedSignature.r); + let sBytes = new Uint8Array(parsedSignature.s); + + if (shouldRemoveLeadingZero(rBytes)) { + rBytes = rBytes.slice(1); + } + + if (shouldRemoveLeadingZero(sBytes)) { + sBytes = sBytes.slice(1); + } + + const finalSignature = isoUint8ArrayConcat([rBytes, sBytes]); + + return finalSignature; +} + +/** + * Determine if the DER-specific `00` byte at the start of an ECDSA signature byte sequence + * should be removed based on the following logic: + * + * "If the leading byte is 0x0, and the high order bit on the second byte is not set to 0, + * then remove the leading 0x0 byte" + * + * @return true if leading zero should be removed + */ +function shouldRemoveLeadingZero(bytes: Uint8Array) { + return bytes[0] === 0x0 && (bytes[1] & (1 << 7)) !== 0; +} + +// from https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/iso/isoUint8Array.ts#L49 +/** + * Combine multiple Uint8Arrays into a single Uint8Array + * + * @param arrays - Uint8Array[] + * @return Uint8Array + */ +function isoUint8ArrayConcat(arrays: Uint8Array[]) { + let pointer = 0; + const totalLength = arrays.reduce((prev, curr) => prev + curr.length, 0); + + const toReturn = new Uint8Array(totalLength); + + arrays.forEach((arr) => { + toReturn.set(arr, pointer); + pointer += arr.length; + }); + + return toReturn; +} diff --git a/src/vc/index.ts b/src/vc/index.ts index 7ab4375..e4a2c2b 100644 --- a/src/vc/index.ts +++ b/src/vc/index.ts @@ -5,3 +5,84 @@ * Other projects: endorser-ch, crowd-funder-for-time-pwa * */ + +import base64url from "base64url"; +import didJwt from "did-jwt"; +import {Resolver} from "did-resolver"; + +import ethResolver from "./did-eth-local-resolver"; +import {verifyJwt as peerVerifyJwt} from "./passkeyDidPeer"; + + +export const TEST_BYPASS_ENV_VALUE = "test-local"; +export const ETHR_DID_PREFIX = 'did:ethr:' +export const PEER_DID_PREFIX = 'did:peer:' +export const JWT_VERIFY_FAILED_CODE = "JWT_VERIFY_FAILED_CODE" +export const UNSUPPORTED_DID_METHOD_CODE = "UNSUPPORTED_DID_METHOD" + +const resolver = new Resolver({ + 'ethr': ethResolver.didEthLocalResolver +}); + +// return Promise of at least { issuer, payload, verified boolean } +// ... and also if successfully verified by did-jwt (not JWANT): data, doc, signature, signer +export async function decodeAndVerifyJwt(jwt: string) { + const pieces = jwt.split('.') + const header = JSON.parse(base64url.decode(pieces[0])) + const payload = JSON.parse(base64url.decode(pieces[1])) + const issuerDid = payload.iss + if (!issuerDid) { + return Promise.reject({ + clientError: { + message: `Missing "iss" field in JWT.`, + } + }) + } + if (issuerDid && issuerDid.startsWith(ETHR_DID_PREFIX) && process.env.NODE_ENV === TEST_BYPASS_ENV_VALUE) { + // Error of "Cannot read property 'toString' of undefined" usually means the JWT is malformed + // eg. no "." separators. + let nowEpoch = Math.floor(new Date().getTime() / 1000) + if (payload.exp < nowEpoch) { + console.log("JWT with exp " + payload.exp + + " has expired but we're in test mode so we'll use a new time." + ) + payload.exp = nowEpoch + 100 + } + return { issuer: issuerDid, payload, verified: true } // other elements will = undefined + } + + if (issuerDid.startsWith(ETHR_DID_PREFIX)) { + try { + let verified = await didJwt.verifyJWT(jwt, {resolver}) + return verified + + } catch (e) { + return Promise.reject({ + clientError: { + message: `JWT failed verification: ` + e, + code: JWT_VERIFY_FAILED_CODE + } + }) + } + } + + if (issuerDid.startsWith(PEER_DID_PREFIX) && header.typ === "JWANT") { + const { claimPayload, verified } = await peerVerifyJwt(payload, issuerDid, pieces[2]) + return { issuer: issuerDid, payload: claimPayload, verified: verified } + } + + if (issuerDid.startsWith(PEER_DID_PREFIX)) { + return Promise.reject({ + clientError: { + message: `JWT with a PEER DID currently only supported with typ == JWANT. Contact us us for JWT suport since it should be straightforward.` + } + }) + } + + return Promise.reject({ + clientError: { + message: `Unsupported DID method ${issuerDid}`, + code: UNSUPPORTED_DID_METHOD_CODE + } + }) +} diff --git a/src/vc/passkeyDidPeer.ts b/src/vc/passkeyDidPeer.ts new file mode 100644 index 0000000..0590f50 --- /dev/null +++ b/src/vc/passkeyDidPeer.ts @@ -0,0 +1,104 @@ +import crypto from "crypto"; +import didJwt from "did-jwt"; + +import {PEER_DID_PREFIX, TEST_BYPASS_ENV_VALUE} from "./index"; +import {verifyPeerSignature} from "./didPeer"; + +/** + * + * @param payload + * @param issuerDid + * @param signatureString + * @returns {Promise<{claimPayload: string, verified: boolean}>} + */ +export async function verifyJwt(payload: any, issuerDid: any, signatureString: any) { + if (!payload.iss) { + return Promise.reject({ + clientError: { + message: `JWT is missing an "iss" field.`, + } + }) + } + let nowEpoch = Math.floor(new Date().getTime() / 1000) + if (!payload.exp) { + return Promise.reject({ + clientError: { + message: `JWT with is missing an "exp" field.`, + } + }) + } + if (payload.exp < nowEpoch && process.env.NODE_ENV !== TEST_BYPASS_ENV_VALUE) { + return Promise.reject({ + clientError: { + message: `JWT with exp ${payload.exp} has expired.`, + } + }); + } + + const authData: string = payload.AuthenticationDataB64URL + const clientData: string = payload.ClientDataJSONB64URL + if (!authData || !clientData) { + return Promise.reject({ + clientError: { + message: `JWT with typ == JWANT requires AuthenticationData and ClientDataJSON.` + } + }) + } + + const decodedAuthDataBuff = Buffer.from(authData, 'base64url') + const decodedClientData = Buffer.from(clientData, 'base64url') + + let claimPayload = JSON.parse(decodedClientData.toString()) + if (claimPayload.challenge) { + claimPayload = JSON.parse(Buffer.from(claimPayload.challenge, "base64url").toString()) + if (!claimPayload.exp) { + claimPayload.exp = payload.exp + } + if (!claimPayload.iat) { + claimPayload.iat = payload.iat + } + if (!claimPayload.iss) { + claimPayload.iss = payload.iss + } + } + if (!claimPayload.exp) { + return Promise.reject({ + clientError: { + message: `JWT client data challenge is missing an "exp" field.`, + } + }) + } + if (claimPayload.exp < nowEpoch && process.env.NODE_ENV !== TEST_BYPASS_ENV_VALUE) { + return Promise.reject({ + clientError: { + message: `JWT client data challenge exp time is past.`, + } + }) + } + if (claimPayload.exp !== payload.exp) { + return Promise.reject({ + clientError: { + message: `JWT client data challenge "exp" field doesn't match the outside payload "exp".`, + } + }) + } + if (claimPayload.iss !== payload.iss) { + return Promise.reject({ + clientError: { + message: `JWT client data challenge "iss" field doesn't match the outside payload "iss".`, + } + }) + } + + const hashedClientDataBuff = crypto.createHash("sha256") + .update(decodedClientData) + .digest() + const preimage = new Uint8Array(Buffer.concat([decodedAuthDataBuff, hashedClientDataBuff])) + const PEER_DID_MULTIBASE_PREFIX = PEER_DID_PREFIX + "0" + // Uint8Array + const publicKey = didJwt.multibaseToBytes(issuerDid.substring(PEER_DID_MULTIBASE_PREFIX.length)); + const signature = new Uint8Array(Buffer.from(signatureString, 'base64url')) + const verified = await verifyPeerSignature(preimage, publicKey, signature) + return { claimPayload, verified } + +}