Browse Source

pull in did:peer verification (from endorser-ch)

master
Trent Larson 2 months ago
parent
commit
537add2488
  1. 1
      CHANGELOG.md
  2. 6
      package.json
  3. 162
      pnpm-lock.yaml
  4. 7
      src/server.ts
  5. 8
      src/vc/did-eth-local-resolver.ts
  6. 139
      src/vc/didPeer.ts
  7. 81
      src/vc/index.ts
  8. 104
      src/vc/passkeyDidPeer.ts

1
CHANGELOG.md

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Replacement of an existing file - Replacement of an existing file
- Local resolver for did:ethr - Local resolver for did:ethr
- Validation of did:peer JWANT
- Testing for file deletion - Testing for file deletion
### Fixed ### Fixed
- Incorrect check for others who recorded same image - Incorrect check for others who recorded same image

6
package.json

@ -6,8 +6,12 @@
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.614.0", "@aws-sdk/client-s3": "^3.614.0",
"@aws-sdk/lib-storage": "^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", "cors": "^2.8.5",
"did-jwt": "^8.0.4", "did-jwt": "^7.4.7",
"did-resolver": "^4.1.0", "did-resolver": "^4.1.0",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",

162
pnpm-lock.yaml

@ -11,12 +11,24 @@ dependencies:
'@aws-sdk/lib-storage': '@aws-sdk/lib-storage':
specifier: ^3.614.0 specifier: ^3.614.0
version: 3.614.0(@aws-sdk/client-s3@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: cors:
specifier: ^2.8.5 specifier: ^2.8.5
version: 2.8.5 version: 2.8.5
did-jwt: did-jwt:
specifier: ^8.0.4 specifier: ^7.4.7
version: 8.0.4 version: 7.4.7
did-resolver: did-resolver:
specifier: ^4.1.0 specifier: ^4.1.0
version: 4.1.0 version: 4.1.0
@ -689,6 +701,54 @@ packages:
tslib: 2.6.3 tslib: 2.6.3
dev: false 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: /@cspotcode/source-map-support@0.8.1:
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -722,8 +782,8 @@ packages:
resolution: {integrity: sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw==} resolution: {integrity: sha512-eMk0b9ReBbV23xXU693TAIrLyeO5iTgBZGSJfpqriG8UkYvr/hC9u9pyMlAakDNHWmbhMZCDs6KQO0jzKD8OTw==}
dev: false dev: false
/@noble/ciphers@0.5.3: /@noble/ciphers@0.4.1:
resolution: {integrity: sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==} resolution: {integrity: sha512-QCOA9cgf3Rc33owG0AYBB9wszz+Ul2kramWN8tXG44Gyciud/tbkEqvxRF/IpqQaBpRBNi9f4jdNxqB2CQCIXg==}
dev: false dev: false
/@noble/curves@1.4.2: /@noble/curves@1.4.2:
@ -757,6 +817,33 @@ packages:
dev: false dev: false
optional: true 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: /@scure/base@1.1.7:
resolution: {integrity: sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g==} resolution: {integrity: sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g==}
dev: false dev: false
@ -1384,6 +1471,15 @@ packages:
safer-buffer: 2.1.2 safer-buffer: 2.1.2
dev: true 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: /assert-plus@1.0.0:
resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==}
engines: {node: '>=0.8'} engines: {node: '>=0.8'}
@ -1409,6 +1505,11 @@ packages:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
dev: false dev: false
/base64url@3.0.1:
resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==}
engines: {node: '>=6.0.0'}
dev: false
/bcrypt-pbkdf@1.0.2: /bcrypt-pbkdf@1.0.2:
resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
dependencies: dependencies:
@ -1556,6 +1657,28 @@ packages:
resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
dev: true 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: /chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'} engines: {node: '>= 8.10.0'}
@ -1736,10 +1859,10 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: false dev: false
/did-jwt@8.0.4: /did-jwt@7.4.7:
resolution: {integrity: sha512-KPtG7H+8GgKGMiDqFvOdNy5BBN3hpA+8xV7VygEnpst5oPIqjvcH3rTtnPF55a8bOxIzE2PudKGIXIQhekv7WA==} resolution: {integrity: sha512-Apz7nIfIHSKWIMaEP5L/K8xkwByvjezjTG0xiqwKdnNj1x8M0+Yasury5Dm/KPltxi2PlGfRPf3IejRKZrT8mQ==}
dependencies: dependencies:
'@noble/ciphers': 0.5.3 '@noble/ciphers': 0.4.1
'@noble/curves': 1.4.2 '@noble/curves': 1.4.2
'@noble/hashes': 1.4.0 '@noble/hashes': 1.4.0
'@scure/base': 1.1.7 '@scure/base': 1.1.7
@ -2279,6 +2402,11 @@ packages:
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
dev: false dev: false
/ipaddr.js@2.2.0:
resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==}
engines: {node: '>= 10'}
dev: false
/is-binary-path@2.1.0: /is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -2605,6 +2733,15 @@ packages:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
dev: false 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: /node-gyp@8.4.1:
resolution: {integrity: sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==} resolution: {integrity: sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==}
engines: {node: '>= 10.12.0'} engines: {node: '>= 10.12.0'}
@ -2810,6 +2947,17 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
dev: true 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: /qs@6.11.0:
resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==}
engines: {node: '>=0.6'} engines: {node: '>=0.6'}

7
src/server.ts

@ -1,4 +1,5 @@
//@ts-nocheck //@ts-nocheck
const { DeleteObjectCommand, PutObjectCommand, S3Client } = require('@aws-sdk/client-s3'); const { DeleteObjectCommand, PutObjectCommand, S3Client } = require('@aws-sdk/client-s3');
const cors = require('cors'); const cors = require('cors');
const crypto = require('crypto'); const crypto = require('crypto');
@ -11,7 +12,7 @@ const multer = require('multer');
const path = require('path'); const path = require('path');
const sqlite3 = require('sqlite3').verbose(); const sqlite3 = require('sqlite3').verbose();
const { didEthLocalResolver } = require("./vc/did-eth-local-resolver"); import { decodeAndVerifyJwt } from "./vc";
require('dotenv').config() 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 bucketName = process.env.S3_BUCKET_NAME || 'gifts-image-test';
const imageServer = process.env.DOWNLOAD_IMAGE_SERVER || 'test-image.timesafari.app'; const imageServer = process.env.DOWNLOAD_IMAGE_SERVER || 'test-image.timesafari.app';
const resolver = new Resolver({ 'ethr': didEthLocalResolver });
// Open a connection to the SQLite database // Open a connection to the SQLite database
const db = new sqlite3.Database(dbFile, (err) => { const db = new sqlite3.Database(dbFile, (err) => {
if (err) { if (err) {
@ -502,7 +501,7 @@ async function decodeJwt(req, res) {
}; };
} }
const jwt = auth.substring('Bearer '.length); const jwt = auth.substring('Bearer '.length);
const verified = await didJwt.verifyJWT(jwt, { resolver }); const verified = await decodeAndVerifyJwt(jwt);
if (!verified.verified) { if (!verified.verified) {
const errorTime = new Date().toISOString(); const errorTime = new Date().toISOString();
console.error(errorTime, 'Got invalid JWT in Authorization header:', verified); console.error(errorTime, 'Got invalid JWT in Authorization header:', verified);

8
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<DIDResolutionResult>;
};
export default value;
}

139
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<boolean>}
*/
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;
}

81
src/vc/index.ts

@ -5,3 +5,84 @@
* Other projects: endorser-ch, crowd-funder-for-time-pwa * 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
}
})
}

104
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 }
}
Loading…
Cancel
Save