shc_vaccination_test_kit 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/shc_vaccination_test_kit/igs/README.md +56 -0
- data/lib/shc_vaccination_test_kit/igs/cards.smarthealth.terminology-0.1.0.tgz +0 -0
- data/lib/shc_vaccination_test_kit/igs/shc-vaccination-1.0.0-updated.tgz +0 -0
- data/lib/shc_vaccination_test_kit/javascript/jsQR.js +10100 -0
- data/lib/shc_vaccination_test_kit/javascript/qr-scanner-worker.min.js +98 -0
- data/lib/shc_vaccination_test_kit/javascript/qr-scanner.min.js +31 -0
- data/lib/shc_vaccination_test_kit/metadata.rb +54 -0
- data/lib/shc_vaccination_test_kit/shc_vaccination_validation_test.rb +152 -0
- data/lib/shc_vaccination_test_kit/version.rb +4 -0
- data/lib/shc_vaccination_test_kit/views/scan_qr_code.html +207 -0
- data/lib/shc_vaccination_test_kit/views/upload_qr_code.html +130 -0
- data/lib/shc_vaccination_test_kit.rb +52 -10
- metadata +26 -21
- data/lib/covid19_vci/fhir_operation.rb +0 -105
- data/lib/covid19_vci/file_download.rb +0 -109
- data/lib/covid19_vci/vc_fhir_validation.rb +0 -75
- data/lib/covid19_vci/vc_headers.rb +0 -21
- data/lib/covid19_vci/vc_payload_verification.rb +0 -115
- data/lib/covid19_vci/vc_signature_verification.rb +0 -69
- data/lib/covid19_vci/version.rb +0 -3
@@ -1,109 +0,0 @@
|
|
1
|
-
require_relative 'vc_fhir_validation'
|
2
|
-
require_relative 'vc_headers'
|
3
|
-
require_relative 'vc_payload_verification'
|
4
|
-
require_relative 'vc_signature_verification'
|
5
|
-
|
6
|
-
module Covid19VCI
|
7
|
-
class FileDownload < Inferno::TestGroup
|
8
|
-
id 'vci_file_download'
|
9
|
-
title 'Download and validate a health card via file download'
|
10
|
-
|
11
|
-
input :file_download_url
|
12
|
-
|
13
|
-
test do
|
14
|
-
id 'vci-file-01'
|
15
|
-
title 'Health card can be downloaded'
|
16
|
-
description 'The health card can be downloaded and is a valid JSON object'
|
17
|
-
makes_request :vci_file_download
|
18
|
-
|
19
|
-
run do
|
20
|
-
get(file_download_url, name: :vci_file_download)
|
21
|
-
|
22
|
-
assert_response_status(200)
|
23
|
-
assert_valid_json(response[:body])
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
test do
|
28
|
-
id 'vci-file-02'
|
29
|
-
title 'Response contains correct Content-Type of application/smart-health-card'
|
30
|
-
uses_request :vci_file_download
|
31
|
-
|
32
|
-
run do
|
33
|
-
skip_if request.status != 200, 'Health card not successfully downloaded'
|
34
|
-
|
35
|
-
content_type = request.response_header('Content-Type')
|
36
|
-
|
37
|
-
assert content_type.present?, 'Response did not include a Content-Type header'
|
38
|
-
assert content_type.value.match?(%r{\Aapplication/smart-health-card(\z|\W)}),
|
39
|
-
"Content-Type header was '#{content_type.value}' instead of 'application/smart-health-card'"
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
test do
|
44
|
-
id 'vci-file-03'
|
45
|
-
title 'Health card is provided as a file download with a .smart-health-card extension'
|
46
|
-
uses_request :vci_file_download
|
47
|
-
|
48
|
-
run do
|
49
|
-
skip_if request.status != 200, 'Health card not successfully downloaded'
|
50
|
-
|
51
|
-
pass_if request.url.ends_with?('.smart-health-card')
|
52
|
-
|
53
|
-
content_disposition = request.response_header('Content-Disposition')
|
54
|
-
assert content_disposition.present?,
|
55
|
-
"Url did not end with '.smart-health-card' and response did not include a Content-Disposition header"
|
56
|
-
|
57
|
-
attachment_pattern = /\Aattachment;/
|
58
|
-
assert content_disposition.value.match?(attachment_pattern),
|
59
|
-
"Url did not end with '.smart-health-card' and " \
|
60
|
-
"Content-Disposition header does not indicate file should be downloaded: '#{content_disposition}'"
|
61
|
-
|
62
|
-
extension_pattern = /filename=".*\.smart-health-card"/
|
63
|
-
assert content_disposition.value.match?(extension_pattern),
|
64
|
-
"Url did not end with '.smart-health-card' and Content-Disposition header does not indicate " \
|
65
|
-
"file should have a '.smart-health-card' extension: '#{content_disposition}'"
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
test do
|
70
|
-
id 'vci-file-04'
|
71
|
-
title 'Response contains an array of Verifiable Credential strings'
|
72
|
-
uses_request :vci_file_download
|
73
|
-
output :credential_strings
|
74
|
-
|
75
|
-
run do
|
76
|
-
skip_if request.status != 200, 'Health card not successfully downloaded'
|
77
|
-
|
78
|
-
body = JSON.parse(response[:body])
|
79
|
-
assert body.include?('verifiableCredential'),
|
80
|
-
"Health card does not contain 'verifiableCredential' field"
|
81
|
-
|
82
|
-
vc = body['verifiableCredential']
|
83
|
-
|
84
|
-
assert vc.is_a?(Array), "'verifiableCredential' field must contain an Array"
|
85
|
-
assert vc.length.positive?, "'verifiableCredential' field must contain at least one verifiable credential"
|
86
|
-
|
87
|
-
output credential_strings: vc.join(',')
|
88
|
-
|
89
|
-
pass "Received #{vc.length} verifiable #{'credential'.pluralize(vc.length)}"
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
test from: :vc_headers do
|
94
|
-
id 'vci-file-05'
|
95
|
-
end
|
96
|
-
|
97
|
-
test from: :vc_signature_verification do
|
98
|
-
id 'vci-file-06'
|
99
|
-
end
|
100
|
-
|
101
|
-
test from: :vc_payload_verification do
|
102
|
-
id 'vci-file-07'
|
103
|
-
end
|
104
|
-
|
105
|
-
test from: :vc_fhir_verification do
|
106
|
-
id 'vci-file-08'
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
@@ -1,75 +0,0 @@
|
|
1
|
-
module Covid19VCI
|
2
|
-
class VCFHIRVerification < Inferno::Test
|
3
|
-
title 'Health Card payloads conform to the Vaccination Credential Bundle Profiles'
|
4
|
-
input :credential_strings
|
5
|
-
|
6
|
-
id :vc_fhir_verification
|
7
|
-
|
8
|
-
run do
|
9
|
-
skip_if credential_strings.blank?, 'No Verifiable Credentials received'
|
10
|
-
|
11
|
-
credential_strings.split(',').each do |credential|
|
12
|
-
raw_payload = HealthCards::JWS.from_jws(credential).payload
|
13
|
-
assert raw_payload&.length&.positive?, 'No payload found'
|
14
|
-
|
15
|
-
decompressed_payload =
|
16
|
-
begin
|
17
|
-
Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(raw_payload)
|
18
|
-
rescue Zlib::DataError
|
19
|
-
assert false, 'Payload compression error. Unable to inflate payload.'
|
20
|
-
end
|
21
|
-
|
22
|
-
assert decompressed_payload.length.positive?, 'Payload compression error. Unable to inflate payload.'
|
23
|
-
|
24
|
-
payload_length = decompressed_payload.length
|
25
|
-
health_card = HealthCards::COVIDHealthCard.from_jws(credential)
|
26
|
-
health_card_length = health_card.to_json.length
|
27
|
-
|
28
|
-
assert_valid_json decompressed_payload, 'Payload is not valid JSON'
|
29
|
-
|
30
|
-
payload = JSON.parse(decompressed_payload)
|
31
|
-
vc = payload['vc']
|
32
|
-
assert vc.is_a?(Hash), "Expected 'vc' claim to be a JSON object, but found #{vc.class}"
|
33
|
-
|
34
|
-
subject = vc['credentialSubject']
|
35
|
-
assert subject.is_a?(Hash), "Expected 'vc.credentialSubject' to be a JSON object, but found #{subject.class}"
|
36
|
-
|
37
|
-
raw_bundle = subject['fhirBundle']
|
38
|
-
assert raw_bundle.is_a?(Hash), "Expected 'vc.fhirBundle' to be a JSON object, but found #{raw_bundle.class}"
|
39
|
-
|
40
|
-
bundle = FHIR::Bundle.new(raw_bundle)
|
41
|
-
|
42
|
-
# assert bundle.entry.any? { |r| r.resource.is_a?(FHIR::Immunization) } || bundle.entry.any? { |r| r.resource.is_a?(FHIR::Observation) },
|
43
|
-
# "Bundle must have either Immunization entries or Observation entries"
|
44
|
-
|
45
|
-
# if bundle.entry.any? { |r| r.resource.is_a?(FHIR::Immunization) }
|
46
|
-
assert_valid_resource(
|
47
|
-
resource: bundle,
|
48
|
-
profile_url: 'http://hl7.org/fhir/uv/smarthealthcards-vaccination/StructureDefinition/vaccination-credential-bundle'
|
49
|
-
)
|
50
|
-
|
51
|
-
warning do
|
52
|
-
assert_valid_resource(
|
53
|
-
resource: bundle,
|
54
|
-
profile_url: 'http://hl7.org/fhir/uv/smarthealthcards-vaccination/StructureDefinition/vaccination-credential-bundle-dm'
|
55
|
-
)
|
56
|
-
end
|
57
|
-
# end
|
58
|
-
|
59
|
-
if bundle.entry.any? { |r| r.resource.is_a?(FHIR::Observation) }
|
60
|
-
assert_valid_resource(
|
61
|
-
resource: bundle,
|
62
|
-
profile_url: 'http://hl7.org/fhir/uv/smarthealthcards-vaccination/StructureDefinition/covid19-laboratory-bundle'
|
63
|
-
)
|
64
|
-
|
65
|
-
warning do
|
66
|
-
assert_valid_resource(
|
67
|
-
resource: bundle,
|
68
|
-
profile_url: 'http://hl7.org/fhir/uv/smarthealthcards-vaccination/StructureDefinition/covid19-laboratory-bundle-dm'
|
69
|
-
)
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|
75
|
-
end
|
@@ -1,21 +0,0 @@
|
|
1
|
-
module Covid19VCI
|
2
|
-
class VCHeaders < Inferno::Test
|
3
|
-
title 'Health Cards contain the correct headers'
|
4
|
-
input :credential_strings
|
5
|
-
|
6
|
-
id :vc_headers
|
7
|
-
|
8
|
-
run do
|
9
|
-
skip_if credential_strings.blank?, 'No Verifiable Credentials received'
|
10
|
-
credential_strings.split(',').each do |credential|
|
11
|
-
header = HealthCards::JWS.from_jws(credential).header
|
12
|
-
|
13
|
-
assert header['zip'] == 'DEF', "Expected 'zip' header to equal 'DEF', but found '#{header['zip']}'"
|
14
|
-
assert header['alg'] == 'ES256', "Expected 'alg' header to equal 'ES256', but found '#{header['alg']}'"
|
15
|
-
assert header['kid'].present?, "No 'kid' header was present"
|
16
|
-
rescue StandardError => e
|
17
|
-
assert false, "Error decoding credential: #{e.message}"
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
@@ -1,115 +0,0 @@
|
|
1
|
-
module Covid19VCI
|
2
|
-
class VCPayloadVerification < Inferno::Test
|
3
|
-
title 'Health Card payloads follow the spec requirements'
|
4
|
-
input :credential_strings
|
5
|
-
|
6
|
-
id :vc_payload_verification
|
7
|
-
|
8
|
-
run do
|
9
|
-
skip_if credential_strings.blank?, 'No Verifiable Credentials received'
|
10
|
-
|
11
|
-
credential_strings.split(',').each do |credential|
|
12
|
-
raw_payload = HealthCards::JWS.from_jws(credential).payload
|
13
|
-
assert raw_payload&.length&.positive?, 'No payload found'
|
14
|
-
|
15
|
-
decompressed_payload =
|
16
|
-
begin
|
17
|
-
Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(raw_payload)
|
18
|
-
rescue Zlib::DataError
|
19
|
-
assert false, 'Payload compression error. Unable to inflate payload.'
|
20
|
-
end
|
21
|
-
|
22
|
-
assert decompressed_payload.length.positive?, 'Payload compression error. Unable to inflate payload.'
|
23
|
-
|
24
|
-
payload_length = decompressed_payload.length
|
25
|
-
health_card = HealthCards::HealthCard.from_jws(credential)
|
26
|
-
health_card_length = health_card.to_json.length
|
27
|
-
|
28
|
-
warning do
|
29
|
-
assert payload_length <= health_card_length,
|
30
|
-
"Payload may not be properly minified. Received a payload with length #{payload_length}, " \
|
31
|
-
"but was able to generate a payload with length #{health_card_length}"
|
32
|
-
end
|
33
|
-
|
34
|
-
assert_valid_json decompressed_payload, 'Payload is not valid JSON'
|
35
|
-
|
36
|
-
payload = JSON.parse(decompressed_payload)
|
37
|
-
|
38
|
-
warning do
|
39
|
-
nbf = payload['nbf']
|
40
|
-
assert nbf.present?, "Payload does not include an 'nbf' claim"
|
41
|
-
assert nbf.is_a?(Numeric), "Expected 'nbf' claim to be Numeric, but found #{nbf.class}"
|
42
|
-
issue_time = Time.at(nbf).to_datetime
|
43
|
-
assert issue_time < DateTime.now, "'nbf' is in the future: #{issue_time.rfc822}"
|
44
|
-
end
|
45
|
-
|
46
|
-
vc = payload['vc']
|
47
|
-
assert vc.is_a?(Hash), "Expected 'vc' claim to be a JSON object, but found #{vc.class}"
|
48
|
-
type = vc['type']
|
49
|
-
|
50
|
-
warning do
|
51
|
-
assert type.is_a?(Array), "Expected 'vc.type' to be an array, but found #{type.class}"
|
52
|
-
assert type.include?('https://smarthealth.cards#health-card'),
|
53
|
-
"'vc.type' does not include 'https://smarthealth.cards#health-card'"
|
54
|
-
end
|
55
|
-
|
56
|
-
subject = vc['credentialSubject']
|
57
|
-
assert subject.is_a?(Hash), "Expected 'vc.credentialSubject' to be a JSON object, but found #{subject.class}"
|
58
|
-
|
59
|
-
warning do
|
60
|
-
assert subject['fhirVersion'].present?, "'vc.credentialSubject.fhirVersion' not provided"
|
61
|
-
end
|
62
|
-
|
63
|
-
raw_bundle = subject['fhirBundle']
|
64
|
-
assert raw_bundle.is_a?(Hash), "Expected 'vc.fhirBundle' to be a JSON object, but found #{raw_bundle.class}"
|
65
|
-
|
66
|
-
resource_scheme_regex = /\Aresource:\d+\z/
|
67
|
-
warning do
|
68
|
-
urls = raw_bundle['entry'].map { |entry| entry['fullUrl'] }
|
69
|
-
bad_urls = urls.reject { |url| url.match?(resource_scheme_regex) }
|
70
|
-
assert bad_urls.empty?,
|
71
|
-
"The following Bundle entry urls do not use short resource-scheme URIs: #{bad_urls.join(', ')}"
|
72
|
-
end
|
73
|
-
|
74
|
-
bundle = FHIR::Bundle.new(raw_bundle)
|
75
|
-
resources = bundle.entry.map(&:resource)
|
76
|
-
bundle.entry.each { |entry| entry.resource = nil }
|
77
|
-
resources << bundle
|
78
|
-
|
79
|
-
resources.each do |resource|
|
80
|
-
warning { assert resource.id.nil?, "#{resource.resourceType} resource should not have an 'id' element" }
|
81
|
-
|
82
|
-
if resource.respond_to? :text
|
83
|
-
warning { assert resource.text.nil?, "#{resource.resourceType} resource should not have a 'text' element" }
|
84
|
-
end
|
85
|
-
|
86
|
-
resource.each_element(resource) do |value, meta, path|
|
87
|
-
case meta['type']
|
88
|
-
when 'CodeableConcept'
|
89
|
-
warning { assert value.text.nil?, "#{resource.resourceType} should not have a #{path}.text element" }
|
90
|
-
when 'Coding'
|
91
|
-
warning do
|
92
|
-
assert value.display.nil?, "#{resource.resourceType} should not have a #{path}.display element"
|
93
|
-
end
|
94
|
-
when 'Reference'
|
95
|
-
warning do
|
96
|
-
next if value.reference.nil?
|
97
|
-
|
98
|
-
assert value.reference.match?(resource_scheme_regex),
|
99
|
-
"#{resource.resourceType}.#{path}.reference is not using the short resource URI scheme: " \
|
100
|
-
"#{value.reference}"
|
101
|
-
end
|
102
|
-
when 'Meta'
|
103
|
-
hash = value.to_hash
|
104
|
-
warning do
|
105
|
-
assert hash.length == 1 && hash.include?('security'),
|
106
|
-
"If present, Bundle 'meta' field should only include 'security', " \
|
107
|
-
"but found: #{hash.keys.join(', ')}"
|
108
|
-
end
|
109
|
-
end
|
110
|
-
end
|
111
|
-
end
|
112
|
-
end
|
113
|
-
end
|
114
|
-
end
|
115
|
-
end
|
@@ -1,69 +0,0 @@
|
|
1
|
-
module Covid19VCI
|
2
|
-
class VCSignatureVerification < Inferno::Test
|
3
|
-
title 'Verifiable Credential signatures can be verified'
|
4
|
-
input :credential_strings
|
5
|
-
|
6
|
-
id :vc_signature_verification
|
7
|
-
|
8
|
-
run do
|
9
|
-
skip_if credential_strings.blank?, 'No Verifiable Credentials received'
|
10
|
-
credential_strings.split(',').each do |credential|
|
11
|
-
card = HealthCards::HealthCard.from_jws(credential)
|
12
|
-
iss = card.issuer
|
13
|
-
|
14
|
-
jws = HealthCards::JWS.from_jws(credential)
|
15
|
-
|
16
|
-
assert iss.present?, 'Credential contains no `iss`'
|
17
|
-
warning { assert iss.start_with?('https://'), "`iss` SHALL use the `https` scheme: #{iss}" }
|
18
|
-
assert !iss.end_with?('/'), "`iss` SHALL NOT include a trailing `/`: #{iss}"
|
19
|
-
|
20
|
-
key_set_url = "#{card.issuer}/.well-known/jwks.json"
|
21
|
-
|
22
|
-
get(key_set_url)
|
23
|
-
|
24
|
-
assert_response_status(200)
|
25
|
-
assert_valid_json(response[:body])
|
26
|
-
|
27
|
-
cors_header = request.response_header('Control-Allow-Origin')
|
28
|
-
warning do
|
29
|
-
assert cors_header.present?,
|
30
|
-
'No CORS header received. Issuers SHALL publish their public keys with CORS enabled'
|
31
|
-
assert cors_header.value == '*',
|
32
|
-
"Expected CORS header value of `*`, but actual value was `#{cors_header.value}`"
|
33
|
-
end
|
34
|
-
|
35
|
-
key_set = JSON.parse(response[:body])
|
36
|
-
|
37
|
-
public_key = key_set['keys'].find { |key| key['kid'] == jws.kid }
|
38
|
-
key_object = HealthCards::Key.from_jwk(public_key)
|
39
|
-
|
40
|
-
assert public_key.present?, "Key set did not contain a key with a `kid` of #{jws.kid}"
|
41
|
-
|
42
|
-
warning { assert public_key['kty'] == 'EC', "Key had a `kty` value of `#{public_key['kty']}` instead of `EC`" }
|
43
|
-
warning do
|
44
|
-
assert public_key['use'] == 'sig', "Key had a `use` value of `#{public_key['use']}` instead of `sig`"
|
45
|
-
end
|
46
|
-
warning do
|
47
|
-
assert public_key['alg'] == 'ES256', "Key had an `alg` value of `#{public_key['alg']}` instead of `ES256`"
|
48
|
-
end
|
49
|
-
warning do
|
50
|
-
assert public_key['crv'] == 'P-256', "Key had a `crv` value of `#{public_key['crv']}` instead of `P-256`"
|
51
|
-
end
|
52
|
-
warning { assert !public_key.include?('d'), 'Key SHALL NOT have the private key parameter `d`' }
|
53
|
-
warning do
|
54
|
-
assert public_key['kid'] == key_object.kid,
|
55
|
-
"'kid' SHALL be equal to the base64url-encoded SHA-256 JWK Thumbprint of the key. " \
|
56
|
-
"Received: '#{public_key['kid']}', Expected: '#{key_object.kid}'"
|
57
|
-
end
|
58
|
-
|
59
|
-
verifier = HealthCards::Verifier.new(keys: key_object, resolve_keys: false)
|
60
|
-
|
61
|
-
begin
|
62
|
-
assert verifier.verify(jws), 'JWS signature invalid'
|
63
|
-
rescue StandardError => e
|
64
|
-
assert false, "Error decoding credential: #{e.message}"
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
69
|
-
end
|
data/lib/covid19_vci/version.rb
DELETED