├── shl-server ├── shl │ └── .gitkeep ├── .gitignore ├── src │ ├── config.ts │ ├── types.d.ts │ ├── store.ts │ └── encode.ts ├── package.json ├── public │ ├── main.css │ ├── main.js │ └── index.html └── README.md ├── .dockerignore ├── testdata ├── badjwks │ └── .well-known │ │ └── jwks.json ├── shlink-qr.png ├── test-invalid-jws-payload.png ├── test-example-00-f-qr-code-numeric-value-16-qr_chunk_too_small.txt ├── shlink-manifest-file.txt ├── test-example-00-g-qr-code-0-corrupted.png ├── test-example-00-g-qr-code-0-scaled-23.jpg ├── test-example-00-g-qr-code-0-scaled-85.jpg ├── test-example-00-f-qr-code-numeric-value-0-qr_chunk_too_small.txt ├── test-example-00-f-qr-code-numeric-value-1-qr_chunk_too_small.txt ├── test-example-00-f-qr-code-numeric-value-10-qr_chunk_too_small.txt ├── test-example-00-f-qr-code-numeric-value-11-qr_chunk_too_small.txt ├── test-example-00-f-qr-code-numeric-value-12-qr_chunk_too_small.txt ├── test-example-00-f-qr-code-numeric-value-13-qr_chunk_too_small.txt ├── test-example-00-f-qr-code-numeric-value-14-qr_chunk_too_small.txt ├── test-example-00-f-qr-code-numeric-value-15-qr_chunk_too_small.txt ├── test-example-00-f-qr-code-numeric-value-2-qr_chunk_too_small.txt ├── test-example-00-f-qr-code-numeric-value-3-qr_chunk_too_small.txt ├── test-example-00-f-qr-code-numeric-value-4-qr_chunk_too_small.txt ├── test-example-00-f-qr-code-numeric-value-5-qr_chunk_too_small.txt ├── test-example-00-f-qr-code-numeric-value-6-qr_chunk_too_small.txt ├── test-example-00-f-qr-code-numeric-value-7-qr_chunk_too_small.txt ├── test-example-00-f-qr-code-numeric-value-8-qr_chunk_too_small.txt ├── test-example-00-f-qr-code-numeric-value-9-qr_chunk_too_small.txt ├── test-example-00-g-qr-code-0-bad_qr_version.png ├── test-example-00-g-qr-code-inflated-to-v22.png ├── test-example-00-g-qr-code-0-single_qr_segment.png ├── test-example-00-g-qr-code-0-qr_chunk_too_small.png ├── test-example-00-g-qr-code-0-too_many_qr_segment.png ├── test-example-00-g-qr-code-1-qr_chunk_too_small.png ├── shlink-payload.txt ├── shlink-payload-direct.txt ├── test-issuers.json ├── wrong_kid_key.json ├── valid_key.json ├── wrong_alg_key.json ├── wrong_use_key.json ├── badcrl │ ├── public.json │ ├── .well-known │ │ └── jwks.json │ ├── duplicate-rid │ │ ├── .well-known │ │ │ ├── jwks.json │ │ │ └── crl │ │ │ │ └── hrvC0uzB16UXK83BnPo7Glg685b1D6iYOyJd5hpRaAA.json │ │ └── jws-crl-duplicate-rid.txt │ ├── invalid-method │ │ ├── .well-known │ │ │ ├── jwks.json │ │ │ └── crl │ │ │ │ └── hrvC0uzB16UXK83BnPo7Glg685b1D6iYOyJd5hpRaAA.json │ │ └── jws-crl-invalid-method.txt │ ├── private.json │ ├── too-long-rid │ │ ├── .well-known │ │ │ ├── jwks.json │ │ │ └── crl │ │ │ │ └── hrvC0uzB16UXK83BnPo7Glg685b1D6iYOyJd5hpRaAA.json │ │ └── jws-crl-too-long-rid.txt │ ├── non-base64url-rid │ │ ├── .well-known │ │ │ ├── jwks.json │ │ │ └── crl │ │ │ │ └── hrvC0uzB16UXK83BnPo7Glg685b1D6iYOyJd5hpRaAA.json │ │ └── jws-crl-non-base64url-rid.txt │ ├── version-mismatch │ │ ├── .well-known │ │ │ ├── jwks.json │ │ │ └── crl │ │ │ │ └── hrvC0uzB16UXK83BnPo7Glg685b1D6iYOyJd5hpRaAA.json │ │ └── jws-crl-version-mismatch.txt │ └── info.md ├── shlink-link.txt ├── wrong_curve_key.json ├── private_key.json ├── shlink-link-direct.txt ├── shlink-manifest.txt ├── README.md ├── shlink-link-with-viewer.txt ├── issuer.jwks.public.not.smart.json ├── wrong_kty_key.json ├── valid_keys.json ├── test-example-00-d-jws-no_jws_header_kid.txt ├── test-example-00-d-jws-no_jws_header_alg.txt ├── test-example-00-d-jws-no_jws_header_zip.txt ├── test-example-00-d-jws-der-signature.txt ├── test-example-00-d-jws-invalid-signature.txt ├── test-example-00-d-jws-issuer-kid-mismatch.txt ├── test-example-00-d-jws-wrong_jws_header_kid.txt ├── test-example-00-d-jws-der-signature-r-neg.txt ├── test-example-00-d-jws-der-signature-rs-neg.txt ├── test-example-00-d-jws-der-signature-s-neg.txt ├── test-example-00-d-jws-issuer-not-valid-with-smart-key.txt ├── test-example-00-d-jws-trailing_chars.txt ├── test-example-00-d-jws-utf8_bom_prefix.txt ├── test-example-00-e-file.wrong-extension ├── test-example-00-e-file-invalid_deflate.smart-health-card ├── test-example-00-e-file-invalid_issuer_url_http.smart-health-card ├── test-example-00-e-file-invalid_issuer_url.smart-health-card ├── test-example-00-e-file-issuer_url_with_trailing_slash.smart-health-card ├── test-example-00-e-file-trailing_chars.smart-health-card ├── test-example-00-d-jws-bad-fhir-metadata.txt ├── test-example-00-e-file-bad_jwks.smart-health-card ├── test-example-00-fhirhealthcard.json ├── test-example-00-d-jws-jws_too_long.txt ├── test-example-00-e-file-jws_too_long.smart-health-card ├── test-example-00-f-qr-code-numeric-value-0-odd-count.txt ├── test-example-00-f-qr-code-numeric-value-0-wrong_qr_header.txt ├── test-example-00-f-qr-code-numeric-value-0-wrong-multi-chunk.txt ├── test-example-00-f-qr-code-numeric-value-0-trailing_chars.txt ├── test-example-00-e-file-multi-jws-issues.smart-health-card ├── test-example-00-e-file-multi-jws.smart-health-card ├── test-example-00-d-jws-no_deflate.txt ├── test-example-00-f-qr-code-numeric-value-0-number-too-big.txt ├── test-example-00-e-file-no_deflate.smart-health-card ├── test-example-00-f-qr-code-numeric-value-0-index-out-of-range.txt ├── test-example-00-a-fhirBundle-empty-values.json ├── test-example-00-fhirhealthcard-multi-jws.json ├── invalid_no_SAN.public.json ├── invalid_DNS_SAN.public.json ├── valid_2_chain.public.json ├── test-example-00-a-fhirBundle-occurrence-issues.json ├── test-example-00-a-fhirBundle-status-not-completed.json ├── test-example-00-a-fhirBundle-trailing_chars.json ├── test-example-00-a-short-refs.json ├── test-example-00-fhirhealthcard-with-resource-link.json ├── test-example-00-a-fhirBundle-bad_meta_non_security.json ├── test-example-00-a-fhirBundle-bad_meta_wrong_security.json ├── test-example-00-a-fhirBundle-bad_meta_extra_key.json ├── test-example-00-a-non-dm-properties.json ├── test-example-1195-byte-qrnumeric.txt ├── test-example-covid-lab-jwspayload-missing-lab-vc-type.json ├── test-example-00-a-fhirBundle-profile-usa.json ├── invalid_chain.public.json ├── cert_mismatch.public.json ├── valid_key_with_x5c.json ├── test-example-00-b-jws-payload-expanded-missing-imm-vc-type.json ├── test-example-00-b-jws-payload-expanded-missing-shc-vc-type.json ├── test-example-00-b-jws-payload-expanded-missing-covid-vc-type.json ├── test-example-00-b-jws-payload-expanded-nbf_milliseconds.json ├── test-example-00-b-jws-payload-expanded-nbf_not_yet_valid.json ├── test-example-00-b-jws-payload-expanded-missing-coding.json ├── test-example-00-b-jws-payload-expanded-trailing_chars.json ├── test-example-00-b-jws-payload-expanded-optional-vc-type.json ├── test-example-00-b-jws-payload-expanded-unknown-vc-types.json ├── test-example-00-b-jws-payload-expanded-expired.json ├── test-example-00-b-jws-payload-expanded-pre-expired.json ├── test-example-00-b-jws-payload-expanded-exp_milliseconds.json ├── test-example-02-f-qr-code-numeric-value-0-qr_chunk_too_big.txt └── test-example-02-f-qr-code-numeric-value-1-qr_chunk_too_big.txt ├── schema ├── jws-schema.json ├── smart-health-card-schema.json ├── keyset-schema.json ├── patient-dm.json └── smart-health-card-vc-schema.json ├── .npmignore ├── Dockerfile ├── jest.config.js ├── SECURITY.md ├── fhir.validator.Dockerfile ├── SUPPORT.md ├── .github └── workflows │ ├── main.yml │ └── node.js.yml ├── .eslintrc.json ├── .vscode └── settings.json ├── src ├── check-for-update.ts ├── shlManifest.ts ├── keys.ts ├── fhirHealthCard.ts ├── fetch-profiles.ts ├── healthCard.ts ├── shlink.ts ├── shlEncode.ts ├── command.ts ├── jwe-compact.ts ├── generate-test-data.ts ├── options.ts └── file.ts ├── LICENSE ├── package.json ├── .gitignore └── tests └── schema.test.ts /shl-server/shl/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /shl-server/.gitignore: -------------------------------------------------------------------------------- 1 | shl/ 2 | src/*.js 3 | src/*.map -------------------------------------------------------------------------------- /testdata/badjwks/.well-known/jwks.json: -------------------------------------------------------------------------------- 1 | This is not a valid JSON Web Key Set! 2 | -------------------------------------------------------------------------------- /testdata/shlink-qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/health-cards-dev-tools/HEAD/testdata/shlink-qr.png -------------------------------------------------------------------------------- /testdata/test-invalid-jws-payload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/health-cards-dev-tools/HEAD/testdata/test-invalid-jws-payload.png -------------------------------------------------------------------------------- /testdata/test-example-00-f-qr-code-numeric-value-16-qr_chunk_too_small.txt: -------------------------------------------------------------------------------- 1 | shc:/17/17/50523942452677533810535343690705207161576722213577401275766409666758 -------------------------------------------------------------------------------- /testdata/shlink-manifest-file.txt: -------------------------------------------------------------------------------- 1 | {"contentType":"application/smart-health-card","location":"http://localhost:8090/shl/TqNMSQQ-QoiRJoVBJOFtn9UKARE4ceJbxHb7mdWYar4"} -------------------------------------------------------------------------------- /testdata/test-example-00-g-qr-code-0-corrupted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/health-cards-dev-tools/HEAD/testdata/test-example-00-g-qr-code-0-corrupted.png -------------------------------------------------------------------------------- /testdata/test-example-00-g-qr-code-0-scaled-23.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/health-cards-dev-tools/HEAD/testdata/test-example-00-g-qr-code-0-scaled-23.jpg -------------------------------------------------------------------------------- /testdata/test-example-00-g-qr-code-0-scaled-85.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/health-cards-dev-tools/HEAD/testdata/test-example-00-g-qr-code-0-scaled-85.jpg -------------------------------------------------------------------------------- /testdata/test-example-00-f-qr-code-numeric-value-0-qr_chunk_too_small.txt: -------------------------------------------------------------------------------- 1 | shc:/1/17/56762909524320603460292437404460312229595326546034602925407728043360287028647167452228092861 -------------------------------------------------------------------------------- /testdata/test-example-00-f-qr-code-numeric-value-1-qr_chunk_too_small.txt: -------------------------------------------------------------------------------- 1 | shc:/2/17/33314564376531415906402203064504590856435503414245413640370636654171372412363803043756220467 -------------------------------------------------------------------------------- /testdata/test-example-00-f-qr-code-numeric-value-10-qr_chunk_too_small.txt: -------------------------------------------------------------------------------- 1 | shc:/11/17/57573366640742533606774323776767656545775255503074083775213776433110746027752162082138425375 -------------------------------------------------------------------------------- /testdata/test-example-00-f-qr-code-numeric-value-11-qr_chunk_too_small.txt: -------------------------------------------------------------------------------- 1 | shc:/12/17/03573845280476721077115323284036036750552333773566455665117372296468092309386210062129210829 -------------------------------------------------------------------------------- /testdata/test-example-00-f-qr-code-numeric-value-12-qr_chunk_too_small.txt: -------------------------------------------------------------------------------- 1 | shc:/13/17/10390424725333433221076860235305522527761210005367212439706962117530277341436460265977072967 -------------------------------------------------------------------------------- /testdata/test-example-00-f-qr-code-numeric-value-13-qr_chunk_too_small.txt: -------------------------------------------------------------------------------- 1 | shc:/14/17/61404140100874051237504460452738566253033066755208777343630539415212243057065358746860580325 -------------------------------------------------------------------------------- /testdata/test-example-00-f-qr-code-numeric-value-14-qr_chunk_too_small.txt: -------------------------------------------------------------------------------- 1 | shc:/15/17/40406352595650293135636941236569066405745745333371607243697321293850321235032601210325106311 -------------------------------------------------------------------------------- /testdata/test-example-00-f-qr-code-numeric-value-15-qr_chunk_too_small.txt: -------------------------------------------------------------------------------- 1 | shc:/16/17/52210424452631412745323259212304391171732475264524382864546159063639416875616865342671676169 -------------------------------------------------------------------------------- /testdata/test-example-00-f-qr-code-numeric-value-2-qr_chunk_too_small.txt: -------------------------------------------------------------------------------- 1 | shc:/3/17/37407532323925433443326057360106452929531270742428435038612212767168360469257255362862105224 -------------------------------------------------------------------------------- /testdata/test-example-00-f-qr-code-numeric-value-3-qr_chunk_too_small.txt: -------------------------------------------------------------------------------- 1 | shc:/4/17/66033138437420565226637070722059545931602150657165522055556234384040063653115727615607121120 -------------------------------------------------------------------------------- /testdata/test-example-00-f-qr-code-numeric-value-4-qr_chunk_too_small.txt: -------------------------------------------------------------------------------- 1 | shc:/5/17/21402255232324342852726870302870587526002360580327246667572112683521566425256168246055032432 -------------------------------------------------------------------------------- /testdata/test-example-00-f-qr-code-numeric-value-5-qr_chunk_too_small.txt: -------------------------------------------------------------------------------- 1 | shc:/6/17/21556972236973653671595733096505103134730966662129367354201139206055275550643550610659052721 -------------------------------------------------------------------------------- /testdata/test-example-00-f-qr-code-numeric-value-6-qr_chunk_too_small.txt: -------------------------------------------------------------------------------- 1 | shc:/7/17/36002456630965612463425037293734577260403167291254033033584128617706526828390063694450404052 -------------------------------------------------------------------------------- /testdata/test-example-00-f-qr-code-numeric-value-7-qr_chunk_too_small.txt: -------------------------------------------------------------------------------- 1 | shc:/8/17/05712172405007240032305623111028722600317506064276734053425620747256446406053909542566673927 -------------------------------------------------------------------------------- /testdata/test-example-00-f-qr-code-numeric-value-8-qr_chunk_too_small.txt: -------------------------------------------------------------------------------- 1 | shc:/9/17/29213452092855655520215062204560457603507207042237100655774029276654657425116723720665236643 -------------------------------------------------------------------------------- /testdata/test-example-00-f-qr-code-numeric-value-9-qr_chunk_too_small.txt: -------------------------------------------------------------------------------- 1 | shc:/10/17/21280336446744622727760467573259651033422463657311102052560476417045586807413739003862376440 -------------------------------------------------------------------------------- /testdata/test-example-00-g-qr-code-0-bad_qr_version.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/health-cards-dev-tools/HEAD/testdata/test-example-00-g-qr-code-0-bad_qr_version.png -------------------------------------------------------------------------------- /testdata/test-example-00-g-qr-code-inflated-to-v22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/health-cards-dev-tools/HEAD/testdata/test-example-00-g-qr-code-inflated-to-v22.png -------------------------------------------------------------------------------- /testdata/test-example-00-g-qr-code-0-single_qr_segment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/health-cards-dev-tools/HEAD/testdata/test-example-00-g-qr-code-0-single_qr_segment.png -------------------------------------------------------------------------------- /testdata/test-example-00-g-qr-code-0-qr_chunk_too_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/health-cards-dev-tools/HEAD/testdata/test-example-00-g-qr-code-0-qr_chunk_too_small.png -------------------------------------------------------------------------------- /testdata/test-example-00-g-qr-code-0-too_many_qr_segment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/health-cards-dev-tools/HEAD/testdata/test-example-00-g-qr-code-0-too_many_qr_segment.png -------------------------------------------------------------------------------- /testdata/test-example-00-g-qr-code-1-qr_chunk_too_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smart-on-fhir/health-cards-dev-tools/HEAD/testdata/test-example-00-g-qr-code-1-qr_chunk_too_small.png -------------------------------------------------------------------------------- /testdata/shlink-payload.txt: -------------------------------------------------------------------------------- 1 | {"url":"http://localhost:8090/ERn18l4lL3_5yPXMSzx3ZejyMM_XIeWeseXa3DXsJD0","flag":"P","key":"v7SjEf2oC4nbbkrhJJ1VAsnp4QaAmrzwIVQtxGM7AIc","label":"Sample Set 1","exp":1704096000} -------------------------------------------------------------------------------- /testdata/shlink-payload-direct.txt: -------------------------------------------------------------------------------- 1 | {"url":"http://localhost:8090/shl/jvb42XYZTjY2BTZ2mcL6nozbTFT8Vs5G2_I-_IuxCZo","flag":"U","key":"lQPSuLGFjSkce1i63hckW3L9u-F94ti1gE_QvqbJc_g","label":"Sample Set 1","exp":1704096000} -------------------------------------------------------------------------------- /testdata/test-issuers.json: -------------------------------------------------------------------------------- 1 | { 2 | "participating_issuers": [ 3 | { 4 | "iss": "https://spec.smarthealth.cards/examples/issuer", 5 | "name": "SMART Health Card specification example issuer" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /testdata/wrong_kid_key.json: -------------------------------------------------------------------------------- 1 | {"keys":[{"kty":"EC","kid":"ThisIsNotTheThumbprintOfTheKey","use":"sig","alg":"ES256","crv":"P-256","x":"kOzbAF6ArH1BLM7MhOIEH4NiEqsrySkkrql56G_iyAA","y":"9f7pZkbEQu0mlpdL3B8JujJBVtjFW4eizxoevh8akbI"}]} -------------------------------------------------------------------------------- /testdata/valid_key.json: -------------------------------------------------------------------------------- 1 | {"keys":[{"kty":"EC","kid":"wXnE30rKv9vfaP-q_A18Mdh45GvxnCyBmvkldEf584g","use":"sig","alg":"ES256","crv":"P-256","x":"VE1AThGRVudJGJYxe3330jTHLcqQgP44WVFymYvaGaw","y":"mnseemWFh1EjliwCetIyO9tleUPDk_oIxS29NIJOQgQ"}]} -------------------------------------------------------------------------------- /testdata/wrong_alg_key.json: -------------------------------------------------------------------------------- 1 | {"keys":[{"kty":"EC","kid":"Nl_oM2xt7AiBtmib0VUVTMxuG_IwP4Bl0Sjg5tWHGqE","use":"sig","alg":"ES256K","crv":"P-256","x":"OvVpwfMToycI8jsY5tSsLqYlcCoNIVelUD3mscDoXnk","y":"poflcGsnhcBktiQXC9HM5uVBJL4picDYfeig6tCm1_Y"}]} -------------------------------------------------------------------------------- /testdata/wrong_use_key.json: -------------------------------------------------------------------------------- 1 | {"keys":[{"kty":"EC","kid":"NDToaP8hnQaoyYmy-8z436x_edSjQhViQ9JbEUsNI_E","use":"enc","alg":"ES256","crv":"P-256","x":"5MGwuvx4Hb-ZuO8l2T_e72n1GM71l9jXyLq6lv-bn6E","y":"YbzeL_DvBr2yVIs3jKUmfxSuznCr1fTEjkeo3Y5OJEI"}]} -------------------------------------------------------------------------------- /testdata/badcrl/public.json: -------------------------------------------------------------------------------- 1 | {"kty":"EC","kid":"hrvC0uzB16UXK83BnPo7Glg685b1D6iYOyJd5hpRaAA","use":"sig","alg":"ES256","crv":"P-256","x":"ACqfiE7QkjLaQmZN8K4rHFSMvVB7y2F50d9QGkS4iyE","y":"UAWLEFZ3ClXftuLF1zZbt5eIIeAWTxvHJ-dCzV3G6FM","crlVersion":1} -------------------------------------------------------------------------------- /testdata/badcrl/.well-known/jwks.json: -------------------------------------------------------------------------------- 1 | {"kty":"EC","kid":"hrvC0uzB16UXK83BnPo7Glg685b1D6iYOyJd5hpRaAA","use":"sig","alg":"ES256","crv":"P-256","x":"ACqfiE7QkjLaQmZN8K4rHFSMvVB7y2F50d9QGkS4iyE","y":"UAWLEFZ3ClXftuLF1zZbt5eIIeAWTxvHJ-dCzV3G6FM","crlVersion":1} -------------------------------------------------------------------------------- /schema/jws-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://smarthealth.cards/schema/jws-schema.json", 4 | "title": "JWS", 5 | "type": "string", 6 | "pattern": "^[a-zA-Z0-9_-]+\\.[a-zA-Z0-9_-]+\\.[a-zA-Z0-9_-]+$" 7 | } -------------------------------------------------------------------------------- /testdata/shlink-link.txt: -------------------------------------------------------------------------------- 1 | shlink:/eyJ1cmwiOiJodHRwOi8vbG9jYWxob3N0OjgwOTAvRVJuMThsNGxMM181eVBYTVN6eDNaZWp5TU1fWEllV2VzZVhhM0RYc0pEMCIsImZsYWciOiJQIiwia2V5IjoidjdTakVmMm9DNG5iYmtyaEpKMVZBc25wNFFhQW1yendJVlF0eEdNN0FJYyIsImxhYmVsIjoiU2FtcGxlIFNldCAxIiwiZXhwIjoxNzA0MDk2MDAwfQ -------------------------------------------------------------------------------- /testdata/badcrl/duplicate-rid/.well-known/jwks.json: -------------------------------------------------------------------------------- 1 | {"keys":[{"kty":"EC","kid":"hrvC0uzB16UXK83BnPo7Glg685b1D6iYOyJd5hpRaAA","use":"sig","alg":"ES256","crv":"P-256","x":"ACqfiE7QkjLaQmZN8K4rHFSMvVB7y2F50d9QGkS4iyE","y":"UAWLEFZ3ClXftuLF1zZbt5eIIeAWTxvHJ-dCzV3G6FM","crlVersion":1}]} -------------------------------------------------------------------------------- /testdata/badcrl/invalid-method/.well-known/jwks.json: -------------------------------------------------------------------------------- 1 | {"keys":[{"kty":"EC","kid":"hrvC0uzB16UXK83BnPo7Glg685b1D6iYOyJd5hpRaAA","use":"sig","alg":"ES256","crv":"P-256","x":"ACqfiE7QkjLaQmZN8K4rHFSMvVB7y2F50d9QGkS4iyE","y":"UAWLEFZ3ClXftuLF1zZbt5eIIeAWTxvHJ-dCzV3G6FM","crlVersion":1}]} -------------------------------------------------------------------------------- /testdata/badcrl/private.json: -------------------------------------------------------------------------------- 1 | {"kty":"EC","kid":"hrvC0uzB16UXK83BnPo7Glg685b1D6iYOyJd5hpRaAA","use":"sig","alg":"ES256","crv":"P-256","x":"ACqfiE7QkjLaQmZN8K4rHFSMvVB7y2F50d9QGkS4iyE","y":"UAWLEFZ3ClXftuLF1zZbt5eIIeAWTxvHJ-dCzV3G6FM","d":"LdwG801miui-5OHGR4iqASHEeHYXj8l-4RP4DxNV2JY"} -------------------------------------------------------------------------------- /testdata/badcrl/too-long-rid/.well-known/jwks.json: -------------------------------------------------------------------------------- 1 | {"keys":[{"kty":"EC","kid":"hrvC0uzB16UXK83BnPo7Glg685b1D6iYOyJd5hpRaAA","use":"sig","alg":"ES256","crv":"P-256","x":"ACqfiE7QkjLaQmZN8K4rHFSMvVB7y2F50d9QGkS4iyE","y":"UAWLEFZ3ClXftuLF1zZbt5eIIeAWTxvHJ-dCzV3G6FM","crlVersion":1}]} -------------------------------------------------------------------------------- /testdata/badcrl/non-base64url-rid/.well-known/jwks.json: -------------------------------------------------------------------------------- 1 | {"keys":[{"kty":"EC","kid":"hrvC0uzB16UXK83BnPo7Glg685b1D6iYOyJd5hpRaAA","use":"sig","alg":"ES256","crv":"P-256","x":"ACqfiE7QkjLaQmZN8K4rHFSMvVB7y2F50d9QGkS4iyE","y":"UAWLEFZ3ClXftuLF1zZbt5eIIeAWTxvHJ-dCzV3G6FM","crlVersion":1}]} -------------------------------------------------------------------------------- /testdata/badcrl/version-mismatch/.well-known/jwks.json: -------------------------------------------------------------------------------- 1 | {"keys":[{"kty":"EC","kid":"hrvC0uzB16UXK83BnPo7Glg685b1D6iYOyJd5hpRaAA","use":"sig","alg":"ES256","crv":"P-256","x":"ACqfiE7QkjLaQmZN8K4rHFSMvVB7y2F50d9QGkS4iyE","y":"UAWLEFZ3ClXftuLF1zZbt5eIIeAWTxvHJ-dCzV3G6FM","crlVersion":1}]} -------------------------------------------------------------------------------- /testdata/wrong_curve_key.json: -------------------------------------------------------------------------------- 1 | {"keys":[{"kty":"EC","kid":"pvmz3nHw7BcVMpU_6bwWaFPQppQwveRjSa3nvn2QjNA","use":"sig","alg":"ES384","crv":"P-384","x":"RmRqlnGi5o9av4VvMc3YkRpJtwyohX_MWF7SuN8zhcACJvBHTbF5spNYlGDbpFge","y":"pfWy8oXm6lU8q9RhYp2mAr6DdU8kUtiDYgJ0Fhdp8CAk7Hy3Kx24gipeelm3fj1c"}]} -------------------------------------------------------------------------------- /testdata/badcrl/info.md: -------------------------------------------------------------------------------- 1 | This folder contains tests to validate CRLs. Each issuer share the same `private.json` key; JWS examples were generated using the demo portal with that key and iss: https://github.com/smart-on-fhir/health-cards-dev-tools/blob/main/testdata/badcrl/. 2 | 3 | -------------------------------------------------------------------------------- /testdata/private_key.json: -------------------------------------------------------------------------------- 1 | {"keys":[{"kty":"EC","kid":"d630duSMWmVfmOtrMKZX6izJfcampjK1h0D4jrXxJwU","use":"sig","alg":"ES256","crv":"P-256","x":"IpNzj8m7NEZdNG4mdEsTmDWFFyKLE7PmtBLWLGIoJuA","y":"mVqRexUnULniMBghiSfb8L3HDZSTxhdKWfIcP6Tvabs","d":"qYg7yrhjPYGJNHc0e9xYNodLaKQvVNG6cShRyhwtwHQ"}]} -------------------------------------------------------------------------------- /testdata/shlink-link-direct.txt: -------------------------------------------------------------------------------- 1 | shlink:/eyJ1cmwiOiJodHRwOi8vbG9jYWxob3N0OjgwOTAvc2hsL2p2YjQyWFlaVGpZMkJUWjJtY0w2bm96YlRGVDhWczVHMl9JLV9JdXhDWm8iLCJmbGFnIjoiVSIsImtleSI6ImxRUFN1TEdGalNrY2UxaTYzaGNrVzNMOXUtRjk0dGkxZ0VfUXZxYkpjX2ciLCJsYWJlbCI6IlNhbXBsZSBTZXQgMSIsImV4cCI6MTcwNDA5NjAwMCwidiI6MX0 -------------------------------------------------------------------------------- /testdata/shlink-manifest.txt: -------------------------------------------------------------------------------- 1 | {"files":[{"contentType":"application/smart-health-card","location":"http://localhost:8090/shl/05pHiEfsMU1kLb9-oTVknO4VUB9oP6Ot4ET46cjJFzU"},{"contentType":"application/smart-health-card","location":"http://localhost:8090/shl/TqNMSQQ-QoiRJoVBJOFtn9UKARE4ceJbxHb7mdWYar4"}]} -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .dockerignore 3 | .eslintrc.json 4 | .github 5 | .vscode 6 | *.tgz 7 | jest.config.js 8 | js/src/*.map 9 | js/src/*.ts 10 | js/tests 11 | /tests 12 | /schema 13 | package.json 14 | src 15 | testdata 16 | tsconfig.json 17 | !js/src/api.d.ts 18 | !js/package.json 19 | *.log -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14 2 | 3 | RUN apt-get update && apt-get install -y \ 4 | libcairo2-dev \ 5 | libjpeg-dev \ 6 | libpango1.0-dev \ 7 | libgif-dev \ 8 | libpng-dev \ 9 | build-essential \ 10 | g++ 11 | 12 | WORKDIR /usr/src/app 13 | COPY package*.json ./ 14 | RUN npm ci 15 | COPY . . 16 | -------------------------------------------------------------------------------- /testdata/README.md: -------------------------------------------------------------------------------- 1 | Contains data for tests: 2 | - examples downloaded from SMART Health Card spec site by `src/fetch-examples.ts` 3 | - generated by `src/generate-crypto-test-data.ts` and [this](https://github.com/microsoft/health-cards/blob/generate-test-files/generate-examples/src/index.ts) script. 4 | -------------------------------------------------------------------------------- /testdata/shlink-link-with-viewer.txt: -------------------------------------------------------------------------------- 1 | https://demo.vaxx.link/viewer#shlink:/eyJ1cmwiOiJodHRwOi8vbG9jYWxob3N0OjgwOTAvRVJuMThsNGxMM181eVBYTVN6eDNaZWp5TU1fWEllV2VzZVhhM0RYc0pEMCIsImZsYWciOiJQIiwia2V5IjoidjdTakVmMm9DNG5iYmtyaEpKMVZBc25wNFFhQW1yendJVlF0eEdNN0FJYyIsImxhYmVsIjoiU2FtcGxlIFNldCAxIiwiZXhwIjoxNzA0MDk2MDAwfQ -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/en/configuration.html 4 | */ 5 | 6 | module.exports = { 7 | coverageProvider: "v8", 8 | testEnvironment: "jsdom", 9 | preset: "ts-jest", 10 | testTimeout: 15000 11 | }; 12 | -------------------------------------------------------------------------------- /testdata/badcrl/version-mismatch/.well-known/crl/hrvC0uzB16UXK83BnPo7Glg685b1D6iYOyJd5hpRaAA.json: -------------------------------------------------------------------------------- 1 | { 2 | "kid": "hrvC0uzB16UXK83BnPo7Glg685b1D6iYOyJd5hpRaAA", 3 | "method": "rid", 4 | "ctr": 2, 5 | "rids": [ 6 | "vwAjHdarZuc.1638311286", 7 | "FKDIxsTCGlU", 8 | "XkNHp2Iyk0Y.1638311286", 9 | "TqB_qu_6OtM" 10 | ] 11 | } -------------------------------------------------------------------------------- /testdata/badcrl/non-base64url-rid/.well-known/crl/hrvC0uzB16UXK83BnPo7Glg685b1D6iYOyJd5hpRaAA.json: -------------------------------------------------------------------------------- 1 | { 2 | "kid": "hrvC0uzB16UXK83BnPo7Glg685b1D6iYOyJd5hpRaAA", 3 | "method": "rid", 4 | "ctr": 1, 5 | "rids": [ 6 | "vwAjHdarZuc.1638311286", 7 | "base64==", 8 | "XkNHp2Iyk0Y.1638311286", 9 | "$wrongrid!" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /testdata/badcrl/duplicate-rid/.well-known/crl/hrvC0uzB16UXK83BnPo7Glg685b1D6iYOyJd5hpRaAA.json: -------------------------------------------------------------------------------- 1 | { 2 | "kid": "hrvC0uzB16UXK83BnPo7Glg685b1D6iYOyJd5hpRaAA", 3 | "method": "rid", 4 | "ctr": 1, 5 | "rids": [ 6 | "XkNHp2Iyk0Y.1638311286", 7 | "vwAjHdarZuc.1638311286", 8 | "XkNHp2Iyk0Y.1638300000", 9 | "XkNHp2Iyk0Y" 10 | ] 11 | } -------------------------------------------------------------------------------- /testdata/badcrl/invalid-method/.well-known/crl/hrvC0uzB16UXK83BnPo7Glg685b1D6iYOyJd5hpRaAA.json: -------------------------------------------------------------------------------- 1 | { 2 | "kid": "hrvC0uzB16UXK83BnPo7Glg685b1D6iYOyJd5hpRaAA", 3 | "method": "invalid-method", 4 | "ctr": 1, 5 | "rids": [ 6 | "vwAjHdarZuc.1638311286", 7 | "FKDIxsTCGlU", 8 | "XkNHp2Iyk0Y.1638311286", 9 | "TqB_qu_6OtM" 10 | ] 11 | } -------------------------------------------------------------------------------- /testdata/badcrl/too-long-rid/.well-known/crl/hrvC0uzB16UXK83BnPo7Glg685b1D6iYOyJd5hpRaAA.json: -------------------------------------------------------------------------------- 1 | { 2 | "kid": "hrvC0uzB16UXK83BnPo7Glg685b1D6iYOyJd5hpRaAA", 3 | "method": "rid", 4 | "ctr": 1, 5 | "rids": [ 6 | "vwAjHdarZuc.1638311286", 7 | "this-rid-is-longer-than-the-24-char-max", 8 | "XkNHp2Iyk0Y.1638311286", 9 | "TqB_qu_6OtM" 10 | ] 11 | } -------------------------------------------------------------------------------- /shl-server/src/config.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | export const config = { 5 | CREATE_LINK: '/create-link', 6 | PERSIST_LINKS: true, 7 | DEFAULT_ATTEMPTS: 8, 8 | SERVER_BASE : process.env.SERVER_BASE || 'http://localhost:' + (process.env.HOST_PORT || 8090) + '/', 9 | SERVICE_PORT: process.env.HOST_PORT || 8090 10 | } -------------------------------------------------------------------------------- /testdata/issuer.jwks.public.not.smart.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "kty": "EC", 5 | "kid": "ARrigjsh8mTqaVdihxO5cxkaRjpjXUg8ARET6IzhvaQ", 6 | "use": "sig", 7 | "alg": "ES256", 8 | "crv": "P-256", 9 | "x": "bvRiNLRBima85Q6U3jY2chbPQWX3Fyn0WvzDw_3VJWk", 10 | "y": "HlpuaCRfCfTjqR2ZLGVHgD54QQdL5R2t-9bH6SvEajc" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /testdata/wrong_kty_key.json: -------------------------------------------------------------------------------- 1 | {"keys":[{"kty":"RSA","kid":"528CbP7VokRCO69SL1rJtuVVgaAIci3nznSRWfmE03g","e":"AQAB","n":"u_SsfG5xo9y3qpwQzg-CcbHVpIhZ26EQAz7EfxpEE4SvMoRfTg_316Cd-ghXxScL_nOGads8jCcPtI7s-egrFuVyQ_j2riiLp1zNO9-eRlXAm6YPI6a_cYPuIwGurTcP13YVu7K6DL3oGxR46_up8OsE0ch6gDGe4jIMI4GWCnNBxBa9oHRMGk2biIW1B-KQNmHjqoNtHPAytfOrrhKFoAjqeLssobGVBRsGi4qOgbqXQTXatIXVrApWgtx2-1ozaGlU6LU-FMUX6te8eCd2kWE4LvIvyc1Dqp2kEsWkQ86s70he7IgMIIw3bVZibBRKtU09wTm670ngPoNqXTWZqw"}]} -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | Security related to the issuance and validation of SMART Health Cards is very important. Although the developer tools provided by this project are not meant to be used in production environments, some issues could impact the robustness of implementations that used them in development. Such security issues can be disclosed privately by emailing [security@smarthealth.cards](mailto:security@smarthealth.cards) to allow for a responsible disclosure to affected parties. -------------------------------------------------------------------------------- /fhir.validator.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM fabric8/java-alpine-openjdk11-jre 2 | 3 | RUN java -version 4 | 5 | RUN curl -L -o validator_cli.jar https://github.com/hapifhir/org.hl7.fhir.core/releases/latest/download/validator_cli.jar 6 | 7 | # Run the validator without anything to validate - to cache the fhir downloads in the image. 8 | # "|| :" forces a '0' exit code as the validator will return an error code when doing no actual validation 9 | RUN java -jar validator_cli.jar -ig hl7.fhir.r5.core#current || : 10 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## How to file issues and get help 4 | 5 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 6 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 7 | feature request as a new Issue. 8 | 9 | For help and questions about using this project, visit the [Discussions](https://github.com/smart-on-fhir/health-cards-dev-tools/discussions) section. 10 | 11 | ## Microsoft Support Policy 12 | 13 | Support for this project is limited to the resources listed above. 14 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [ $default-branch ] 4 | pull_request: 5 | branches: [ $default-branch ] 6 | 7 | jobs: 8 | build: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | node-version: [14.x] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - run: npm ci 23 | - run: npm run build --if-present 24 | - run: npm test 25 | -------------------------------------------------------------------------------- /testdata/valid_keys.json: -------------------------------------------------------------------------------- 1 | {"keys":[{"kty":"EC","kid":"Qfjo1OC05q1qXWbiiE2D4hkPlEqaJd5qdH4K4b9h97c","use":"sig","alg":"ES256","crv":"P-256","x":"5FJFDayDc-L2qfhh88hoLnUGuOh40NHjOZqqf8cxAKI","y":"-FMxeQtP8_EQBDev_AOmXpX9rJSw3sQWnw0P8XyNQRk"},{"kty":"EC","kid":"Ym-X9Xmsh1jNTDziB_5abLvRH8fIEURXAWisVC2sPpc","use":"sig","alg":"ES256","crv":"P-256","x":"GTIiMO8A9t6FoJDVD-malbKydh2NRn_GxI-77vohDbI","y":"5yGFaBMo6t9QFCZquKEunTy3QXNFu90o1_PNkFVxdB0"},{"kty":"EC","kid":"51VtwbXpEmvmuJqTdaXIQ3UarQ33LLer8mQradarYWM","use":"sig","alg":"ES256","crv":"P-256","x":"5fjwitvt7DY7j1xwjywUIOQpAsRBdhHgcUvAO4WUGaI","y":"iGHF6R1YZUrbzsSmtrC25UGH7V8Dqaz-tg-9rvNpBjk"}]} -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": false 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/eslint-recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:@typescript-eslint/recommended-requiring-type-checking" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": 12, 15 | "sourceType": "module", 16 | "project": ["./tsconfig.json", "./shl-server/tsconfig.json"] 17 | }, 18 | "plugins": [ 19 | "@typescript-eslint" 20 | ], 21 | "rules": { 22 | } 23 | } -------------------------------------------------------------------------------- /schema/smart-health-card-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://smarthealth.cards/schema/smart-health-card.json", 4 | "title": "Root", 5 | "type": "object", 6 | "required": [ 7 | "verifiableCredential" 8 | ], 9 | "properties": { 10 | "verifiableCredential": { 11 | "$id": "#root/verifiableCredential", 12 | "title": "Verifiablecredential", 13 | "type": "array", 14 | "minItems": 1, 15 | "items": { 16 | "$id": "#root/verifiableCredential/items", 17 | "title": "Items", 18 | "type": "string", 19 | "default": "", 20 | "pattern": "^[a-zA-Z0-9_-]+\\.[a-zA-Z0-9_-]+\\.[a-zA-Z0-9_-]+$" 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /testdata/test-example-00-d-jws-no_jws_header_kid.txt: -------------------------------------------------------------------------------- 1 | eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiJ9.3ZJJb9swEIX_SjC9ytraVLFudQp0ORQFmuZS-EBTY4sFF4GLEDfQf-8M7aALkpx6qm4jPn5875H3oEKAHsYYp9BXVZhQlsEIH0cUOo6lFH4IFd4JM2kMFakTeijA7vbQN6_b9rK7qrt1-fKqLWCW0N9DPE4I_bdfzL9xL07DigdCPa1TxiSrfoionH1WKN2shmYN2wKkxwFtVEJ_SbvvKCNb2o_K36IPzOnhVVmXDfH47ybZQSNrPAaXvMSbbB_OC8U5DkinNdFOTugAf6SMRE5af_WaBA_7-5oED8Mj4M8Uh_Zzh8LgCSKM0sSDN5Y0PuQzDmpGyz1-dCPPmxK2CwXcKQr_VkRmNevLZlU3q7aGZSkeddM87-bDnxWHKGIKOS5feES-oFlIqSxeuyETpBuUPWTj4RgimvP7oZsZdVc6f6i42SqooZLzHQFk3glt3cGyXQqYzhVkO3v0aNnb7w2SyEmZfF7isDfKnBBtDlxzLKpq77yh98hehIzOM3JQYdIi17m5vniHFr3QF-9dmFQUmoqiErWLn5LZ8Vao89c82WD7XzbYrv91gx0vLPT9BA.87iYe-m3TojgcTnaGfJLFlwWnV88Dbaw6E-Vpy3CdwEodoCJDdTaZvl_E_9YosrAkmjv32VxzBKvtvpn5yxqgA -------------------------------------------------------------------------------- /testdata/test-example-00-d-jws-no_jws_header_alg.txt: -------------------------------------------------------------------------------- 1 | eyJ6aXAiOiJERUYiLCJraWQiOiIzS2ZkZy1Yd1AtN2dYeXl3dFVmVUFEd0J1bURPUEtNUXgtaUVMTDExVzlzIn0.3ZJLb9swEIT_SrC9yhIlNFGsW50CfRyKAk17CXygqbXFgg-BpIS4gf57d2kHfSDJqafqtuLw48yQD6BjhA6GlMbYVVUcUZXRypAGlCYNpZKhjxXeSzsajBWpJwxQgNvtoauvmuayvRZXbSmu1wXMCroHSMcRobv7xfwb9-o0rHgg1PM6be3k9A-ZtHcvCpWfdV-vYVuACtijS1qaL9PuO6rElvaDDt8wROZ08LoUZU08_ruZXG-QNQGjn4LC22wfzgvFOQ4obwzRTk7ogHCkjESejPkaDAke93eCBI_DE-DPFIf2c4fS4gkirTbEgzeONCHmMw56Rsc9fvQDz5sStgsF3GkK_1YmZtXry3ol6lUjYFmKJ93UL7v58GfFMck0xRyXLzwhX9AsldIOb3yfCcr32h2y8XiMCe35_dDNDKYtfThU3GwVdV-p-Z4AKu-ERrSwbJcCxnMF2c4eAzr29nuDJPJKTSEvcdhbbU-IJgcWHIuq2vtg6T2yF6mSD4zsdRyNzHVubi7eocMgzcV7H0edpKGiqETj06fJ7ngriPzVzzbY_JcNNut_3WDLCwt9PwE.9ricS3s_xR1fl7YndWq5q0mc4-VeFBJNWpsaJULhccl8QIUnwWxtXc8Ps0C-vU-w_vcsoTnRggW_dex7afcj5w -------------------------------------------------------------------------------- /testdata/test-example-00-d-jws-no_jws_header_zip.txt: -------------------------------------------------------------------------------- 1 | eyJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.3ZJJb9swEIX_SjC9ylqIOo51q1Ogy6Eo0LSXwAeaGlssuAgkJcQN9N87QzvogiSnnKLbiI8f33vkPegYoYU-pSG2VRUHVGW0MqQepUl9qWToYoV30g4GY0XqEQMU4HZ7aJtLIZarq3opyitxWcCkoL2HdBwQ2ts_zP9xb07DggdCPa3T1o5O_5JJe_esUPlJd80atgWogB26pKX5Nu5-okpsad_r8ANDZE4Lb8u6bIjHfzej6wyyJmD0Y1B4k-3DeaE4xwHljSHayQkdEI6UkcijMd-DIcHD_rYmwcPwCPgrxaH93KG0eIJIqw3x4J0jTYj5jIOe0HGPn33P86aE7UwBd5rCv5eJWc162SzqZiFqmOfiUTfN824-_VtxTDKNMcflC0_IFzRJpbTDa99lgvKddodsPB5jQnt-P3QzvVmVPhwqbraKuqvUdEcAlXeCqFcwb-cChnMF2c4eAzr29neDJPJKjSEvcdgbbU8IkQPXHIuq2vtg6T2yF6mSD4zsdByMzHVuri8-oMMgzcVHHwedpKGiqETj05fR7ngr1PlrnmxQvMoGxfqlG1zxwkzfbw.zJk9rUTuxesBy9tBi-BmkqhutomkHd0357Yp58Mgwz6xYphxnEPA6lxj2lDZv6h9IeLFQP506DHH0EeVUik0HQ -------------------------------------------------------------------------------- /testdata/test-example-00-d-jws-der-signature.txt: -------------------------------------------------------------------------------- 1 | eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.3ZJJb9swEIX_SjC9ytqaxJVudQp0ORQFmvZS-EBTY4sFF4GLEDfQf-8M7aALkpx6qm4jPn5875H3oEKAHsYYp9BXVZhQlsEIH0cUOo6lFH4IFd4JM2kMFakTeijA7vbQN9dte7W-ftm9Krv2soBZQn8P8Tgh9N9-Mf_GvTgNKx4I9bROGZOs-iGicvZZoXSzGpoOtgVIjwPaqIT-nHbfUUa2tB-V_4o-MKeHy7IuG-Lx302yg0bWeAwueYm32T6cF4pzHJBOa6KdnNAB_kgZiZy0_uI1CR729zUJHoZHwJ8oDu3nDoXBE0QYpYkHry1pfMhnHNSMlnv84EaeNyVsFwq4UxT-jYjMarqrZlU3q7aGZSkeddM87-b9nxWHKGIKOS5feES-oFlIqSzeuCETpBuUPWTj4RgimvP7oZsZ9bp0_lBxs1VQQyXnOwLIvBPaeg3LdilgOleQ7ezRo2VvvzdIIidl8nmJw94qc0K0OXDNsaiqvfOG3iN7ETI6z8hBhUmLXOfm5uItWvRCX7xzYVJRaCqKStQufkxmx1uhzl_zZIPtf9lg2_3rBte8sND3Ew.MEQCIDKJ8bCwdn8ZVhy1ZbWqGbuHjx12agrxX_gjzxBnRtvmAiARrudEoblB8xMXXma2zmP_1gh7Oo3MDxJHWk36WnFiCA -------------------------------------------------------------------------------- /testdata/test-example-00-d-jws-invalid-signature.txt: -------------------------------------------------------------------------------- 1 | eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.3ZJJb9swEIX_SjC9ytqaxJVudQp0ORQFmvZS-EBTY4sFF4GLEDfQf-8M7aALkpx6qm4jPn5875H3oEKAHsYYp9BXVZhQlsEIH0cUOo6lFH4IFd4JM2kMFakTeijA7vbQN9dte7W-ftm9Krv2soBZQn8P8Tgh9N9-Mf_GvTgNKx4I9bROGZOs-iGicvZZoXSzGpoOtgVIjwPaqIT-nHbfUUa2tB-V_4o-MKeHy7IuG-Lx302yg0bWeAwueYm32T6cF4pzHJBOa6KdnNAB_kgZiZy0_uI1CR729zUJHoZHwJ8oDu3nDoXBE0QYpYkHry1pfMhnHNSMlnv84EaeNyVsFwq4UxT-jYjMarqrZlU3q7aGZSkeddM87-b9nxWHKGIKOS5feES-oFlIqSzeuCETpBuUPWTj4RgimvP7oZsZ9bp0_lBxs1VQQyXnOwLIvBPaeg3LdilgOleQ7ezRo2VvvzdIIidl8nmJw94qc0K0OXDNsaiqvfOG3iN7ETI6z8hBhUmLXOfm5uItWvRCX7xzYVJRaCqKStQufkxmx1uhzl_zZIPtf9lg2_3rBte8sND3Ew.Ma9Hatts9reee7DA7Sj8rfcIkH816fI1LkFSXj3s1aT6tdGmDiveDE2yNqi7ZHHKMViFInas_37Hi6mMrQW4BA -------------------------------------------------------------------------------- /testdata/test-example-00-d-jws-issuer-kid-mismatch.txt: -------------------------------------------------------------------------------- 1 | eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IkFScmlnanNoOG1UcWFWZGloeE81Y3hrYVJqcGpYVWc4QVJFVDZJemh2YVEifQ.3ZJLb9swEIT_SrC9yhKlJHatW50CfRyKAk1zKXygqbXFgg-BpIS4gf57d2kHfSDJqafqtuLw48yQD6BjhBb6lIbYVlUcUJXRypB6lCb1pZKhixXeSzsYjBWpRwxQgNvtoa2Xl-JyeSWWr8vruilgUtA-QDoOCO23X8y_ca9Ow4IHQj2v09aOTv-QSXv3olD5SXf1GrYFqIAduqSl-TLuvqNKbGnf63CHITKnhatSlDXx-O9mdJ1B1gSMfgwKb7N9OC8U5zigvDFEOzmhA8KRMhJ5NOZrMCR43N8KEjwOT4A_Uxzazx1KiyeItNoQD9440oSYzzjoCR33-NH3PG9K2M4UcKcp_FuZmFWvr-uFqBeNgHkunnRTv-zmw58VxyTTGHNcvvCEfEGTVEo7vPFdJijfaXfIxuMxJrTn90M305tV6cOh4marqLtKTfcEUHknNGIF83YuYDhXkO3sMaBjb783SCKv1BjyEoe91faEaHJgwbGoqr0Plt4je5Eq-cDITsfByFzn5ubiHToM0ly893HQSRoqiko0Pn0a7Y63gshf_WyDzX_ZYLP-1w2ueGGm7yc.ppJ_XIPRcT4J4bLzLdBisVRqCcgJdhlWyTE85rFyw4Amvf2uuV6p6ZxeA9pHi24pIDFICMlEhl-Hk8ciBbK2BQ -------------------------------------------------------------------------------- /testdata/test-example-00-d-jws-wrong_jws_header_kid.txt: -------------------------------------------------------------------------------- 1 | eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAifQ.3ZJLb9swEIT_SrC9ypIoxHGtW50CfRyKAk17CXygqbXFgg-BpIS4gf57d2kHfSDJKafotuLw48yQ96BjhBb6lIbYVlUcUJXRypB6lCb1pZKhixXeSTsYjBWpRwxQgNvtoRVXTbNcvRX1ZbkUVwVMCtp7SMcBob39w_wf9-Y0LHgg1NM6be3o9C-ZtHfPCpWfdCfWsC1ABezQJS3Nt3H3E1ViS_tehx8YInNauCzrUhCP_25G1xlkTcDox6DwJtuH80JxjgPKG0O0kxM6IBwpI5FHY74HQ4KH_W1NgofhEfBXikP7uUNp8QSRVhviwTtHmhDzGQc9oeMeP_ue500J25kC7jSFfy8Ts8R6KRa1WDQ1zHPxqBvxvJtP_1Yck0xjzHH5whPyBU1SKe3w2neZoHyn3SEbj8eY0J7fD91Mb1alD4eKm62i7io13RFA5Z3Q1CuYt3MBw7mCbGePAR17-7tBEnmlxpCXOOyNtidEkwPXHIuq2vtg6T2yF6mSD4zsdByMzHVuri8-oMMgzcVHHwedpKGiqETj05fR7ngr1PkTTzbYvMoGm_VLN7jihZm-3w.QqKxqmYHnXxtdGySTF6cfd3nAngHVk1AMbz_6RnvcjzLQPKKlyTren8kUgB_LQbu4ycG6oIsDmaLHd3tVIUbhQ -------------------------------------------------------------------------------- /testdata/test-example-00-d-jws-der-signature-r-neg.txt: -------------------------------------------------------------------------------- 1 | eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.3ZJJb9swEIX_SjC9ytqaxJVudQp0ORQFmvZS-EBTY4sFF4GLEDfQf-8M7aALkpx6qm4jPn5875H3oEKAHsYYp9BXVZhQlsEIH0cUOo6lFH4IFd4JM2kMFakTeijA7vbQN9dte7W-ftm9Krv2soBZQn8P8Tgh9N9-Mf_GvTgNKx4I9bROGZOs-iGicvZZoXSzGpoOtgVIjwPaqIT-nHbfUUa2tB-V_4o-MKeHy7IuG-Lx302yg0bWeAwueYm32T6cF4pzHJBOa6KdnNAB_kgZiZy0_uI1CR729zUJHoZHwJ8oDu3nDoXBE0QYpYkHry1pfMhnHNSMlnv84EaeNyVsFwq4UxT-jYjMarqrZlU3q7aGZSkeddM87-b9nxWHKGIKOS5feES-oFlIqSzeuCETpBuUPWTj4RgimvP7oZsZ9bp0_lBxs1VQQyXnOwLIvBPaeg3LdilgOleQ7ezRo2VvvzdIIidl8nmJw94qc0K0OXDNsaiqvfOG3iN7ETI6z8hBhUmLXOfm5uItWvRCX7xzYVJRaCqKStQufkxmx1uhzl_zZIPtf9lg2_3rBte8sND3Ew.MEUCIQCvejwlbwIaRmlL8NH-0kRnW_xOwmk8zFErIYJoAy_NMgIgJ4J1zgy58iWycFJc_oojUURr7SRzyCcA2Gpib2IOTfg -------------------------------------------------------------------------------- /testdata/test-example-00-d-jws-der-signature-rs-neg.txt: -------------------------------------------------------------------------------- 1 | eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.3ZJJb9swEIX_SjC9ytqaxJVudQp0ORQFmvZS-EBTY4sFF4GLEDfQf-8M7aALkpx6qm4jPn5875H3oEKAHsYYp9BXVZhQlsEIH0cUOo6lFH4IFd4JM2kMFakTeijA7vbQN9dte7W-ftm9Krv2soBZQn8P8Tgh9N9-Mf_GvTgNKx4I9bROGZOs-iGicvZZoXSzGpoOtgVIjwPaqIT-nHbfUUa2tB-V_4o-MKeHy7IuG-Lx302yg0bWeAwueYm32T6cF4pzHJBOa6KdnNAB_kgZiZy0_uI1CR729zUJHoZHwJ8oDu3nDoXBE0QYpYkHry1pfMhnHNSMlnv84EaeNyVsFwq4UxT-jYjMarqrZlU3q7aGZSkeddM87-b9nxWHKGIKOS5feES-oFlIqSzeuCETpBuUPWTj4RgimvP7oZsZ9bp0_lBxs1VQQyXnOwLIvBPaeg3LdilgOleQ7ezRo2VvvzdIIidl8nmJw94qc0K0OXDNsaiqvfOG3iN7ETI6z8hBhUmLXOfm5uItWvRCX7xzYVJRaCqKStQufkxmx1uhzl_zZIPtf9lg2_3rBte8sND3Ew.MEYCIQCd2lvDmNmiHTxHC5vHUaZlNntXCKIedaHYloYJfDZ0QwIhANrHbCyIYSVqMP4ZmuK6O1ErEo5bZSAhzZoGQbG0LzY5 -------------------------------------------------------------------------------- /testdata/test-example-00-d-jws-der-signature-s-neg.txt: -------------------------------------------------------------------------------- 1 | eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.3ZJJb9swEIX_SjC9ytqaxJVudQp0ORQFmvZS-EBTY4sFF4GLEDfQf-8M7aALkpx6qm4jPn5875H3oEKAHsYYp9BXVZhQlsEIH0cUOo6lFH4IFd4JM2kMFakTeijA7vbQN9dte7W-ftm9Krv2soBZQn8P8Tgh9N9-Mf_GvTgNKx4I9bROGZOs-iGicvZZoXSzGpoOtgVIjwPaqIT-nHbfUUa2tB-V_4o-MKeHy7IuG-Lx302yg0bWeAwueYm32T6cF4pzHJBOa6KdnNAB_kgZiZy0_uI1CR729zUJHoZHwJ8oDu3nDoXBE0QYpYkHry1pfMhnHNSMlnv84EaeNyVsFwq4UxT-jYjMarqrZlU3q7aGZSkeddM87-b9nxWHKGIKOS5feES-oFlIqSzeuCETpBuUPWTj4RgimvP7oZsZ9bp0_lBxs1VQQyXnOwLIvBPaeg3LdilgOleQ7ezRo2VvvzdIIidl8nmJw94qc0K0OXDNsaiqvfOG3iN7ETI6z8hBhUmLXOfm5uItWvRCX7xzYVJRaCqKStQufkxmx1uhzl_zZIPtf9lg2_3rBte8sND3Ew.MEUCID2lGtFskO3E7_ai9JEg2SXH-hL3tQbGbLmCWoGAlGwxAiEA38kYQ5XfOv7Z-7sj0lVs6ICxHRl29UGio1Ygyg2d1bk -------------------------------------------------------------------------------- /testdata/test-example-00-d-jws-issuer-not-valid-with-smart-key.txt: -------------------------------------------------------------------------------- 1 | eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.3ZJLb9swEIT_SrC9ynrFqWrd6hTo41AUaNpL4QNNrSwWfAgkJcQN9N-7SztBiyY55VTdVhx-nBnyDlQI0MIQ4xjaouidy4MRPg4odBxyKXwXCrwVZtQYChJP6CEDu--hrV5fllfry_pNkzfrOoNZQnsH8TgitD8ekP_gXp2GFQ-EelqnjJms-iWicvZZoXSz6qoN7DKQHju0UQn9ddr_RBnZUj8o_x19YE4L67zMK-Lx3-1kO42s8Rjc5CXeJPtwXsjOcUA6rYl2ckIH-CNlJPKk9TevSXC_vy1JcD88Av5CcWg_dygMniDCKE08eGtJ40M646BmtNzjJzfwvM1ht1DAvaLw70RkVrW5qlZltapLWJbsUTfV824-_l1xiCJOIcXlC4_IFzQLKZXFa9clgnSdsodkPBxDRHN-PnQzg25y5w8FN1sE1RVyviWATDuhLhtYdksG47mCZKdHj5a9_dkgiZyUk09LHPZGmROiToFLjkVV9c4beo_sRcjoPCM7FUYtUp3b64v3aNELffHBhVFFoakoKlG7-Hkye94KZfqqJxus_8sG681LN9jwwkLfbw.ZZJ6oBVioXmGwDDp8__jaCBLhsYnag7ghqmYIbN85a1iyaXAda8MnFiNzbv-YN0DBi47IajycvhcINILK9dLhw -------------------------------------------------------------------------------- /testdata/test-example-00-d-jws-trailing_chars.txt: -------------------------------------------------------------------------------- 1 | 2 | eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.3ZJLj9MwFIX_yuiyTZM4UEKzo4PEY4GQGNigLlzntjHyI7KdaMoo_5173Y54aGZWrMjuxsefzzn2HegYoYMhpTF2VRVHVGW0MqQBpUlDqWToY4W30o4GY0XqCQMU4PYH6MTLplm3r9r181K0BcwKujtIpxGh-_YL-Tft2XlY8UCkx3Xa2snpHzJp754UKj_rXmxgV4AK2KNLWprP0_47qsSWDoMOXzFE5nTwoqxLQTz-u51cb5A1AaOfgsKbbB8uC8UlDihvDNHOTuiAcKKMRJ6M-RIMCe73dzUJ7ocHwJ8oDu3nCqXFM0RabYgHrx1pQsxnHPWMjnv84AeetyXsFgq41xT-jUzMEpu1WNVi1dSwLMWDbsTTbt7_WXFMMk0xx-X7TsgXNEultMNr32eC8r12x2w8nmJCe3k-dDODaUsfjhU3W0XdV2q-JYDKO6GpW1h2SwHjpYJs54ABHXv7vUESeaWmkJc47I22Z0STA9cci6o6-GDpObIXqZIPjOx1HI3MdW6vr96iwyDN1TsfR52koaKoROPTx8nueSvU-ROPNtj8lw02m3_dYMsLC30_AQ.drW2nyE3vROTNI4SpQmIb2P1gp5jGRDqr_cnwZGmsZ3-Yl7WfpEO2lll_WrjjYO0IpDtFxkksyKIt3O3R7yvJA 3 | -------------------------------------------------------------------------------- /testdata/test-example-00-d-jws-utf8_bom_prefix.txt: -------------------------------------------------------------------------------- 1 | eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.3ZLLbtswEEV_JZhuZb3yUKxdnQJ9LIoCTbMpvKCpscWCD4GkhLiBvqyLflJ_oTO0g7ZBklVW1W7Ey8N7L_nrx887UCFAC32MQ2iLIgwo82CEjz0KHftcCt-FAm-FGTSGgtQjesjAbrbQVhenF815dXna5JdnTQaThPYO4n5AaL_-YT7EvToMCx4I9bROGTNa9V1E5eyzQukm1VVLWGcgPXZooxL687j5hjKypW2v_A36wJwWzvIyr4jHf1ej7TSyxmNwo5d4nezDcSE7xgHptCbawQkd4PeUkcij1l-8JsH9_rYkwf3wCPgTxaH93KEweIAIozTx4LUljQ_pjJ2a0HKPH1zP8yqH9UwBN4rCvxGRWdXyvFqU1aIuYZ6zR91Uz7t5_2_FIYo4hhSXLzwiX9AkpFQWr1yXCNJ1yu6S8bAPEc3x_dDN9LrJnd8V3GwRVFfI6ZYAMu2EumxgXs8ZDMcKkp0terTs7e8GSeSkHH1a4rDXyhwQdQpcciyqauu8offIXoSMzjOyU2HQItW5ujp5ixa90CfvXBhUFJqKohK1ix9Hs-GtUKaverLB-r9ssF6-dIMNL8z0_QY.MK1eigU285yG0Q9De9bfLKdIrOyi_WGiDleaN32k60yA5HSCTgF40oT0MfN8s-qP97ZkiYSVPsrtSqozSFF0yA -------------------------------------------------------------------------------- /shl-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shl-server", 3 | "version": "1.0.0", 4 | "description": "Simple SMART Health Link server for testing", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "deploy": "node ./src/server.js", 9 | "deploy.developer": "nodemon ./src/server.js --ignore shl/", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "@types/express": "^4.17.15", 17 | "@types/node": "^18.11.18", 18 | "@types/node-jose": "^1.1.10", 19 | "@types/qrcode": "^1.5.0", 20 | "nodemon": "^2.0.20", 21 | "typescript": "^4.9.4" 22 | }, 23 | "dependencies": { 24 | "express": "^4.18.2", 25 | "node-jose": "^2.1.1", 26 | "qrcode": "^1.5.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /testdata/test-example-00-e-file.wrong-extension: -------------------------------------------------------------------------------- 1 | { 2 | "verifiableCredential": [ 3 | "eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.3ZJJb9swEIX_SjC9ytqaxJVudQp0ORQFmvZS-EBTY4sFF4GLEDfQf-8M7aALkpx6qm4jPn5875H3oEKAHsYYp9BXVZhQlsEIH0cUOo6lFH4IFd4JM2kMFakTeijA7vbQN9dte7W-ftm9Krv2soBZQn8P8Tgh9N9-Mf_GvTgNKx4I9bROGZOs-iGicvZZoXSzGpoOtgVIjwPaqIT-nHbfUUa2tB-V_4o-MKeHy7IuG-Lx302yg0bWeAwueYm32T6cF4pzHJBOa6KdnNAB_kgZiZy0_uI1CR729zUJHoZHwJ8oDu3nDoXBE0QYpYkHry1pfMhnHNSMlnv84EaeNyVsFwq4UxT-jYjMarqrZlU3q7aGZSkeddM87-b9nxWHKGIKOS5feES-oFlIqSzeuCETpBuUPWTj4RgimvP7oZsZ9bp0_lBxs1VQQyXnOwLIvBPaeg3LdilgOleQ7ezRo2VvvzdIIidl8nmJw94qc0K0OXDNsaiqvfOG3iN7ETI6z8hBhUmLXOfm5uItWvRCX7xzYVJRaCqKStQufkxmx1uhzl_zZIPtf9lg2_3rBte8sND3Ew.EtHJLQTEwQ1Fq0XwZ7WhU1EXNkpRrcSdUTyL0n_8bfRZ2lmrlG30zffy22j4gD3Xb2e1d7I_08ZKCZFF3D2bZw" 4 | ] 5 | } -------------------------------------------------------------------------------- /testdata/badcrl/duplicate-rid/jws-crl-duplicate-rid.txt: -------------------------------------------------------------------------------- 1 | eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6ImhydkMwdXpCMTZVWEs4M0JuUG83R2xnNjg1YjFENmlZT3lKZDVocFJhQUEifQ.3VLLbhQxEPyVqLnOW0lWMzc2kXgcEBKBC9qDx-7dMfJj5MfAEs2_0_ZuUJCSnDjFN7urq6uqfQ_SexhgCmH2Q1079rM6yDDFMXp03JqAJlTc6tpr5kJpTbmfpKsnZCpMJWdO-FLgUgZrla81k6YO6INggdUjE9ypWsRZSc4Clk4KKMCMexja68u-3_Rd01TXV5sCFg7DPYTjjDB8_ysnDz3NqvKsN48GE9XzOKl1NPI3C9KaF4HcLlK0PewK4A4F2ZVMfYnjD-QhSUp2v6HziWeAy6qpWuJLr9tohMKEcehtdBzvsnw4F4qzHeBWKWI7KaEB7kgeiTkq9dUpAjz0Dw0BHi5PEH8mO9SfMmQaTyRMS0V88NYQxvk84yAXNCnHj3ZK920Fu5UMjpLM39ImCN_2V23ZtGXXwLoWT6ppX1bz4d-IfWAh-mxXzwoDpgUtjHNp8MaKzMCtkOaQhfujD6jPX482M6lNZd2hzt_LS1Hz5RcR8NwJXbOBdbcWMJ8jyHL26NAkbY8TJJDlPLpcSmbvpD5RdNlwk2xRVHvrNLqshfFgXaIU0s-K5Ti3Nxfv0KBj6uK99bMMTFFQFKKy4VPUY2qFJp_22QS7V5lg1__vBDepsNL5Aw.VrH5ou6UgdrFxW_VfEX40ZvKQy6T8KTYDN9aGTFksn9e3CQHI7bLeL66dicsBwqCeFn9WsgiZ7S5dK9Q8U3w6Q -------------------------------------------------------------------------------- /testdata/badcrl/too-long-rid/jws-crl-too-long-rid.txt: -------------------------------------------------------------------------------- 1 | eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6ImhydkMwdXpCMTZVWEs4M0JuUG83R2xnNjg1YjFENmlZT3lKZDVocFJhQUEifQ.3VLLjhMxEPyVVXPNPFkSMjeySDwOCIllLygHj93JGPkxsnsGwir_TttJ0CLt7okTvrW7urqq7HvQMUIHA9EYu6oK4ke51zRM_RQxSO8IHZXS2ypaEajwrtgNOlQDCkNDIUVQsVA4F-S9iZUV2lWEkZQgUfVCyWAqbhXGu30RtIIFuH4HXbN8uW6Wr6_buqyb5QJmCd09JEAHopcKGUiHEaH79kdbVnBaXObFLx6oYPzTOG3t5PQvQdq7Z4HSz1o1a9guQAZU7F0L82Xqv6OkJDB5v8MQE08H12VdNsyXbjeTUwazCYx-ChJvs3w4Ny52QHpjmO2khBeEA3tk5smYr8Ew4DLf1Qy4FI8Qf2Y7PJ8SFRZPJMJqw3zwxjEmxLxjr2d0KcePfkj1poTtkQ32ms2_FZS4mvWrpqiboq3heFw8qqZ5Xs2HvyOOJGiK2a4dDRKmB5qFlNrhjVeZQXql3T4Lj4dIaM__kF9mMKvSh32V_1rUqpLzTyaQeRLaegXH7XEB4zmCLGeHAV3S9jBBBnkpp5BbyeyttieKNhuuky2OaueDxZC1CEk-JEql42hEjnNzc_UOHQZhrt77OGoShoPiEI2nT5Pt0yjU-TRPJtj-lwm263-d4Co1jnx-Aw.7bFpHzNxnj4sB83JqfrmK0Ojdgd24c5-zFdxMqzRbZuylYY6l_1cPrTwfTwKZt6K5iIDkpIdO0McTe2gRxUCnQ -------------------------------------------------------------------------------- /testdata/badcrl/invalid-method/jws-crl-invalid-method.txt: -------------------------------------------------------------------------------- 1 | eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6ImhydkMwdXpCMTZVWEs4M0JuUG83R2xnNjg1YjFENmlZT3lKZDVocFJhQUEifQ.3ZJLbxMxFIX_SnXZZl4JaZrZkSLxWCAkSjcoC499kzHyY2TfGQhV_jvXToKK1HbFitl5fHzud479ADpGaKEnGmJbVUH8KPea-rEbIwbpHaGjUnpbRSsCFd4Vu16HqkdhqC-kCCoWCqeCvDexskK7ijCSEiSqTigZTKXdJIxWhUXqvYIZuG4HbXO9WDfXN4vFsryplzOYJLQPELRiGtFJhSykw4DQfvtDlxlOo8s8-tUjDtY_r9PWjk7_EqS9e1Eo_aRVs4btDGRAxem1MF_G7jtKSoAp_T2GmHxaeF3WZcN-6e9mdMpgDoHRj0HiXcaH88YlDkhvDLudSHhAOHBGdh6N-RoMCy7n25oFl8UTxp85Dp9PjQqLJxNhtWE_eONYE2KesdcTutTjR9-n9aaE7ZEDdprDvxWUvJr1sinqppjXcDzOnqRpXqb58HfFkQSNMce1g0HCdEGTkFI7vPUqO0ivtNtn8HiIhPb8EvlmerMqfdhX-bVFrSo5_WQDmU_CvF7BcXucwXCuIOPsMKBLbI8bZJGXcgx5K4W90_ZkMc-B6xSLq9r5YDFkFiHJh2SpdByMyHVubq_eocMgzNV7HwdNwnBRXKLx9Gm0XToKdf6aZxuc_5cNztf_usFV2jjy9xs.DWI9iiIXZwVv_OQ6ixun9mPXyE5S_5zzyUM62qdTDD-jUJAhboEU544Ezg0DSmTiI9My2w0rCaXnKT70ixowBg -------------------------------------------------------------------------------- /testdata/badcrl/version-mismatch/jws-crl-version-mismatch.txt: -------------------------------------------------------------------------------- 1 | eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6ImhydkMwdXpCMTZVWEs4M0JuUG83R2xnNjg1YjFENmlZT3lKZDVocFJhQUEifQ.3ZJLj9MwFIX_yshsmzjJ0CnNjg4SjwVCYmCDunDs28bIj8i-CZRR_jvXTosGaWZWrMjO8bnH3zn2PdMxspb1iENsOQ_iR3nU2I_dGCFI7xAcltJbHq0IWHhXHHodeA_CYF9IEVQsFEwFem8it0I7jhBRCRS8E0oGwycIUdOg1eSBsmcr5roDa-ub621982pdNeV6fb1ik2TtPQtaEY_opAIS4mkA1n77w5cplsPLfPiLBySkf1qnrR2d_iWQUJ4VSj9pVW_ZfsVkAEX5tTCfx-47SEyAKf_XJRKBviyrsia_9Hc3OmUgh4DoxyDhLuOz88YlDpPeGHJbSOiAcKKM5Dwa8yUYElzm24oEl8Ujxp8oDs2nRoWFxURYbciPvXakCTGfcdQTuNTjB9-n9a5k-5kCdprCvxGYvOrtui6qumgqNs-rR2nq52ne_11xRIFjzHHtYAAhXdAkpNQObr3KDtIr7Y4ZPJ4igj2_RbqZ3mxKH448v7eoFZfTTzKQeZI11YbN-3nFhnMFGecAAVxie9ggibyUY8hbKeydtotFkwNXKRZVdfDBQsgsQqIPyVLpOBiR69zdXr0FB0GYq3c-DhqFoaKoROPx42i7NMqq_NVPNtj8lw0223_d4CZtzPT9Bg.LW3MQVKvoNASv9tVhC6OXtAM2OCS7oLlBkUuilvUe5_NhuYIq9sN1gSTPGMLsWqrRp-Glpo57inEluRIjSVpjA -------------------------------------------------------------------------------- /testdata/badcrl/non-base64url-rid/jws-crl-non-base64url-rid.txt: -------------------------------------------------------------------------------- 1 | eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6ImhydkMwdXpCMTZVWEs4M0JuUG83R2xnNjg1YjFENmlZT3lKZDVocFJhQUEifQ.3VK7bhsxEPwVY9PqnlGk6LrIAfIoggBx3AQqeORKx4CPAx-XKMb9e5aUFDiA7cqV2S13dnZmyDuQ3kMHQwij76rKsV_lQYYh9tGj49YENKHkVldeMxcKa4r9IF01IFNhKDhzwhcCpyJYq3ylmTRVQB8EC6zqmeBOVYaGeuZxtYxOFU4KWIDp99A1q9ebZvW2XW7K1XoBE4fuDlK_A9ZzgYQLxxGh-_FPX1ZxWl7m5a_uKSH84zipdTTyDwvSmieB3E5SNBvYLYA7FORfMvUt9j-RhyQw-b9F5xNPB8uyLhviS7fbaITCbAK9jY7jTZYP58bFDnCrFLGdlNACdySPxByV-u4UAS7zXU2AS_EA8VeyQ_MpUKbxRMK0VMQH7wxhnM87DnJCk3L8bIdUb0vYzWSwl2T-PQuJq9m8aYq6Kdoa5nnxoJrmaTWf_o_YBxaiz3b1qDBgeqCJcS4NXluRGbgV0hyycH_0AfX5L9LLDGpdWneo8n_zUlR8-k0EPE9CW69h3s0LGM8RZDl7dGiStvsJEshyHl1uJbM3Up8o2my4TrYoqr11Gl3WwniwLlEK6UfFcpzb66sPaNAxdfXR-lEGpigoClHZ8CXqPo1CnU_zaILti0yw3Tx3guvUmOn8BQ.2gjvKF-v1DKHXjFLzvbLXIaZ4G7IgYXPlCjhqGzuhP_cv9dkkYvvwvmgJ36QeUg02_hCJMHnDJiWyifdTszmng -------------------------------------------------------------------------------- /testdata/test-example-00-e-file-invalid_deflate.smart-health-card: -------------------------------------------------------------------------------- 1 | { 2 | "verifiableCredential": [ 3 | "eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.eJzdkklv2zAQhf9KML3K2tBUkG51CnQ5FAWa9lL4QFNjiwUXgYsQN9B_7wztoAuSnHqqbiM-fnzvkfegQoABphjnMFRVmFGWwQgfJxQ6TqUUfgwV3gkzawwVqRN6KMDuDzA0r9r2uuv6ui-7ti9gkTDcQzzNCMO3X8y_cS_Ow4YHQj2tU8Ykq36IqJx9Vijdosamh10B0uOINiqhP6f9d5SRLR0m5b-iD8wZ4GVZlw3x-O822VEjazwGl7zE22wfLgvFJQ5IpzXRzk7oAH-ijEROWn_xmgQP-4eaBA_DI-BPFIf2c4fC4BkijNLEg9eWND7kM45qQcs9fnATz9sSdisF3CsK_0ZEZjX9dbOpm01bw7oWj7ppnnfz_s-KQxQxhRyXLzwiX9AipFQWb9yYCdKNyh6z8XAKEc3l_dDNTLornT9W3GwV1FjJ5Y4AMu-Etu5g3a0FzJcKsp0DerTs7fcGSeSkTD4vcdhbZc6INgeuORZVdXDe0HtkL0JG5xk5qjBrkevc3ly9RYte6Kt3LswqCk1FUYnaxY_J7Hkr1Plrnmyw_S8bbPt_3WDHCyt9PwEOH3YA.QAeMpU7g7nqOJhJb3A1yIagMfOXKmILG6Sk269Tn_Lu-TfymQJNxGnrHPIyReyFqjf9eYGAYPYbWpdc2JCBBJQ" 4 | ] 5 | } -------------------------------------------------------------------------------- /testdata/test-example-00-e-file-invalid_issuer_url_http.smart-health-card: -------------------------------------------------------------------------------- 1 | { 2 | "verifiableCredential": [ 3 | "eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.3ZJLb9swEIT_SrC9ynoQSV3rVqdAmxyKAk17KXygqbXFgg-BpIS4gf57d2kHTYIkp5yq24rDjzND3oGOEVroUxraqooDqjJaGVKP0qS-VDJ0scJbaQeDsSLxiAEKcNsdtM17IS6WH4RYlY04L2BS0N5BOgwI7a-MjMx8int3HBY8EOplnbZ2dPqPTNq7V4XKT7prVrApQAXs0CUtzfdx-xtVYku7XoefGCJzWjgv67IhHv9dj64zyJqA0Y9B4U22D6eF4hQHlDeGaEcndEA4UEYij8b8CIYE9_vbmgT3wzPgbxSH9nOH0uIRIq02xIOPjjQh5jP2ekLHPV77nud1CZuZAm41hf8kE7Oa1UWzqJuFqGGei2fdNK-7uXpccUwyjTHH5QtPyBc0SaW0w0vfZYLynXb7bDweYkL77_n0Zln6sK-42SrqrlLTLQFU3gmiXsK8mQsYThVkOzsM6NjbwwZJ5JUaQ17isDfaHhEiB645FlW188HSe2QvUiUfGNnpOBiZ61xfnn1Gh0Gasy8-DjpJQ0VRicanr6Pd8lao89e82KD4LxsUq7ducMkLM31_AQ.Pkz3ry_PGCZGPbCLispYVcaEbqi0DVJ6H2YAj5bGdD9lzQREVndlCyNm1EEmbl_EvCB1kjSyA08a4uDQv00TVQ" 4 | ] 5 | } -------------------------------------------------------------------------------- /testdata/test-example-00-e-file-invalid_issuer_url.smart-health-card: -------------------------------------------------------------------------------- 1 | { 2 | "verifiableCredential": [ 3 | "eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.3ZJJb9swEIX_SjC9ytpaV7VudQp0ORQFmuZSGAFNji0WXAQuQtxA_71D2mkWJDn1VN5GM_PxvUfdgPQeehhCGH1fVX5EXnrNXBiQqTCUnDnhK7xmelToK5qO6KSZmJLiKjoFBZjtDvrmbdsuu3dNtyyX7esCJg79DYTDiND_vMM_Jr86FotUEOr5Oal1NPI3C9KaFwe5naRoVrApgDsUaIJk6nvc_kIekqTdIN0lOp84Pbwp67IhXvq6jkYoTDMOvY2O40WWD6dGcbID3CpFtKMSusAdyCORo1I_KJH-735f08Bt8QT4G9mh_ZQh03iEMC0V8eC9oRnn8x17OaFJOX6xQ6rXJWxmMriVZP4DC4nVrJbNom4WbQ3zXDyppnlZzeeHEfvAQvTZbnr7gOmBJsa5NHhuRSZwK6TZZ-H-4APq069ELzOorrRuX6VkKy9FxadrAvC8CW3dwbyZCxhPEWQ5O3Rokrb7CdKQ5Ty63EpmL6Q-ItpsuE62KKqddRpd1sJ4sC4hhfSjYjnO9fnZRzTomDr7ZP0oA1MUFIWobPga9TatQp1P82yC7X-ZYLv61wl2qTHT-QM.n-7E7wskNJhkAOYXABYDWeFtcpONEUAD4Vki2_Jlq33Z7mCDipbGVu25Z7AXRjkqCS04gewJoXkJNN6mXZgP7Q" 4 | ] 5 | } -------------------------------------------------------------------------------- /testdata/test-example-00-e-file-issuer_url_with_trailing_slash.smart-health-card: -------------------------------------------------------------------------------- 1 | { 2 | "verifiableCredential": [ 3 | "eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.3ZJLb9swEIT_SrC9ypKoNHGtW50ASXsoCjTNpfCBptYWCz6EJSXEDfTfu6Qd9IEkp57K24rDjzNDPYIOAVroYxxCW1VhQFUGKyn2KE3sSyWpCxU-SDsYDBWrR6QKCnDbHbTismkulu8acVmK8wImBe0jxMOA0H77xfwb9-Y4LNLApJd12trR6R8yau9eFSo_6U6sYFOAIuzQRS3Nl3H7HVVMlna9pnukkDgtvC3rUjAvfV2PrjOYNITBj6TwLtuH00ZxigPKG8O0oxO-gA6ckcmjMV_JsODpfFuz4Gl4BvyZ4_D5VKG0eIRIqw3z4L1jDYV8x15P6FKPH32f5nUJm5kDbjWHv5YxscTqQixqsWhqmOfiWTfidTcf_qw4RBnHkOOmB4-YHmiSSmmHV77LBOU77fbZeDiEiPb0__DL9GZZetpXqdkq6K5S0wMDVD4JTb2EeTMXMJwqyHZ2SOiSt98bZJFXaqS8lcLeaXtENDlwnWJxVTtPFil7kSp6SshOh8HIXOf66uwGHZI0Z7c-DDpKw0VxicbHT6PdpqNQ5yVebLD5LxtsVv-6wWXamHn9BA.6eW7Pyvc42ozgCmGbPCTTOKreEyESKho_ChGJW_Z-HHA28HWG2e-7fOzcD_6u_BRS_R04cYL0j1tgmmZom5zaw" 4 | ] 5 | } -------------------------------------------------------------------------------- /testdata/test-example-00-e-file-trailing_chars.smart-health-card: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "verifiableCredential": [ 4 | "eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.3ZJLj9MwFIX_yuiyTZM4UEKzo4PEY4GQGNigLlzntjHyI7KdaMoo_5173Y54aGZWrMjuxsefzzn2HegYoYMhpTF2VRVHVGW0MqQBpUlDqWToY4W30o4GY0XqCQMU4PYH6MTLplm3r9r181K0BcwKujtIpxGh-_YL-Tft2XlY8UCkx3Xa2snpHzJp754UKj_rXmxgV4AK2KNLWprP0_47qsSWDoMOXzFE5nTwoqxLQTz-u51cb5A1AaOfgsKbbB8uC8UlDihvDNHOTuiAcKKMRJ6M-RIMCe73dzUJ7ocHwJ8oDu3nCqXFM0RabYgHrx1pQsxnHPWMjnv84AeetyXsFgq41xT-jUzMEpu1WNVi1dSwLMWDbsTTbt7_WXFMMk0xx-X7TsgXNEultMNr32eC8r12x2w8nmJCe3k-dDODaUsfjhU3W0XdV2q-JYDKO6GpW1h2SwHjpYJs54ABHXv7vUESeaWmkJc47I22Z0STA9cci6o6-GDpObIXqZIPjOx1HI3MdW6vr96iwyDN1TsfR52koaKoROPTx8nueSvU-ROPNtj8lw02m3_dYMsLC30_AQ.drW2nyE3vROTNI4SpQmIb2P1gp5jGRDqr_cnwZGmsZ3-Yl7WfpEO2lll_WrjjYO0IpDtFxkksyKIt3O3R7yvJA" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /testdata/test-example-00-d-jws-bad-fhir-metadata.txt: -------------------------------------------------------------------------------- 1 | eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.3VJNb9swDP0rgXaNP9csiG9JWuwD21o0WYFiyEGRmViDLBmSbCQr_N9HyunWDU0Pw04zfCH5-Ei-pwcmnWMFq7xvXJEkrgERu5pbXwFXvooFt6VL4MDrRoFLEN2CZWOmtztWZG8uXk8nE_zjdDIZs06w4oH5YwOs-PqL80-6V0MQUYBU53Gyrlstv3MvjX4RKEwny2zGNmMmLJSgveRq1W6_gfC00q6S9g6sI56CXcRpnCEfZRetLhUQxoIzrRWwDuuzU2HMZInRSoDmVprsZ3q4kgmjFA4ZFsS59oin48BWqS9WIeCRtkgR8Bg8M-8Gr8R-kpbXMJDwWirkY3ONGOvCjL3sQJO8H0xF8SJmmx7v3krU5JJ74spmkyxKsyhPWd-Pn90me3mb978rX4PnhApawMFbPqLUqOOqJTEciNZKP9wuTBko5h8z1m96rHruWxfEolfkgVzvuBBSwzKAQ5PU-9Dvjs5DfXqUaHelprGx-4TsSpwsE9EdovXVp5vr2_ntfbS8vryKVvcrzCDtaXieTofZzUnWcOIOLGi696krCDIC1w8lEnAt64EiDyKmJBXKvzO2xqdPG3LhjSXKUrpG8WDRYjl6CxosV6N3xjXSc4XiozHK-M9tvaVWloYvO-tK_leunFM_zv8H_fPZv9Z_SoUevx8.m6jbsH0YelM3D8PlTViUgyJK9fS69clhAmMzYljxntz_p2szQ6Dp-Z6koVJWS5RfHARqhWXstOULRwZvwtb_2w -------------------------------------------------------------------------------- /testdata/test-example-00-e-file-bad_jwks.smart-health-card: -------------------------------------------------------------------------------- 1 | { 2 | "verifiableCredential": [ 3 | "eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.3ZLLbtswEEV_JZhuZb3a1JB2dQL0BRQFkmZTeEFRY4spHwI5UuoG-vcOaadIgSSrrsodyZk79x7yHlQI0MJANIa2KLy4y_eKhqmbAnrpLKGlXDpTGCW9C25HxYBC07CSwvdhNQutekHK2dXV5efCCGULwkB8JopO9Ld3PwJkYLsdtNXbuqybdVO-zs-bdQazhPYe6DAitN__WAhGeDrOyNOMV48GstTzdcqYyapfyc2LhdLNqq8a2GYgPfYcUQl9NXW3KCla2g3K36APUaeFN3mZV6wXTzeT7TXGGo_BTV7idbIPp4vsFAek05rVjk54gD9wRlaetP7mNRc89LclFzxsnhD-ynG4PzIUBo8iwijNevDOco0PacZezWgjx09uiPtNDtuFA3aKw18KilpVc16tympVl7As2ZNuqpfdfPwbcSBBU0hxzaiRMD7QLKRUFi9cnxSk65XdJ-PhEAjN6bvxywx6nTu_LyLZIqi-kPNPFpCpE-pyDct2yWA8IUh2dujRRm-PCXKRk3Ly6SqGvVbmKFGnwGWMxah2zhv0yYuQ5HyU7FUYtUg4Nxdn79GiF_rsgwujIqEZFEPUjr5MpoutUKZVPUuw_i8J1s2_JriOFwuv3w.mSgEsv1KogzkzSWbDNxpVS8-iPDtcKnWqVbgOxup2wal5BPgwsyvGmbMFochz335nA-GZGlImawLbsOyULOpqw" 4 | ] 5 | } -------------------------------------------------------------------------------- /testdata/test-example-00-fhirhealthcard.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Parameters", 3 | "parameter":[{ 4 | "name": "verifiableCredential", 5 | "valueString": "eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.3ZJLb9swEIT_SrC9ynohjSDd6hTI41AUaNpL4QNNrS0WfAgkJcQN9N-7SztIWyQ55RTdVhx-nBnyAVQI0MEQ4xi6oggjyjwY4eOAQschl8L3ocB7YUaNoSD1hB4ysNsddNVFXV-0ZX3e5G3TZjBL6B4gHkaE7ucT83_ch-Ow4oFQL-uUMZNVv0VUzr4qlG5WfdXCJgPpsUcbldDfpu0vlJEt7Qblf6APzOngPC_zinj8dz3ZXiNrPAY3eYl3yT6cFrJTHJBOa6IdndAB_kAZiTxp_d1rEjzu70oSPA7PgL9SHNrPHQqDR4gwShMPPlnS-JDO2KsZLfd46wae1zlsFgq4VRT-s4jMqtqP1aqsVnUJy5I966Z63c3NvxWHKOIUUly-8Ih8QbOQUlm8dH0iSNcru0_GwyFENKf3Qzcz6CZ3fl9ws0VQfSHnewLItBPqsoFls2QwnipIdnbo0bK3vxskkZNy8mmJw94pc0TUKXDJsaiqnfOG3iN7ETI6z8hehVGLVOf68uwKLXqhz65dGFUUmoqiErWLXyaz5a1Qpq96scH6XTZYt2_dYMMLC31_AA.CdIxK-awV5j6TbhbYtQ3YA9toe9kfRNvPTL72JlkXD7DXuaNKkTYMKvLv8gVdR_h7YFf5gzOZeWPi69wMLDuCA" 6 | }] 7 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.alwaysShowStatus": true, 3 | "eslint.format.enable": true, 4 | "cSpell.words": [ 5 | "badcrl", 6 | "clearline", 7 | "Codeable", 8 | "Excludable", 9 | "execa", 10 | "exitcode", 11 | "fhir", 12 | "fhirbundle", 13 | "fhirhealthcard", 14 | "fhirout", 15 | "fhirvalidator", 16 | "fidm", 17 | "grayscale", 18 | "healthcard", 19 | "istextorbinary", 20 | "jsqr", 21 | "jwks", 22 | "jwkset", 23 | "jwspayload", 24 | "libressl", 25 | "logfile", 26 | "loglevel", 27 | "loinc", 28 | "LOINC", 29 | "myfhirbundle", 30 | "npmpackage", 31 | "outdir", 32 | "pako", 33 | "passcode", 34 | "pngjs", 35 | "qrcode", 36 | "qrnumeric", 37 | "repos", 38 | "shlink", 39 | "testdata", 40 | "testif", 41 | "uuidv" 42 | ], 43 | "npm.exclude": "**/js" 44 | } -------------------------------------------------------------------------------- /shl-server/public/main.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | background-color: #dbdbdb; 7 | } 8 | 9 | div { 10 | margin-left: 2em; 11 | margin-right: 2em; 12 | 13 | } 14 | 15 | textarea { 16 | font-family: 'Consolas'; 17 | font-size: medium; 18 | width: 100%; 19 | display: block; 20 | padding: 1em; 21 | margin-bottom: 2em; 22 | resize: vertical; 23 | border: solid 1px black; 24 | border-radius: 0.3em; 25 | box-shadow: 4px 4px 4px #A0A0A0; 26 | } 27 | 28 | input[type="button"] { 29 | font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif; 30 | font-weight: 600; 31 | font-size: large; 32 | padding: 1em; 33 | border: solid 1px black; 34 | border-radius: 0.3em; 35 | box-shadow: 4px 4px 4px #A0A0A0; 36 | } 37 | 38 | label { 39 | font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif; 40 | font-weight: 600; 41 | font-size: large; 42 | display: block; 43 | margin-bottom: 1em; 44 | text-shadow: 4px 4px 4px #A0A0A0; 45 | } -------------------------------------------------------------------------------- /src/check-for-update.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import got from 'got'; 5 | import semver from 'semver'; 6 | 7 | export async function latestDevToolsVersion(): Promise { 8 | try { 9 | const packageJson : {version: string} = await got('https://raw.githubusercontent.com/smart-on-fhir/health-cards-dev-tools/main/package.json').json(); 10 | if (!packageJson?.version) return; 11 | const v = semver.valid(packageJson.version); 12 | return v ? v : undefined; 13 | } catch { 14 | return; 15 | } 16 | } 17 | 18 | export async function latestSpecVersion(): Promise { 19 | try { 20 | const SPEC_PREFIX_LENGTH = '# Changelog\n\n## '.length; 21 | const VERSION_LENGTH = 'v.v.v'.length; 22 | const body = (await got('https://raw.githubusercontent.com/smart-on-fhir/health-cards/main/docs/changelog.md')).body; 23 | if (!body) return; 24 | const v = semver.valid(body.substring(SPEC_PREFIX_LENGTH, SPEC_PREFIX_LENGTH + VERSION_LENGTH)); 25 | return v ? v : undefined; 26 | } catch { 27 | return; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | workflow_dispatch: 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ${{ matrix.os }} 17 | 18 | strategy: 19 | matrix: 20 | os: [ubuntu-latest, macos-latest, windows-latest] 21 | node-version: [14.x, 15.x, 16.x, 17.x] 22 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-java@v1 27 | with: 28 | java-version: '11' 29 | - name: Use Node.js ${{ matrix.node-version }} 30 | uses: actions/setup-node@v1 31 | with: 32 | node-version: ${{ matrix.node-version }} 33 | - run: npm ci 34 | - run: npm run build --if-present 35 | - run: npm test -- --verbose --runInBand 36 | timeout-minutes: 10 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /testdata/test-example-00-d-jws-jws_too_long.txt: -------------------------------------------------------------------------------- 1 | eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.3VRbb6M4GP0rI_c1TbikgeRpQ9LpNNvcRNJOZlUhxxhsMJC1DYSO8t_XJs3srDSptA_7sgYJ2T4-Pud8Nt8BFQKMAJHyIEa9nsgglwRDJkkXQR6KHj7C7MCw6ClgiTnoAAolGJkDsz_oO_2B0x1YVgdUCIy-g99QkUt8VPN__KCs67pb292Cxz3LMN0e4jjEuaSQiV5lgtcOkM0B6xXPmNOIwj3Dkx8Ytd9VbTc1L_L49jx0q4c-RNMsK3P6BiUt8g-BqKhoaA61tL_F-uU-wUhqlxGhXGkVmmcE-l2jayo-PeqVeciwxnAsipIjvGm9gfeJi1eACsYU21mJ2oA3KgDFXDK25UwBLutHhgJcOr8gXik7ar0C5TDDZxKYUab4wDhXGC7aPWJa4VyHPCuI7ntd8HpSBmOch6qqI5DBVt-eqjCmUGpuc3hn3hrmrWWA06nzS3Xmx-oe_xm5kFCWorWvz5TEumAVRIjmeFKELQMqQprHrRHRCImz99OpKkWY0x4jnXRP0LCHqqMiQO1KYBkOOL2eOuDwHkkrJ8Ic51rbz4kqUIFQydspbXZDszOF1Ro2tK0Dp-pkNP67N8lL3AGsQGcz18htTc4KuSizfZvrUyE_3RhtM6_GaP0vY7SG_0mMztUY7Y9jfLrserksYOxNPq0I5BlEzVXW_r9l3RAqAvXCoMK8CVhxaXkcaEiAjwhjXZ9A_XgCU92zACkVEEl1XQNGMyqDqOCBupuqKGEwe_GDBSWlcSCPKXFdWyQpS5PoJk2ICCOX2Gnih6oTuXGqPmFczfhd9Dax-95qNZ9Gc7JsUOjf2_fZ8c2fMQH73ng6drKHceKxOmGDyHJ3S-KYxWLhht_u7tcQfVlXokqwb7PZtDT77E_6_DBh5m5mrIW1eYYvd1-304UfOQcrcuZNY3nNNzN52zrrrSwnD5-bXWKbS_Flvfg93u9QRMXjSzGvpuvN08PWFSlJiTbxOa1JVDeOerzH5dbaLD2H7MnQjfOhO7TdJI-HbmIP8zjZ3u-iNXuiz5XhVaval9ncHwyrZep643GqCvh6Uu0v.WZZrR7E2e3pZgovfID3UdQoUeY7kQSpDsw_VrUJIqlIe4Ocs-ytVi-S3NY5ZQ9kmgrRmIA-aKLGu3UKvfNvtFA -------------------------------------------------------------------------------- /testdata/test-example-00-e-file-jws_too_long.smart-health-card: -------------------------------------------------------------------------------- 1 | { 2 | "verifiableCredential": [ 3 | "eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.3VRbb6M4GP0rI_c1TbikgeRpQ9LpNNvcRNJOZlUhxxhsMJC1DYSO8t_XJs3srDSptA_7sgYJ2T4-Pud8Nt8BFQKMAJHyIEa9nsgglwRDJkkXQR6KHj7C7MCw6ClgiTnoAAolGJkDsz_oO_2B0x1YVgdUCIy-g99QkUt8VPN__KCs67pb292Cxz3LMN0e4jjEuaSQiV5lgtcOkM0B6xXPmNOIwj3Dkx8Ytd9VbTc1L_L49jx0q4c-RNMsK3P6BiUt8g-BqKhoaA61tL_F-uU-wUhqlxGhXGkVmmcE-l2jayo-PeqVeciwxnAsipIjvGm9gfeJi1eACsYU21mJ2oA3KgDFXDK25UwBLutHhgJcOr8gXik7ar0C5TDDZxKYUab4wDhXGC7aPWJa4VyHPCuI7ntd8HpSBmOch6qqI5DBVt-eqjCmUGpuc3hn3hrmrWWA06nzS3Xmx-oe_xm5kFCWorWvz5TEumAVRIjmeFKELQMqQprHrRHRCImz99OpKkWY0x4jnXRP0LCHqqMiQO1KYBkOOL2eOuDwHkkrJ8Ic51rbz4kqUIFQydspbXZDszOF1Ro2tK0Dp-pkNP67N8lL3AGsQGcz18htTc4KuSizfZvrUyE_3RhtM6_GaP0vY7SG_0mMztUY7Y9jfLrserksYOxNPq0I5BlEzVXW_r9l3RAqAvXCoMK8CVhxaXkcaEiAjwhjXZ9A_XgCU92zACkVEEl1XQNGMyqDqOCBupuqKGEwe_GDBSWlcSCPKXFdWyQpS5PoJk2ICCOX2Gnih6oTuXGqPmFczfhd9Dax-95qNZ9Gc7JsUOjf2_fZ8c2fMQH73ng6drKHceKxOmGDyHJ3S-KYxWLhht_u7tcQfVlXokqwb7PZtDT77E_6_DBh5m5mrIW1eYYvd1-304UfOQcrcuZNY3nNNzN52zrrrSwnD5-bXWKbS_Flvfg93u9QRMXjSzGvpuvN08PWFSlJiTbxOa1JVDeOerzH5dbaLD2H7MnQjfOhO7TdJI-HbmIP8zjZ3u-iNXuiz5XhVaval9ncHwyrZep643GqCvh6Uu0v.WZZrR7E2e3pZgovfID3UdQoUeY7kQSpDsw_VrUJIqlIe4Ocs-ytVi-S3NY5ZQ9kmgrRmIA-aKLGu3UKvfNvtFA" 4 | ] 5 | } -------------------------------------------------------------------------------- /shl-server/README.md: -------------------------------------------------------------------------------- 1 | # SMART Health Links Test Server 2 | 3 | A simple implementation of a SMART Health Links server for testing purposes. 4 | This should not be used in production. 5 | 6 |
7 | 8 | ### Build 9 | ``` 10 | cd ./shl-server 11 | tsc 12 | ``` 13 | 14 | ### Run 15 | ``` 16 | node ./src/server.js 17 | ``` 18 | 19 |
20 | 21 | ### Port 22 | By default, the SHL server listens on port `8090`. 23 | To change this, update the port in the `./shl-server/src/config.ts` file and rebuild. 24 | 25 |
26 | 27 | ### Data 28 | Data for the server is placed in the `./shl` folder in a specific JSON format or created with the 'create-link' rest call. 29 | See the html sample in `./public/index.html` 30 | 31 |
32 |
33 | 34 | ### REST Endpoints 35 | 36 |
37 | 38 | #### `/create-link` 39 | This adds new data to the server and returns a new __SMART Health Link__ to retrieve that data. 40 | ``` 41 | POST http://localhost:8090/create-link data:SHLLinkRequest 42 | ``` 43 | 44 |
45 | 46 | #### `/*` 47 | This is for requesting a manifest by a particular random path: 48 | ``` 49 | POST http://localhost:8090/ERn18l4lL3_5yPXMSzx3ZejyMM_XIeWeseXa3DXsJD0 data:ShlinkManifestRequest 50 | ``` 51 | 52 |
53 | 54 | #### `/shl/*` 55 | This is for requesting a manifest file by a particular random path: 56 | ``` 57 | GET http://localhost:8090/shl/05pHiEfsMU1kLb9-oTVknO4VUB9oP6Ot4ET46cjJFzU 58 | ``` -------------------------------------------------------------------------------- /testdata/test-example-00-f-qr-code-numeric-value-0-odd-count.txt: -------------------------------------------------------------------------------- 1 | shc:/67629095243206034602924374044603122295953265460346029254077280433602870286471674522280928613331456437653141590640220306450459085643550341424541364037063665417137241236380304375622046737407532323925433443326057360106452931531270742428395038692212766728660427527042080322573776302062041022437658685343255858002167283807585708105505622752282407670809680507692361773323356626342441696640756760420443377667210963225304674530596075400038617044612140293774753658337057662140695453717052692609314157047066212936577024093936587120504354425034740728273663064352427127670650296731041041526111672372755841032130642227316463677366061065095838427769033473772024296539747172763158437500341205552904210458383257587430101033127222744063776058732325243477725920113029325929083334451032223707067150422927585643582150673960036577724025701136525340592769757767206275650627402477697211533573565509427029706707250839003554763240717063642937600341397477744331106637691073081227572532546203755976433177745943122062635528341006764322546706055905771143603022560663095523331226455500652270723362680968083803763221413307293953052656395443322574687627443426332275717711532929254352587508605435565206712633236374775326666866323563710761560824680054232839705721762577651272106420776632037175111227273638376668606266033565034510450906366703505453432108691270703365110974534373232339127570013542650411585366432827590028706105672459077525712221726061360466106934602226403158625341200570210644745059203758773734373940117244687075123404571224453168276435523552346420 -------------------------------------------------------------------------------- /testdata/test-example-00-f-qr-code-numeric-value-0-wrong_qr_header.txt: -------------------------------------------------------------------------------- 1 | shc:5676290952432060346029243740446031222959532654603460292540772804336028702864716745222809286133314564376531415906402203064504590856435503414245413640370636654171372412363803043756220467374075323239254334433260573601064529315375327525284350386543453972454105613877280341684444263630267536256610652964356275116035404032045008041065413640042753256071655511572765540710122022662409262228543675554144403745376432112725206656333638722710403426123230352640292409664454221027442743433545715957316850353171637655337441322469662760285437665773006450620610700475423521206829550476675962044011376353347322684253413300704425342012336061614069663105651020054162381071210042506620063209562573424540321150697134717157282666502129385075317172270303297560583538524005036675320974211166280826390463001229702761506807647456357423355877755227120630247456283628667739750729036361365008613105520327303527120620111068247740742171076922737556374254066866636540775230714445093145120303691073081126572528446844366610312575093731055838406068310409103521346309455757455623602460345303577259642169037065551243062674414125502909450720327200247163102273296231262411414523701012266145051232263856386239230773545558104544092833585672343745417143355603277162310329260875654568772168625671544308505566244073123364712209343036633337413029051141327642120730555709522575717050117026051242505365232922773312737420012075102069530473325444060934396161087528117767647736761003552752603668743308280767362544093375773323642760336531742353673463036325683371600931526911421206656555650368253520 -------------------------------------------------------------------------------- /testdata/test-example-00-f-qr-code-numeric-value-0-wrong-multi-chunk.txt: -------------------------------------------------------------------------------- 1 | shc:/1/1/56762909524320603460292437404460312229595326546034602925407728043360287028647167452228092861333145643765314159064022030645045908564355034142454136403706366541713724123638030437562204673740753232392543344332605736010645292953127074242843503861221276716852752941725536670334373625647345380024213944077025250726312423573657001132105220316267750968640761356508111008270666243020277044446712214341455936637024282703544034660963252707282555072932056232255262395660612010735336331255715610420057716412306973057066214536651135113958591233120032575026733958333075072812533734264534700060266054734545664338772667663471584128617435526828390065275357404052057121004150076600323056277610287226003175060305765803534256207472564464060539095425076777272921345209305565332021506258456045760350722804223710051277402927664527742911662372066523664321240336446744622769760467573259652733383263657311072452563376417025746807407539006144613252696869456340066810522645386256555532111000531265754227302628303438085756243800662563286838775672222439672172403542396107375860647335106645704512536703506321757004413636764365347431287321355256580631556063583463563610567737660541737377552828605563116564297412076854033003344323337052606873573426066033102439280977115921594064314334576408722871427337224310757744412937522268303871367257627564750472597763507745283571571263580550066921715611703323062474012471272931363924743604256803437445104259400424433362673769543855403976310365501153573745056364696326060377575776050561075823064353055604551028500311453022452525062305534574 -------------------------------------------------------------------------------- /testdata/test-example-00-f-qr-code-numeric-value-0-trailing_chars.txt: -------------------------------------------------------------------------------- 1 | 2 | shc:/56762909524320603460292437404460312229595326546034602925407728043360287028647167452228092861333145643765314159064022030645045908564355034142454136403706366541713724123638030437562204673740753232392543344332605736010645293161123274252843507672607639453207402430776607352444072636263360583163776571612776281030555232666650080410064408075226454269326172757056577777650527565844664432596739250541374127412642033268362167406323684239664407420603660726440343682236324007354427093239316763640669126904110430032154743072617128677526590050443100395771054363441140226275064352057065672777296710080740306150694364755841072030053033314267693503500710687038422366323443772524086539746668753136397700720804545308200420523457587030535321117222114063236059732333273439726020543030323729093200372832225610065577402910665427742911662372066522684325320337525344582769750467367075652735423261657311072056567176437025586807047539006140773224677204423341600455387431324223537039395371105042432532326203757500431039705843332472637132336906055622116904057505741165642922560662005523342352407057615940064203435541056800294423303409266742045905387427616744297008072021274373107340243856524264622954071028050545033839201254546009660900262367345328436845283561347504272806325542097369120960747623330439705737080562665230663734353975116572563873400037343533716111637403056406505544327031220603502036015569420565762406733734393328073867366428530535045867086126372368695054657445266470450600446310425767243405636363504269616144340328672371257562627076302871063406371076732920 3 | -------------------------------------------------------------------------------- /testdata/test-example-00-e-file-multi-jws-issues.smart-health-card: -------------------------------------------------------------------------------- 1 | { 2 | "verifiableCredential": [ 3 | "eyJ6aXAiOiJERUYiLCJraWQiOiIzS2ZkZy1Yd1AtN2dYeXl3dFVmVUFEd0J1bURPUEtNUXgtaUVMTDExVzlzIn0.3ZJLb9swEIT_SrC9yhIlNFGsW50CfRyKAk17CXygqbXFgg-BpIS4gf57d2kHfSDJqafqtuLw48yQD6BjhA6GlMbYVVUcUZXRypAGlCYNpZKhjxXeSzsajBWpJwxQgNvtoauvmuayvRZXbSmu1wXMCroHSMcRobv7xfwb9-o0rHgg1PM6be3k9A-ZtHcvCpWfdV-vYVuACtijS1qaL9PuO6rElvaDDt8wROZ08LoUZU08_ruZXG-QNQGjn4LC22wfzgvFOQ4obwzRTk7ogHCkjESejPkaDAke93eCBI_DE-DPFIf2c4fS4gkirTbEgzeONCHmMw56Rsc9fvQDz5sStgsF3GkK_1YmZtXry3ol6lUjYFmKJ93UL7v58GfFMck0xRyXLzwhX9AsldIOb3yfCcr32h2y8XiMCe35_dDNDKYtfThU3GwVdV-p-Z4AKu-ERrSwbJcCxnMF2c4eAzr29nuDJPJKTSEvcdhbbU-IJgcWHIuq2vtg6T2yF6mSD4zsdRyNzHVubi7eocMgzcV7H0edpKGiqETj06fJ7ngriPzVzzbY_JcNNut_3WDLCwt9PwE.9ricS3s_xR1fl7YndWq5q0mc4-VeFBJNWpsaJULhccl8QIUnwWxtXc8Ps0C-vU-w_vcsoTnRggW_dex7afcj5w", 4 | "eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.3ZJLj9MwFIX_yuiyTZM4UEKzo4PEY4GQGNigLlzntjHyI7KdaMoo_5173Y54aGZWrMjuxsefzzn2HegYoYMhpTF2VRVHVGW0MqQBpUlDqWToY4W30o4GY0XqCQMU4PYH6MTLplm3r9r181K0BcwKujtIpxGh-_YL-Tft2XlY8UCkx3Xa2snpHzJp754UKj_rXmxgV4AK2KNLWprP0_47qsSWDoMOXzFE5nTwoqxLQTz-u51cb5A1AaOfgsKbbB8uC8UlDihvDNHOTuiAcKKMRJ6M-RIMCe73dzUJ7ocHwJ8oDu3nCqXFM0RabYgHrx1pQsxnHPWMjnv84AeetyXsFgq41xT-jUzMEpu1WNVi1dSwLMWDbsTTbt7_WXFMMk0xx-X7TsgXNEultMNr32eC8r12x2w8nmJCe3k-dDODaUsfjhU3W0XdV2q-JYDKO6GpW1h2SwHjpYJs54ABHXv7vUESeaWmkJc47I22Z0STA9cci6o6-GDpObIXqZIPjOx1HI3MdW6vr96iwyDN1TsfR52koaKoROPTx8nueSvU-ROPNtj8lw02m3_dYMsLC30_AQ.drW2nyE3vROTNI4SpQmIb2P1gp5jGRDqr_cnwZGmsZ3-Yl7WfpEO2lll_WrjjYO0IpDtFxkksyKIt3O3R7yvJA" 5 | ] 6 | } -------------------------------------------------------------------------------- /testdata/test-example-00-e-file-multi-jws.smart-health-card: -------------------------------------------------------------------------------- 1 | { 2 | "verifiableCredential": [ 3 | "eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.3ZJJb9swEIX_SjC9ytqaxJVudQp0ORQFmvZS-EBTY4sFF4GLEDfQf-8M7aALkpx6qm4jPn5875H3oEKAHsYYp9BXVZhQlsEIH0cUOo6lFH4IFd4JM2kMFakTeijA7vbQN9dte7W-ftm9Krv2soBZQn8P8Tgh9N9-Mf_GvTgNKx4I9bROGZOs-iGicvZZoXSzGpoOtgVIjwPaqIT-nHbfUUa2tB-V_4o-MKeHy7IuG-Lx302yg0bWeAwueYm32T6cF4pzHJBOa6KdnNAB_kgZiZy0_uI1CR729zUJHoZHwJ8oDu3nDoXBE0QYpYkHry1pfMhnHNSMlnv84EaeNyVsFwq4UxT-jYjMarqrZlU3q7aGZSkeddM87-b9nxWHKGIKOS5feES-oFlIqSzeuCETpBuUPWTj4RgimvP7oZsZ9bp0_lBxs1VQQyXnOwLIvBPaeg3LdilgOleQ7ezRo2VvvzdIIidl8nmJw94qc0K0OXDNsaiqvfOG3iN7ETI6z8hBhUmLXOfm5uItWvRCX7xzYVJRaCqKStQufkxmx1uhzl_zZIPtf9lg2_3rBte8sND3Ew.EtHJLQTEwQ1Fq0XwZ7WhU1EXNkpRrcSdUTyL0n_8bfRZ2lmrlG30zffy22j4gD3Xb2e1d7I_08ZKCZFF3D2bZw", 4 | "eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IkVCS09yNzJRUURjVEJVdVZ6QXprZkJUR2V3MFpBMTZHdVd0eTY0blMtc3cifQ.3ZJJb9swEIX_SjC9ytraOrZujQt0ORQFmuZS-EBTY4sFF4GLEDfQf-8M7aBtkOSUU3Wj-Obje4-8AxUCdDDEOIauqsKIsgxG-Dig0HEopfB9qPBWmFFjqEid0EMBdreHrlm27dvL5ev1qlzVTQGThO4O4nFE6H78YT7EvTotFrwg1NM6ZUyy6peIytlnhdJNqm_WsC1AeuzRRiX0t7T7iTKypf2g_A36wJwO3pR12RCP_14l22tkjcfgkpd4ne3DeaM4xwHptCbayQkd4I-UkchJ6-9ek-B-vqtJcL94BPyV4tA8dygMniDCKE08eGdJ40M-46AmtNzjZ2HZx6aE7UwBd4rCvxeRWc162SzqZtHWMM_Fo26a5918-rfiEEVMIcflC4_IFzQJKZXFjeszQbpe2UM2Ho4hojm_H7qZQV-Wzh8qbrYKqq_kdEsAmSehrVcwb-cCxnMF2c4ePVr29neDJHJSJp-3OOy1MidEmwPXHIuq2jtv6D2yFyGj84zsVRi1yHVebS4-oEUv9MVHF0YVhaaiqETt4pdkdjwKdf7aJxts_8sG2_VLN7jijZm-3w.Ma9Hatts9reee7DA7Sj8rfcIkH816fI1LkFSXj3s1aT6tdGmDiveDE2yNqi7ZHHKMViFInas_37Hi6mMrQW4BA" 5 | ] 6 | } -------------------------------------------------------------------------------- /testdata/test-example-00-d-jws-no_deflate.txt: -------------------------------------------------------------------------------- 1 | eyJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.eyJpc3MiOiJodHRwczovL3NwZWMuc21hcnRoZWFsdGguY2FyZHMvZXhhbXBsZXMvaXNzdWVyIiwibmJmIjoxNjIyNTc3NzE5LjUxMiwidmMiOnsidHlwZSI6WyJodHRwczovL3NtYXJ0aGVhbHRoLmNhcmRzI2hlYWx0aC1jYXJkIiwiaHR0cHM6Ly9zbWFydGhlYWx0aC5jYXJkcyNpbW11bml6YXRpb24iLCJodHRwczovL3NtYXJ0aGVhbHRoLmNhcmRzI2NvdmlkMTkiXSwiY3JlZGVudGlhbFN1YmplY3QiOnsiZmhpclZlcnNpb24iOiI0LjAuMSIsImZoaXJCdW5kbGUiOnsicmVzb3VyY2VUeXBlIjoiQnVuZGxlIiwidHlwZSI6ImNvbGxlY3Rpb24iLCJlbnRyeSI6W3siZnVsbFVybCI6InJlc291cmNlOjAiLCJyZXNvdXJjZSI6eyJyZXNvdXJjZVR5cGUiOiJQYXRpZW50IiwibmFtZSI6W3siZmFtaWx5IjoiQW55cGVyc29uIiwiZ2l2ZW4iOlsiSm9obiIsIkIuIl19XSwiYmlydGhEYXRlIjoiMTk1MS0wMS0yMCJ9fSx7ImZ1bGxVcmwiOiJyZXNvdXJjZToxIiwicmVzb3VyY2UiOnsicmVzb3VyY2VUeXBlIjoiSW1tdW5pemF0aW9uIiwic3RhdHVzIjoiY29tcGxldGVkIiwidmFjY2luZUNvZGUiOnsiY29kaW5nIjpbeyJzeXN0ZW0iOiJodHRwOi8vaGw3Lm9yZy9maGlyL3NpZC9jdngiLCJjb2RlIjoiMjA3In1dfSwicGF0aWVudCI6eyJyZWZlcmVuY2UiOiJyZXNvdXJjZTowIn0sIm9jY3VycmVuY2VEYXRlVGltZSI6IjIwMjEtMDEtMDEiLCJwZXJmb3JtZXIiOlt7ImFjdG9yIjp7ImRpc3BsYXkiOiJBQkMgR2VuZXJhbCBIb3NwaXRhbCJ9fV0sImxvdE51bWJlciI6IjAwMDAwMDEifX0seyJmdWxsVXJsIjoicmVzb3VyY2U6MiIsInJlc291cmNlIjp7InJlc291cmNlVHlwZSI6IkltbXVuaXphdGlvbiIsInN0YXR1cyI6ImNvbXBsZXRlZCIsInZhY2NpbmVDb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL2hsNy5vcmcvZmhpci9zaWQvY3Z4IiwiY29kZSI6IjIwNyJ9XX0sInBhdGllbnQiOnsicmVmZXJlbmNlIjoicmVzb3VyY2U6MCJ9LCJvY2N1cnJlbmNlRGF0ZVRpbWUiOiIyMDIxLTAxLTI5IiwicGVyZm9ybWVyIjpbeyJhY3RvciI6eyJkaXNwbGF5IjoiQUJDIEdlbmVyYWwgSG9zcGl0YWwifX1dLCJsb3ROdW1iZXIiOiIwMDAwMDA3In19XX19fX0.pS1aZw1kiIXvh4YjK_-HPBDrHqUfbM5mKeghZQXN7g3goAOvxnccR8GiFZl31T9m2Nlh4qmAe0eeSIHC9TvqKQ -------------------------------------------------------------------------------- /testdata/test-example-00-f-qr-code-numeric-value-0-number-too-big.txt: -------------------------------------------------------------------------------- 1 | shc:/567829095243206034602924374044603122295953265460346029254077280433602870286471674522280928613331456437653141590640220306450459085643550341424541364037063665417137241236380304375622046737407532323925433443326057360106412933611232742435036968002752296526207739446552292707342220625569645827637565055958085564361052455541506170775369722071337039290674530011055312081065204336284024325344755068352000551231252552423053305664536234332155690621622133743620117770297071043429524128736872687630393374704561206868215058614328050760252250570537546950574550724364573410732277243952071133646061636053626607230423342912611277752353065652696275722769240331756571530307256532723172290304740442500837373406703768337666261025584053126357590305350421251269557171385641584363685629423756563161050041582607343211376154072143562963557456616010213442322853526124596958101162093236502606276323582733504337235843237521502843704023125060347774382228105344583506636922002921641035382863723512082568726375647029773007034239006544773229375273755374361110302052456612684045544173357510075923632724287638087339033900662524686738057243293244632672030638435907390824102408103823503964606671232256532112066252617436584068554021444139347167217303675860376560750771525070773636280867365257657161706956093425264076433120716064696957245731774168626070077745523323530538305410640956661142733937432704776655523925652359240700316566557174307539676026543931355031213273257303097407665629776011014475347045122211642952221060656835312366242468036569092028613863757153733750321150500027506162672405557361767272036245243967390758003236600540352572574264230958690327344174 -------------------------------------------------------------------------------- /schema/keyset-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://smarthealth.cards/schema/keyset-schema.json", 4 | "title": "root", 5 | "type": "object", 6 | "required": [ 7 | "keys" 8 | ], 9 | "properties": { 10 | "keys": { 11 | "type": "array", 12 | "minItems": 1, 13 | "items": { 14 | "type": "object", 15 | "properties": { 16 | "kty": { 17 | "type": "string" 18 | }, 19 | "kid": { 20 | "type": "string", 21 | "pattern": "^[0-9a-zA-Z_-]+$" 22 | }, 23 | "use": { 24 | "type": "string", 25 | "pattern": "^(sig|enc)$" 26 | }, 27 | "alg": { 28 | "type": "string" 29 | }, 30 | "crv": { 31 | "type": "string" 32 | }, 33 | "x": { 34 | "type": "string", 35 | "pattern": "^[0-9a-zA-Z_-]+$" 36 | }, 37 | "y": { 38 | "type": "string", 39 | "pattern": "^[0-9a-zA-Z_-]+$" 40 | } 41 | }, 42 | "required": [ 43 | "kty", 44 | "kid" 45 | ] 46 | } 47 | } 48 | }, 49 | "additionalProperties": false 50 | } -------------------------------------------------------------------------------- /src/shlManifest.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import { ErrorCode } from "./error"; 5 | import Log from "./logger"; 6 | import { IOptions } from "./options"; 7 | import { parseJson, unexpectedProperties } from "./utils"; 8 | import * as shlManifestFile from "./shlManifestFile"; 9 | 10 | export async function validate(shlinkManifestJson: string, options: IOptions): Promise { 11 | const log = new Log("SHL-Manifest"); 12 | 13 | const manifest = parseJson(shlinkManifestJson); 14 | 15 | if (!manifest) { 16 | return log.fatal(`Cannot decode payload as JSON`, ErrorCode.INVALID_SHLINK); 17 | } 18 | 19 | const files = manifest?.files; 20 | 21 | if (!(files instanceof Array) || files.length === 0) { 22 | return log.fatal(`manifest files property missing or not Array`, ErrorCode.SHLINK_VERIFICATION_ERROR); 23 | } 24 | 25 | log.debug(`Manifest:\n${JSON.stringify(manifest, null, 2)}`); 26 | 27 | const unexpectedProps = unexpectedProperties(manifest as unknown as Record, ["files"]); 28 | if (unexpectedProps.length) { 29 | log.warn( 30 | `Unexpected properties on manifest : ${unexpectedProps.join(",")}`, 31 | ErrorCode.SHLINK_VERIFICATION_ERROR 32 | ); 33 | } 34 | 35 | log.info(`${files.length} shlink files returned.`); 36 | 37 | if (options.cascade) { 38 | for (let i = 0; i < files.length; i++) { 39 | const file = files[i]; 40 | log.child.push(await shlManifestFile.validate(JSON.stringify(file), { ...options, index: i })); 41 | } 42 | } 43 | return log; 44 | } 45 | -------------------------------------------------------------------------------- /testdata/test-example-00-e-file-no_deflate.smart-health-card: -------------------------------------------------------------------------------- 1 | { 2 | "verifiableCredential": [ 3 | "eyJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.eyJpc3MiOiJodHRwczovL3NwZWMuc21hcnRoZWFsdGguY2FyZHMvZXhhbXBsZXMvaXNzdWVyIiwibmJmIjoxNjIyNTc3NzE5LjUxMiwidmMiOnsidHlwZSI6WyJodHRwczovL3NtYXJ0aGVhbHRoLmNhcmRzI2hlYWx0aC1jYXJkIiwiaHR0cHM6Ly9zbWFydGhlYWx0aC5jYXJkcyNpbW11bml6YXRpb24iLCJodHRwczovL3NtYXJ0aGVhbHRoLmNhcmRzI2NvdmlkMTkiXSwiY3JlZGVudGlhbFN1YmplY3QiOnsiZmhpclZlcnNpb24iOiI0LjAuMSIsImZoaXJCdW5kbGUiOnsicmVzb3VyY2VUeXBlIjoiQnVuZGxlIiwidHlwZSI6ImNvbGxlY3Rpb24iLCJlbnRyeSI6W3siZnVsbFVybCI6InJlc291cmNlOjAiLCJyZXNvdXJjZSI6eyJyZXNvdXJjZVR5cGUiOiJQYXRpZW50IiwibmFtZSI6W3siZmFtaWx5IjoiQW55cGVyc29uIiwiZ2l2ZW4iOlsiSm9obiIsIkIuIl19XSwiYmlydGhEYXRlIjoiMTk1MS0wMS0yMCJ9fSx7ImZ1bGxVcmwiOiJyZXNvdXJjZToxIiwicmVzb3VyY2UiOnsicmVzb3VyY2VUeXBlIjoiSW1tdW5pemF0aW9uIiwic3RhdHVzIjoiY29tcGxldGVkIiwidmFjY2luZUNvZGUiOnsiY29kaW5nIjpbeyJzeXN0ZW0iOiJodHRwOi8vaGw3Lm9yZy9maGlyL3NpZC9jdngiLCJjb2RlIjoiMjA3In1dfSwicGF0aWVudCI6eyJyZWZlcmVuY2UiOiJyZXNvdXJjZTowIn0sIm9jY3VycmVuY2VEYXRlVGltZSI6IjIwMjEtMDEtMDEiLCJwZXJmb3JtZXIiOlt7ImFjdG9yIjp7ImRpc3BsYXkiOiJBQkMgR2VuZXJhbCBIb3NwaXRhbCJ9fV0sImxvdE51bWJlciI6IjAwMDAwMDEifX0seyJmdWxsVXJsIjoicmVzb3VyY2U6MiIsInJlc291cmNlIjp7InJlc291cmNlVHlwZSI6IkltbXVuaXphdGlvbiIsInN0YXR1cyI6ImNvbXBsZXRlZCIsInZhY2NpbmVDb2RlIjp7ImNvZGluZyI6W3sic3lzdGVtIjoiaHR0cDovL2hsNy5vcmcvZmhpci9zaWQvY3Z4IiwiY29kZSI6IjIwNyJ9XX0sInBhdGllbnQiOnsicmVmZXJlbmNlIjoicmVzb3VyY2U6MCJ9LCJvY2N1cnJlbmNlRGF0ZVRpbWUiOiIyMDIxLTAxLTI5IiwicGVyZm9ybWVyIjpbeyJhY3RvciI6eyJkaXNwbGF5IjoiQUJDIEdlbmVyYWwgSG9zcGl0YWwifX1dLCJsb3ROdW1iZXIiOiIwMDAwMDA3In19XX19fX0.pS1aZw1kiIXvh4YjK_-HPBDrHqUfbM5mKeghZQXN7g3goAOvxnccR8GiFZl31T9m2Nlh4qmAe0eeSIHC9TvqKQ" 4 | ] 5 | } -------------------------------------------------------------------------------- /testdata/test-example-00-f-qr-code-numeric-value-0-index-out-of-range.txt: -------------------------------------------------------------------------------- 1 | shc:/3/2/567629095243206034602924374044603122295953265460346029254077280433602870286471674522280928613331456437653141590640220306450459085643550341424541364037063665417137241236380304375622046737407532323925433443326057360108412933611266742435036976277033544576635734394036684100683066681053524360663375217231302708277121345868501004612012714242696103423239256439557306657077390337093976693859332309606330737236703354214252432721435674662769637025656030742003547628656258254572767642280564400065005675006567255662346829732153422123595850577077084309507750570672434010425945703966422326627636432765523210313438236005242372005845335376395430417011446527554105120667324409356477606755425762397509623339562174735276456670660969573259051077200338285069523355306138370809303935286045115843737607104076722826205756556210204406292577664231387630704070590540662823062126712806346504255665242723676974662443210773242712213412603528323204652460076358690876217223263457396129067074543707351266647367540842361266542056647565044237353462537632756131541137260905563243262137724321376426102441041031520923061227105832356540500507774025602073697237233874326942604027444267675532723035356620000676417268632677552420755256676339404150362030373462634577326074276131386560332954695358742858506268283520587506036929315533596059565954206253696104305722327162730758646336055734100429404305764443374367251170445445530906367066425872586541755560041268066152740628045658542627255661121252063272212523096104386861635775216872535856097277573670435032357422017038052908613670093458572205544253120957442304067059714358745536603156542763252424607177117509357507214175230829620322085007643726082129744006550407715064282241777377545636 -------------------------------------------------------------------------------- /shl-server/src/types.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | type JWE = string; 5 | type JWS = string; 6 | 7 | type PayloadFlags = "L" | "P" | "LP" | "U" | "LU"; 8 | 9 | interface VerifiableCredential { 10 | verifiableCredential: JWS[]; 11 | } 12 | 13 | type ShlinkContentType = "application/smart-health-card" | "application/smart-api-access" | "application/fhir+json"; 14 | 15 | interface ShlinkManifest { 16 | files: ShlinkFile[]; 17 | } 18 | 19 | interface ShlinkFile { 20 | contentType: ShlinkContentType; 21 | embedded?: string; 22 | location?: string; 23 | } 24 | 25 | interface ShlinkManifestRequest { 26 | url: string, 27 | recipient: string; 28 | passcode?: string; 29 | embeddedLengthMax?: number; 30 | } 31 | 32 | interface ShlinkPayload { 33 | url: string; 34 | key: string; 35 | exp?: number; 36 | flag?: PayloadFlags; 37 | label?: string; 38 | v?: number; 39 | } 40 | 41 | interface SHLEncoding { 42 | link: string, 43 | passcode: string, 44 | attempts: number, 45 | randomUrlSegment: string, 46 | preserveFilePaths: boolean, 47 | filePaths: string[], 48 | payload: ShlinkPayload, 49 | jweFiles: string[], 50 | qrcode: string; 51 | } 52 | 53 | type SHLStore = Record; 54 | 55 | interface SHLLinkRequest { 56 | passcode?: string; 57 | viewer?: string; 58 | attempts?: number; 59 | filePaths?: string[]; 60 | payload?: { 61 | path?: string; 62 | key?: string; 63 | exp?: number | string; 64 | flag?: PayloadFlags; 65 | label?: string; 66 | v?: number; 67 | }; 68 | files: { verifiableCredential: JWS[] }[]; 69 | } -------------------------------------------------------------------------------- /src/keys.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import { JWK } from "node-jose"; 5 | 6 | 7 | // 1-to-many mapping of Issuer URL to key-ids (kid) 8 | // keyMap = { 'https://spec.smarthealth.cards/examples/issuer' : { "ARrigjsh8mTqaVdihxO5cxkaRjpjXUg8ARET6IzhvaQ" : 1, }) 9 | let keyMap = Object.create(null) as Record>; 10 | 11 | const keyStore = { 12 | add: add, 13 | clear: clear, 14 | check: check, 15 | get: get, 16 | store: JWK.createKeyStore() 17 | } 18 | 19 | 20 | function get(kid: string): JWK.RawKey { 21 | return keyStore.store.get(kid); 22 | } 23 | 24 | 25 | function check(kid: string, issuer: string): boolean { 26 | return !!keyMap[issuer]?.[kid]; 27 | } 28 | 29 | 30 | async function add(key: JWK.Key, issuer?: string): Promise { 31 | 32 | const keyOut : JWK.Key = await keyStore.store.add(key); 33 | 34 | if (issuer) { 35 | keyMap[issuer] = keyMap[issuer] || Object.create(null) as Record; 36 | keyMap[issuer][key.kid] = keyMap[issuer][key.kid] || 1; 37 | } 38 | 39 | return keyOut; 40 | } 41 | 42 | 43 | function clear(): void { 44 | keyStore.store = JWK.createKeyStore(); 45 | keyMap = Object.create(null) as Record>; 46 | // To Delete the keys out of the store without blowing it away. 47 | // const p: Promise[] = []; 48 | // internalStore.all().forEach(rawKey => p.push(JWK.asKey(rawKey))); 49 | // return Promise.all(p) 50 | // .then(keys => keys.forEach(key => internalStore.remove(key))); 51 | } 52 | 53 | 54 | export type KeySet = { 55 | keys: JWK.Key[] 56 | } 57 | 58 | 59 | export default keyStore; -------------------------------------------------------------------------------- /testdata/test-example-00-a-fhirBundle-empty-values.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "collection", 4 | "entry": [ 5 | { 6 | "fullUrl": "resource:0", 7 | "resource": { 8 | "resourceType": "Patient", 9 | "name": [ 10 | { 11 | "family": "Anyperson", 12 | "given": [ 13 | "John", 14 | "B." 15 | ] 16 | } 17 | ], 18 | "birthDate": "1951-01-20" 19 | } 20 | }, 21 | { 22 | "fullUrl": "resource:1", 23 | "resource": { 24 | "resourceType": "Immunization", 25 | "status": "completed", 26 | "vaccineCode": { 27 | "coding": [] 28 | }, 29 | "patient": { 30 | "reference": "resource:0" 31 | }, 32 | "occurrenceDateTime": "2021-01-01", 33 | "performer": [ 34 | { 35 | "actor": { 36 | "display": "ABC General Hospital" 37 | } 38 | } 39 | ], 40 | "lotNumber": "0000001" 41 | } 42 | }, 43 | { 44 | "fullUrl": "resource:2", 45 | "resource": { 46 | "resourceType": "Immunization", 47 | "status": "completed", 48 | "vaccineCode": { 49 | "coding": [ 50 | { 51 | "system": "http://hl7.org/fhir/sid/cvx", 52 | "code": "207" 53 | } 54 | ] 55 | }, 56 | "patient": { 57 | "reference": "resource:0" 58 | }, 59 | "occurrenceDateTime": "2021-01-29", 60 | "performer": [ 61 | { 62 | "actor": {} 63 | } 64 | ], 65 | "lotNumber": "" 66 | } 67 | } 68 | ] 69 | } -------------------------------------------------------------------------------- /testdata/test-example-00-fhirhealthcard-multi-jws.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Parameters", 3 | "parameter":[{ 4 | "name": "verifiableCredential", 5 | "valueString": "eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.3ZJLb9swEIT_SrC9ynohjSDd6hTI41AUaNpL4QNNrS0WfAgkJcQN9N-7SztIWyQ55RTdVhx-nBnyAVQI0MEQ4xi6oggjyjwY4eOAQschl8L3ocB7YUaNoSD1hB4ysNsddNVFXV-0ZX3e5G3TZjBL6B4gHkaE7ucT83_ch-Ow4oFQL-uUMZNVv0VUzr4qlG5WfdXCJgPpsUcbldDfpu0vlJEt7Qblf6APzOngPC_zinj8dz3ZXiNrPAY3eYl3yT6cFrJTHJBOa6IdndAB_kAZiTxp_d1rEjzu70oSPA7PgL9SHNrPHQqDR4gwShMPPlnS-JDO2KsZLfd46wae1zlsFgq4VRT-s4jMqtqP1aqsVnUJy5I966Z63c3NvxWHKOIUUly-8Ih8QbOQUlm8dH0iSNcru0_GwyFENKf3Qzcz6CZ3fl9ws0VQfSHnewLItBPqsoFls2QwnipIdnbo0bK3vxskkZNy8mmJw94pc0TUKXDJsaiqnfOG3iN7ETI6z8hehVGLVOf68uwKLXqhz65dGFUUmoqiErWLXyaz5a1Qpq96scH6XTZYt2_dYMMLC31_AA.CdIxK-awV5j6TbhbYtQ3YA9toe9kfRNvPTL72JlkXD7DXuaNKkTYMKvLv8gVdR_h7YFf5gzOZeWPi69wMLDuCA" 6 | },{ 7 | "name": "verifiableCredential", 8 | "valueString": "eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IkVCS09yNzJRUURjVEJVdVZ6QXprZkJUR2V3MFpBMTZHdVd0eTY0blMtc3cifQ.3ZJLb9swEIT_SrC9ynoQgWPr1rhAkxyCAk17KXygqbXFgg-BpIS4gf57d2kHfSDJqafqRnH248yQT6BjhBb6lIbYVlUcUJXRypB6lCb1pZKhixU-SjsYjBWpRwxQgNvtoW2WQizXtbhclU19VcCkoH2CdBwQ2m-_mH_j3p0WC14Q6nWdtnZ0-odM2rs3hcpPumvWsC1ABezQJS3N53H3HVViS_teh68YInNauCzrsiEe_70eXWeQNQGjH4PCh2wfzhvFOQ4obwzRTk7ogHCkjEQejfkSDAme59uaBM-LF8CfKA7Nc4fS4gkirTbEg_eONCHmMw56Qsc93knHPjYlbGcKuNMU_oNMzGrWy2ZRNwtRwzwXL7pp3nZz-2fFMck0xhyXLzwhX9AkldION77LBOU77Q7ZeDzGhPb8fuhmenNV-nCouNkq6q5S0yMBVJ4EUa9g3s4FDOcKsp09BnTs7fcGSeSVGkPe4rAP2p4QIgeuORZVtffB0ntkL1IlHxjZ6TgYmeu83lx8RIdBmosbHwedpKGiqETj0_1odzwKdf7Eqw2K_7JBsf7XDa54Y6bvJw.DxbnmSq1ByYBi12POr7HBH1xdxYViFmL6T6hYcrj4GYmjOHQTA6_QU2guaeMvs_CDwu8ayYoIEp8LhwCfnz07Q" 9 | }] 10 | } -------------------------------------------------------------------------------- /shl-server/public/main.js: -------------------------------------------------------------------------------- 1 | 2 | const cache = {}; 3 | 4 | // Calls the Rest API on the server. 5 | // Caller will specify when return type is other than JSON 6 | // 7 | export async function restCall(url, data, options) { 8 | 9 | const xhr = new XMLHttpRequest(); 10 | 11 | options = { 12 | method: 'POST', 13 | responseType: 'json', 14 | cache: false, 15 | ...options 16 | } 17 | 18 | if (options.cache && cache[url]?.[data instanceof Object ? JSON.stringify(data) : data]) { 19 | const cached = cache[url][data instanceof Object ? JSON.stringify(data) : data]; 20 | console.log(`cached: ${url}`) 21 | return Promise.resolve(cached); 22 | } 23 | 24 | return new Promise(function (resolve, reject) { 25 | 26 | xhr.open(options.method, url); 27 | 28 | if (data instanceof Object) { 29 | xhr.setRequestHeader("Content-Type", "application/json"); 30 | data = JSON.stringify(data); 31 | } 32 | else if (typeof data === 'string') { 33 | xhr.setRequestHeader("Content-Type", "text/plain"); 34 | } 35 | 36 | xhr.responseType = options.responseType; 37 | 38 | xhr.onreadystatechange = function () { 39 | if (xhr.readyState === 4) { 40 | 41 | if (options.cache) { 42 | cache[url] = cache[url] || {} 43 | cache[url][data instanceof Object ? JSON.stringify(data) : data] = xhr.response; 44 | } 45 | 46 | resolve(xhr.response); 47 | } 48 | }; 49 | 50 | xhr.onerror = function (err) { 51 | reject(err); 52 | } 53 | 54 | options.method === 'POST' ? xhr.send(data) : xhr.send(); 55 | 56 | }); 57 | } -------------------------------------------------------------------------------- /src/fhirHealthCard.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as utils from './utils'; 5 | import { ErrorCode } from './error'; 6 | import * as jws from './jws-compact'; 7 | import Log from './logger'; 8 | import { IOptions } from './options'; 9 | 10 | // TODO: add schema validation as in healthCards.ts 11 | 12 | export async function validate(fhirHealthCardText: string, options: IOptions): Promise { 13 | 14 | const log = new Log('FHIR $health-cards-issue response'); 15 | 16 | if (fhirHealthCardText.trim() !== fhirHealthCardText) { 17 | log.error(`FHIR Health Card response has leading or trailing spaces`, ErrorCode.TRAILING_CHARACTERS); 18 | fhirHealthCardText = fhirHealthCardText.trim(); 19 | } 20 | 21 | const fhirHealthCard = utils.parseJson(fhirHealthCardText); 22 | if (fhirHealthCard == undefined) { 23 | return log.fatal("Failed to parse FHIR Health Card response data as JSON.", ErrorCode.JSON_PARSE_ERROR); 24 | } 25 | 26 | const param = fhirHealthCard.parameter; 27 | if ( 28 | !param || 29 | !(param instanceof Array) || 30 | param.length === 0 31 | ) { 32 | return log.fatal("fhirHealthCard.parameter array required to continue.", ErrorCode.CRITICAL_DATA_MISSING); 33 | } 34 | await Promise.all(param.filter(p => p.name === 'verifiableCredential').map(async (vc, i, VCs) => { 35 | if (!vc.valueString) { 36 | log.error(`Missing FHIR Health Card response data verifiableCredential #${i + 1} valueString`, i); 37 | } else { 38 | options.cascade && log.child.push(await jws.validate(vc.valueString, options, VCs.length > 1 ? i.toString() : '')); 39 | } 40 | })); 41 | 42 | return log; 43 | } 44 | -------------------------------------------------------------------------------- /testdata/invalid_no_SAN.public.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "kty": "EC", 5 | "kid": "MK_nzCdQcQciCSBeeFIX0iL0b3DIhdi8wUjZFgnfrfA", 6 | "use": "sig", 7 | "alg": "ES256", 8 | "x5c": [ 9 | "MIIBtzCCARmgAwIBAgIUfNpb4yqsSAykOKdAiIorUHwicpswCgYIKoZIzj0EAwIwLDEqMCgGA1UEAwwhU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBSb290IENBMB4XDTIxMDMzMDE5MjUyOFoXDTMxMDMyODE5MjUyOFowKzEpMCcGA1UEAwwgU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBJc3N1ZXIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQF1pE6s+W5MANm/Z01iBwdjcZBkS1beoA0QIvB9SVByMRyzOIWlBvxocrWLDYyt8tDl1JlEUM+1lwUf+t9zfY0oxowGDAJBgNVHRMEAjAAMAsGA1UdDwQEAwIHgDAKBggqhkjOPQQDAgOBiwAwgYcCQgH5SrlHjJ9HFK5tgqrguWrsAe8RHUUYrEwmAtwM3qAfKy8nBAlgUr2jutWMtTXAMVSNiBBJ9cn3nTOz31b3qKgPHwJBMokgQp5ZQF2400YzzVl8pG082n1uwcnMAfr3zP/fasrvOClDTCvYSfUCGMEf0A543D6e4Fw0RsJ7umojRSzYqvY=", 10 | "MIICezCCAd2gAwIBAgIUIN1Nm0YccN+P6yvNebNTlVpfP5wwCgYIKoZIzj0EAwIwLDEqMCgGA1UEAwwhU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBSb290IENBMB4XDTIxMDMzMDE5MjUyN1oXDTMxMDMyODE5MjUyN1owLDEqMCgGA1UEAwwhU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBSb290IENBMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAAXxtYgUAj4Ebz6VKAdB8EXZD87H8Ve1CBZOQtJX/X4i9alUOyLarJq4z0adPQqxM/T64WC3eGh4LpPPqxLJ/5KoAiE95FvK7H7Sv488RvDiiaV+QM6iTy78h4RTVCJZFBRILnV2ebay7qyTTMKzofiFC3vjt++b99Edq3Bd2IUWkdNGjgZkwgZYwDAYDVR0TBAUwAwEB/zAdBgNVHQ4EFgQUwamFeylvmYdym6D1Aq0WB6454p4wZwYDVR0jBGAwXoAUwamFeylvmYdym6D1Aq0WB6454p6hMKQuMCwxKjAoBgNVBAMMIVNNQVJUIEhlYWx0aCBDYXJkIEV4YW1wbGUgUm9vdCBDQYIUIN1Nm0YccN+P6yvNebNTlVpfP5wwCgYIKoZIzj0EAwIDgYsAMIGHAkFjjLV26lJV5PpX7QOrCH0ih3TBP/43lVmgIDB/AjSMvrfQ/kNz1wi9DgYVMXXx0Rv3vxCcW0pxjdd6zvS7W69vWgJCATvqILrYQ2Vp3twByx2HRPLpFi0iDkDggrOnF8j9+7kecvDOQg95a72HQ47YuW0LlCZU1OdNEWbaZZv2oXuXq5zR" 11 | ], 12 | "crv": "P-256", 13 | "x": "BdaROrPluTADZv2dNYgcHY3GQZEtW3qANECLwfUlQcg", 14 | "y": "xHLM4haUG_GhytYsNjK3y0OXUmURQz7WXBR_633N9jQ" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /testdata/invalid_DNS_SAN.public.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "kty": "EC", 5 | "kid": "Fz8GLb1XIVC5ymZu_479xtYRWEM0jaY_4Apn8BKwsUA", 6 | "use": "sig", 7 | "alg": "ES256", 8 | "x5c": [ 9 | "MIIB1jCCATegAwIBAgIUfNpb4yqsSAykOKdAiIorUHwicpwwCgYIKoZIzj0EAwIwLDEqMCgGA1UEAwwhU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBSb290IENBMB4XDTIxMDMzMDE5MjUyOFoXDTMxMDMyODE5MjUyOFowKzEpMCcGA1UEAwwgU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBJc3N1ZXIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAR/55Jm5T6sij1YmCcrabKDdERaETI4DEWsSFKez7qQ1Y09wSkacS/C5IR3ZFn58sfbCqDiPwamKvDy/qguicigozgwNjAJBgNVHRMEAjAAMAsGA1UdDwQEAwIHgDAcBgNVHREEFTATghFzbWFydGhlYWx0aC5jYXJkczAKBggqhkjOPQQDAgOBjAAwgYgCQgEeVIA6D/GQ/DYFHgrTdrK6I46keBaOqKrqpKlQBFWAvm58Itarwfwdz89PuFQqwwtIMM9pTZgKvSnbbeERvksT8QJCAZJDuZO0TKvT9KUw47KXYrTBCEY02teDt8l+v7cXQc+Lr33XvgzybL13HcII1XiaT2vwUOVcnGoORvQPQdSgLPnr", 10 | "MIICezCCAd2gAwIBAgIUIN1Nm0YccN+P6yvNebNTlVpfP5wwCgYIKoZIzj0EAwIwLDEqMCgGA1UEAwwhU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBSb290IENBMB4XDTIxMDMzMDE5MjUyN1oXDTMxMDMyODE5MjUyN1owLDEqMCgGA1UEAwwhU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBSb290IENBMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAAXxtYgUAj4Ebz6VKAdB8EXZD87H8Ve1CBZOQtJX/X4i9alUOyLarJq4z0adPQqxM/T64WC3eGh4LpPPqxLJ/5KoAiE95FvK7H7Sv488RvDiiaV+QM6iTy78h4RTVCJZFBRILnV2ebay7qyTTMKzofiFC3vjt++b99Edq3Bd2IUWkdNGjgZkwgZYwDAYDVR0TBAUwAwEB/zAdBgNVHQ4EFgQUwamFeylvmYdym6D1Aq0WB6454p4wZwYDVR0jBGAwXoAUwamFeylvmYdym6D1Aq0WB6454p6hMKQuMCwxKjAoBgNVBAMMIVNNQVJUIEhlYWx0aCBDYXJkIEV4YW1wbGUgUm9vdCBDQYIUIN1Nm0YccN+P6yvNebNTlVpfP5wwCgYIKoZIzj0EAwIDgYsAMIGHAkFjjLV26lJV5PpX7QOrCH0ih3TBP/43lVmgIDB/AjSMvrfQ/kNz1wi9DgYVMXXx0Rv3vxCcW0pxjdd6zvS7W69vWgJCATvqILrYQ2Vp3twByx2HRPLpFi0iDkDggrOnF8j9+7kecvDOQg95a72HQ47YuW0LlCZU1OdNEWbaZZv2oXuXq5zR" 11 | ], 12 | "crv": "P-256", 13 | "x": "f-eSZuU-rIo9WJgnK2myg3REWhEyOAxFrEhSns-6kNU", 14 | "y": "jT3BKRpxL8LkhHdkWfnyx9sKoOI_BqYq8PL-qC6JyKA" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /shl-server/src/store.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import { config } from "./config.js"; 5 | import fs from 'fs'; 6 | 7 | const shlStore: SHLStore = {}; 8 | 9 | // 10 | // Load links/files from filesystem if PERSIST_LINKS is true 11 | // 12 | function initStore() { 13 | if (!config.PERSIST_LINKS) return; 14 | const fileNames = fs.readdirSync('./shl'); 15 | fileNames.forEach(f => { 16 | let entry; 17 | const path = `./shl/${f}`; 18 | try { 19 | const text = fs.readFileSync(path).toString('utf-8') 20 | entry = JSON.parse(text) as SHLEncoding; 21 | } catch { 22 | console.error(`Could not parse file ${path}. Deleting.`); 23 | fs.rmSync(path); 24 | return; 25 | } 26 | shlStore[entry.randomUrlSegment] = entry; 27 | }); 28 | } 29 | 30 | // 31 | // Looks up an entry by id (random base64url string) 32 | // 33 | export function lookup(id: string): SHLEncoding | undefined { 34 | return shlStore[id]; 35 | } 36 | 37 | // 38 | // Stores an entry and persists to the file system if PERSIST_LINKS=true 39 | // 40 | export function update(entry: SHLEncoding): void { 41 | shlStore[entry.randomUrlSegment] = entry; 42 | if (!config.PERSIST_LINKS) return; 43 | const filePath = `./shl/${entry.randomUrlSegment}.json`; 44 | fs.writeFile(filePath, JSON.stringify(entry, null, 4), () => { 45 | console.debug(`${filePath} updated`); 46 | }); 47 | } 48 | 49 | // 50 | // Looks up a file by id (random base64url string) 51 | // 52 | export function file(id: string): JWE | undefined { 53 | const entry = Object.values(shlStore).find(entry => entry.filePaths.find(s => s === id)); 54 | if (!entry) return undefined; 55 | return entry.jweFiles[entry.filePaths.indexOf(id)]; 56 | } 57 | 58 | initStore(); 59 | -------------------------------------------------------------------------------- /testdata/valid_2_chain.public.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "kty": "EC", 5 | "kid": "IbSGhbvMoZcgHMYNuYGfmKi72O871daOtz0dgUPzxrE", 6 | "use": "sig", 7 | "alg": "ES256", 8 | "x5c": [ 9 | "MIIB7jCCAU+gAwIBAgIUfNpb4yqsSAykOKdAiIorUHwicpowCgYIKoZIzj0EAwIwLDEqMCgGA1UEAwwhU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBSb290IENBMB4XDTIxMDMzMDE5MjUyN1oXDTMxMDMyODE5MjUyN1owKzEpMCcGA1UEAwwgU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBJc3N1ZXIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQXK3RDGsJjxR/YNaprn+79l8BPFDjRcC7jEHtch0A10EmUYLk8s73JjpWQUM21Mph+bvtgyzBoQtNBgFtMNBENo1AwTjAJBgNVHRMEAjAAMAsGA1UdDwQEAwIHgDA0BgNVHREELTArhilodHRwczovL3NtYXJ0aGVhbHRoLmNhcmRzL2V4YW1wbGVzL2lzc3VlcjAKBggqhkjOPQQDAgOBjAAwgYgCQgDqvLGBE76M0TqieBvxTaL39NVuu8cCsZmyjQtt7f8lSHQzeBd4MWNP3tKDt4GhX+vtjnbcmqDmMyUxB15bUoxWiwJCAKU9nEUzE8IAz1n+CiGzxFIpGRyTT8zOs4c9Qz8MPR+79cQWQPRrzHTCxJjk0SlGnU3y0d5LrBNTdzlhwuUCfEZ7", 10 | "MIICezCCAd2gAwIBAgIUIN1Nm0YccN+P6yvNebNTlVpfP5wwCgYIKoZIzj0EAwIwLDEqMCgGA1UEAwwhU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBSb290IENBMB4XDTIxMDMzMDE5MjUyN1oXDTMxMDMyODE5MjUyN1owLDEqMCgGA1UEAwwhU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBSb290IENBMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAAXxtYgUAj4Ebz6VKAdB8EXZD87H8Ve1CBZOQtJX/X4i9alUOyLarJq4z0adPQqxM/T64WC3eGh4LpPPqxLJ/5KoAiE95FvK7H7Sv488RvDiiaV+QM6iTy78h4RTVCJZFBRILnV2ebay7qyTTMKzofiFC3vjt++b99Edq3Bd2IUWkdNGjgZkwgZYwDAYDVR0TBAUwAwEB/zAdBgNVHQ4EFgQUwamFeylvmYdym6D1Aq0WB6454p4wZwYDVR0jBGAwXoAUwamFeylvmYdym6D1Aq0WB6454p6hMKQuMCwxKjAoBgNVBAMMIVNNQVJUIEhlYWx0aCBDYXJkIEV4YW1wbGUgUm9vdCBDQYIUIN1Nm0YccN+P6yvNebNTlVpfP5wwCgYIKoZIzj0EAwIDgYsAMIGHAkFjjLV26lJV5PpX7QOrCH0ih3TBP/43lVmgIDB/AjSMvrfQ/kNz1wi9DgYVMXXx0Rv3vxCcW0pxjdd6zvS7W69vWgJCATvqILrYQ2Vp3twByx2HRPLpFi0iDkDggrOnF8j9+7kecvDOQg95a72HQ47YuW0LlCZU1OdNEWbaZZv2oXuXq5zR" 11 | ], 12 | "crv": "P-256", 13 | "x": "Fyt0QxrCY8Uf2DWqa5_u_ZfATxQ40XAu4xB7XIdANdA", 14 | "y": "SZRguTyzvcmOlZBQzbUymH5u-2DLMGhC00GAW0w0EQ0" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /testdata/test-example-00-a-fhirBundle-occurrence-issues.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "collection", 4 | "entry": [ 5 | { 6 | "fullUrl": "resource:0", 7 | "resource": { 8 | "resourceType": "Patient", 9 | "name": [ 10 | { 11 | "family": "Anyperson", 12 | "given": [ 13 | "John", 14 | "B." 15 | ] 16 | } 17 | ], 18 | "birthDate": "1951-01-20" 19 | } 20 | }, 21 | { 22 | "fullUrl": "resource:1", 23 | "resource": { 24 | "resourceType": "Immunization", 25 | "status": "completed", 26 | "vaccineCode": { 27 | "coding": [ 28 | { 29 | "system": "http://hl7.org/fhir/sid/cvx", 30 | "code": "207" 31 | } 32 | ] 33 | }, 34 | "patient": { 35 | "reference": "resource:0" 36 | }, 37 | "occurrenceDateTime": "2021-01-01", 38 | "occurrenceString": "2021-01-01", 39 | "performer": [ 40 | { 41 | "actor": { 42 | "display": "ABC General Hospital" 43 | } 44 | } 45 | ], 46 | "lotNumber": "0000001" 47 | } 48 | }, 49 | { 50 | "fullUrl": "resource:2", 51 | "resource": { 52 | "resourceType": "Immunization", 53 | "status": "completed", 54 | "vaccineCode": { 55 | "coding": [ 56 | { 57 | "system": "http://hl7.org/fhir/sid/cvx", 58 | "code": "207" 59 | } 60 | ] 61 | }, 62 | "patient": { 63 | "reference": "resource:0" 64 | }, 65 | "performer": [ 66 | { 67 | "actor": { 68 | "display": "ABC General Hospital" 69 | } 70 | } 71 | ], 72 | "lotNumber": "0000007" 73 | } 74 | } 75 | ] 76 | } -------------------------------------------------------------------------------- /testdata/test-example-00-a-fhirBundle-status-not-completed.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "collection", 4 | "entry": [ 5 | { 6 | "fullUrl": "resource:0", 7 | "resource": { 8 | "resourceType": "Patient", 9 | "name": [ 10 | { 11 | "family": "Anyperson", 12 | "given": [ 13 | "John", 14 | "B." 15 | ] 16 | } 17 | ], 18 | "birthDate": "1951-01-20" 19 | } 20 | }, 21 | { 22 | "fullUrl": "resource:1", 23 | "resource": { 24 | "resourceType": "Immunization", 25 | "status": "Completed", 26 | "vaccineCode": { 27 | "coding": [ 28 | { 29 | "system": "http://hl7.org/fhir/sid/cvx", 30 | "code": "207" 31 | } 32 | ] 33 | }, 34 | "patient": { 35 | "reference": "resource:0" 36 | }, 37 | "occurrenceDateTime": "2021-01-01", 38 | "performer": [ 39 | { 40 | "actor": { 41 | "display": "ABC General Hospital" 42 | } 43 | } 44 | ], 45 | "lotNumber": "0000001" 46 | } 47 | }, 48 | { 49 | "fullUrl": "resource:2", 50 | "resource": { 51 | "resourceType": "Immunization", 52 | "status": "Partial", 53 | "vaccineCode": { 54 | "coding": [ 55 | { 56 | "system": "http://hl7.org/fhir/sid/cvx", 57 | "code": "207" 58 | } 59 | ] 60 | }, 61 | "patient": { 62 | "reference": "resource:0" 63 | }, 64 | "occurrenceDateTime": "2021-01-29", 65 | "performer": [ 66 | { 67 | "actor": { 68 | "display": "ABC General Hospital" 69 | } 70 | } 71 | ], 72 | "lotNumber": "0000007" 73 | } 74 | } 75 | ] 76 | } -------------------------------------------------------------------------------- /src/fetch-profiles.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import fs from 'fs'; 5 | import got from 'got'; 6 | 7 | 8 | interface Profile { 9 | "snapshot": { 10 | "element": { 11 | "min": number, 12 | "max": string, 13 | "path": string, 14 | "patternCode": string 15 | }[] 16 | } 17 | } 18 | 19 | const patient_profile_dm_url = 'http://build.fhir.org/ig/dvci/vaccine-credential-ig/branches/main/StructureDefinition-vaccination-credential-patient-dm.json'; 20 | const immunization_profile_dm_url = 'http://build.fhir.org/ig/dvci/vaccine-credential-ig/branches/main/StructureDefinition-vaccination-credential-immunization-dm.json'; 21 | 22 | 23 | 24 | async function fetchProfile(url: string, fileName: string): Promise { 25 | 26 | let profileJson: Profile; 27 | 28 | try { 29 | profileJson = await got(url).json(); 30 | } catch { 31 | // if we didn't get a profile, just return and the existing one will get used 32 | console.warn("could not download profile from : " + url); 33 | console.warn("existing profile will be used : " + fileName); 34 | return; 35 | } 36 | 37 | const profile = { 38 | mustNotContain: profileJson.snapshot.element 39 | .filter(e => e.min === 0 && parseInt(e.max) === 0) 40 | .map(e => { return { "path": e.path } }), 41 | pattern: profileJson.snapshot.element 42 | .filter(e => e.patternCode) 43 | .map(e => { return { "path": e.path, "pattern": e.patternCode } }), 44 | }; 45 | 46 | // keep just the paths of the 0..0 properties 47 | fs.writeFileSync(fileName, JSON.stringify(profile, null, 4)); 48 | 49 | return; 50 | } 51 | 52 | 53 | void (async () => { 54 | await fetchProfile(patient_profile_dm_url, './schema/patient-dm.json'); 55 | await fetchProfile(immunization_profile_dm_url, './schema/immunization-dm.json'); 56 | })(); -------------------------------------------------------------------------------- /src/healthCard.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as utils from './utils'; 5 | import { validateSchema } from './schema'; 6 | import { ErrorCode } from './error'; 7 | import healthCardSchema from '../schema/smart-health-card-schema.json'; 8 | import * as jws from './jws-compact'; 9 | import Log from './logger'; 10 | import { IOptions } from './options'; 11 | 12 | 13 | export const schema = healthCardSchema; 14 | 15 | 16 | export async function validate(healthCardText: string, options: IOptions): Promise { 17 | 18 | const log = new Log('SMART Health Card'); 19 | 20 | if (healthCardText.trim() !== healthCardText) { 21 | log.error(`Health Card has leading or trailing spaces`, ErrorCode.TRAILING_CHARACTERS); 22 | healthCardText = healthCardText.trim(); 23 | } 24 | 25 | const healthCard = utils.parseJson(healthCardText); 26 | if (healthCard == undefined) { 27 | return log.fatal("Failed to parse HealthCard data as JSON.", ErrorCode.JSON_PARSE_ERROR); 28 | } 29 | 30 | // failures will be recorded in the log. we can continue processing. 31 | validateSchema(healthCardSchema, healthCard, log); 32 | 33 | 34 | // to continue validation, we must have a jws-compact string to validate 35 | const vc = healthCard.verifiableCredential; 36 | if ( 37 | !vc || 38 | !(vc instanceof Array) || 39 | vc.length === 0 || 40 | vc.find(e => {typeof e !== 'string'}) 41 | ) { 42 | // The schema check above will list the expected properties/type 43 | return log.fatal("HealthCard.verifiableCredential[jws-compact] required to continue.", ErrorCode.CRITICAL_DATA_MISSING); 44 | } 45 | 46 | if (options.cascade) { 47 | for (let i = 0; i < vc.length; i++) { 48 | log.child.push((await jws.validate(vc[i], options, vc.length > 1 ? i.toString() : ''))); 49 | } 50 | } 51 | 52 | return log; 53 | } 54 | -------------------------------------------------------------------------------- /testdata/test-example-00-a-fhirBundle-trailing_chars.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "resourceType": "Bundle", 4 | "type": "collection", 5 | "entry": [ 6 | { 7 | "fullUrl": "resource:0", 8 | "resource": { 9 | "resourceType": "Patient", 10 | "name": [ 11 | { 12 | "family": "Anyperson", 13 | "given": [ 14 | "John", 15 | "B." 16 | ] 17 | } 18 | ], 19 | "birthDate": "1951-01-20" 20 | } 21 | }, 22 | { 23 | "fullUrl": "resource:1", 24 | "resource": { 25 | "resourceType": "Immunization", 26 | "status": "completed", 27 | "vaccineCode": { 28 | "coding": [ 29 | { 30 | "system": "http://hl7.org/fhir/sid/cvx", 31 | "code": "207" 32 | } 33 | ] 34 | }, 35 | "patient": { 36 | "reference": "resource:0" 37 | }, 38 | "occurrenceDateTime": "2021-01-01", 39 | "performer": [ 40 | { 41 | "actor": { 42 | "display": "ABC General Hospital" 43 | } 44 | } 45 | ], 46 | "lotNumber": "0000001" 47 | } 48 | }, 49 | { 50 | "fullUrl": "resource:2", 51 | "resource": { 52 | "resourceType": "Immunization", 53 | "status": "completed", 54 | "vaccineCode": { 55 | "coding": [ 56 | { 57 | "system": "http://hl7.org/fhir/sid/cvx", 58 | "code": "207" 59 | } 60 | ] 61 | }, 62 | "patient": { 63 | "reference": "resource:0" 64 | }, 65 | "occurrenceDateTime": "2021-01-29", 66 | "performer": [ 67 | { 68 | "actor": { 69 | "display": "ABC General Hospital" 70 | } 71 | } 72 | ], 73 | "lotNumber": "0000007" 74 | } 75 | } 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /testdata/test-example-00-a-short-refs.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "collection", 4 | "entry": [ 5 | { 6 | "fullUrl": "http://resource:0", 7 | "resource": { 8 | "resourceType": "Patient", 9 | "name": [ 10 | { 11 | "family": "Anyperson", 12 | "given": [ 13 | "John", 14 | "B." 15 | ] 16 | } 17 | ], 18 | "gender": "male", 19 | "birthDate": "1951-01-20" 20 | } 21 | }, 22 | { 23 | "fullUrl": "resource:resource", 24 | "resource": { 25 | "resourceType": "Immunization", 26 | "status": "completed", 27 | "vaccineCode": { 28 | "coding": [ 29 | { 30 | "system": "http://hl7.org/fhir/sid/cvx", 31 | "code": "207" 32 | } 33 | ] 34 | }, 35 | "patient": { 36 | "reference": "resource0" 37 | }, 38 | "occurrenceDateTime": "2021-01-01", 39 | "lotNumber": "Lot #0000001", 40 | "performer": [ 41 | { 42 | "actor": { 43 | "display": "ABC General Hospital" 44 | } 45 | } 46 | ] 47 | } 48 | }, 49 | { 50 | "fullUrl": "not-a-short-resource-path-to-something", 51 | "resource": { 52 | "resourceType": "Immunization", 53 | "status": "completed", 54 | "vaccineCode": { 55 | "coding": [ 56 | { 57 | "system": "http://hl7.org/fhir/sid/cvx", 58 | "code": "207" 59 | } 60 | ] 61 | }, 62 | "patient": { 63 | "reference": "Patient/resource:0" 64 | }, 65 | "occurrenceDateTime": "2021-01-29", 66 | "lotNumber": "Lot #0000007", 67 | "performer": [ 68 | { 69 | "actor": { 70 | "display": "ABC General Hospital" 71 | } 72 | } 73 | ] 74 | } 75 | } 76 | ] 77 | } -------------------------------------------------------------------------------- /testdata/test-example-00-fhirhealthcard-with-resource-link.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Parameters", 3 | "parameter":[{ 4 | "name": "verifiableCredential", 5 | "valueString": "eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IjNLZmRnLVh3UC03Z1h5eXd0VWZVQUR3QnVtRE9QS01ReC1pRUxMMTFXOXMifQ.3ZJLb9swEIT_SrC9ynohjSDd6hTI41AUaNpL4QNNrS0WfAgkJcQN9N-7SztIWyQ55RTdVhx-nBnyAVQI0MEQ4xi6oggjyjwY4eOAQschl8L3ocB7YUaNoSD1hB4ysNsddNVFXV-0ZX3e5G3TZjBL6B4gHkaE7ucT83_ch-Ow4oFQL-uUMZNVv0VUzr4qlG5WfdXCJgPpsUcbldDfpu0vlJEt7Qblf6APzOngPC_zinj8dz3ZXiNrPAY3eYl3yT6cFrJTHJBOa6IdndAB_kAZiTxp_d1rEjzu70oSPA7PgL9SHNrPHQqDR4gwShMPPlnS-JDO2KsZLfd46wae1zlsFgq4VRT-s4jMqtqP1aqsVnUJy5I966Z63c3NvxWHKOIUUly-8Ih8QbOQUlm8dH0iSNcru0_GwyFENKf3Qzcz6CZ3fl9ws0VQfSHnewLItBPqsoFls2QwnipIdnbo0bK3vxskkZNy8mmJw94pc0TUKXDJsaiqnfOG3iN7ETI6z8hehVGLVOf68uwKLXqhz65dGFUUmoqiErWLXyaz5a1Qpq96scH6XTZYt2_dYMMLC31_AA.CdIxK-awV5j6TbhbYtQ3YA9toe9kfRNvPTL72JlkXD7DXuaNKkTYMKvLv8gVdR_h7YFf5gzOZeWPi69wMLDuCA" 6 | },{ 7 | "name": "verifiableCredential", 8 | "valueString": "eyJ6aXAiOiJERUYiLCJhbGciOiJFUzI1NiIsImtpZCI6IkVCS09yNzJRUURjVEJVdVZ6QXprZkJUR2V3MFpBMTZHdVd0eTY0blMtc3cifQ.3ZJLb9swEIT_SrC9ynoQgWPr1rhAkxyCAk17KXygqbXFgg-BpIS4gf57d2kHfSDJqafqRnH248yQT6BjhBb6lIbYVlUcUJXRypB6lCb1pZKhixU-SjsYjBWpRwxQgNvtoW2WQizXtbhclU19VcCkoH2CdBwQ2m-_mH_j3p0WC14Q6nWdtnZ0-odM2rs3hcpPumvWsC1ABezQJS3N53H3HVViS_teh68YInNauCzrsiEe_70eXWeQNQGjH4PCh2wfzhvFOQ4obwzRTk7ogHCkjEQejfkSDAme59uaBM-LF8CfKA7Nc4fS4gkirTbEg_eONCHmMw56Qsc93knHPjYlbGcKuNMU_oNMzGrWy2ZRNwtRwzwXL7pp3nZz-2fFMck0xhyXLzwhX9AkldION77LBOU77Q7ZeDzGhPb8fuhmenNV-nCouNkq6q5S0yMBVJ4EUa9g3s4FDOcKsp09BnTs7fcGSeSVGkPe4rAP2p4QIgeuORZVtffB0ntkL1IlHxjZ6TgYmeu83lx8RIdBmosbHwedpKGiqETj0_1odzwKdf7Eqw2K_7JBsf7XDa54Y6bvJw.DxbnmSq1ByYBi12POr7HBH1xdxYViFmL6T6hYcrj4GYmjOHQTA6_QU2guaeMvs_CDwu8ayYoIEp8LhwCfnz07Q" 9 | },{ 10 | "name": "resourceLink", 11 | "part": [{ 12 | "name": "vcIndex", 13 | "valueInteger": 0 14 | }, { 15 | "name": "bundledResource", 16 | "valueUri": "resource:2" 17 | }, { 18 | "name": "hostedResource", 19 | "valueUri": "https://fhir.example.org/Immunization/123" 20 | }] 21 | }] 22 | } -------------------------------------------------------------------------------- /shl-server/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | SHL Server Client 8 | 9 | 10 | 11 | 12 | 13 | 14 | 19 | 20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 |
28 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /testdata/test-example-00-a-fhirBundle-bad_meta_non_security.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "id": "Scenario1Bundle", 4 | "type": "collection", 5 | "entry": [ 6 | { 7 | "fullUrl": "resource:0", 8 | "resource": { 9 | "resourceType": "Patient", 10 | "name": [ 11 | { 12 | "family": "Anyperson", 13 | "given": [ 14 | "John", 15 | "B." 16 | ] 17 | } 18 | ], 19 | "birthDate": "1951-01-20" 20 | } 21 | }, 22 | { 23 | "fullUrl": "resource:1", 24 | "resource": { 25 | "resourceType": "Immunization", 26 | "meta": { 27 | "id": "bad-meta" 28 | }, 29 | "status": "completed", 30 | "vaccineCode": { 31 | "coding": [ 32 | { 33 | "system": "http://hl7.org/fhir/sid/cvx-TEMPORARY-CODE-SYSTEM", 34 | "code": "207" 35 | } 36 | ] 37 | }, 38 | "patient": { 39 | "reference": "resource:0" 40 | }, 41 | "occurrenceDateTime": "2021-01-01", 42 | "performer": [ 43 | { 44 | "actor": { 45 | "display": "ABC General Hospital" 46 | } 47 | } 48 | ], 49 | "lotNumber": "0000001" 50 | } 51 | }, 52 | { 53 | "fullUrl": "resource:2", 54 | "resource": { 55 | "resourceType": "Immunization", 56 | "meta": { 57 | "security": [{"code": "IAL1.2"}] 58 | }, 59 | "status": "completed", 60 | "vaccineCode": { 61 | "coding": [ 62 | { 63 | "system": "http://hl7.org/fhir/sid/cvx-TEMPORARY-CODE-SYSTEM", 64 | "code": "207" 65 | } 66 | ] 67 | }, 68 | "patient": { 69 | "reference": "resource:0" 70 | }, 71 | "occurrenceDateTime": "2021-01-29", 72 | "performer": [ 73 | { 74 | "actor": { 75 | "display": "ABC General Hospital" 76 | } 77 | } 78 | ], 79 | "lotNumber": "0000007" 80 | } 81 | } 82 | ] 83 | } -------------------------------------------------------------------------------- /testdata/test-example-00-a-fhirBundle-bad_meta_wrong_security.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "id": "Scenario1Bundle", 4 | "type": "collection", 5 | "entry": [ 6 | { 7 | "fullUrl": "resource:0", 8 | "resource": { 9 | "resourceType": "Patient", 10 | "name": [ 11 | { 12 | "family": "Anyperson", 13 | "given": [ 14 | "John", 15 | "B." 16 | ] 17 | } 18 | ], 19 | "birthDate": "1951-01-20" 20 | } 21 | }, 22 | { 23 | "fullUrl": "resource:1", 24 | "resource": { 25 | "resourceType": "Immunization", 26 | "meta": { 27 | "security": [{"badKey": "IAL1"}] 28 | }, 29 | "status": "completed", 30 | "vaccineCode": { 31 | "coding": [ 32 | { 33 | "system": "http://hl7.org/fhir/sid/cvx-TEMPORARY-CODE-SYSTEM", 34 | "code": "207" 35 | } 36 | ] 37 | }, 38 | "patient": { 39 | "reference": "resource:0" 40 | }, 41 | "occurrenceDateTime": "2021-01-01", 42 | "performer": [ 43 | { 44 | "actor": { 45 | "display": "ABC General Hospital" 46 | } 47 | } 48 | ], 49 | "lotNumber": "0000001" 50 | } 51 | }, 52 | { 53 | "fullUrl": "resource:2", 54 | "resource": { 55 | "resourceType": "Immunization", 56 | "meta": { 57 | "security": [{"code": "IAL1.2"}] 58 | }, 59 | "status": "completed", 60 | "vaccineCode": { 61 | "coding": [ 62 | { 63 | "system": "http://hl7.org/fhir/sid/cvx-TEMPORARY-CODE-SYSTEM", 64 | "code": "207" 65 | } 66 | ] 67 | }, 68 | "patient": { 69 | "reference": "resource:0" 70 | }, 71 | "occurrenceDateTime": "2021-01-29", 72 | "performer": [ 73 | { 74 | "actor": { 75 | "display": "ABC General Hospital" 76 | } 77 | } 78 | ], 79 | "lotNumber": "0000007" 80 | } 81 | } 82 | ] 83 | } -------------------------------------------------------------------------------- /testdata/test-example-00-a-fhirBundle-bad_meta_extra_key.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "id": "Scenario1Bundle", 4 | "type": "collection", 5 | "entry": [ 6 | { 7 | "fullUrl": "resource:0", 8 | "resource": { 9 | "resourceType": "Patient", 10 | "name": [ 11 | { 12 | "family": "Anyperson", 13 | "given": [ 14 | "John", 15 | "B." 16 | ] 17 | } 18 | ], 19 | "birthDate": "1951-01-20" 20 | } 21 | }, 22 | { 23 | "fullUrl": "resource:1", 24 | "resource": { 25 | "resourceType": "Immunization", 26 | "meta": { 27 | "id": "extra meta value", 28 | "security": [{"code": "IAL1"}] 29 | }, 30 | "status": "completed", 31 | "vaccineCode": { 32 | "coding": [ 33 | { 34 | "system": "http://hl7.org/fhir/sid/cvx-TEMPORARY-CODE-SYSTEM", 35 | "code": "207" 36 | } 37 | ] 38 | }, 39 | "patient": { 40 | "reference": "resource:0" 41 | }, 42 | "occurrenceDateTime": "2021-01-01", 43 | "performer": [ 44 | { 45 | "actor": { 46 | "display": "ABC General Hospital" 47 | } 48 | } 49 | ], 50 | "lotNumber": "0000001" 51 | } 52 | }, 53 | { 54 | "fullUrl": "resource:2", 55 | "resource": { 56 | "resourceType": "Immunization", 57 | "meta": { 58 | "security": [{"code": "IAL1.2"}] 59 | }, 60 | "status": "completed", 61 | "vaccineCode": { 62 | "coding": [ 63 | { 64 | "system": "http://hl7.org/fhir/sid/cvx-TEMPORARY-CODE-SYSTEM", 65 | "code": "207" 66 | } 67 | ] 68 | }, 69 | "patient": { 70 | "reference": "resource:0" 71 | }, 72 | "occurrenceDateTime": "2021-01-29", 73 | "performer": [ 74 | { 75 | "actor": { 76 | "display": "ABC General Hospital" 77 | } 78 | } 79 | ], 80 | "lotNumber": "0000007" 81 | } 82 | } 83 | ] 84 | } -------------------------------------------------------------------------------- /testdata/test-example-00-a-non-dm-properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "collection", 4 | "entry": [ 5 | { 6 | "fullUrl": "resource:0", 7 | "resource": { 8 | "managingOrganization": {"identifier" : {"type" : {"text" : "CodeableConcept.txt"}}}, 9 | "resourceType": "Patient", 10 | "meta" : "should not include meta", 11 | "name": [ 12 | { 13 | "family": "Anyperson", 14 | "given": [ 15 | "John", 16 | "B." 17 | ] 18 | } 19 | ], 20 | "gender": "male", 21 | "birthDate": "1951-01-20" 22 | } 23 | }, 24 | { 25 | "fullUrl": "resource:1", 26 | "resource": { 27 | "resourceType": "Immunization", 28 | "status": "completed", 29 | "id" : "ShouldNotIncludeId", 30 | "vaccineCode": { 31 | "coding": [ 32 | { 33 | "system": "http://hl7.org/fhir/sid/cvx", 34 | "code": "207", 35 | "display": "should not include .display" 36 | } 37 | ] 38 | }, 39 | "patient": { 40 | "reference": "resource:0" 41 | }, 42 | "occurrenceDateTime": "2021-01-01", 43 | "lotNumber": "Lot #0000001", 44 | "performer": [ 45 | { 46 | "actor": { 47 | "display": "ABC General Hospital" 48 | } 49 | } 50 | ] 51 | } 52 | }, 53 | { 54 | "fullUrl": "resource:2", 55 | "resource": { 56 | "resourceType": "Immunization", 57 | "status": "completed", 58 | "vaccineCode": { 59 | "coding": [ 60 | { 61 | "system": "http://hl7.org/fhir/sid/cvx", 62 | "code": "207" 63 | } 64 | ], 65 | "text": "textField" 66 | }, 67 | "patient": { 68 | "reference": "resource:0" 69 | }, 70 | "occurrenceDateTime": "2021-01-29", 71 | "lotNumber": "Lot #0000007", 72 | "performer": [ 73 | { 74 | "actor": { 75 | "display": "ABC General Hospital" 76 | } 77 | } 78 | ] 79 | } 80 | } 81 | ] 82 | } -------------------------------------------------------------------------------- /src/shlink.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import { ErrorCode } from "./error"; 5 | import Log from "./logger"; 6 | import { IOptions } from "./options"; 7 | import * as shlPayload from "./shlPayload"; 8 | 9 | export async function validate(shlinkText: string, options: IOptions): Promise { 10 | const log = new Log("SMART Health Link"); 11 | 12 | const regEx = /^(\s*)?(.+?#)?(.+?)\/?([0-9a-zA-Z-_+/]+)\n?(\s*)?$/; 13 | 14 | const shlinkHeader = "shlink:"; 15 | 16 | const [match, leadingWhiteSpace, viewer, header, body, trailingWhiteSpace] = [...(regEx.exec(shlinkText) ?? [])]; 17 | 18 | if (viewer) log.info(`Viewer: ${viewer}`); 19 | 20 | log.debug(`${JSON.stringify({ match: !!match, leadingWhiteSpace : !!leadingWhiteSpace, viewer: viewer ?? 'none', header, body, trailingWhiteSpace : !!trailingWhiteSpace }, null, 2)}`); 21 | 22 | if (viewer && !/https:\/\/.+?#/.test(viewer)) { 23 | log.error(`Unexpected viewer format`, ErrorCode.INVALID_SHLINK); 24 | } 25 | 26 | if (!match) { 27 | const expectedBody = `^(https://#)?${shlinkHeader}/[0-9a-zA-Z-_]+$`; 28 | return log.fatal(`Invalid shlink : expected ${expectedBody}`, ErrorCode.INVALID_SHLINK); 29 | } 30 | 31 | if (leadingWhiteSpace || trailingWhiteSpace) { 32 | log.warn(`SHLink contains leading or trailing whitespace`, ErrorCode.INVALID_SHLINK); 33 | } 34 | 35 | if (!header || header !== shlinkHeader) { 36 | log.error(`Invalid header (${header}). Expect shlink:/`, ErrorCode.INVALID_SHLINK); 37 | } 38 | 39 | if (!body) { 40 | return log.fatal(`Invalid body. Expect [0-9a-zA-Z-_]+ (Base64urlencoded)`, ErrorCode.INVALID_SHLINK); 41 | } 42 | 43 | if (/[+/]/.test(body)) { 44 | log.error(`Incorrect base64 format. Expect [0-9a-zA-Z-_]+ (Base64urlencoded)`, ErrorCode.INVALID_SHLINK); 45 | } 46 | 47 | let decodedText; 48 | try { 49 | decodedText = Buffer.from(body, "base64").toString("utf-8"); 50 | } catch (_err) { 51 | return log.fatal(`Cannot base64 decode body`, ErrorCode.INVALID_NUMERIC_QR); 52 | } 53 | 54 | log.debug(`Decoded Body:\n${decodedText}`); 55 | 56 | options.cascade && log.child.push((await shlPayload.validate(decodedText, options))); 57 | 58 | return log; 59 | } 60 | -------------------------------------------------------------------------------- /testdata/test-example-1195-byte-qrnumeric.txt: -------------------------------------------------------------------------------- 1 | shc:/567629095243206034602924374044603122295953265460346029254077280433602870286471674522280928613331456437653141590640220306450459085643550341424541364037063665417137241236380304375622046737407532323925433443326057360106454155531266742528535022732972716667113421200054714242395670037152734210623452722322293677745434102855426353750606557074662205402543687555366643235808577306091143732227422952667436417563360922362955035539432941266466283853746200297665372053036345545266236830096736640362296163305430503909545223363939743410603308606330106721454121366373775238121243566920525627442338506167426369415857075959405974703934425445436028756405403067673744376159415043623303053371453643392705653863725521274312033256584508005604603161043344666864427140656971103035643753734523626663080922055458283168204370244537697769116723584369004062282156732128113043702105442303433226540345113137387153527352040339506450453404580644053168227337222604660559744054232540632408313109542873563300435259683245245273432503746110747422617258213767420073746942660943104404346940366042454558355028103537120656060711613503281275712254112969347533275432756605410953632765344072451106615604336431592167676336520312695442420533273762365406757224205024223838246406202675670809376227236061770439282257347245667053746454725704320567113126102928521200224022676875643641107460267328663527440073086570770508220058006632286820326159702829760630435541412836535754730006595069714227325671436955730752085406035804700055347520775274681009592430652610536027710766632623332142096324560466032928747368415952547634640910036956640439222621087026083203572159121275285343456438040808700862385903005424425467746423616571356120732370285459424107247500577435647421573158253572060571395324043369046433053466456377640945320329263110702405562922075527091022430340126356676908680054064576686860740669535325666762647153307353305373425363386030222355705345227268546865306764552952683876625965392538540545225665661245277630286073274064555440324527636766432967317267760769602610702910224052736264200357266422505303243450113974542731243475091066357533322737004457426457722706203234107457435744435844601209537332002158075739257711227024336558007212573358047407525972326343271121012227664268752010086109365526552234210545533244303038210676075853377665437664342053743700293169204226577604616866655373454333556731720629091230643750403558575560305305057474 -------------------------------------------------------------------------------- /testdata/test-example-covid-lab-jwspayload-missing-lab-vc-type.json: -------------------------------------------------------------------------------- 1 | { 2 | "iss": "https://spec.smarthealth.cards/examples/issuer", 3 | "nbf": 1624455003.481, 4 | "vc": { 5 | "type": [ 6 | "https://smarthealth.cards#health-card", 7 | "https://smarthealth.cards#covid19" 8 | ], 9 | "credentialSubject": { 10 | "fhirVersion": "4.0.1", 11 | "fhirBundle": { 12 | "resourceType": "Bundle", 13 | "type": "collection", 14 | "entry": [ 15 | { 16 | "fullUrl": "resource:0", 17 | "resource": { 18 | "resourceType": "Patient", 19 | "name": [ 20 | { 21 | "family": "Anyperson", 22 | "given": [ 23 | "John", 24 | "B." 25 | ] 26 | } 27 | ], 28 | "birthDate": "1951-01-20" 29 | } 30 | }, 31 | { 32 | "fullUrl": "resource:1", 33 | "resource": { 34 | "resourceType": "Observation", 35 | "meta": { 36 | "security": [ 37 | { 38 | "system": "https://smarthealth.cards/ial", 39 | "code": "IAL2" 40 | } 41 | ] 42 | }, 43 | "status": "final", 44 | "code": { 45 | "coding": [ 46 | { 47 | "system": "http://loinc.org", 48 | "code": "94558-4" 49 | } 50 | ] 51 | }, 52 | "subject": { 53 | "reference": "resource:0" 54 | }, 55 | "effectiveDateTime": "2021-02-17", 56 | "valueCodeableConcept": { 57 | "coding": [ 58 | { 59 | "system": "http://snomed.info/sct", 60 | "code": "260373001" 61 | } 62 | ] 63 | }, 64 | "performer": [ 65 | { 66 | "display": "ABC General Hospital" 67 | } 68 | ] 69 | } 70 | } 71 | ] 72 | } 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /schema/patient-dm.json: -------------------------------------------------------------------------------- 1 | { 2 | "mustNotContain": [ 3 | { 4 | "path": "Patient.id" 5 | }, 6 | { 7 | "path": "Patient.meta.versionId" 8 | }, 9 | { 10 | "path": "Patient.meta.lastUpdated" 11 | }, 12 | { 13 | "path": "Patient.meta.source" 14 | }, 15 | { 16 | "path": "Patient.meta.profile" 17 | }, 18 | { 19 | "path": "Patient.meta.tag" 20 | }, 21 | { 22 | "path": "Patient.implicitRules" 23 | }, 24 | { 25 | "path": "Patient.language" 26 | }, 27 | { 28 | "path": "Patient.text" 29 | }, 30 | { 31 | "path": "Patient.contained" 32 | }, 33 | { 34 | "path": "Patient.extension" 35 | }, 36 | { 37 | "path": "Patient.modifierExtension" 38 | }, 39 | { 40 | "path": "Patient.identifier" 41 | }, 42 | { 43 | "path": "Patient.active" 44 | }, 45 | { 46 | "path": "Patient.name.id" 47 | }, 48 | { 49 | "path": "Patient.name.extension" 50 | }, 51 | { 52 | "path": "Patient.name.use" 53 | }, 54 | { 55 | "path": "Patient.telecom" 56 | }, 57 | { 58 | "path": "Patient.gender" 59 | }, 60 | { 61 | "path": "Patient.deceased[x]" 62 | }, 63 | { 64 | "path": "Patient.address" 65 | }, 66 | { 67 | "path": "Patient.address.text" 68 | }, 69 | { 70 | "path": "Patient.address.line" 71 | }, 72 | { 73 | "path": "Patient.maritalStatus" 74 | }, 75 | { 76 | "path": "Patient.multipleBirth[x]" 77 | }, 78 | { 79 | "path": "Patient.photo" 80 | }, 81 | { 82 | "path": "Patient.contact" 83 | }, 84 | { 85 | "path": "Patient.communication" 86 | }, 87 | { 88 | "path": "Patient.generalPractitioner" 89 | }, 90 | { 91 | "path": "Patient.managingOrganization" 92 | }, 93 | { 94 | "path": "Patient.link" 95 | } 96 | ], 97 | "pattern": [] 98 | } -------------------------------------------------------------------------------- /testdata/test-example-00-a-fhirBundle-profile-usa.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "collection", 4 | "entry": [ 5 | { 6 | "fullUrl": "resource:0", 7 | "resource": { 8 | "resourceType": "Patient", 9 | "name": [ 10 | { 11 | "id": "Patient-ID", 12 | "family": "Anyperson", 13 | "given": [ 14 | "John", 15 | "B." 16 | ] 17 | } 18 | ], 19 | "birthDate": "1951-01-20" 20 | } 21 | }, 22 | { 23 | "fullUrl": "resource:1", 24 | "resource": { 25 | "resourceType": "Patient", 26 | "name": [ 27 | { 28 | "family": "Anyperson", 29 | "given": [ 30 | "Joe", 31 | "B." 32 | ] 33 | } 34 | ], 35 | "birthDate": "1951-01-20" 36 | } 37 | }, 38 | { 39 | "fullUrl": "resource:2", 40 | "resource": { 41 | "resourceType": "Medication" 42 | } 43 | }, 44 | { 45 | "fullUrl": "resource:3", 46 | "resource": { 47 | "resourceType": "Immunization", 48 | "status": "Completed", 49 | "vaccineCode": { 50 | "coding": [ 51 | { 52 | "system": "http://hl7.org/fhir/sid/cvx" 53 | } 54 | ] 55 | }, 56 | "patient": { 57 | "reference": "resource:0" 58 | }, 59 | "occurrenceDateTime": "01-01-2021", 60 | "performer": [ 61 | { 62 | "actor": { 63 | "display": "ABC General Hospital" 64 | } 65 | } 66 | ], 67 | "location": "Safeway Parking Lot", 68 | "lotNumber": "0000001" 69 | } 70 | }, 71 | { 72 | "fullUrl": "resource:4", 73 | "resource": { 74 | "resourceType": "Immunization", 75 | "status": "Partial", 76 | "vaccineCode": { 77 | "coding": [ 78 | { 79 | "system": "http://hl7.org/fhir/sid/cvx", 80 | "code": "xxx-bad-code" 81 | } 82 | ] 83 | }, 84 | "patient": { 85 | "reference": "resource:0" 86 | }, 87 | "performer": [ 88 | { 89 | "actor": { 90 | "display": "ABC General Hospital" 91 | } 92 | } 93 | ], 94 | "lotNumber": "0000007" 95 | } 96 | } 97 | ] 98 | } -------------------------------------------------------------------------------- /src/shlEncode.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as jose from "node-jose"; 5 | import { qrCode } from "./utils"; 6 | 7 | interface SHLEncoding { 8 | link: string, 9 | payload: ShlinkPayload, 10 | manifest: ShlinkManifest, 11 | files: string[] 12 | } 13 | 14 | 15 | export async function encode( 16 | files: string[] = [], 17 | key = '', 18 | passcode = '', 19 | expiration = Date.now(), 20 | viewer = '', 21 | label = '', 22 | baseUrl = 'https://test-link', 23 | baseLocation = 'https://api.vaxx.link/api/shl'): Promise { 24 | 25 | // generate random path for files 26 | 27 | const randomUrlSegment = Buffer.from(Buffer.alloc(32).map(() => Math.floor(Math.random() * 256))).toString('base64url'); 28 | const randomLocationSegment = Buffer.from(Buffer.alloc(32).map(() => Math.floor(Math.random() * 256))).toString('base64url'); 29 | 30 | const keystore = jose.JWK.createKeyStore(); 31 | 32 | const jwkKey = key ? 33 | await keystore.add({ 34 | alg: "A256GCM", 35 | ext: true, 36 | k: key, 37 | key_ops: ["encrypt"], 38 | kty: "oct", 39 | }) : 40 | await keystore.generate('oct', 256, { alg: 'A256GCM', use: 'enc', }); 41 | 42 | const exportKey = (jwkKey.toJSON(true) as { k: string }).k; 43 | 44 | // create each file 45 | const manifest: ShlinkManifest = { 46 | files: await Promise.all(files.map(async (file, i) => { 47 | const jwe = await jose.JWE.createEncrypt({ format: 'compact' }, jwkKey) 48 | .update(Buffer.from(file, 'utf-8')) 49 | .final(); 50 | return { 51 | "contentType": "application/smart-health-card", 52 | "embedded": jwe, 53 | "location": `${baseLocation}/${randomLocationSegment}/file/${i}` 54 | } 55 | })) 56 | }; 57 | 58 | const payload: ShlinkPayload = { 59 | "url": `${baseUrl}/${randomUrlSegment}`, 60 | "flag": `${expiration ? '' : 'L'}${passcode ? "P" : ""}` as PayloadFlags, 61 | "key": exportKey, 62 | "label": label, 63 | "exp": expiration 64 | } 65 | 66 | const link = `${viewer}${viewer ? '#' : ''}shlink:/${Buffer.from(JSON.stringify(payload)).toString('base64url')}`; 67 | 68 | 69 | return { 70 | link, 71 | payload, 72 | manifest, 73 | files 74 | }; 75 | 76 | } 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "health-cards-dev-tools", 3 | "version": "1.3.0-2", 4 | "description": "", 5 | "main": "js/src/shc-validator.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "prepare": "npm run build", 9 | "fetch-examples": "ts-node --files src/fetch-examples.ts", 10 | "fetch-profiles": "ts-node --files src/fetch-profiles.ts", 11 | "validate-example": "ts-node --files src/shc-validator.ts --path testdata/valid_key.json --type jwkset --loglevel info", 12 | "pretest": "npm run fetch-examples && npm run fetch-profiles", 13 | "test": "concurrently --kill-others --success=\"first\" \"jest --silent --rootDir tests\" \"(cd ./shl-server && node ./src/server.js)\"", 14 | "generate-test-data": "ts-node --files src/generate-test-data", 15 | "update-validator": "git pull && npm install && npm run build", 16 | "lint": "eslint --config .eslintrc.json --ext .ts ./src ./tests", 17 | "prune-fhir-schema": "ts-node --files src/prune-fhir-schema.ts", 18 | "shl-server": "node ./shl-server/src/server.js", 19 | "postinstall": "cd ./shl-server && npm install && tsc" 20 | }, 21 | "bin": { 22 | "shc-validator": "js/src/shc-validator.js" 23 | }, 24 | "author": "", 25 | "license": "MIT", 26 | "dependencies": { 27 | "@fidm/x509": "^1.2.1", 28 | "ajv": "^8.6.3", 29 | "colors": "^1.4.0", 30 | "commander": "^7.2.0", 31 | "execa": "^5.0.0", 32 | "got": "^11.8.5", 33 | "jpeg-js": "^0.4.4", 34 | "json-beautify": "^1.1.1", 35 | "jsqr": "^1.3.1", 36 | "node-jose": "^2.0.0", 37 | "pako": "^2.0.3", 38 | "qrcode": "^1.3.2", 39 | "semver": "^7.3.5", 40 | "sharp": "^0.30.5", 41 | "uuid": "^8.3.2" 42 | }, 43 | "devDependencies": { 44 | "@types/jest": "^27.0.3", 45 | "@types/node": "^17.0.8", 46 | "@types/node-jose": "^1.1.5", 47 | "@types/pako": "^1.0.1", 48 | "@types/qrcode": "^1.4.0", 49 | "@types/semver": "^7.3.4", 50 | "@types/sharp": "^0.29.5", 51 | "@types/uuid": "^8.3.0", 52 | "@typescript-eslint/eslint-plugin": "^4.33.0", 53 | "@typescript-eslint/parser": "^4.33.0", 54 | "concurrently": "^7.6.0", 55 | "eslint": "^7.24.0", 56 | "jest": "^27.3.0", 57 | "jszip": "^3.6.0", 58 | "ts-jest": "^27.0.7", 59 | "ts-node": "^9.1.1", 60 | "typescript": "^4.4.3" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/command.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import execa from 'execa'; 5 | import color from 'colors'; 6 | import Log from './logger'; 7 | 8 | function workingAnimation(message: string, interval = 200) { 9 | const chars = ['|', '/', '―', '\\']; 10 | let x = 0; 11 | 12 | // For the animation, we write to stdout a message, then use process.stdout.clearLine() to erase the line 13 | // and on the same line write an updated message. This keeps the message updating on the same console line. 14 | // When we run the entire test suite using 'npm run test', process.stdout.clearline() is not available. 15 | // We don't really need to print the animation in this case anyway, so we skip the animation when 16 | // process.stdout.clearline() is not available. 17 | if (!process.stdout.clearLine) return { stop: () => {/*noop*/ } }; 18 | 19 | const handle = setInterval(() => { 20 | process.stdout.write(`\r ${color.green(chars[x++])} ${message}`); 21 | x %= chars.length; 22 | }, interval); 23 | 24 | return { 25 | stop: () => { 26 | clearInterval(handle); 27 | process.stdout.clearLine(0); 28 | process.stdout.write('\n'); 29 | } 30 | } 31 | } 32 | 33 | export function runCommandSync(command: string, log?: Log): CommandResult { 34 | let result; 35 | const start = Date.now(); 36 | 37 | try { 38 | result = execa.commandSync(command) as CommandResult; 39 | } catch (failed) { 40 | result = failed as CommandResult; 41 | } 42 | 43 | log?.debug(resultToString(result, start)); 44 | return result; 45 | } 46 | 47 | export async function runCommand(command: string, message?: string, log?: Log): Promise { 48 | 49 | let result; 50 | const start = Date.now(); 51 | 52 | const animation = workingAnimation(message || command); 53 | 54 | try { 55 | result = await execa.command(command) as CommandResult; 56 | } catch (failed) { 57 | result = failed as CommandResult; 58 | } 59 | 60 | animation.stop(); 61 | log?.debug(resultToString(result, start)); 62 | return result; 63 | } 64 | 65 | 66 | function resultToString(result: CommandResult, start: number): string { 67 | return `Running command : ${result.command}\n \ 68 | duration: ${((Date.now() - start) / 1000).toFixed(2)} seconds\n \ 69 | exitcode : ${result.exitCode}\n \ 70 | stdout: ${result.stdout.split('\n').join("\n ")}\n \ 71 | stderr: ${result.stderr.split('\n').join("\n ")}`; 72 | } -------------------------------------------------------------------------------- /testdata/invalid_chain.public.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "kty": "EC", 5 | "kid": "TBX-A5_oZhgItwBT2rdya1sYFc8VKEeI4PdV-w_K9XQ", 6 | "use": "sig", 7 | "alg": "ES256", 8 | "x5c": [ 9 | "MIIBwzCCAUqgAwIBAgIUGhOHNFrXrogeNSox6R056KmofBAwCgYIKoZIzj0EAwIwJzElMCMGA1UEAwwcU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBDQTAeFw0yMTAzMzAxOTI1MjdaFw0zMTAzMjgxOTI1MjdaMCsxKTAnBgNVBAMMIFNNQVJUIEhlYWx0aCBDYXJkIEV4YW1wbGUgSXNzdWVyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAs6KMEkz6ENvr+5P6xDziSsvnsHR44KYnHu5OuQc6VEGppjGBgRvjWePWn/JqZcGqa/PsB7lkZ0FTz0B8S2NCKNQME4wCQYDVR0TBAIwADALBgNVHQ8EBAMCB4AwNAYDVR0RBC0wK4YpaHR0cHM6Ly9zbWFydGhlYWx0aC5jYXJkcy9leGFtcGxlcy9pc3N1ZXIwCgYIKoZIzj0EAwIDZwAwZAIwHwsH1bN2igkNkVwbGd1bvPOrUNilkLEBg5zW/WDBSGN3310LdGL0rXJZhMTGVTL3AjAxOz13QpAtXkYQZ7RFMcWQ715RIZwUJxZT+zhW2sUkj+PRkflVuKbwJH9PbHV1+c0=", 10 | "MIICUDCCAbKgAwIBAgIUON+07RfrxBX3q+niTfuSl8lHFyAwCgYIKoZIzj0EAwIwLDEqMCgGA1UEAwwhU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBSb290IENBMB4XDTIxMDMzMDE5MjUyN1oXDTMxMDMyODE5MjUyN1owJzElMCMGA1UEAwwcU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABNz091f2PsVqY2d7iZmdna1v7a8r5Eof2qRL5pETv2s9RKDCTRN1H5pYQBCulu87QSJYEa3bVYYAYnYhZNBF5hrghuOmfSNSSjI1JSDnA/xHci4CE/4NzagG7vGvQV3RDKOBmTCBljAMBgNVHRMEBTADAQH/MB0GA1UdDgQWBBR+sB+oIp/XLjl0y+tYXU3N0aRllzBnBgNVHSMEYDBegBQOwYa3THrDSQZN8ZriddbFlb3DgqEwpC4wLDEqMCgGA1UEAwwhU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBSb290IENBghQkwx2NQJUQS/oyz1vHovr+yYmfvjAKBggqhkjOPQQDAgOBiwAwgYcCQgHM5FBMZLC/aFxPpJInb1JUbQKHZJVeUPYAhkd8X7ANxtz7uOBmXPqCZ+4LBuurQ0Y41/BApjFMZq+YLnP9S/UFXQJBAZu8NA6Q3kwoxbfEClAVWCKEl3RkfMbneCpXHsJ8mDnFg1Cw39u80AsKNZSYqWtI7NHbQhIPeif330GbQXriONk=", 11 | "MIICfDCCAd2gAwIBAgIUJMMdjUCVEEv6Ms9bx6L6/smJn74wCgYIKoZIzj0EAwIwLDEqMCgGA1UEAwwhU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBSb290IENBMB4XDTIxMDMzMDE5MjUyN1oXDTMxMDMyODE5MjUyN1owLDEqMCgGA1UEAwwhU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBSb290IENBMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBGSVIBf+AanQgheiRh7wg2VdmJevGV4UldeqhgH04Jltyz0D6tk1CAd9D5Y7GzwwZk0HTbqrSkG1rcPRb43Bi/LkA7lb14OHpj9ZCz84ka+1tRSagZ9nW94MG7k1AmXTp04YeEW71LLVdbpE9654FoYXXtEcYcPlINn7YoT7vlM5I7sGjgZkwgZYwDAYDVR0TBAUwAwEB/zAdBgNVHQ4EFgQUDsGGt0x6w0kGTfGa4nXWxZW9w4IwZwYDVR0jBGAwXoAUDsGGt0x6w0kGTfGa4nXWxZW9w4KhMKQuMCwxKjAoBgNVBAMMIVNNQVJUIEhlYWx0aCBDYXJkIEV4YW1wbGUgUm9vdCBDQYIUJMMdjUCVEEv6Ms9bx6L6/smJn74wCgYIKoZIzj0EAwIDgYwAMIGIAkIB+oWMnr7nkUZ8O9MfYv33T0UnY2RijKj3ldYvG/rlkZBehrc9Zsx2lXyBxLB9zzNWu7XBLZ8vCJaCpK5dARw5J6YCQgF+Mvx8jaSJFXtxGaazKsxbueGhXxy4zF3qTXnLavumLFm1N+J0XCA4ixc+CQuAl/XTxjJzaawtiQpOn2fzgryUFQ==" 12 | ], 13 | "crv": "P-256", 14 | "x": "As6KMEkz6ENvr-5P6xDziSsvnsHR44KYnHu5OuQc6VE", 15 | "y": "BqaYxgYEb41nj1p_yamXBqmvz7Ae5ZGdBU89AfEtjQg" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /testdata/cert_mismatch.public.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "kty": "EC", 5 | "kid": "TBX-A5_oZhgItwBT2rdya1sYFc8VKEeI4PdV-w_K9XQ", 6 | "use": "sig", 7 | "alg": "ES256", 8 | "x5c": [ 9 | "MIIBxTCCAUqgAwIBAgIULMcAl7QdfwKTfZjP3DbT5U6Z/d0wCgYIKoZIzj0EAwIwJzElMCMGA1UEAwwcU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBDQTAeFw0yMTAzMzAxOTI1MjdaFw0zMTAzMjgxOTI1MjdaMCsxKTAnBgNVBAMMIFNNQVJUIEhlYWx0aCBDYXJkIEV4YW1wbGUgSXNzdWVyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuFOVFvV/VeAFK4+HmiNgkYqdcELWfYNdqbx9fOzcCKoNVuzqO/b23MJcrLcBqvkIyEkjxt4SwWWx8iY0Hcb5HaNQME4wCQYDVR0TBAIwADALBgNVHQ8EBAMCB4AwNAYDVR0RBC0wK4YpaHR0cHM6Ly9zbWFydGhlYWx0aC5jYXJkcy9leGFtcGxlcy9pc3N1ZXIwCgYIKoZIzj0EAwIDaQAwZgIxAOLqDJyMU8nIGCr4t6pPpcKrKFEO5a48xAe8yB6B4G2sOaIh0SpxhEGCXdVdln5lUgIxAKg5veQtwnHmv29+3wxdMMhu1QFieqUYcnIvjuAj9F2HD8XYB5bmf+2MwZc3baPpmg==", 10 | "MIICUDCCAbKgAwIBAgIUON+07RfrxBX3q+niTfuSl8lHFyAwCgYIKoZIzj0EAwIwLDEqMCgGA1UEAwwhU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBSb290IENBMB4XDTIxMDMzMDE5MjUyN1oXDTMxMDMyODE5MjUyN1owJzElMCMGA1UEAwwcU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABNz091f2PsVqY2d7iZmdna1v7a8r5Eof2qRL5pETv2s9RKDCTRN1H5pYQBCulu87QSJYEa3bVYYAYnYhZNBF5hrghuOmfSNSSjI1JSDnA/xHci4CE/4NzagG7vGvQV3RDKOBmTCBljAMBgNVHRMEBTADAQH/MB0GA1UdDgQWBBR+sB+oIp/XLjl0y+tYXU3N0aRllzBnBgNVHSMEYDBegBQOwYa3THrDSQZN8ZriddbFlb3DgqEwpC4wLDEqMCgGA1UEAwwhU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBSb290IENBghQkwx2NQJUQS/oyz1vHovr+yYmfvjAKBggqhkjOPQQDAgOBiwAwgYcCQgHM5FBMZLC/aFxPpJInb1JUbQKHZJVeUPYAhkd8X7ANxtz7uOBmXPqCZ+4LBuurQ0Y41/BApjFMZq+YLnP9S/UFXQJBAZu8NA6Q3kwoxbfEClAVWCKEl3RkfMbneCpXHsJ8mDnFg1Cw39u80AsKNZSYqWtI7NHbQhIPeif330GbQXriONk=", 11 | "MIICfDCCAd2gAwIBAgIUJMMdjUCVEEv6Ms9bx6L6/smJn74wCgYIKoZIzj0EAwIwLDEqMCgGA1UEAwwhU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBSb290IENBMB4XDTIxMDMzMDE5MjUyN1oXDTMxMDMyODE5MjUyN1owLDEqMCgGA1UEAwwhU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBSb290IENBMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBGSVIBf+AanQgheiRh7wg2VdmJevGV4UldeqhgH04Jltyz0D6tk1CAd9D5Y7GzwwZk0HTbqrSkG1rcPRb43Bi/LkA7lb14OHpj9ZCz84ka+1tRSagZ9nW94MG7k1AmXTp04YeEW71LLVdbpE9654FoYXXtEcYcPlINn7YoT7vlM5I7sGjgZkwgZYwDAYDVR0TBAUwAwEB/zAdBgNVHQ4EFgQUDsGGt0x6w0kGTfGa4nXWxZW9w4IwZwYDVR0jBGAwXoAUDsGGt0x6w0kGTfGa4nXWxZW9w4KhMKQuMCwxKjAoBgNVBAMMIVNNQVJUIEhlYWx0aCBDYXJkIEV4YW1wbGUgUm9vdCBDQYIUJMMdjUCVEEv6Ms9bx6L6/smJn74wCgYIKoZIzj0EAwIDgYwAMIGIAkIB+oWMnr7nkUZ8O9MfYv33T0UnY2RijKj3ldYvG/rlkZBehrc9Zsx2lXyBxLB9zzNWu7XBLZ8vCJaCpK5dARw5J6YCQgF+Mvx8jaSJFXtxGaazKsxbueGhXxy4zF3qTXnLavumLFm1N+J0XCA4ixc+CQuAl/XTxjJzaawtiQpOn2fzgryUFQ==" 12 | ], 13 | "crv": "P-256", 14 | "x": "As6KMEkz6ENvr-5P6xDziSsvnsHR44KYnHu5OuQc6VE", 15 | "y": "BqaYxgYEb41nj1p_yamXBqmvz7Ae5ZGdBU89AfEtjQg" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /testdata/valid_key_with_x5c.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "kty": "EC", 5 | "kid": "hpmpBeYB5yb4W9oH8ZjRjl8BsmxtjRMVm8_SRjCBx4o", 6 | "use": "sig", 7 | "alg": "ES256", 8 | "x5c": [ 9 | "MIIBxDCCAUqgAwIBAgIUWMU4ScZJgyyOm23tWe0pS77zB1kwCgYIKoZIzj0EAwIwJzElMCMGA1UEAwwcU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBDQTAeFw0yMTAzMjkxNjAwNDVaFw0zMTAzMjcxNjAwNDVaMCsxKTAnBgNVBAMMIFNNQVJUIEhlYWx0aCBDYXJkIEV4YW1wbGUgSXNzdWVyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZ/V+fqGX4cR/xzzCb9yd7LlKBisHQ7778OEyYjp6+s+djLQN+Z6jw2bWu/dYX13of0auSc7xrAHWKbImRSDDRKNQME4wCQYDVR0TBAIwADALBgNVHQ8EBAMCB4AwNAYDVR0RBC0wK4YpaHR0cHM6Ly9zbWFydGhlYWx0aC5jYXJkcy9leGFtcGxlcy9pc3N1ZXIwCgYIKoZIzj0EAwIDaAAwZQIwEJ5BI3Q0ciO1cT6hqBfAuVMHeef0YjleaGJrDmyUVlHSnTZjlF5uzlbnxcvWl0J5AjEAsqLZcFQIXYeE/iQl/9mzTPRrpnc2TEmOP4oDSmIy7MjyeZtIiuhDjtQ4aq6rmBDm", 10 | "MIICUDCCAbKgAwIBAgIUO/qesGJdlSl090pxw4d3CdRNIJYwCgYIKoZIzj0EAwIwLDEqMCgGA1UEAwwhU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBSb290IENBMB4XDTIxMDMyOTE2MDA0NVoXDTMxMDMyNzE2MDA0NVowJzElMCMGA1UEAwwcU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABBi0SFSmENDMMVsbpauhyTzmFwzcM1rZtJhQBfzoUsvxfU4Unmn3n77UBgp6qZheCH81zRaPj2yUAeQmv83F3wc9Z/5FLkIg79VY0j5qIbkhPjjXuczvU/NcftJ3Q/GACaOBmTCBljAMBgNVHRMEBTADAQH/MB0GA1UdDgQWBBQDAFuz0NcUDZWaCNyCC3xUX5IWPzBnBgNVHSMEYDBegBRzjpJZDCzRcwzC57u1yvJQyYcEEqEwpC4wLDEqMCgGA1UEAwwhU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBSb290IENBghQkv+E4KKSXgxjnL8QJvgOF1pmjMTAKBggqhkjOPQQDAgOBiwAwgYcCQgDwyGwc/g/eVVnfKqlUfCvAvwJPu82eaJnLr+c3/kubWbOsbuWZmuPT7HFSGPXUN6bDXT5OPxegNxpbtUpp+DRmywJBRFDgahlGHNOv4VuLYz6jUigsAWyUm0n/00DCVmVHj3bc8y3sxmjGOQoDxXFbk2bgJPf0/mDfZgKEgnESB3jJ5fQ=", 11 | "MIICfDCCAd2gAwIBAgIUJL/hOCikl4MY5y/ECb4DhdaZozEwCgYIKoZIzj0EAwIwLDEqMCgGA1UEAwwhU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBSb290IENBMB4XDTIxMDMyOTE2MDA0NVoXDTMxMDMyNzE2MDA0NVowLDEqMCgGA1UEAwwhU01BUlQgSGVhbHRoIENhcmQgRXhhbXBsZSBSb290IENBMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQB4mqDm+ojst500OGy20w/sUXbXIS5AQjMAVnpAT73jA2zaUPueUGPQhVvPAmKKEDgiaPD+uQ6dwhJ9L4ZyshD9JwBSHlCuHhjUUwXe9Caqp51/rjpDCOTM/C7GHChxQA7q0+eVz3j0u81FXYh0jarYZPQvaoRVw27nXQRQb/cregyRsijgZkwgZYwDAYDVR0TBAUwAwEB/zAdBgNVHQ4EFgQUc46SWQws0XMMwue7tcryUMmHBBIwZwYDVR0jBGAwXoAUc46SWQws0XMMwue7tcryUMmHBBKhMKQuMCwxKjAoBgNVBAMMIVNNQVJUIEhlYWx0aCBDYXJkIEV4YW1wbGUgUm9vdCBDQYIUJL/hOCikl4MY5y/ECb4DhdaZozEwCgYIKoZIzj0EAwIDgYwAMIGIAkIBWJb+OfEjEZDl7bZmd3aRmSgxLZYdx5s2wsg5TVEDWh1gemVq1IoxMyAap83uXll/kfRcefkcxzW56oRFKQmMyt8CQgHkFwAKsMf9K2yk5CEKOj5fDMG5ASOdXotC3Es9R4Cwtjs/mioAoOL8KielL53BZztBpyYK+X1JKqk4YrXQ9w7q8g==" 12 | ], 13 | "crv": "P-256", 14 | "x": "Z_V-fqGX4cR_xzzCb9yd7LlKBisHQ7778OEyYjp6-s8", 15 | "y": "nYy0Dfmeo8Nm1rv3WF9d6H9GrknO8awB1imyJkUgw0Q" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /testdata/test-example-00-b-jws-payload-expanded-missing-imm-vc-type.json: -------------------------------------------------------------------------------- 1 | { 2 | "iss": "https://spec.smarthealth.cards/examples/issuer", 3 | "nbf": 1622690247.979, 4 | "vc": { 5 | "type": [ 6 | "https://smarthealth.cards#health-card", 7 | "https://smarthealth.cards#covid19" 8 | ], 9 | "credentialSubject": { 10 | "fhirVersion": "4.0.1", 11 | "fhirBundle": { 12 | "resourceType": "Bundle", 13 | "type": "collection", 14 | "entry": [ 15 | { 16 | "fullUrl": "resource:0", 17 | "resource": { 18 | "resourceType": "Patient", 19 | "name": [ 20 | { 21 | "family": "Anyperson", 22 | "given": [ 23 | "John", 24 | "B." 25 | ] 26 | } 27 | ], 28 | "birthDate": "1951-01-20" 29 | } 30 | }, 31 | { 32 | "fullUrl": "resource:1", 33 | "resource": { 34 | "resourceType": "Immunization", 35 | "status": "completed", 36 | "vaccineCode": { 37 | "coding": [ 38 | { 39 | "system": "http://hl7.org/fhir/sid/cvx", 40 | "code": "207" 41 | } 42 | ] 43 | }, 44 | "patient": { 45 | "reference": "resource:0" 46 | }, 47 | "occurrenceDateTime": "2021-01-01", 48 | "performer": [ 49 | { 50 | "actor": { 51 | "display": "ABC General Hospital" 52 | } 53 | } 54 | ], 55 | "lotNumber": "0000001" 56 | } 57 | }, 58 | { 59 | "fullUrl": "resource:2", 60 | "resource": { 61 | "resourceType": "Immunization", 62 | "status": "completed", 63 | "vaccineCode": { 64 | "coding": [ 65 | { 66 | "system": "http://hl7.org/fhir/sid/cvx", 67 | "code": "207" 68 | } 69 | ] 70 | }, 71 | "patient": { 72 | "reference": "resource:0" 73 | }, 74 | "occurrenceDateTime": "2021-01-29", 75 | "performer": [ 76 | { 77 | "actor": { 78 | "display": "ABC General Hospital" 79 | } 80 | } 81 | ], 82 | "lotNumber": "0000007" 83 | } 84 | } 85 | ] 86 | } 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /testdata/test-example-00-b-jws-payload-expanded-missing-shc-vc-type.json: -------------------------------------------------------------------------------- 1 | { 2 | "iss": "https://spec.smarthealth.cards/examples/issuer", 3 | "nbf": 1622690247.979, 4 | "vc": { 5 | "type": [ 6 | "https://smarthealth.cards#immunization", 7 | "https://smarthealth.cards#covid19" 8 | ], 9 | "credentialSubject": { 10 | "fhirVersion": "4.0.1", 11 | "fhirBundle": { 12 | "resourceType": "Bundle", 13 | "type": "collection", 14 | "entry": [ 15 | { 16 | "fullUrl": "resource:0", 17 | "resource": { 18 | "resourceType": "Patient", 19 | "name": [ 20 | { 21 | "family": "Anyperson", 22 | "given": [ 23 | "John", 24 | "B." 25 | ] 26 | } 27 | ], 28 | "birthDate": "1951-01-20" 29 | } 30 | }, 31 | { 32 | "fullUrl": "resource:1", 33 | "resource": { 34 | "resourceType": "Immunization", 35 | "status": "completed", 36 | "vaccineCode": { 37 | "coding": [ 38 | { 39 | "system": "http://hl7.org/fhir/sid/cvx", 40 | "code": "207" 41 | } 42 | ] 43 | }, 44 | "patient": { 45 | "reference": "resource:0" 46 | }, 47 | "occurrenceDateTime": "2021-01-01", 48 | "performer": [ 49 | { 50 | "actor": { 51 | "display": "ABC General Hospital" 52 | } 53 | } 54 | ], 55 | "lotNumber": "0000001" 56 | } 57 | }, 58 | { 59 | "fullUrl": "resource:2", 60 | "resource": { 61 | "resourceType": "Immunization", 62 | "status": "completed", 63 | "vaccineCode": { 64 | "coding": [ 65 | { 66 | "system": "http://hl7.org/fhir/sid/cvx", 67 | "code": "207" 68 | } 69 | ] 70 | }, 71 | "patient": { 72 | "reference": "resource:0" 73 | }, 74 | "occurrenceDateTime": "2021-01-29", 75 | "performer": [ 76 | { 77 | "actor": { 78 | "display": "ABC General Hospital" 79 | } 80 | } 81 | ], 82 | "lotNumber": "0000007" 83 | } 84 | } 85 | ] 86 | } 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /testdata/test-example-00-b-jws-payload-expanded-missing-covid-vc-type.json: -------------------------------------------------------------------------------- 1 | { 2 | "iss": "https://spec.smarthealth.cards/examples/issuer", 3 | "nbf": 1622690247.979, 4 | "vc": { 5 | "type": [ 6 | "https://smarthealth.cards#health-card", 7 | "https://smarthealth.cards#immunization" 8 | ], 9 | "credentialSubject": { 10 | "fhirVersion": "4.0.1", 11 | "fhirBundle": { 12 | "resourceType": "Bundle", 13 | "type": "collection", 14 | "entry": [ 15 | { 16 | "fullUrl": "resource:0", 17 | "resource": { 18 | "resourceType": "Patient", 19 | "name": [ 20 | { 21 | "family": "Anyperson", 22 | "given": [ 23 | "John", 24 | "B." 25 | ] 26 | } 27 | ], 28 | "birthDate": "1951-01-20" 29 | } 30 | }, 31 | { 32 | "fullUrl": "resource:1", 33 | "resource": { 34 | "resourceType": "Immunization", 35 | "status": "completed", 36 | "vaccineCode": { 37 | "coding": [ 38 | { 39 | "system": "http://hl7.org/fhir/sid/cvx", 40 | "code": "207" 41 | } 42 | ] 43 | }, 44 | "patient": { 45 | "reference": "resource:0" 46 | }, 47 | "occurrenceDateTime": "2021-01-01", 48 | "performer": [ 49 | { 50 | "actor": { 51 | "display": "ABC General Hospital" 52 | } 53 | } 54 | ], 55 | "lotNumber": "0000001" 56 | } 57 | }, 58 | { 59 | "fullUrl": "resource:2", 60 | "resource": { 61 | "resourceType": "Immunization", 62 | "status": "completed", 63 | "vaccineCode": { 64 | "coding": [ 65 | { 66 | "system": "http://hl7.org/fhir/sid/cvx", 67 | "code": "207" 68 | } 69 | ] 70 | }, 71 | "patient": { 72 | "reference": "resource:0" 73 | }, 74 | "occurrenceDateTime": "2021-01-29", 75 | "performer": [ 76 | { 77 | "actor": { 78 | "display": "ABC General Hospital" 79 | } 80 | } 81 | ], 82 | "lotNumber": "0000007" 83 | } 84 | } 85 | ] 86 | } 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /testdata/test-example-00-b-jws-payload-expanded-nbf_milliseconds.json: -------------------------------------------------------------------------------- 1 | { 2 | "iss": "https://spec.smarthealth.cards/examples/issuer", 3 | "nbf": 1618554442496, 4 | "vc": { 5 | "type": [ 6 | "https://smarthealth.cards#health-card", 7 | "https://smarthealth.cards#immunization", 8 | "https://smarthealth.cards#covid19" 9 | ], 10 | "credentialSubject": { 11 | "fhirVersion": "4.0.1", 12 | "fhirBundle": { 13 | "resourceType": "Bundle", 14 | "type": "collection", 15 | "entry": [ 16 | { 17 | "fullUrl": "resource:0", 18 | "resource": { 19 | "resourceType": "Patient", 20 | "name": [ 21 | { 22 | "family": "Anyperson", 23 | "given": [ 24 | "John", 25 | "B." 26 | ] 27 | } 28 | ], 29 | "birthDate": "1951-01-20" 30 | } 31 | }, 32 | { 33 | "fullUrl": "resource:1", 34 | "resource": { 35 | "resourceType": "Immunization", 36 | "status": "completed", 37 | "vaccineCode": { 38 | "coding": [ 39 | { 40 | "system": "http://hl7.org/fhir/sid/cvx", 41 | "code": "207" 42 | } 43 | ] 44 | }, 45 | "patient": { 46 | "reference": "resource:0" 47 | }, 48 | "occurrenceDateTime": "2021-01-01", 49 | "performer": [ 50 | { 51 | "actor": { 52 | "display": "ABC General Hospital" 53 | } 54 | } 55 | ], 56 | "lotNumber": "0000001" 57 | } 58 | }, 59 | { 60 | "fullUrl": "resource:2", 61 | "resource": { 62 | "resourceType": "Immunization", 63 | "status": "completed", 64 | "vaccineCode": { 65 | "coding": [ 66 | { 67 | "system": "http://hl7.org/fhir/sid/cvx", 68 | "code": "207" 69 | } 70 | ] 71 | }, 72 | "patient": { 73 | "reference": "resource:0" 74 | }, 75 | "occurrenceDateTime": "2021-01-29", 76 | "performer": [ 77 | { 78 | "actor": { 79 | "display": "ABC General Hospital" 80 | } 81 | } 82 | ], 83 | "lotNumber": "0000007" 84 | } 85 | } 86 | ] 87 | } 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /testdata/test-example-00-b-jws-payload-expanded-nbf_not_yet_valid.json: -------------------------------------------------------------------------------- 1 | { 2 | "iss": "https://spec.smarthealth.cards/examples/issuer", 3 | "nbf": 2533406445.73, 4 | "vc": { 5 | "type": [ 6 | "https://smarthealth.cards#health-card", 7 | "https://smarthealth.cards#immunization", 8 | "https://smarthealth.cards#covid19" 9 | ], 10 | "credentialSubject": { 11 | "fhirVersion": "4.0.1", 12 | "fhirBundle": { 13 | "resourceType": "Bundle", 14 | "type": "collection", 15 | "entry": [ 16 | { 17 | "fullUrl": "resource:0", 18 | "resource": { 19 | "resourceType": "Patient", 20 | "name": [ 21 | { 22 | "family": "Anyperson", 23 | "given": [ 24 | "John", 25 | "B." 26 | ] 27 | } 28 | ], 29 | "birthDate": "1951-01-20" 30 | } 31 | }, 32 | { 33 | "fullUrl": "resource:1", 34 | "resource": { 35 | "resourceType": "Immunization", 36 | "status": "completed", 37 | "vaccineCode": { 38 | "coding": [ 39 | { 40 | "system": "http://hl7.org/fhir/sid/cvx", 41 | "code": "207" 42 | } 43 | ] 44 | }, 45 | "patient": { 46 | "reference": "resource:0" 47 | }, 48 | "occurrenceDateTime": "2021-01-01", 49 | "performer": [ 50 | { 51 | "actor": { 52 | "display": "ABC General Hospital" 53 | } 54 | } 55 | ], 56 | "lotNumber": "0000001" 57 | } 58 | }, 59 | { 60 | "fullUrl": "resource:2", 61 | "resource": { 62 | "resourceType": "Immunization", 63 | "status": "completed", 64 | "vaccineCode": { 65 | "coding": [ 66 | { 67 | "system": "http://hl7.org/fhir/sid/cvx", 68 | "code": "207" 69 | } 70 | ] 71 | }, 72 | "patient": { 73 | "reference": "resource:0" 74 | }, 75 | "occurrenceDateTime": "2021-01-29", 76 | "performer": [ 77 | { 78 | "actor": { 79 | "display": "ABC General Hospital" 80 | } 81 | } 82 | ], 83 | "lotNumber": "0000007" 84 | } 85 | } 86 | ] 87 | } 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /testdata/test-example-00-b-jws-payload-expanded-missing-coding.json: -------------------------------------------------------------------------------- 1 | { 2 | "iss": "https://spec.smarthealth.cards/examples/issuer", 3 | "nbf": 1624919431.65, 4 | "vc": { 5 | "type": [ 6 | "https://smarthealth.cards#health-card", 7 | "https://smarthealth.cards#immunization", 8 | "https://smarthealth.cards#covid19" 9 | ], 10 | "credentialSubject": { 11 | "fhirVersion": "4.0.1", 12 | "fhirBundle": { 13 | "resourceType": "Bundle", 14 | "type": "collection", 15 | "entry": [ 16 | { 17 | "fullUrl": "resource:0", 18 | "resource": { 19 | "resourceType": "Patient", 20 | "name": [ 21 | { 22 | "family": "Anyperson", 23 | "given": [ 24 | "John", 25 | "B." 26 | ] 27 | } 28 | ], 29 | "birthDate": "1951-01-20" 30 | } 31 | }, 32 | { 33 | "fullUrl": "resource:1", 34 | "resource": { 35 | "resourceType": "Immunization", 36 | "status": "completed", 37 | "vaccineCode": { 38 | }, 39 | "patient": { 40 | "reference": "resource:0" 41 | }, 42 | "occurrenceDateTime": "2021-01-01", 43 | "performer": [ 44 | { 45 | "actor": { 46 | "display": "ABC General Hospital" 47 | } 48 | } 49 | ], 50 | "lotNumber": "0000001" 51 | } 52 | }, 53 | { 54 | "fullUrl": "resource:2", 55 | "resource": { 56 | "resourceType": "Observation", 57 | "meta": { 58 | "security": [ 59 | { 60 | "system": "https://smarthealth.cards/ial", 61 | "code": "IAL2" 62 | } 63 | ] 64 | }, 65 | "status": "final", 66 | "code": { 67 | }, 68 | "subject": { 69 | "reference": "resource:0" 70 | }, 71 | "effectiveDateTime": "2021-02-17", 72 | "valueCodeableConcept": { 73 | "coding": [ 74 | { 75 | "system": "http://snomed.info/sct", 76 | "code": "260373001" 77 | } 78 | ] 79 | }, 80 | "performer": [ 81 | { 82 | "display": "ABC General Hospital" 83 | } 84 | ] 85 | } 86 | } 87 | ] 88 | } 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /testdata/test-example-00-b-jws-payload-expanded-trailing_chars.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "iss": "https://spec.smarthealth.cards/examples/issuer", 4 | "nbf": 1622578753.17, 5 | "vc": { 6 | "type": [ 7 | "https://smarthealth.cards#health-card", 8 | "https://smarthealth.cards#immunization", 9 | "https://smarthealth.cards#covid19" 10 | ], 11 | "credentialSubject": { 12 | "fhirVersion": "4.0.1", 13 | "fhirBundle": { 14 | "resourceType": "Bundle", 15 | "type": "collection", 16 | "entry": [ 17 | { 18 | "fullUrl": "resource:0", 19 | "resource": { 20 | "resourceType": "Patient", 21 | "name": [ 22 | { 23 | "family": "Anyperson", 24 | "given": [ 25 | "John", 26 | "B." 27 | ] 28 | } 29 | ], 30 | "birthDate": "1951-01-20" 31 | } 32 | }, 33 | { 34 | "fullUrl": "resource:1", 35 | "resource": { 36 | "resourceType": "Immunization", 37 | "status": "completed", 38 | "vaccineCode": { 39 | "coding": [ 40 | { 41 | "system": "http://hl7.org/fhir/sid/cvx", 42 | "code": "207" 43 | } 44 | ] 45 | }, 46 | "patient": { 47 | "reference": "resource:0" 48 | }, 49 | "occurrenceDateTime": "2021-01-01", 50 | "performer": [ 51 | { 52 | "actor": { 53 | "display": "ABC General Hospital" 54 | } 55 | } 56 | ], 57 | "lotNumber": "0000001" 58 | } 59 | }, 60 | { 61 | "fullUrl": "resource:2", 62 | "resource": { 63 | "resourceType": "Immunization", 64 | "status": "completed", 65 | "vaccineCode": { 66 | "coding": [ 67 | { 68 | "system": "http://hl7.org/fhir/sid/cvx", 69 | "code": "207" 70 | } 71 | ] 72 | }, 73 | "patient": { 74 | "reference": "resource:0" 75 | }, 76 | "occurrenceDateTime": "2021-01-29", 77 | "performer": [ 78 | { 79 | "actor": { 80 | "display": "ABC General Hospital" 81 | } 82 | } 83 | ], 84 | "lotNumber": "0000007" 85 | } 86 | } 87 | ] 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /testdata/test-example-00-b-jws-payload-expanded-optional-vc-type.json: -------------------------------------------------------------------------------- 1 | { 2 | "iss": "https://spec.smarthealth.cards/examples/issuer", 3 | "nbf": 1626733777.575, 4 | "vc": { 5 | "type": [ 6 | "VerifiableCredential", 7 | "https://smarthealth.cards#health-card", 8 | "https://smarthealth.cards#immunization", 9 | "https://smarthealth.cards#covid19" 10 | ], 11 | "credentialSubject": { 12 | "fhirVersion": "4.0.1", 13 | "fhirBundle": { 14 | "resourceType": "Bundle", 15 | "type": "collection", 16 | "entry": [ 17 | { 18 | "fullUrl": "resource:0", 19 | "resource": { 20 | "resourceType": "Patient", 21 | "name": [ 22 | { 23 | "family": "Anyperson", 24 | "given": [ 25 | "John", 26 | "B." 27 | ] 28 | } 29 | ], 30 | "birthDate": "1951-01-20" 31 | } 32 | }, 33 | { 34 | "fullUrl": "resource:1", 35 | "resource": { 36 | "resourceType": "Immunization", 37 | "status": "completed", 38 | "vaccineCode": { 39 | "coding": [ 40 | { 41 | "system": "http://hl7.org/fhir/sid/cvx", 42 | "code": "207" 43 | } 44 | ] 45 | }, 46 | "patient": { 47 | "reference": "resource:0" 48 | }, 49 | "occurrenceDateTime": "2021-01-01", 50 | "performer": [ 51 | { 52 | "actor": { 53 | "display": "ABC General Hospital" 54 | } 55 | } 56 | ], 57 | "lotNumber": "0000001" 58 | } 59 | }, 60 | { 61 | "fullUrl": "resource:2", 62 | "resource": { 63 | "resourceType": "Immunization", 64 | "status": "completed", 65 | "vaccineCode": { 66 | "coding": [ 67 | { 68 | "system": "http://hl7.org/fhir/sid/cvx", 69 | "code": "207" 70 | } 71 | ] 72 | }, 73 | "patient": { 74 | "reference": "resource:0" 75 | }, 76 | "occurrenceDateTime": "2021-01-29", 77 | "performer": [ 78 | { 79 | "actor": { 80 | "display": "ABC General Hospital" 81 | } 82 | } 83 | ], 84 | "lotNumber": "0000007" 85 | } 86 | } 87 | ] 88 | } 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /src/jwe-compact.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import { ErrorCode } from "./error"; 5 | import Log from "./logger"; 6 | import { IOptions } from "./options"; 7 | import * as jose from "node-jose"; 8 | import * as healthCard from "./healthCard"; 9 | import { isJwe } from "./utils"; 10 | 11 | 12 | export async function validate(jwe: string, options: IOptions): Promise<{ result: string, log: Log }> { 13 | const log = new Log(`JWE`); 14 | 15 | if (!options.decryptionKey) { 16 | return { log: log.fatal(`Decryption failed, "options.decryptionKey" required`, ErrorCode.SHLINK_VERIFICATION_ERROR), result: '' }; 17 | } 18 | 19 | log.debug(`Decryption Key\n${options.decryptionKey}`); 20 | 21 | log.debug(`JWE\n${jwe}`); 22 | 23 | if (!/[\w-]+/g.test(options.decryptionKey)) { 24 | return { 25 | log: log.fatal('Failed to parse key as base64url string.', ErrorCode.JSON_PARSE_ERROR), 26 | result: '' 27 | }; 28 | } 29 | 30 | if (!isJwe(jwe)) { 31 | return { 32 | log: log.fatal('Failed to parse JWE-compact serialization as \'base64url.base64url.base64url.base64url.base64url\' string.', ErrorCode.JSON_PARSE_ERROR), 33 | result: '' 34 | }; 35 | } 36 | 37 | log.debug(`Decoded JWE\n${JSON.stringify(decodeJwe(jwe), null, 2)}`); 38 | 39 | const key = await jose.JWK.asKey({ 40 | alg: "A256GCM", 41 | ext: true, 42 | k: options.decryptionKey, 43 | key_ops: ["decrypt"], 44 | kty: "oct", 45 | }).catch(() => { 46 | log.error(`Failed to import key ${options.decryptionKey}`, ErrorCode.SHLINK_VERIFICATION_ERROR); 47 | return undefined; 48 | }); 49 | 50 | if (!key) return { log, result: '' }; 51 | 52 | log.info(`Decrypting`); 53 | const decryptor = jose.JWE.createDecrypt(key); 54 | 55 | let payload; 56 | 57 | try { 58 | payload = (await decryptor.decrypt(jwe)).payload; 59 | } catch (_err) { 60 | return { log: log.fatal(`Decryption failed`, ErrorCode.SHLINK_VERIFICATION_ERROR), result: '' }; 61 | } 62 | 63 | const decrypted = Buffer.from(payload).toString("utf-8"); 64 | 65 | log.debug(`Decrypted\n${decrypted}`); 66 | 67 | if (options.cascade && options.shlFile?.contentType == "application/smart-health-card") { 68 | log.child.push(await healthCard.validate(decrypted, options)); 69 | } 70 | 71 | return { log, result: decrypted }; 72 | } 73 | 74 | 75 | export function decodeJwe(jwe: string): { header: JWEHeader, encryptedKey: string, iv: string, ciphertext: string, authenticationTag: string } { 76 | const [header, encryptedKey, iv, ciphertext, authenticationTag] = jwe.split('.'); 77 | return { header : JSON.parse(Buffer.from(header, 'base64url').toString('utf-8')) as JWEHeader, encryptedKey, iv, ciphertext, authenticationTag } 78 | } 79 | -------------------------------------------------------------------------------- /testdata/test-example-00-b-jws-payload-expanded-unknown-vc-types.json: -------------------------------------------------------------------------------- 1 | { 2 | "iss": "https://spec.smarthealth.cards/examples/issuer", 3 | "nbf": 1626733777.575, 4 | "vc": { 5 | "type": [ 6 | "https://smarthealth.cards#extra-type-1", 7 | "https://smarthealth.cards#health-card", 8 | "https://smarthealth.cards#immunization", 9 | "https://smarthealth.cards#covid19", 10 | "https://smarthealth.cards#extra-type-2" 11 | ], 12 | "credentialSubject": { 13 | "fhirVersion": "4.0.1", 14 | "fhirBundle": { 15 | "resourceType": "Bundle", 16 | "type": "collection", 17 | "entry": [ 18 | { 19 | "fullUrl": "resource:0", 20 | "resource": { 21 | "resourceType": "Patient", 22 | "name": [ 23 | { 24 | "family": "Anyperson", 25 | "given": [ 26 | "John", 27 | "B." 28 | ] 29 | } 30 | ], 31 | "birthDate": "1951-01-20" 32 | } 33 | }, 34 | { 35 | "fullUrl": "resource:1", 36 | "resource": { 37 | "resourceType": "Immunization", 38 | "status": "completed", 39 | "vaccineCode": { 40 | "coding": [ 41 | { 42 | "system": "http://hl7.org/fhir/sid/cvx", 43 | "code": "207" 44 | } 45 | ] 46 | }, 47 | "patient": { 48 | "reference": "resource:0" 49 | }, 50 | "occurrenceDateTime": "2021-01-01", 51 | "performer": [ 52 | { 53 | "actor": { 54 | "display": "ABC General Hospital" 55 | } 56 | } 57 | ], 58 | "lotNumber": "0000001" 59 | } 60 | }, 61 | { 62 | "fullUrl": "resource:2", 63 | "resource": { 64 | "resourceType": "Immunization", 65 | "status": "completed", 66 | "vaccineCode": { 67 | "coding": [ 68 | { 69 | "system": "http://hl7.org/fhir/sid/cvx", 70 | "code": "207" 71 | } 72 | ] 73 | }, 74 | "patient": { 75 | "reference": "resource:0" 76 | }, 77 | "occurrenceDateTime": "2021-01-29", 78 | "performer": [ 79 | { 80 | "actor": { 81 | "display": "ABC General Hospital" 82 | } 83 | } 84 | ], 85 | "lotNumber": "0000007" 86 | } 87 | } 88 | ] 89 | } 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /shl-server/src/encode.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as jose from "node-jose"; 5 | import { toDataURL } from 'qrcode'; 6 | import { config } from "./config.js"; 7 | 8 | export async function encode(request: SHLLinkRequest): Promise { 9 | 10 | const { passcode, payload, files, viewer } = request; 11 | const randomUrlSegment = payload?.path || randomBase64Url(); 12 | const jwkKey = await createKey(payload?.key); 13 | const exportKey = (jwkKey.toJSON(true) as { k: string }).k; 14 | 15 | const jweFiles = await encodeFiles(files.map(f => JSON.stringify(f)), jwkKey); 16 | 17 | const url = payload?.flag?.includes('U') ? `${config.SERVER_BASE}shl/${randomUrlSegment}` : `${config.SERVER_BASE}${randomUrlSegment}`; 18 | 19 | const respPayload: ShlinkPayload = { 20 | "url": url, 21 | "flag": `${payload?.flag?.includes('L') ? 'L' : ''}${payload?.flag?.includes('P') ? 'P' : payload?.flag?.includes('U') ? 'U' : ''}` as PayloadFlags, 22 | "key": exportKey, 23 | "label": payload?.label ?? "", 24 | } 25 | 26 | if (payload?.exp) { 27 | respPayload.exp = typeof payload.exp === 'string' ? 28 | new Date(payload.exp).getTime() / 1000.0 : 29 | payload.exp; 30 | } 31 | 32 | if (payload?.v) { 33 | respPayload.v = payload.v; 34 | } 35 | 36 | const link = `${viewer ?? ''}${viewer ? '#' : ''}shlink:/${Buffer.from(JSON.stringify(respPayload)).toString('base64url')}`; 37 | 38 | const qrcode = await toDataURL(link, { errorCorrectionLevel: 'Q' }); 39 | 40 | return { 41 | link, 42 | passcode: passcode ?? '', 43 | attempts: request.attempts || config.DEFAULT_ATTEMPTS, 44 | randomUrlSegment, 45 | preserveFilePaths: !!request.filePaths?.length, 46 | filePaths: request.filePaths || [], 47 | payload: respPayload, 48 | jweFiles, 49 | qrcode 50 | }; 51 | 52 | } 53 | 54 | async function createKey(key?: string): Promise { 55 | let jwkKey; 56 | if (key) { 57 | jwkKey = await jose.JWK.asKey(Buffer.from(`{"kty":"oct","use":"enc","alg":"A256GCM","k":"${key}"}`, 'utf-8')); 58 | } else { 59 | const keystore = jose.JWK.createKeyStore(); 60 | jwkKey = await keystore.generate('oct', 256, { alg: 'A256GCM', use: 'enc', }); 61 | } 62 | return jwkKey; 63 | } 64 | 65 | async function encodeFiles(files: string[], jwkKey: jose.JWK.Key): Promise { 66 | return await Promise.all(files.map((file) => ( 67 | jose.JWE.createEncrypt({ format: 'compact' }, jwkKey) 68 | .update(Buffer.from(file, 'utf-8')) 69 | .final() 70 | ))); 71 | } 72 | 73 | export function randomBase64Url(byteLength = 32): string { 74 | return Buffer.from(Buffer.alloc(byteLength).map(() => Math.floor(Math.random() * 256))).toString('base64url'); 75 | } 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* 117 | 118 | # emacs 119 | *~ 120 | 121 | # examples from fetch-examples 122 | testdata/example* 123 | testdata/issuer.jwks.public.json 124 | 125 | # compiled js files 126 | js/* 127 | testdata/missing_kid_key.json 128 | 129 | # temp files 130 | tmp/* 131 | 132 | # full fhir schema 133 | schema/fhir-schema-full.json 134 | 135 | # temp fhir bundle file 136 | ~temp.fhirbundle.json 137 | 138 | # downloaded HL7 validator for JRE deployment 139 | validator_cli.jar 140 | 141 | # personal setting 142 | .vscode/settings.json 143 | -------------------------------------------------------------------------------- /testdata/test-example-00-b-jws-payload-expanded-expired.json: -------------------------------------------------------------------------------- 1 | { 2 | "iss": "https://spec.smarthealth.cards/examples/issuer", 3 | "nbf": 1639496673.998, 4 | "exp": 1639496674.998, 5 | "vc": { 6 | "type": [ 7 | "https://smarthealth.cards#health-card", 8 | "https://smarthealth.cards#immunization", 9 | "https://smarthealth.cards#covid19" 10 | ], 11 | "credentialSubject": { 12 | "fhirVersion": "4.0.1", 13 | "fhirBundle": { 14 | "resourceType": "Bundle", 15 | "type": "collection", 16 | "entry": [ 17 | { 18 | "fullUrl": "resource:0", 19 | "resource": { 20 | "resourceType": "Patient", 21 | "name": [ 22 | { 23 | "family": "Anyperson", 24 | "given": [ 25 | "John", 26 | "B." 27 | ] 28 | } 29 | ], 30 | "birthDate": "1951-01-20" 31 | } 32 | }, 33 | { 34 | "fullUrl": "resource:1", 35 | "resource": { 36 | "resourceType": "Immunization", 37 | "status": "completed", 38 | "vaccineCode": { 39 | "coding": [ 40 | { 41 | "system": "http://hl7.org/fhir/sid/cvx", 42 | "code": "207" 43 | } 44 | ] 45 | }, 46 | "patient": { 47 | "reference": "resource:0" 48 | }, 49 | "occurrenceDateTime": "2021-01-01", 50 | "performer": [ 51 | { 52 | "actor": { 53 | "display": "ABC General Hospital" 54 | } 55 | } 56 | ], 57 | "lotNumber": "0000001" 58 | } 59 | }, 60 | { 61 | "fullUrl": "resource:2", 62 | "resource": { 63 | "resourceType": "Immunization", 64 | "status": "completed", 65 | "vaccineCode": { 66 | "coding": [ 67 | { 68 | "system": "http://hl7.org/fhir/sid/cvx", 69 | "code": "207" 70 | } 71 | ] 72 | }, 73 | "patient": { 74 | "reference": "resource:0" 75 | }, 76 | "occurrenceDateTime": "2021-01-29", 77 | "performer": [ 78 | { 79 | "actor": { 80 | "display": "ABC General Hospital" 81 | } 82 | } 83 | ], 84 | "lotNumber": "0000007" 85 | } 86 | } 87 | ] 88 | } 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /testdata/test-example-00-b-jws-payload-expanded-pre-expired.json: -------------------------------------------------------------------------------- 1 | { 2 | "iss": "https://spec.smarthealth.cards/examples/issuer", 3 | "nbf": 1639496673.998, 4 | "exp": 1639496672.998, 5 | "vc": { 6 | "type": [ 7 | "https://smarthealth.cards#health-card", 8 | "https://smarthealth.cards#immunization", 9 | "https://smarthealth.cards#covid19" 10 | ], 11 | "credentialSubject": { 12 | "fhirVersion": "4.0.1", 13 | "fhirBundle": { 14 | "resourceType": "Bundle", 15 | "type": "collection", 16 | "entry": [ 17 | { 18 | "fullUrl": "resource:0", 19 | "resource": { 20 | "resourceType": "Patient", 21 | "name": [ 22 | { 23 | "family": "Anyperson", 24 | "given": [ 25 | "John", 26 | "B." 27 | ] 28 | } 29 | ], 30 | "birthDate": "1951-01-20" 31 | } 32 | }, 33 | { 34 | "fullUrl": "resource:1", 35 | "resource": { 36 | "resourceType": "Immunization", 37 | "status": "completed", 38 | "vaccineCode": { 39 | "coding": [ 40 | { 41 | "system": "http://hl7.org/fhir/sid/cvx", 42 | "code": "207" 43 | } 44 | ] 45 | }, 46 | "patient": { 47 | "reference": "resource:0" 48 | }, 49 | "occurrenceDateTime": "2021-01-01", 50 | "performer": [ 51 | { 52 | "actor": { 53 | "display": "ABC General Hospital" 54 | } 55 | } 56 | ], 57 | "lotNumber": "0000001" 58 | } 59 | }, 60 | { 61 | "fullUrl": "resource:2", 62 | "resource": { 63 | "resourceType": "Immunization", 64 | "status": "completed", 65 | "vaccineCode": { 66 | "coding": [ 67 | { 68 | "system": "http://hl7.org/fhir/sid/cvx", 69 | "code": "207" 70 | } 71 | ] 72 | }, 73 | "patient": { 74 | "reference": "resource:0" 75 | }, 76 | "occurrenceDateTime": "2021-01-29", 77 | "performer": [ 78 | { 79 | "actor": { 80 | "display": "ABC General Hospital" 81 | } 82 | } 83 | ], 84 | "lotNumber": "0000007" 85 | } 86 | } 87 | ] 88 | } 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /testdata/test-example-00-b-jws-payload-expanded-exp_milliseconds.json: -------------------------------------------------------------------------------- 1 | { 2 | "iss": "https://spec.smarthealth.cards/examples/issuer", 3 | "nbf": 1639496673.998, 4 | "exp": 1739496673998, 5 | "vc": { 6 | "type": [ 7 | "https://smarthealth.cards#health-card", 8 | "https://smarthealth.cards#immunization", 9 | "https://smarthealth.cards#covid19" 10 | ], 11 | "credentialSubject": { 12 | "fhirVersion": "4.0.1", 13 | "fhirBundle": { 14 | "resourceType": "Bundle", 15 | "type": "collection", 16 | "entry": [ 17 | { 18 | "fullUrl": "resource:0", 19 | "resource": { 20 | "resourceType": "Patient", 21 | "name": [ 22 | { 23 | "family": "Anyperson", 24 | "given": [ 25 | "John", 26 | "B." 27 | ] 28 | } 29 | ], 30 | "birthDate": "1951-01-20" 31 | } 32 | }, 33 | { 34 | "fullUrl": "resource:1", 35 | "resource": { 36 | "resourceType": "Immunization", 37 | "status": "completed", 38 | "vaccineCode": { 39 | "coding": [ 40 | { 41 | "system": "http://hl7.org/fhir/sid/cvx", 42 | "code": "207" 43 | } 44 | ] 45 | }, 46 | "patient": { 47 | "reference": "resource:0" 48 | }, 49 | "occurrenceDateTime": "2021-01-01", 50 | "performer": [ 51 | { 52 | "actor": { 53 | "display": "ABC General Hospital" 54 | } 55 | } 56 | ], 57 | "lotNumber": "0000001" 58 | } 59 | }, 60 | { 61 | "fullUrl": "resource:2", 62 | "resource": { 63 | "resourceType": "Immunization", 64 | "status": "completed", 65 | "vaccineCode": { 66 | "coding": [ 67 | { 68 | "system": "http://hl7.org/fhir/sid/cvx", 69 | "code": "207" 70 | } 71 | ] 72 | }, 73 | "patient": { 74 | "reference": "resource:0" 75 | }, 76 | "occurrenceDateTime": "2021-01-29", 77 | "performer": [ 78 | { 79 | "actor": { 80 | "display": "ABC General Hospital" 81 | } 82 | } 83 | ], 84 | "lotNumber": "0000007" 85 | } 86 | } 87 | ] 88 | } 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /testdata/test-example-02-f-qr-code-numeric-value-0-qr_chunk_too_big.txt: -------------------------------------------------------------------------------- 1 | shc:/1/2/567629095243206034602924374044603122295953265460346029254077280433602870286471674522280928613331456437653141590640220306450459085643550341424541364037063665417137241236380304375622046737407532323925433443326057360110410431530032072473076958374577264158760675382373547703203370334523261265423635700060235333347521692962382729057066350010037009231170354042296059422509236226581125045741532707705239112652374125457427601067564156576154534238030959522964421262246342310929674070096870577461425207764244044110534070580426363900532134423628033760212624404062632574347406345074776872084032777300771043345072105055350739092857580922685263653264113967357054610608434008690654711162455350636442412569645227076320213342740068115069503345296542554063532572677730681105287440352109262426745339283271425310642440682027303477407104573209760910313925410753271134412843275976325550082864330663370456650360047441455204686708704577043263233304655541574154736258383143644531684003533744616933623260070661386941096153290469692364114068572921671124042966306839566804756430550862686430030530056444610965564406654033406139277264745350003559455557043312530541523865657067380843643377105268386532000467261063310362716912306369662420672226283611607220237065072177760650393071366932647241116706746376066337315911240375777604502864765535502937683732447475285977312432036322206325707722453821276009427460446007663665354105767671416364605820710040727012720358643745654037436206347372403900002567063742773958502634126760432265667612765565454169320972045932635041253126103939086030086357633263610352076705260405425276103531315538680661686905005630555857007029233171253068565044087642434235607745535753772103334156612325293172690362666961260804752430580540287227772444215463033060717171671152297130637362003374653942332138714476222631203620286828041255090561454537085441387768280365755761526367735336292258376221202571443770342953245355352143635768353545522868094268762732113736280505547145634145720956450366435362066475096906212758556605436304545326257237373411213852285459682020602756320832414121083107353820294136612109577205765670350408503340212877633112343063655629580059772734605952752811056340632357534255264436212260561064506174235805437108702460685711696308326062650668591039706852765060104041755608256631073120402044334554593659580463752521646220620322204052672028334466242672382223432120035923534058560475262857547472341272620471653505500466655231566610355354271074110826744369353006043042451166384276710467554221437767407668553863066309045357217171085011637642506245626345115550643029600057293725566933661259642363505345696235325936102528667512042224526038546250662358370760700743597004405645382837343433765641692639555505724333762154592530382658396210035510556809615950037533301163724268633542125935397023302936554555674364392577410503763158427673602324706563677110072566543354435052105767295621000406311005402403541011080439106266525952063003617075357374751261373150033642005477717743420054415304096126297357320456066766285957046426100458666542627123086835420772042670106875592424045634335260505931411056657144060711416255266661366169722244553842547711064403312839662062655932576053096521586734337160563567263955420636692009345737233344627420355055752469274340090639525727006837363469 -------------------------------------------------------------------------------- /testdata/test-example-02-f-qr-code-numeric-value-1-qr_chunk_too_big.txt: -------------------------------------------------------------------------------- 1 | shc:/2/2/045455613512202758730756395925662125770503265944503605232368070064247466332135233220563657203856063236126375541228081059652869353031230075404363754454701223640372503309353720577040295644045922277724742756500371567174345940405441592823761142062721674132215224591204346421502866317767623908345663052265123868557558056074683424417760293468266828504476232110297129113669592710382332680539055573316161440660367060322158332056285958115462213364256120501243664406072954542958542426506159713508636035055742222452355855276963582675272109266435571256676132454326576762323829236859117667356659360966573268390928403468277730620060252368591105673567593674666233683720446633547024247205222043532920313370582526003822234328592169623658047628363810242821552223266441762043636136395575296261647475033620366420275908502855627207117363034342341126382705640557757462590840325631725858042670502309057408703076692665542355772638065875660669582552072309704474275321347635205236617074225232365860206377625024217523693733707443684433592005523172584530255864082334217203074570397643522806587744092163730623422835741153442164704377220032255552426366050770585072683700275908214445522603261053416372052261414137213724597074325332032160721254120674774235236826425056560636030561097736557027103659415311082353646144426127703033776222606340683823735425642161250536662922752666532023647432215728063636273662616930630563737521632461341020270822090741050657053352537574765905257328113927280606290573553061437168265340092005204335687574325969535267582925253869092638557243571009344173297258067075720635725971682503765658596629057732212563762760410856763427653627032807742740703606383161581175636370734435247039296707264142217437533830101027585320576734231274616653580323762409073476414142053056224206327432726943407671325609226435372705206357671235065328500459202433704411284010303504087234204000654324072836662620313763325222744420332127083969031276122726354526052040716022592671722456592858393975262626256723272755596122616623532831335920674305126975597726062556762024423331067744622825376506705525232653247771753658636036444104375845614332542557711061273856282726277259593340585876595537453477277755697227375028665675337475101021507061733871742574290064435440757153417571062669691273343059050467725308577126335873116200394433053075585855623127094557293152507563246439642769300359216261572756377472354525602106596010283428586023267561702421244175075270616620312772535369316224635062290336543637395073583369275655306450092427007341276360383311645973377144626157296653035342672806095226122604683833006459733771526269572960070737323667700666330505542553604042710811720009614300222925530743432144726507623772123852721250216836615645453468062664714155076070503668532037740650710621282169595935403961256812350064123759741235386762210735412154002732412327215728120756683767413243372223223725591272566273614359256143212124056665585307730800003938695858246124586371125911630763617040233974562011576760702132205540246144662025713567447477702556256670411164647372695810112769283409244132383262721269071055245727623075310068650550502174016153523541586023346256226944744228644176220657640355596409726360434470327639605323113545343906352743417157636364076429336728722832701266622009037420387771626668554033206974 -------------------------------------------------------------------------------- /schema/smart-health-card-vc-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://smarthealth.cards/schema/smart-health-card-vc.json", 4 | "title": "Root", 5 | "type": "object", 6 | "required": [ 7 | "iss", 8 | "nbf", 9 | "vc" 10 | ], 11 | "properties": { 12 | "iss": { 13 | "$id": "#root/iss", 14 | "title": "Iss", 15 | "type": "string" 16 | }, 17 | "nbf": { 18 | "$id": "#root/nbf", 19 | "title": "Nbf", 20 | "type": "number", 21 | "$comment": "TODO: more restriction" 22 | }, 23 | "exp": { 24 | "$id": "#root/exp", 25 | "title": "Exp", 26 | "type": "number", 27 | "$comment": "TODO: more restriction" 28 | }, 29 | "vc": { 30 | "$id": "#root/vc", 31 | "title": "Vc", 32 | "type": "object", 33 | "required": [ 34 | "type", 35 | "credentialSubject" 36 | ], 37 | "properties": { 38 | "type": { 39 | "$id": "#root/vc/type", 40 | "title": "Type", 41 | "type": "array", 42 | "default": [], 43 | "items": { 44 | "$id": "#root/vc/type/items", 45 | "title": "Items", 46 | "type": "string", 47 | "default": "", 48 | "examples": [ 49 | "https://smarthealth.cards#health-card", 50 | "https://smarthealth.cards#immunization", 51 | "https://smarthealth.cards#covid19" 52 | ], 53 | "pattern": "^.*$" 54 | } 55 | }, 56 | "credentialSubject": { 57 | "$id": "#root/vc/credentialSubject", 58 | "title": "Credentialsubject", 59 | "type": "object", 60 | "required": [ 61 | "fhirVersion", 62 | "fhirBundle" 63 | ], 64 | "properties": { 65 | "fhirVersion": { 66 | "$id": "#root/vc/credentialSubject/fhirVersion", 67 | "title": "Fhirversion", 68 | "type": "string", 69 | "default": "", 70 | "examples": [ 71 | "4.0.1" 72 | ], 73 | "pattern": "^.*$" 74 | }, 75 | "fhirBundle": { 76 | "$id": "#root/vc/credentialSubject/fhirBundle", 77 | "title": "Fhirbundle", 78 | "$comment": "\"$ref\": \"fhir-schema.json#\" to reference sub-schema", 79 | "type": "object", 80 | "default": "" 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /src/generate-test-data.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | // NOTE: generate some of the testdata file. Many others are generated by the script: 5 | // https://github.com/microsoft/health-cards/blob/generate-test-files/generate-examples/src/index.ts 6 | 7 | import fs from 'fs'; 8 | import path from 'path'; 9 | import jose from 'node-jose'; 10 | import { parseJson } from './utils'; 11 | 12 | interface KeyGenerationArgs { 13 | kty: string; 14 | size?: string | number; 15 | props?: { alg: string, crv?: string, use: string, kid?: string } 16 | } 17 | const outdir = 'testdata'; 18 | 19 | async function generateAndStoreKey(outFileName: string, keyGenArgs: KeyGenerationArgs, count = 1, isPrivate = false, omit = ''): Promise { 20 | const outFilePath = path.join(outdir, outFileName); 21 | if (!fs.existsSync(outFilePath)) { 22 | console.log("Generating " + outFilePath); 23 | const keystore = jose.JWK.createKeyStore(); 24 | for (let i = 0; i < count; i++) { 25 | await keystore.generate(keyGenArgs.kty, keyGenArgs.size, keyGenArgs.props); 26 | } 27 | const jwkSet = keystore.toJSON(isPrivate); 28 | if (omit) { 29 | // TODO: delete this property 30 | } 31 | fs.writeFileSync(outFilePath, JSON.stringify(jwkSet)); 32 | } 33 | } 34 | 35 | void generateAndStoreKey('valid_key.json', { kty: 'EC', size: 'P-256', props: { alg: 'ES256', crv: 'P-256', use: 'sig' } }); 36 | void generateAndStoreKey('private_key.json', { kty: 'EC', size: 'P-256', props: { alg: 'ES256', crv: 'P-256', use: 'sig' }}, 1, true); 37 | void generateAndStoreKey('valid_keys.json', { kty: 'EC', size: 'P-256', props: { alg: 'ES256', crv: 'P-256', use: 'sig' } }, 3); 38 | void generateAndStoreKey('wrong_kid_key.json', { kty: 'EC', size: 'P-256', props: { alg: 'ES256', crv: 'P-256', use: 'sig', kid: 'ThisIsNotTheThumbprintOfTheKey' } }); 39 | void generateAndStoreKey('wrong_curve_key.json', { kty: 'EC', size: 'P-384', props: { alg: 'ES256', crv: 'P-384', use: 'sig' } }); 40 | void generateAndStoreKey('wrong_use_key.json', { kty: 'EC', size: 'P-256', props: { alg: 'ES256', crv: 'P-256', use: 'enc' } }); 41 | void generateAndStoreKey('wrong_alg_key.json', { kty: 'EC', size: 'P-256', props: { alg: 'ES256K', crv: 'P-256', use: 'sig' } }); 42 | void generateAndStoreKey('wrong_kty_key.json', { kty: 'RSA', size: 2048 }); 43 | 44 | function generateMultiVCHealthCardFile(outFileName: string, inFileNames: string[]) { 45 | const outFilePath = path.join(outdir, outFileName); 46 | if (!fs.existsSync(outFilePath)) { 47 | console.log("Generating " + outFilePath); 48 | const hc: HealthCard = {verifiableCredential: []} 49 | hc.verifiableCredential = inFileNames.map(file => { 50 | const hc = parseJson(fs.readFileSync(path.join(outdir, file)).toString('utf-8')); 51 | if (hc) { 52 | return hc.verifiableCredential; 53 | } else { 54 | return []; 55 | } 56 | }).reduce( (a,c) => a?.concat(c), []); 57 | fs.writeFileSync(outFilePath, JSON.stringify(hc)); 58 | } 59 | } 60 | 61 | generateMultiVCHealthCardFile('test-example-00-e-file-multi-jws.smart-health-card', ['example-00-e-file.smart-health-card', 'example-01-e-file.smart-health-card']); -------------------------------------------------------------------------------- /tests/schema.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import { AnySchemaObject } from 'ajv'; 5 | import fs from 'fs'; 6 | import path from 'path'; 7 | import Log from '../src/logger'; 8 | import { validateSchema } from '../src/schema'; 9 | 10 | 11 | const schemaDir = './schema'; 12 | const exampleDir = './testdata'; 13 | 14 | 15 | // Map our schema files to the examples. The tests below can lookup 16 | // the appropriate schema with the 'lookup' function 17 | const schemaMappings: { [key: string]: string[] } = { 18 | "keyset-schema": [ 19 | 'issuer.jwks.public.json' 20 | ], 21 | "fhir-schema": [ 22 | 'example-00-a-fhirBundle.json', 23 | 'example-01-a-fhirBundle.json' 24 | ], 25 | "smart-health-card-schema": [ 26 | 'example-00-e-file.smart-health-card', 27 | 'example-01-e-file.smart-health-card' 28 | ], 29 | "smart-health-card-vc-schema": [ 30 | 'example-00-b-jws-payload-expanded.json', 31 | 'example-00-c-jws-payload-minified.json', 32 | 'example-01-b-jws-payload-expanded.json', 33 | 'example-01-c-jws-payload-minified.json', 34 | ], 35 | "jws-schema": [ 36 | 'example-00-d-jws.txt', 37 | 'example-01-d-jws.txt' 38 | ] 39 | }; 40 | 41 | const lookup = function (example: string): string { 42 | for (const schema in schemaMappings) { 43 | if (schemaMappings[schema].indexOf(example) >= 0) return schema; 44 | } 45 | throw new Error('Example not found : ' + example); 46 | }; 47 | 48 | 49 | const log = new Log('Schema Tests'); 50 | 51 | 52 | function testSchema(exampleFile: string): boolean { 53 | 54 | const schemaName = lookup(exampleFile); 55 | const schemaPath = path.resolve(schemaDir, schemaName + ".json"); 56 | const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf-8')) as AnySchemaObject; 57 | 58 | const examplePath = path.resolve(exampleDir, exampleFile); 59 | const fileData = fs.readFileSync(examplePath, 'utf-8'); 60 | const ext = path.extname(examplePath); 61 | const dataObj = ext !== '.txt' ? JSON.parse(fileData) as FhirBundle | JWS | JWSPayload | HealthCard : fileData; 62 | 63 | const result = validateSchema(schema, dataObj, log); 64 | 65 | expect(result).toBe(true); 66 | 67 | return result; 68 | } 69 | 70 | 71 | test("Schema: valid 00-a-fhirBundle", () => { testSchema('example-00-a-fhirBundle.json'); }); 72 | test("Schema: valid 00-b-jws-payload-expanded", () => { testSchema('example-00-b-jws-payload-expanded.json'); }); 73 | test("Schema: valid 00-c-jws-payload-minified", () => { testSchema('example-00-c-jws-payload-minified.json'); }); 74 | test("Schema: valid 00-d-jws", () => { testSchema('example-00-d-jws.txt'); }); 75 | test("Schema: valid 00-e-file.smart-health-card", () => { testSchema('example-00-e-file.smart-health-card'); }); 76 | 77 | test("Schema: valid issuer.jwks.public", () => { testSchema('issuer.jwks.public.json'); }); 78 | 79 | test("Schema: valid 01-a-fhirBundle", () => { testSchema('example-01-a-fhirBundle.json'); }); 80 | test("Schema: valid 01-b-jws-payload-expanded", () => { testSchema('example-01-b-jws-payload-expanded.json'); }); 81 | test("Schema: valid 01-c-jws-payload-minified", () => { testSchema('example-01-c-jws-payload-minified.json'); }); 82 | test("Schema: valid 01-d-jws", () => { testSchema('example-01-d-jws.txt'); }); 83 | test("Schema: valid 01-e-file.smart-health-card", () => { testSchema('example-01-e-file.smart-health-card'); }); 84 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import { LogLevels } from './logger'; 5 | import { ValidationProfiles, Validators } from './fhirBundle'; 6 | 7 | interface IOptions { 8 | logLevel: LogLevels, 9 | profile: ValidationProfiles, 10 | issuerDirectory: string, 11 | clearKeyStore: boolean, 12 | cascade: boolean, 13 | logOutputPath: string, 14 | skipJwksDownload: boolean, 15 | jwkset: string, 16 | validator: Validators, 17 | validationTime: string, 18 | passCode: string, 19 | decryptionKey: string, 20 | index: number, 21 | exclude: string[], 22 | shlFile?: ShlinkFile 23 | } 24 | 25 | const defaultOptions: IOptions = { 26 | logLevel: LogLevels.WARNING, 27 | profile: ValidationProfiles['any'], 28 | issuerDirectory: '', 29 | clearKeyStore: false, 30 | cascade: true, 31 | logOutputPath: '', 32 | skipJwksDownload: false, 33 | jwkset: '', 34 | validator: Validators.default, 35 | validationTime: '', 36 | passCode: '', 37 | decryptionKey: '', 38 | index: 0, 39 | exclude: [], 40 | shlFile: undefined 41 | } 42 | 43 | const setOptions = function (options: Partial = {}): IOptions { 44 | 45 | // check if the passed in options are valid 46 | Object.keys(options).forEach(k => { 47 | if (!(k in defaultOptions)) throw new Error(`Unknown option ${k}`); 48 | }); 49 | 50 | if (options) { 51 | /* Apparently TS does not consider 'isInteger()' as type-guard so we need the ' ?? -n' to coerce not-really-undefined to an invalid integer*/ 52 | if ('logLevel' in options && (!Number.isInteger(options.logLevel) || !((options.logLevel ?? -2) in LogLevels))) throw new Error(`Invalid logLevel ${options.logLevel ?? ''}`); 53 | if ('profile' in options && (!Number.isInteger(options.profile) || !((options.profile ?? -1) in ValidationProfiles))) throw new Error(`Invalid profile ${options.profile ?? ''}`); 54 | if ('issuerDirectory' in options && typeof options.issuerDirectory !== 'string') throw new Error(`Invalid issuerDirectory ${options.issuerDirectory ?? ''}`); 55 | if ('clearKeyStore' in options && typeof options.clearKeyStore !== 'boolean') throw new Error(`Invalid clearKeyStore ${options.clearKeyStore ?? ''}`); 56 | if ('cascade' in options && typeof options.cascade !== 'boolean') throw new Error(`Invalid cascade ${options.cascade ?? ''}`); 57 | if ('skipJwksDownload' in options && typeof options.skipJwksDownload !== 'boolean') throw new Error(`Invalid skipJwksDownload ${options.skipJwksDownload ?? ''}`); 58 | if ('logOutputPath' in options && typeof options.logOutputPath !== 'string') throw new Error(`Invalid logOutputPath ${options.logOutputPath ?? ''}`); 59 | if ('jwkset' in options && typeof options.jwkset !== 'string') throw new Error(`Invalid jwkset ${options.jwkset ?? ''}`); 60 | if ('validator' in options && (!Number.isInteger(options.validator) || !((options.validator ?? -1) in Validators))) throw new Error(`Invalid validator ${options.validator ?? ''}`); 61 | if ('exclude' in options && (!(options.exclude instanceof Array) || !options.exclude?.every(e => typeof e === 'string'))) throw new Error(`Invalid exclude: not a string array`); 62 | } 63 | 64 | return { // this syntax merges two object into a single object. For common properties, the second entry wins. 65 | ...defaultOptions, 66 | ...options 67 | }; 68 | } 69 | 70 | export { IOptions, setOptions }; 71 | -------------------------------------------------------------------------------- /src/file.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import fs from 'fs'; 5 | import path from 'path'; 6 | import { getImageBuffer } from './image'; 7 | 8 | 9 | /* 10 | 11 | Reads a user file from the file system and returns an object with the data buffer and additional metadata. 12 | If we can get all the data/metadata we need here, then ideally, none of the other code needs to deal with the file system. 13 | 14 | This is used for all user supplied input files: 15 | 16 | shc decoded qr code data 17 | svg qr code as vector data 18 | png qr code as an image 19 | bmp " 20 | gif " 21 | tif " 22 | jpg " 23 | jws serialized json web signature (jws) 24 | smart-health-card json with a verifiable credential array 25 | fhir-bundle json FHIR data 26 | keys json with an array of keys 27 | 28 | */ 29 | 30 | export interface FileInfo { 31 | name: string, 32 | path: string, 33 | ext: string, 34 | buffer: Buffer, 35 | fileType: string | undefined, 36 | image?: FileImage 37 | } 38 | 39 | 40 | export interface FileImage { 41 | data: Buffer, height: number, width: number, density?: number 42 | } 43 | 44 | 45 | // Reads a file and determines what kind of file it is 46 | export async function getFileData(filepath: string): Promise { 47 | 48 | if (!fs.existsSync(filepath)) { 49 | throw new Error("File not found : " + filepath); 50 | } 51 | 52 | // read the file data 53 | const buffer: Buffer = fs.readFileSync(filepath); 54 | 55 | // collect file metadata 56 | const fileInfo: FileInfo = { 57 | name: path.basename(filepath, path.extname(filepath)), 58 | path: path.resolve(filepath), 59 | ext: path.extname(filepath), 60 | buffer: buffer, 61 | fileType: fileType(buffer) 62 | }; 63 | 64 | // get the image data if this is an image file 65 | switch (fileInfo.fileType) { 66 | case 'png': 67 | case 'jpg': 68 | case 'gif': 69 | case 'tif': 70 | case 'bmp': 71 | case 'svg': 72 | fileInfo.image = await getImageBuffer(fileInfo); 73 | break; 74 | } 75 | 76 | return fileInfo; 77 | } 78 | 79 | 80 | // determine the type of a file by examining its contents 81 | function fileType(buffer: Buffer): string | undefined { 82 | 83 | const hex = (start: number, end?: number) => buffer.toString('hex', start, end); 84 | const ascii = (start: number, end?: number) => buffer.toString('ascii', start, end); 85 | 86 | if (buffer.length < 8) return undefined; 87 | 88 | if (ascii(0, 2) === 'BM' && buffer.readUInt32LE(2) === buffer.length) return 'bmp'; 89 | 90 | if (hex(0, 2) === 'ffd8' && buffer.subarray(-2).toString('hex') === 'ffd9') return 'jpg'; 91 | 92 | if (hex(0, 8) === '89504e470d0a1a0a') return 'png'; 93 | 94 | if (/GIF8(9|7)a/.test(ascii(0, 6))) return 'gif'; 95 | 96 | if (hex(0, 4) === '49492a00') return 'tif'; 97 | 98 | // we didn't match it to an image type, try text 99 | 100 | const fileText = buffer.toString('utf-8'); 101 | 102 | if (/^\s*