зеркало из
1
0
Форкнуть 0
This commit is contained in:
Larry Joy 2021-03-10 11:34:42 -08:00
Родитель d2e390d6ba
Коммит 39cad35573
17 изменённых файлов: 810 добавлений и 209 удалений

1
.gitignore поставляемый
Просмотреть файл

@ -124,3 +124,4 @@ testdata/issuer.jwks.public.json
# compiled js files
js/*
testdata/missing_kid_key.json

477
package-lock.json сгенерированный
Просмотреть файл

@ -22,6 +22,7 @@
"istextorbinary": "^5.12.0",
"jimp": "^0.16.1",
"jpeg-js": "^0.4.2",
"json-beautify": "^1.1.1",
"jsqr": "^1.3.1",
"node-jose": "^2.0.0",
"pako": "^2.0.3",
@ -36,10 +37,12 @@
"@types/node-jose": "^1.1.5",
"@types/pako": "^1.0.1",
"@types/pngjs": "^6.0.0",
"@types/qrcode": "^1.4.0",
"@typescript-eslint/eslint-plugin": "^4.15.2",
"@typescript-eslint/parser": "^4.15.2",
"eslint": "^7.20.0",
"jest": "^26.6.3",
"qrcode": "^1.4.4",
"ts-jest": "^26.5.2",
"ts-node": "^9.1.1",
"typescript": "^4.2.2"
@ -1631,6 +1634,15 @@
"integrity": "sha512-i99hy7Ki19EqVOl77WplDrvgNugHnsSjECVR/wUrzw2TJXz1zlUfT2ngGckR6xN7yFYaijsMAqPkOLx9HgUqHg==",
"dev": true
},
"node_modules/@types/qrcode": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.4.0.tgz",
"integrity": "sha512-BwDnCjdZKVOyy6+SPJ4ph+0DAftZGn5JFCY/MhetdEQ8yF6+YndhJWlfdBP8vtMe0w5/FH29Xi6bnEwVWkU1LQ==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/raf": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.0.tgz",
@ -2400,6 +2412,22 @@
"ieee754": "^1.1.13"
}
},
"node_modules/buffer-alloc": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
"integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
"dev": true,
"dependencies": {
"buffer-alloc-unsafe": "^1.1.0",
"buffer-fill": "^1.0.0"
}
},
"node_modules/buffer-alloc-unsafe": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
"integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==",
"dev": true
},
"node_modules/buffer-equal": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz",
@ -2408,6 +2436,12 @@
"node": ">=0.4.0"
}
},
"node_modules/buffer-fill": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
"integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=",
"dev": true
},
"node_modules/buffer-from": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
@ -3091,6 +3125,12 @@
"node": ">= 10.14.2"
}
},
"node_modules/dijkstrajs": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.1.tgz",
"integrity": "sha1-082BIh4+pAdCz83lVtTpnpjdxxs=",
"dev": true
},
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@ -5628,6 +5668,14 @@
"node": ">=4"
}
},
"node_modules/json-beautify": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/json-beautify/-/json-beautify-1.1.1.tgz",
"integrity": "sha512-17j+Hk2lado0xqKtUcyAjK0AtoHnPSIgktWRsEXgdFQFG9UnaGw6CHa0J7xsvulxRpFl6CrkDFHght1p5ZJc4A==",
"bin": {
"json-beautify": "bin/json-beautify"
}
},
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@ -6856,6 +6904,218 @@
"node": ">=6"
}
},
"node_modules/qrcode": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.4.4.tgz",
"integrity": "sha512-oLzEC5+NKFou9P0bMj5+v6Z40evexeE29Z9cummZXZ9QXyMr3lphkURzxjXgPJC5azpxcshoDWV1xE46z+/c3Q==",
"dev": true,
"dependencies": {
"buffer": "^5.4.3",
"buffer-alloc": "^1.2.0",
"buffer-from": "^1.1.1",
"dijkstrajs": "^1.0.1",
"isarray": "^2.0.1",
"pngjs": "^3.3.0",
"yargs": "^13.2.4"
},
"bin": {
"qrcode": "bin/qrcode"
},
"engines": {
"node": ">=4"
}
},
"node_modules/qrcode/node_modules/ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/qrcode/node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/qrcode/node_modules/cliui": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
"dev": true,
"dependencies": {
"string-width": "^3.1.0",
"strip-ansi": "^5.2.0",
"wrap-ansi": "^5.1.0"
}
},
"node_modules/qrcode/node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/qrcode/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
},
"node_modules/qrcode/node_modules/emoji-regex": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
"integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
"dev": true
},
"node_modules/qrcode/node_modules/find-up": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
"dev": true,
"dependencies": {
"locate-path": "^3.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/qrcode/node_modules/is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/qrcode/node_modules/isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
"dev": true
},
"node_modules/qrcode/node_modules/locate-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
"dev": true,
"dependencies": {
"p-locate": "^3.0.0",
"path-exists": "^3.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/qrcode/node_modules/p-locate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
"dev": true,
"dependencies": {
"p-limit": "^2.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/qrcode/node_modules/path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/qrcode/node_modules/pngjs": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz",
"integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==",
"dev": true,
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/qrcode/node_modules/string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"dev": true,
"dependencies": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/qrcode/node_modules/strip-ansi": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dev": true,
"dependencies": {
"ansi-regex": "^4.1.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/qrcode/node_modules/wrap-ansi": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
"dev": true,
"dependencies": {
"ansi-styles": "^3.2.0",
"string-width": "^3.0.0",
"strip-ansi": "^5.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/qrcode/node_modules/yargs": {
"version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
"integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
"dev": true,
"dependencies": {
"cliui": "^5.0.0",
"find-up": "^3.0.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^13.1.2"
}
},
"node_modules/qrcode/node_modules/yargs-parser": {
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
"integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
"dev": true,
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
},
"node_modules/qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
@ -10674,6 +10934,15 @@
"integrity": "sha512-i99hy7Ki19EqVOl77WplDrvgNugHnsSjECVR/wUrzw2TJXz1zlUfT2ngGckR6xN7yFYaijsMAqPkOLx9HgUqHg==",
"dev": true
},
"@types/qrcode": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.4.0.tgz",
"integrity": "sha512-BwDnCjdZKVOyy6+SPJ4ph+0DAftZGn5JFCY/MhetdEQ8yF6+YndhJWlfdBP8vtMe0w5/FH29Xi6bnEwVWkU1LQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/raf": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.0.tgz",
@ -11212,11 +11481,33 @@
"ieee754": "^1.1.13"
}
},
"buffer-alloc": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
"integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==",
"dev": true,
"requires": {
"buffer-alloc-unsafe": "^1.1.0",
"buffer-fill": "^1.0.0"
}
},
"buffer-alloc-unsafe": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz",
"integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==",
"dev": true
},
"buffer-equal": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz",
"integrity": "sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs="
},
"buffer-fill": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
"integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=",
"dev": true
},
"buffer-from": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
@ -11746,6 +12037,12 @@
"integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==",
"dev": true
},
"dijkstrajs": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.1.tgz",
"integrity": "sha1-082BIh4+pAdCz83lVtTpnpjdxxs=",
"dev": true
},
"dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@ -13722,6 +14019,11 @@
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
"dev": true
},
"json-beautify": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/json-beautify/-/json-beautify-1.1.1.tgz",
"integrity": "sha512-17j+Hk2lado0xqKtUcyAjK0AtoHnPSIgktWRsEXgdFQFG9UnaGw6CHa0J7xsvulxRpFl6CrkDFHght1p5ZJc4A=="
},
"json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@ -14701,6 +15003,181 @@
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
},
"qrcode": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.4.4.tgz",
"integrity": "sha512-oLzEC5+NKFou9P0bMj5+v6Z40evexeE29Z9cummZXZ9QXyMr3lphkURzxjXgPJC5azpxcshoDWV1xE46z+/c3Q==",
"dev": true,
"requires": {
"buffer": "^5.4.3",
"buffer-alloc": "^1.2.0",
"buffer-from": "^1.1.1",
"dijkstrajs": "^1.0.1",
"isarray": "^2.0.1",
"pngjs": "^3.3.0",
"yargs": "^13.2.4"
},
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"dev": true
},
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
},
"cliui": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
"dev": true,
"requires": {
"string-width": "^3.1.0",
"strip-ansi": "^5.2.0",
"wrap-ansi": "^5.1.0"
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
},
"emoji-regex": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
"integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
"dev": true
},
"find-up": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
"dev": true,
"requires": {
"locate-path": "^3.0.0"
}
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
"dev": true
},
"isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
"dev": true
},
"locate-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
"dev": true,
"requires": {
"p-locate": "^3.0.0",
"path-exists": "^3.0.0"
}
},
"p-locate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
"dev": true,
"requires": {
"p-limit": "^2.0.0"
}
},
"path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
"dev": true
},
"pngjs": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz",
"integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==",
"dev": true
},
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"dev": true,
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0"
}
},
"strip-ansi": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dev": true,
"requires": {
"ansi-regex": "^4.1.0"
}
},
"wrap-ansi": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.0",
"string-width": "^3.0.0",
"strip-ansi": "^5.0.0"
}
},
"yargs": {
"version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
"integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
"dev": true,
"requires": {
"cliui": "^5.0.0",
"find-up": "^3.0.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^13.1.2"
}
},
"yargs-parser": {
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
"integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
"dev": true,
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
}
}
},
"qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",

Просмотреть файл

@ -29,6 +29,7 @@
"istextorbinary": "^5.12.0",
"jimp": "^0.16.1",
"jpeg-js": "^0.4.2",
"json-beautify": "^1.1.1",
"jsqr": "^1.3.1",
"node-jose": "^2.0.0",
"pako": "^2.0.3",
@ -43,10 +44,12 @@
"@types/node-jose": "^1.1.5",
"@types/pako": "^1.0.1",
"@types/pngjs": "^6.0.0",
"@types/qrcode": "^1.4.0",
"@typescript-eslint/eslint-plugin": "^4.15.2",
"@typescript-eslint/parser": "^4.15.2",
"eslint": "^7.20.0",
"jest": "^26.6.3",
"qrcode": "^1.4.4",
"ts-jest": "^26.5.2",
"ts-node": "^9.1.1",
"typescript": "^4.2.2"

Просмотреть файл

@ -29,6 +29,7 @@ export enum ErrorCode {
INVALID_WRONG_USE,
INVALID_MISSING_KID,
INVALID_WRONG_KID,
INVALID_SCHEMA,
INVALID_SCHEMA,
INVALID_PRIVATE,
INVALID_UNKNOWN
}

Просмотреть файл

@ -9,6 +9,7 @@ import stream from 'stream';
import {promisify} from 'util';
import {JWK} from 'node-jose';
import got from 'got';
import { svgToQRImage } from './image';
const outPath = 'testdata';
@ -76,6 +77,7 @@ async function fetchExamples(outdir: string) : Promise<void> {
const issuerPrivateKeyUrl = 'https://raw.githubusercontent.com/smart-on-fhir/health-cards/main/generate-examples/src/config/issuer.jwks.private.json';
const issuerPublicKeyFileName = 'issuer.jwks.public.json';
async function fetchKeys(outdir: string) : Promise<void> {
const filePath = path.join(outdir, issuerPublicKeyFileName);
@ -92,10 +94,23 @@ async function fetchKeys(outdir: string) : Promise<void> {
}
}
// for each .svg file, generate a png, jpg, and bmp QR image
async function generateImagesFromSvg(dir: string) {
const files = fs.readdirSync(dir);
for (let i = 0; i < files.length; i++) {
const file = path.join(dir, files[i]);
if (path.extname(file) === '.svg') {
await svgToQRImage(file);
}
}
}
// We have to wrap these calls in an async function for ES5 support
// Typescript error: Top-level 'await' expressions are only allowed when the 'module' option is set to 'esnext'
// eslint-disable-next-line @typescript-eslint/no-floating-promises
(async () => {
void (async () => {
await fetchExamples(outPath);
await fetchKeys(outPath);
await generateImagesFromSvg(outPath);
})();

Просмотреть файл

@ -7,7 +7,7 @@ import { ErrorCode } from './error';
import fhirBundleSchema from '../schema/fhir-bundle-schema.json';
import Log from './logger';
import { ValidationResult } from './validate';
import beautify from 'json-beautify'
export const schema = fhirBundleSchema;
@ -44,8 +44,18 @@ export function validate(fhirBundleText: string): ValidationResult {
output.info("Fhir Bundle Contents:");
output.info(JSON.stringify(fhirBundle, null, 2));
output.info(beautify(fhirBundle, null as unknown as Array<string>, 3, 100));
return { result: fhirBundle, log: output };
}
// payload .vc.credentialSubject.fhirBundle is created:
// without Resource.id elements
// without Resource.meta elements
// without Resource.text elements
// without CodeableConcept.text elements
// without Coding.display elements
// with Bundle.entry.fullUrl populated with short resource-scheme URIs (e.g., {"fullUrl": "resource:0})
// with Reference.reference populated with short resource-scheme URIs (e.g., {"patient": {"reference": "resource:0"}})

Просмотреть файл

@ -4,8 +4,7 @@
import fs from 'fs';
import path from 'path';
import jose, { JWK } from 'node-jose';
import svg2img from 'svg2img';
import Jimp from 'jimp';
interface KeyGenerationArgs {
kty: string;
@ -47,47 +46,3 @@ generateAndStoreKey('wrong_use_key.json', { kty: 'EC', size: 'P-256', props: { a
generateAndStoreKey('wrong_alg_key.json', { kty: 'EC', size: 'P-256', props: { alg: 'ES256K', crv: 'P-256', use: 'sig' } });
generateAndStoreKey('wrong_kty_key.json', { kty: 'RSA', size: 2048 });
generateAndStoreKey('missing_kid_key.json', { kty: 'EC', size: 'P-256', props: { alg: 'ES256', crv: 'P-256', use: 'sig' } });
function svgToImage(filePath: string): Promise<unknown> {
const baseFileName = filePath.slice(0, filePath.lastIndexOf('.'));
return new
Promise<Buffer>((resolve, reject) => {
svg2img(filePath, { width: 600, height: 600 },
(error: unknown, buffer: Buffer) => {
error ? reject("Could not create image from svg") : resolve(buffer);
});
})
.then((buffer) => {
fs.writeFileSync(baseFileName + '.png', buffer);
return Jimp.read(baseFileName + '.png');
})
.then(png => {
return Promise.all([
png.write(baseFileName + '.bmp'),
png.grayscale().quality(100).write(baseFileName + '.jpg')
]);
})
.catch(err => { console.error(err); });
}
async function generateImagesFromSvg(dir: string) {
const files = fs.readdirSync(dir);
for (let i = 0; i < files.length; i++) {
const file = path.join(dir, files[i]);
if (path.extname(file) === '.svg') {
await svgToImage(file);
}
}
}
// TODO: generate files with missing algs, once omit is implemented
void generateImagesFromSvg(outdir);

Просмотреть файл

@ -9,6 +9,8 @@ import { FileInfo } from './file';
import * as qr from './qr';
import { PNG } from 'pngjs';
import fs from 'fs';
import Jimp from 'jimp';
import { toFile, QRCodeSegment } from 'qrcode';
export async function validate(images: FileInfo[]): Promise<{ result: JWS | undefined, log: Log }> {
@ -19,11 +21,11 @@ export async function validate(images: FileInfo[]): Promise<{ result: JWS | unde
'QR image');
const shcStrings : SHC[] = [];
const shcStrings: SHC[] = [];
for (let i = 0; i < images.length; i++) {
const shc = await decode(images[i], log);
if(shc === undefined) return {result: undefined, log: log};
if (shc === undefined) return { result: undefined, log: log };
shcStrings.push(shc);
log.info(images[i].name + " decoded");
log.debug(images[i].name + ' = ' + shc);
@ -42,7 +44,7 @@ async function decode(fileInfo: FileInfo, log: Log): Promise<string | undefined>
let svgBuffer;
switch (fileInfo.fileType) {
switch (fileInfo.fileType) {
case 'svg':
svgBuffer = await svgToImageBuffer(fileInfo.buffer.toString(), log);
@ -93,7 +95,7 @@ function decodeQrBuffer(fileInfo: FileInfo, log: Log): string | undefined {
//const png = PNG.sync.read(image);
const data = fileInfo.image;
if(!data) {
if (!data) {
log.fatal('Could not read image data from : ' + fileInfo.name);
return undefined;
}
@ -107,4 +109,48 @@ function decodeQrBuffer(fileInfo: FileInfo, log: Log): string | undefined {
}
return code.data;
}
}
export function svgToQRImage(filePath: string): Promise<unknown> {
const baseFileName = filePath.slice(0, filePath.lastIndexOf('.'));
return new
Promise<Buffer>((resolve, reject) => {
svg2img(filePath, { width: 600, height: 600 },
(error: unknown, buffer: Buffer) => {
error ? reject("Could not create image from svg") : resolve(buffer);
});
})
.then((buffer) => {
fs.writeFileSync(baseFileName + '.png', buffer);
return Jimp.read(baseFileName + '.png');
})
.then(png => {
return Promise.all([
png.write(baseFileName + '.bmp'),
png.grayscale().quality(100).write(baseFileName + '.jpg')
]);
})
.catch(err => { console.error(err); });
}
export async function dataToQRImage(path: string, data: QRCodeSegment[]) : Promise<void> {
await toFile(path, data, { type: 'png', errorCorrectionLevel: 'low' })
.catch((error) => {
throw error;
});
}
// const exampleQrCodes: string[] = await Promise.all(
// qrSet.map((qrSegments): Promise<string> => new Promise((resolve, reject) =>
// QrCode.toString(qrSegments, { type: 'svg', errorCorrectionLevel: 'low' }, function (err: any, result: string) {
// if (err) return reject(err);
// resolve(result as string);
// })
// )));

Просмотреть файл

@ -8,10 +8,11 @@ import * as jwsPayload from './jws-payload';
import * as keys from './keys';
import pako from 'pako';
import got from 'got';
import jose, { JWK } from 'node-jose';
import jose from 'node-jose';
import path from 'path';
import Log from './logger';
import { ValidationResult } from './validate';
import { verifyHealthCardIssuerKey } from './shcKeyValidator';
//const MAX_JWS_LENGTH = 1195;
@ -103,18 +104,15 @@ export async function validate(jws: JWS): Promise<ValidationResult> {
}
async function downloadKey(keyPath: string, log: Log): Promise<JWK.Key[] | undefined> {
async function downloadKey(keyPath: string, log: Log): Promise<keys.KeySet | undefined> {
log.info("Retrieving issuer key from " + keyPath);
return await got(keyPath).json<{ keys: unknown[] }>()
return await got(keyPath).json<keys.KeySet>()
// TODO: split up download/parsing to provide finer-grainded error message
.then(async keysObj => {
log.debug("Downloaded issuer key : " + JSON.stringify(keysObj, null, 2));
return [
await keys.store.add(JSON.stringify(keysObj.keys[0]), 'json'),
await keys.store.add(JSON.stringify(keysObj.keys[1]), 'json')
];
.then(async keySet => {
log.debug("Downloaded issuer key(s) : ");
return (await verifyHealthCardIssuerKey(keySet, log)).result as (keys.KeySet | undefined);
})
.catch(() => {
log.error("Can't parse downloaded issuer keys as a key set",

Просмотреть файл

@ -4,6 +4,10 @@
import { JWK } from "node-jose";
import * as utils from './utils';
export type KeySet = {
keys : JWK.Key[]
}
export let store = JWK.createKeyStore();
export async function initKeyStoreFromFile(filePath: string): Promise<JWK.KeyStore> {
@ -15,3 +19,27 @@ export async function initKeyStoreFromFile(filePath: string): Promise<JWK.KeySto
return store;
}
// export function importKeySet(keySet : keySet, log : Log) : void {
// // failures will be recorded in the log. we can continue processing.
// validateSchema(keySetSchema, keySet, log);
// for (let i = 0; i < keySet.keys.length; i++) {
// let key: JWK.Key = keySet.keys[i];
// log.info('Validating key : ' + key.kid || i.toString());
// try {
// key = await keyStore.add(key);
// } catch (error) {
// log.error('Error adding key to keyStore : ' + (error as Error).message, ErrorCode.INVALID_UNKNOWN);
// return new ValidationResult(undefined, log);
// }
// }

Просмотреть файл

@ -7,6 +7,8 @@ import metaSchema from 'ajv/lib/refs/json-schema-draft-06.json';
import fhirSchema from '../schema/fhir-definitions-schema.json';
import Ajv, { AnySchemaObject } from "ajv";
import { AnyValidateFunction } from 'ajv/dist/core';
import { JWK } from 'node-jose';
import { KeySet } from './keys';
// http://json-schema.org/
// https://github.com/ajv-validator/ajv
@ -15,7 +17,7 @@ import { AnyValidateFunction } from 'ajv/dist/core';
const schemaCache: Record<string, AnyValidateFunction> = {};
export function validateSchema(schema: AnySchemaObject | AnySchemaObject[], data: FhirBundle | JWS | JWSPayload | HealthCard, log: Log): boolean {
export function validateSchema(schema: AnySchemaObject | AnySchemaObject[], data: FhirBundle | JWS | JWSPayload | HealthCard | KeySet, log: Log): boolean {
// by default, the validator will stop at the first failure. 'allErrors' allows it to keep going.
const schemaId = (schema as { [key: string]: string })["$id"];

Просмотреть файл

@ -9,13 +9,9 @@ import * as validator from './validate';
import { LogLevels } from './logger';
import { getFileData } from './file';
import { ErrorCode } from './error';
import * as keys from './keys';
import * as utils from './utils'
import npmpackage from '../package.json';
// function collect(value: string, previous: string[]) {
// return previous.concat([value]);
// }
import { KeySet } from './keys';
/**
* Defines the program
@ -23,16 +19,18 @@ import npmpackage from '../package.json';
* -h/--help auto-generated
*/
const loglevelChoices = ['debug', 'info', 'warning', 'error', 'fatal'];
const artifactTypes = ['fhirbundle', 'jwspayload', 'jws', 'healthcard', 'qrnumeric', 'qr', 'jwkset'];
const program = new Command();
program.version(npmpackage.version, '-v, --version', 'display specification and tool version');
program.requiredOption('-p, --path <path>', 'path of the file(s) to validate. Can be repeated for the qr and qrnumeric types, to provide multiple file chunks',
(p: string, paths: string[]) => paths.concat([p]), []);
program.addOption(new Option('-t, --type <type>', 'type of file to validate').choices(['fhirbundle', 'jwspayload', 'jws', 'healthcard', 'qrnumeric', 'qr', 'jwkset'])); // TODO: populate this from the validate enum.
program.addOption(new Option('-t, --type <type>', 'type of file to validate').choices(artifactTypes)); // TODO: populate this from the validate enum.
program.addOption(new Option('-l, --loglevel <loglevel>', 'set the minimum log level').choices(loglevelChoices).default('warning'));
program.option('-o, --logout <path>', 'output path for log (if not specified log will be printed on console)');
program.option('-k, --jwkset <key>', 'path to trusted issuer key set');
program.parse(process.argv);
export interface CliOptions {
path: string[];
type: validator.ValidationType;
@ -42,83 +40,122 @@ export interface CliOptions {
}
//const log = new Log('main');
function exit(message: string, exitCode: ErrorCode = 0): void {
process.exitCode = exitCode;
console.log(message);
}
/**
* Processes the program options and launches validation
*/
async function processOptions() {
const options = program.opts() as CliOptions;
let logFilePathIsValid = false;
async function processOptions(options: CliOptions) {
// verify that the directory of the logfile exists
// map the --loglevel option to the Log.LogLevel enum
const level = loglevelChoices.indexOf(options.loglevel) as LogLevels;
// verify that the directory of the logfile exists, if provided
if (options.logout) {
const logDir = path.dirname(path.resolve(options.logout));
if (!fs.existsSync(logDir)) {
console.log('Cannot create log file at: ' + logDir);
process.exitCode = ErrorCode.LOG_PATH_NOT_FOUND;
return;
return exit('Log file directory does not exist : ' + logDir, ErrorCode.LOG_PATH_NOT_FOUND);
}
logFilePathIsValid = true;
}
if (options.path.length > 0 && options.type) {
if (options.path.length > 1 && !(options.type === 'qr' || options.type === 'qrnumeric')) {
console.log("Only the qr and qrnumeric types can have multiple path options")
return; // TODO: add unit test
}
// read the file to validate
const fileData = [];
for (const path of options.path) {
try {
fileData.push(await getFileData(path));
} catch (error) {
console.log((error as Error).message);
process.exitCode = ErrorCode.DATA_FILE_NOT_FOUND;
return;
}
}
// if we have a key option, parse is and add it to the global key store
if (options.jwkset) {
// creates a new keyStore from a JSON key set file
// const keyStore: JWK.KeyStore = await createKeyStoreFromFile(options.key);
await keys.initKeyStoreFromFile(options.jwkset);
//log.debug('keyStore');
}
if (options.type === 'jwkset') {
// validate a key file
await validator.validateKey(fileData[0].buffer);
} else {
// validate a health card
const output = await validator.validateCard(fileData, options.type);
process.exitCode = output.log.exitCode;
const level = loglevelChoices.indexOf(options.loglevel) as LogLevels;
// append to the specified logfile
if (logFilePathIsValid) {
output.log.toFile(options.logout, options, true);
} else {
console.log(output.log.toString(level));
}
}
console.log("Validation completed ");
} else {
console.log("Invalid option, missing 'path' or 'type'");
// requires both --path and --type properties
if (options.path.length === 0 || !options.type) {
console.log("Invalid option, missing '--path' or '--type'");
console.log(options);
program.help();
return;
}
// only 'qr' and 'qrnumeric' --type supports multiple --path arguments
if (options.path.length > 1 && !(options.type === 'qr') && !(options.type === 'qrnumeric')) {
return exit("Only the 'qr' and 'qrnumeric' types can have multiple --path options");
}
// read the data file(s) to validate
const fileData = [];
for (let i = 0; i < options.path.length; i++) {
const path = options.path[i];
try {
fileData.push(await getFileData(path));
} catch (error) {
return exit((error as Error).message, ErrorCode.DATA_FILE_NOT_FOUND);
}
}
// cannot provide a key file to both --path and --jwkset
if (options.jwkset && options.type === 'jwkset') {
return exit("Cannot pass a key file to both --path and --jwkset");
}
// if we have a key option, validate is and add it to the global key store
if (options.jwkset) {
let keys;
try {
keys = utils.loadJSONFromFile<KeySet>(options.jwkset);
} catch (error) {
return exit((error as Error).message, ErrorCode.DATA_FILE_NOT_FOUND);
}
// validate the key/keyset
const output = await validator.validateKey(keys);
process.exitCode = output.log.exitCode;
// if a logfile is specified, append to the specified logfile
options.logout ?
output.log.toFile(options.logout, options, true) :
console.log(output.log.toString(level));
}
// validate the specified key-set
if (options.type === 'jwkset') {
const keys = JSON.parse(fileData[0].buffer.toString('utf-8')) as KeySet;
// validate the key/keyset
const output = await validator.validateKey(keys);
process.exitCode = output.log.exitCode;
// if a logfile is specified, append to the specified logfile
options.logout ?
output.log.toFile(options.logout, options, true) :
console.log(output.log.toString(level));
}
// validate the specified artifact ('fhirbundle', 'jwspayload', 'jws', 'healthcard', 'qrnumeric', 'qr')
if (options.type !== 'jwkset') {
// validate a health card
const output = await validator.validateCard(fileData, options.type);
process.exitCode = output.log.exitCode;
// if a logfile is specified, append to the specified logfile
options.logout ?
output.log.toFile(options.logout, options, true) :
console.log(output.log.toString(level));
}
console.log("Validation completed");
}
// start the program
// eslint-disable-next-line @typescript-eslint/no-floating-promises
(async () => {
await processOptions();
// es5 compat requires await not be at the top level
void (async () => {
await processOptions(program.opts() as CliOptions);
})();

Просмотреть файл

@ -4,62 +4,89 @@
import Log from './logger';
import jose, { JWK } from 'node-jose';
import { ErrorCode } from './error';
import { ValidationResult } from './validate';
import { validateSchema } from './schema';
import keySetSchema from '../schema/keyset-schema.json';
import { KeySet, store } from './keys';
export class shcKeyValidator {
export async function verifyHealthCardIssuerKey(keySet: KeySet, log = new Log('Validate Key-Set')): Promise<ValidationResult> {
async verifyHealthCardIssuerKey(jwk: string | Buffer): Promise<Log> {
const log: Log = new Log('IssuerKey');
const keyStore = JWK.createKeyStore();
// check that keySet is valid
if (!(keySet instanceof Object) || !keySet.keys || !(keySet.keys instanceof Array)) {
log.fatal("keySet not valid. Expect {keys : JWK.Key[]}", ErrorCode.INVALID_SCHEMA);
return new ValidationResult(undefined, log);
}
return keyStore.add(jwk)
// failures will be recorded in the log. we can continue processing.
validateSchema(keySetSchema, keySet, log);
.then(async key => {
// check that key type is 'EC'
if (!key.kty) {
log.error("'kty' missing in issuer key", ErrorCode.INVALID_MISSING_KTY);
} else if (key.kty !== 'EC') {
log.error("wrong key type in issuer key. expected: 'EC', actual: " + key.kty, ErrorCode.INVALID_WRONG_KTY);
}
// check that EC curve is 'ES256'
if (!key.alg) {
log.error("'alg' missing in issuer key", ErrorCode.INVALID_MISSING_ALG);
} else if (key.alg !== 'ES256') {
log.error("wrong algorithm in issuer key. expected: 'ES256', actual: " + key.alg, ErrorCode.INVALID_WRONG_ALG);
}
for (let i = 0; i < keySet.keys.length; i++) {
// check that usage is 'sig'
if (!key.use) {
log.error("'use' missing in issuer key", ErrorCode.INVALID_MISSING_USE);
} else if (key.use !== 'sig') {
log.error("wrong usage in issuer key. expected: 'sig', actual: " + key.use, ErrorCode.INVALID_WRONG_USE);
}
let key: JWK.Key = keySet.keys[i];
// check that kid is properly generated
if (!key.kid) {
log.error("'kid' missing in issuer key", ErrorCode.INVALID_MISSING_KID);
} else {
await key.thumbprint('SHA-256')
.then(tpDigest => {
const thumbprint = jose.util.base64url.encode(tpDigest);
if (key.kid !== thumbprint) {
log.error("'kid' does not match thumbprint in issuer key. expected: "
+ thumbprint + ", actual: " + key.kid, ErrorCode.INVALID_WRONG_KID);
}
})
.catch(err => {
log.error("Failed to calculate issuer key thumbprint : " + (err as Error).message, ErrorCode.INVALID_UNKNOWN);
});
}
const keyName = 'key[' + (key.kid || i.toString()) + ']';
return log;
})
.catch(err => {
log.error("Failed to parse issuer key : " + (err as Error).message, ErrorCode.INVALID_SCHEMA);
return log;
});
log.info('Validating key : ' + keyName);
log.debug("Key " + i.toString() + ":");
log.debug(JSON.stringify(key, null, 3));
try {
key = await store.add(key);
} catch (error) {
log.error('Error adding key to keyStore : ' + (error as Error).message, ErrorCode.INVALID_UNKNOWN);
return new ValidationResult(undefined, log);
}
// check that kid is properly generated
if (!key.kid) {
log.error(keyName + ': ' + "'kid' missing in issuer key", ErrorCode.INVALID_MISSING_KID);
} else {
await key.thumbprint('SHA-256')
.then(tpDigest => {
const thumbprint = jose.util.base64url.encode(tpDigest);
if (key.kid !== thumbprint) {
log.error(keyName + ': ' + "'kid' does not match thumbprint in issuer key. expected: "
+ thumbprint + ", actual: " + key.kid, ErrorCode.INVALID_WRONG_KID);
}
})
.catch(err => {
log.error(keyName + ': ' + "Failed to calculate issuer key thumbprint : " + (err as Error).message, ErrorCode.INVALID_UNKNOWN);
});
}
// check that key type is 'EC'
if (!key.kty) {
log.error(keyName + ': ' + "'kty' missing in issuer key", ErrorCode.INVALID_MISSING_KTY);
} else if (key.kty !== 'EC') {
log.error(keyName + ': ' + "wrong key type in issuer key. expected: 'EC', actual: " + key.kty, ErrorCode.INVALID_WRONG_KTY);
}
// check that EC curve is 'ES256'
if (!key.alg) {
log.error(keyName + ': ' + "'alg' missing in issuer key", ErrorCode.INVALID_MISSING_ALG);
} else if (key.alg !== 'ES256') {
log.warn(keyName + ': ' + "wrong algorithm in issuer key. expected: 'ES256', actual: " + key.alg, ErrorCode.INVALID_WRONG_ALG);
}
// check that usage is 'sig'
if (!key.use) {
log.error(keyName + ': ' + "'use' missing in issuer key", ErrorCode.INVALID_MISSING_USE);
} else if (key.use !== 'sig') {
log.warn(keyName + ': ' + "wrong usage in issuer key. expected: 'sig', actual: " + key.use, ErrorCode.INVALID_WRONG_USE);
}
// check for private key material
if ((key as (JWK.Key & { d: string })).d) {
log.error(keyName + ': ' + "key contains private key material.", ErrorCode.INVALID_PRIVATE);
}
}
return new ValidationResult(keySet, log);
}

Просмотреть файл

@ -2,7 +2,7 @@
// Licensed under the MIT license.
import Log from './logger';
import { shcKeyValidator } from './shcKeyValidator';
import { verifyHealthCardIssuerKey } from './shcKeyValidator';
import { FileInfo } from './file';
import { ErrorCode } from './error';
import * as healthCard from './healthCard';
@ -11,40 +11,29 @@ import * as jwsPayload from './jws-payload';
import * as fhirBundle from './fhirBundle';
import * as qr from './qr';
import * as image from './image';
/** Validate the issuer key */
export async function validateKey(key: Buffer): Promise<void> {
const log = new Log('Validate Key');
log.debug('Validating key : ' + key.toString('utf-8'));
const keyValidator = new shcKeyValidator();
return keyValidator
.verifyHealthCardIssuerKey(key)
.then(() => { return Promise.resolve(); })
.catch(err => {
log.error("Error validating issuer key : " + (err as Error).message);
return Promise.reject();
});
}
//import { JWK } from 'node-jose';
import { KeySet } from './keys';
export type ValidationType = "qr" | "qrnumeric" | "healthcard" | "jws" | "jwspayload" | "fhirbundle" | "jwkset";
export class ValidationResult {
constructor(
public result: HealthCard | JWS | JWSPayload | FhirBundle | undefined,
public result: HealthCard | JWS | JWSPayload | FhirBundle | KeySet | undefined,
public log: Log
) { }
}
/** Validate the issuer key */
export async function validateKey(keySet: KeySet): Promise<ValidationResult> {
return await verifyHealthCardIssuerKey(keySet);
}
/** Validates SMART Health Card */
export async function validateCard(fileData: FileInfo[], type: ValidationType): Promise<ValidationResult> {
@ -57,7 +46,7 @@ export async function validateCard(fileData: FileInfo[], type: ValidationType):
break;
case "qrnumeric":
result = await qr.validate(fileData.map((fi)=>fi.buffer.toString('utf-8')));
result = await qr.validate(fileData.map((fi) => fi.buffer.toString('utf-8')));
break;
case "healthcard":

Просмотреть файл

@ -9,6 +9,7 @@ import { getFileData } from '../src/file';
import { ErrorCode } from '../src/error';
import { LogLevels } from '../src/logger';
const testdataDir = './testdata/';
@ -119,3 +120,13 @@ test("Cards: invalid signature", async () => {
expect(results).toHaveLength(1);
expect(results[0].code).toBe(ErrorCode.JWS_VERIFICATION_ERROR);
});
// test("Cards: rubbish QR code", async () => {
// const shc = fs.readFileSync(path.join(testdataDir, 'example-00-f-qr-code-numeric-value-0.txt'));
// const qrFile = 'foo.png';
// await dataToQRImage(path.join(testdataDir, qrFile), [{ 'data': shc, 'mode': 'numeric'}]);
// const result = await testCard(qrFile, "qr");
// expect(result).toHaveLength(1);
// });

Просмотреть файл

@ -87,7 +87,7 @@ function testCliCommand(command: string): number {
}
// Valid calls to examples
test("Cards: valid 00 health card", () => expect(testCliCommand('node . --path testdata/example-00-e-file.smart-health-card --type healthcard --loglevel info')).toBe(0));
test("Cards: valid 00 health card", () => expect(testCliCommand('node . --path testdata/example-00-e-file.smart-health-card --type healthcard --loglevel info --jwkset testdata/issuer.jwks.public.json')).toBe(0));
test("Cards: valid 00 jws", () => expect(testCliCommand('node . --path testdata/example-00-d-jws.txt --type jws --loglevel info')).toBe(0));
test("Cards: valid 00 jws-payload", () => expect(testCliCommand('node . --path testdata/example-00-c-jws-payload-minified.json --type jwspayload --loglevel info')).toBe(0));
test("Cards: valid 00 fhirBundle", () => expect(testCliCommand('node . --path testdata/example-00-a-fhirBundle.json --type fhirbundle --loglevel info')).toBe(0));

Просмотреть файл

@ -1,17 +1,18 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import fs from 'fs';
import path from 'path';
import { ErrorCode } from '../src/error';
import { shcKeyValidator } from '../src/shcKeyValidator';
import { LogLevels } from '../src/logger';
import { verifyHealthCardIssuerKey } from '../src/shcKeyValidator';
import * as utils from '../src/utils';
const testdataDir = './testdata/';
async function testKey(fileName: string): Promise<ErrorCode[]> {
const filePath = path.join(testdataDir, fileName);
const keyValidator = new shcKeyValidator();
const log = (await keyValidator.verifyHealthCardIssuerKey(fs.readFileSync(filePath))).log;
return log.map(item => item.code);
const result = (await verifyHealthCardIssuerKey({keys : [utils.loadJSONFromFile(filePath)]}));
return result.log.flatten(LogLevels.WARNING).map(item => item.code);
}
test("Keys: valid", async () => {