smart_health_cards_test_kit 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (27) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/lib/smart_health_cards_test_kit/fhir_operation_group.rb +105 -0
  4. data/lib/smart_health_cards_test_kit/file_download_group.rb +108 -0
  5. data/lib/smart_health_cards_test_kit/health_card.rb +33 -0
  6. data/lib/smart_health_cards_test_kit/javascript/jsQR.js +10100 -0
  7. data/lib/smart_health_cards_test_kit/javascript/qr-scanner-worker.min.js +98 -0
  8. data/lib/smart_health_cards_test_kit/javascript/qr-scanner.min.js +31 -0
  9. data/lib/smart_health_cards_test_kit/qr_code_group.rb +107 -0
  10. data/lib/smart_health_cards_test_kit/shc_fhir_validation.rb +39 -0
  11. data/lib/smart_health_cards_test_kit/shc_header_verification.rb +36 -0
  12. data/lib/smart_health_cards_test_kit/shc_payload_verification.rb +123 -0
  13. data/lib/smart_health_cards_test_kit/shc_signature_verification.rb +95 -0
  14. data/lib/smart_health_cards_test_kit/utils/chunking_utils.rb +71 -0
  15. data/lib/smart_health_cards_test_kit/utils/encoding.rb +26 -0
  16. data/lib/smart_health_cards_test_kit/utils/jws.rb +132 -0
  17. data/lib/smart_health_cards_test_kit/utils/key.rb +82 -0
  18. data/lib/smart_health_cards_test_kit/utils/key_set.rb +88 -0
  19. data/lib/smart_health_cards_test_kit/utils/private_key.rb +53 -0
  20. data/lib/smart_health_cards_test_kit/utils/public_key.rb +33 -0
  21. data/lib/smart_health_cards_test_kit/utils/verification.rb +41 -0
  22. data/lib/smart_health_cards_test_kit/utils/verifier.rb +71 -0
  23. data/lib/smart_health_cards_test_kit/version.rb +3 -0
  24. data/lib/smart_health_cards_test_kit/views/scan_qr_code.html +207 -0
  25. data/lib/smart_health_cards_test_kit/views/upload_qr_code.html +130 -0
  26. data/lib/smart_health_cards_test_kit.rb +67 -0
  27. metadata +168 -0
@@ -0,0 +1,95 @@
1
+ require_relative 'utils/verifier'
2
+
3
+ module SmartHealthCardsTestKit
4
+ class SHCSignatureVerification < Inferno::Test
5
+ include HealthCard
6
+
7
+ id :shc_signature_verification_test
8
+ title 'Verifiable Credential JWS payload has correct JWS signature'
9
+ description %(
10
+ Each public key used to verify signatures is represented as a JSON Web Key
11
+ (see [RFC7517](https://tools.ietf.org/html/rfc7517)), with some of its properties encoded using
12
+ base64url (see section 5 of [RFC4648](https://tools.ietf.org/html/rfc4648#section-5)):
13
+
14
+ * SHALL have "kty": "EC", "use": "sig", and "alg": "ES256"
15
+ * SHALL have "kid" equal to the base64url-encoded SHA-256 JWK Thumbprint of the key
16
+ (see [RFC7638](https://tools.ietf.org/html/rfc7638))
17
+ * SHALL have "crv": "P-256", and "x", "y" equal to the base64url-encoded values for the public Elliptic
18
+ Curve point coordinates (see [RFC7518](https://tools.ietf.org/html/rfc7518#section-6.2))
19
+ * SHALL NOT have the Elliptic Curve private key parameter "d"
20
+ * If the issuer has an X.509 certificate for the public key, SHALL have "x5c" equal to an array of one
21
+ or more base64-encoded (not base64url-encoded) DER representations of the public certificate or
22
+ certificate chain (see [RFC7517](https://tools.ietf.org/html/rfc7517#section-4.7)). The public key
23
+ listed in the first certificate in the "x5c" array SHALL match the public key specified by the "crv",
24
+ "x", and "y" parameters of the same JWK entry. If the issuer has more than one certificate for the same
25
+ public key (e.g. participation in more than one trust community), then a separate JWK entry is used for
26
+ each certificate with all JWK parameter values identical except "x5c".
27
+
28
+ Issuers SHALL publish their public keys as JSON Web Key Sets (see
29
+ [RFC7517](https://tools.ietf.org/html/rfc7517#section-4.7)), available at
30
+ <<iss value from JWS>> + /.well-known/jwks.json, with Cross-Origin Resource Sharing (CORS) enabled, using
31
+ TLS version 1.2 following the IETF BCP 195 recommendations or TLS version 1.3 (with any configuration).
32
+
33
+ The URL at <<iss value from JWS>> SHALL use the https scheme and SHALL NOT include a trailing /.
34
+ For example, https://smarthealth.cards/examples/issuer is a valid iss value
35
+ (https://smarthealth.cards/examples/issuer/ is not).
36
+ )
37
+
38
+ input :credential_strings
39
+
40
+ run do
41
+ skip_if credential_strings.blank?, 'No Verifiable Credentials received'
42
+
43
+ credential_strings.split(',').each do |credential|
44
+
45
+ jws = SmartHealthCardsTestKit::Utils::JWS.from_jws(credential)
46
+ payload = payload_from_jws(jws)
47
+ iss = payload['iss']
48
+
49
+ assert iss.present?, 'Credential contains no `iss`'
50
+ warning { assert iss.start_with?('https://'), "`iss` SHALL use the `https` scheme: #{iss}" }
51
+ assert !iss.end_with?('/'), "`iss` SHALL NOT include a trailing `/`: #{iss}"
52
+
53
+ key_set_url = "#{iss}/.well-known/jwks.json"
54
+
55
+ get(key_set_url)
56
+
57
+ assert_response_status(200)
58
+ assert_valid_json(response[:body])
59
+
60
+ cors_header = request.response_header('Control-Allow-Origin')
61
+ warning do
62
+ assert cors_header.present?,
63
+ 'No CORS header received. Issuers SHALL publish their public keys with CORS enabled'
64
+ assert cors_header.value == '*',
65
+ "Expected CORS header value of `*`, but actual value was `#{cors_header.value}`"
66
+ end
67
+
68
+ key_set = JSON.parse(response[:body])
69
+
70
+ public_key = key_set['keys'].find { |key| key['kid'] == jws.kid }
71
+ key_object = SmartHealthCardsTestKit::Utils::Key.from_jwk(public_key)
72
+
73
+ assert public_key.present?, "Key set did not contain a key with a `kid` of #{jws.kid}"
74
+
75
+ assert public_key['kty'] == 'EC', "Key had a `kty` value of `#{public_key['kty']}` instead of `EC`"
76
+ assert public_key['use'] == 'sig', "Key had a `use` value of `#{public_key['use']}` instead of `sig`"
77
+ assert public_key['alg'] == 'ES256', "Key had an `alg` value of `#{public_key['alg']}` instead of `ES256`"
78
+ assert public_key['crv'] == 'P-256', "Key had a `crv` value of `#{public_key['crv']}` instead of `P-256`"
79
+ assert !public_key.include?('d'), 'Key SHALL NOT have the private key parameter `d`'
80
+ assert public_key['kid'] == key_object.kid,
81
+ "'kid' SHALL be equal to the base64url-encoded SHA-256 JWK Thumbprint of the key. " \
82
+ "Received: '#{public_key['kid']}', Expected: '#{key_object.kid}'"
83
+
84
+ verifier = SmartHealthCardsTestKit::Utils::Verifier.new(keys: key_object, resolve_keys: false)
85
+
86
+ begin
87
+ assert verifier.verify(jws), 'JWS signature invalid'
88
+ rescue StandardError => e
89
+ assert false, "Error decoding credential: #{e.message}"
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmartHealthCardsTestKit
4
+ module Utils
5
+ # Split up a JWS into chunks if encoded size is above QR Code Size constraint
6
+ module ChunkingUtils
7
+ extend self
8
+ MAX_SINGLE_JWS_SIZE = 1195
9
+ MAX_CHUNK_SIZE = 1191
10
+
11
+ def split_jws(jws)
12
+ if jws.length <= MAX_SINGLE_JWS_SIZE
13
+ [jws]
14
+ else
15
+ chunk_count = (jws.length / MAX_CHUNK_SIZE.to_f).ceil
16
+ chunk_size = (jws.length / chunk_count.to_f).ceil
17
+ jws.scan(/.{1,#{chunk_size}}/)
18
+ end
19
+ end
20
+
21
+ # Splits jws into chunks and converts each string into numeric
22
+ def jws_to_qr_chunks(jws)
23
+ chunks = split_jws(jws.to_s).map { |c| convert_jws_to_numeric(c) }
24
+
25
+ # if 1 chunk, attach prefix shc:/
26
+ # if multiple chunks, attach prefix shc:/$orderNumber/$totalChunkCount
27
+ if chunks.length == 1
28
+ chunks[0] = "shc:/#{chunks[0]}"
29
+ else
30
+ chunks.map!.with_index(1) { |ch, i| "shc:/#{i}/#{chunks.length}/#{ch}" }
31
+ end
32
+ chunks
33
+ end
34
+
35
+ # Assemble jws from qr code chunks
36
+ def qr_chunks_to_jws(qr_chunks)
37
+ if qr_chunks.length == 1
38
+ # Strip off shc:/ and convert numeric jws
39
+ numeric_jws = qr_chunks[0].delete_prefix('shc:/')
40
+ convert_numeric_jws numeric_jws
41
+ else
42
+ ordered_qr_chunks = strip_prefix_and_sort qr_chunks
43
+ ordered_qr_chunks.map { |c| convert_numeric_jws(c) }.join
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ # Each character "c" of the jws is converted into a sequence of two digits by taking c.ord - 45
50
+ def convert_jws_to_numeric(jws)
51
+ jws.chars.map { |c| format('%02d', c.ord - 45) }.join
52
+ end
53
+
54
+ def convert_numeric_jws(numeric_jws)
55
+ result_jws = ''.dup
56
+ numeric_jws.chars.each_slice(2) do |a, b|
57
+ result_jws << ((a + b).to_i + 45).chr
58
+ end
59
+ result_jws
60
+ end
61
+
62
+ def strip_prefix_and_sort(qr_chunks)
63
+ # Multiple QR codes are prefixed with 'shc:/C/N' where C is the index and N is the total number of chunks
64
+ # Sorts chunks by C
65
+ sorted_chunks = qr_chunks.sort_by { |c| c[%r{/(.*?)/}, 1].to_i }
66
+ # Strip prefix
67
+ sorted_chunks.map { |c| c.sub(%r{shc:/(.*?)/(.*?)/}, '') }
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+
5
+ module SmartHealthCardsTestKit
6
+ module Utils
7
+ # Encoding utilities for producing JWS
8
+ #
9
+ # @see https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.3.1
10
+ module Encoding
11
+ # Encodes the provided data using url safe base64 without padding
12
+ # @param data [String] the data to be encoded
13
+ # @return [String] the encoded data
14
+ def encode(data)
15
+ Base64.urlsafe_encode64(data, padding: false).gsub("\n", '')
16
+ end
17
+
18
+ # Decodes the provided data using url safe base 64
19
+ # @param data [String] the data to be decoded
20
+ # @return [String] the decoded data
21
+ def decode(data)
22
+ Base64.urlsafe_decode64(data)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'encoding'
3
+ require_relative 'private_key'
4
+ require_relative 'public_key'
5
+
6
+ module SmartHealthCardsTestKit
7
+ module Utils
8
+ # Create JWS from a payload
9
+ class JWS
10
+ class << self
11
+ include Encoding
12
+
13
+ # Creates a JWS from a String representation, or returns the SmartHealthCardsTestKit::Utils::JWS object
14
+ # that was passed in
15
+ # @param jws [String, SmartHealthCardsTestKit::Utils::JWS] the JWS string, or a JWS object
16
+ # @param public_key [SmartHealthCardsTestKit::Utils::PublicKey] the public key associated with the JWS
17
+ # @param key [SmartHealthCardsTestKit::Utils::PrivateKey] the private key associated with the JWS
18
+ # @return [SmartHealthCardsTestKit::Utils::JWS] A new JWS object, or the JWS object that was passed in
19
+ def from_jws(jws, public_key: nil, key: nil)
20
+ return jws if jws.is_a?(SmartHealthCardsTestKit::Utils::JWS) && public_key.nil? && key.nil?
21
+
22
+ unless jws.is_a?(SmartHealthCardsTestKit::Utils::JWS) || jws.is_a?(String)
23
+ raise ArgumentError,
24
+ 'Expected either a SmartHealthCardsTestKit::Utils::JWS or String'
25
+ end
26
+
27
+ header, payload, signature = jws.to_s.split('.').map { |entry| decode(entry) }
28
+ header = JSON.parse(header)
29
+ JWS.new(header: header, payload: payload, signature: signature,
30
+ public_key: public_key, key: key)
31
+ end
32
+ end
33
+
34
+ attr_reader :key, :public_key, :payload
35
+ attr_writer :signature
36
+ attr_accessor :header
37
+
38
+ # Create a new JWS
39
+
40
+ def initialize(header: nil, payload: nil, signature: nil, key: nil, public_key: nil)
41
+ # Not using accessors because they reset the signature which requires both a key and a payload
42
+ @header = header
43
+ @payload = payload
44
+ @signature = signature if signature
45
+ @key = key
46
+ @public_key = public_key || key&.public_key
47
+ end
48
+
49
+ # The kid value from the JWS header, used to identify the key to use to verify
50
+ # @return [String]
51
+ def kid
52
+ header['kid']
53
+ end
54
+
55
+ # Set the private key used for signing issued health cards
56
+ #
57
+ # @param key [SmartHealthCardsTestKit::Utils::PrivateKey, nil] the private key used for signing issued health cards
58
+ def key=(key)
59
+ PrivateKey.enforce_valid_key_type!(key, allow_nil: true)
60
+
61
+ @key = key
62
+
63
+ # If it's a new private key then the public key and signature should be updated
64
+ return if @key.nil?
65
+
66
+ reset_signature
67
+ self.public_key = @key.public_key
68
+ end
69
+
70
+ # Set the public key used for signing issued health cards
71
+ #
72
+ # @param public_key [SmartHealthCardsTestKit::Utils::PublicKey, nil] the private key used for signing issued health cards
73
+ def public_key=(public_key)
74
+ PublicKey.enforce_valid_key_type!(public_key, allow_nil: true)
75
+
76
+ @public_key = public_key
77
+ end
78
+
79
+ # Set the JWS payload. Setting a new payload will result in the a new signature
80
+ # @param new_payload [Object]
81
+ def payload=(new_payload)
82
+ @payload = new_payload
83
+ reset_signature
84
+ end
85
+
86
+ # The signature component of the card
87
+ #
88
+ # @return [String] the unencoded signature
89
+ def signature
90
+ return @signature if @signature
91
+
92
+ raise MissingPrivateKeyError unless key
93
+
94
+ @signature ||= key.sign(jws_signing_input)
95
+ end
96
+
97
+ # Export the card to a JWS String
98
+ # @return [String] the JWS
99
+ def to_s
100
+ [JSON.generate(header), payload, signature].map { |entry| JWS.encode(entry) }.join('.')
101
+ end
102
+
103
+ # # Verify the digital signature on the jws
104
+ # #
105
+ # # @return [Boolean]
106
+ def verify
107
+ raise MissingPublicKeyError unless public_key
108
+
109
+ public_key.verify(jws_signing_input, signature)
110
+ end
111
+
112
+ private
113
+
114
+ def jws_signing_input
115
+ "#{JWS.encode(@header.to_json)}.#{encoded_payload}"
116
+ end
117
+
118
+ def encoded_payload
119
+ JWS.encode(payload)
120
+ end
121
+
122
+ # Resets the signature
123
+ #
124
+ # This method is primarily used when an attribute that affects
125
+ # the signature is changed (e.g. the private key changes, the payload changes)
126
+ def reset_signature
127
+ @signature = nil
128
+ signature if key && payload
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+ require 'base64'
5
+
6
+ module SmartHealthCardsTestKit
7
+ module Utils
8
+ # Methods to generate signing keys and jwk
9
+ class Key
10
+ BASE = { kty: 'EC', crv: 'P-256' }.freeze
11
+ DIGEST = OpenSSL::Digest.new('SHA256')
12
+
13
+ # Checks if obj is the the correct key type or nil
14
+ # @param obj Object that should be of same type as caller or nil
15
+ # @param allow_nil Allow/Disallow key to be nil
16
+ def self.enforce_valid_key_type!(obj, allow_nil: false)
17
+ raise InvalidKeyError.new(self, obj) unless obj.is_a?(self) || (allow_nil && obj.nil?)
18
+ end
19
+
20
+ # Create a key from a JWK
21
+ #
22
+ # @param jwk_key [Hash] The JWK represented by a Hash
23
+ # @return [SmartHealthCardsTestKit::Utils::Key] The key represented by the JWK
24
+ def self.from_jwk(jwk_key)
25
+ jwk_key = jwk_key.transform_keys(&:to_sym)
26
+ curvename = 'prime256v1'
27
+ group = OpenSSL::PKey::EC::Group.new(curvename)
28
+ public_key_bn = ['04'].pack('H*') + Base64.urlsafe_decode64(jwk_key[:x]) + Base64.urlsafe_decode64(jwk_key[:y])
29
+ point = OpenSSL::PKey::EC::Point.new(group, OpenSSL::BN.new(public_key_bn, 2))
30
+
31
+ asn1 = OpenSSL::ASN1::Sequence([
32
+ OpenSSL::ASN1::Sequence([
33
+ OpenSSL::ASN1::ObjectId("id-ecPublicKey"),
34
+ OpenSSL::ASN1::ObjectId(curvename)
35
+ ]),
36
+ OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed))
37
+ ])
38
+
39
+ key = OpenSSL::PKey::EC.new(asn1.to_der)
40
+ key.private_key = OpenSSL::BN.new(Base64.urlsafe_decode64(jwk_key[:d]), 2) if jwk_key[:d]
41
+
42
+ key.private_key? ? SmartHealthCardsTestKit::Utils::PrivateKey.new(key) : SmartHealthCardsTestKit::Utils::PublicKey.new(key)
43
+ end
44
+
45
+ def initialize(ec_key)
46
+ @key = ec_key
47
+ end
48
+
49
+ def group
50
+ @key.group
51
+ end
52
+
53
+ def to_json(*_args)
54
+ to_jwk.to_json
55
+ end
56
+
57
+ def to_jwk
58
+ coordinates.merge(kid: kid, use: 'sig', alg: 'ES256')
59
+ end
60
+
61
+ def kid
62
+ Base64.urlsafe_encode64(DIGEST.digest(public_coordinates.to_json), padding: false)
63
+ end
64
+
65
+ def public_coordinates
66
+ coordinates.slice(:crv, :kty, :x, :y)
67
+ end
68
+
69
+ def coordinates
70
+ return @coordinates if @coordinates
71
+
72
+ key_binary = @key.public_key.to_bn.to_s(2)
73
+ coords = { x: key_binary[1, key_binary.length / 2],
74
+ y: key_binary[key_binary.length / 2 + 1, key_binary.length] }
75
+ coords[:d] = @key.private_key.to_s(2) if @key.private_key?
76
+ @coordinates = coords.transform_values do |val|
77
+ Base64.urlsafe_encode64(val, padding: false)
78
+ end.merge(BASE)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module SmartHealthCardsTestKit
6
+ module Utils
7
+ # A set of keys used for signing or verifying HealthCards
8
+ class KeySet
9
+ extend Forwardable
10
+
11
+ def_delegator :keys, :empty?
12
+
13
+ # Create a KeySet from a JWKS
14
+ #
15
+ # @param jwks [String] the JWKS as a string
16
+ # @return [SmartHealthCardsTestKit::Utils::KeySet]
17
+ def self.from_jwks(jwks)
18
+ jwks = JSON.parse(jwks)
19
+ keys = jwks['keys'].map { |jwk| SmartHealthCardsTestKit::Utils::Key.from_jwk(jwk) }
20
+ KeySet.new(keys)
21
+ end
22
+
23
+ # Create a new KeySet
24
+ #
25
+ # @param keys [SmartHealthCardsTestKit::Utils::Key, Array<SmartHealthCardsTestKit::Utils::Key>, nil] the initial keys
26
+ def initialize(keys = nil)
27
+ @key_map = {}
28
+ add_keys(keys) unless keys.nil?
29
+ end
30
+
31
+ # The contained keys
32
+ #
33
+ # @return [Array]
34
+ def keys
35
+ @key_map.values
36
+ end
37
+
38
+ # Returns the keys as a JWK
39
+ #
40
+ # @return JSON string in JWK format
41
+ def to_jwk
42
+ { keys: keys.map(&:to_jwk) }.to_json
43
+ end
44
+
45
+ # Retrieves a key from the keyset with a kid
46
+ # that matches the parameter
47
+ # @param kid [String] a Base64 encoded kid from a JWS or Key
48
+ # @return [Payload::Key] a key with a matching kid or nil if not found
49
+ def find_key(kid)
50
+ @key_map[kid]
51
+ end
52
+
53
+ # Add keys to KeySet
54
+ #
55
+ # Keys are added based on the key kid
56
+ #
57
+ # @param new_keys [SmartHealthCardsTestKit::Utils::Key, Array<SmartHealthCardsTestKit::Utils::Key>, SmartHealthCardsTestKit::Utils::KeySet] the initial keys
58
+ def add_keys(new_keys)
59
+ if new_keys.is_a? KeySet
60
+ add_keys(new_keys.keys)
61
+ else
62
+ [*new_keys].each { |new_key| @key_map[new_key.kid] = new_key }
63
+ end
64
+ end
65
+
66
+ # Remove keys from KeySet
67
+ #
68
+ # Keys are remove based on the key kid
69
+ #
70
+ # @param new_keys [SmartHealthCardsTestKit::Utils::Key, Array<SmartHealthCardsTestKit::Utils::Key>, SmartHealthCardsTestKit::Utils::KeySet] the initial keys
71
+ def remove_keys(removed_keys)
72
+ if removed_keys.is_a? KeySet
73
+ remove_keys(removed_keys.keys)
74
+ else
75
+ [*removed_keys].each { |removed_key| @key_map.delete(removed_key.kid) }
76
+ end
77
+ end
78
+
79
+ # Check if key is included in the KeySet
80
+ #
81
+ # @param key [SmartHealthCardsTestKit::Utils::Key]
82
+ # @return [Boolean]
83
+ def include?(key)
84
+ !@key_map[key.kid].nil?
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'key'
3
+
4
+ module SmartHealthCardsTestKit
5
+ module Utils
6
+ # A key used for signing JWS
7
+ class PrivateKey < Key
8
+ def self.from_file(path)
9
+ pem = OpenSSL::PKey::EC.new(File.read(path))
10
+ PrivateKey.new(pem)
11
+ end
12
+
13
+ def self.load_from_or_create_from_file(path)
14
+ if File.exist?(path)
15
+ from_file(path)
16
+ else
17
+ generate_key(file_path: path)
18
+ end
19
+ end
20
+
21
+ def self.generate_key(file_path: nil)
22
+ key = OpenSSL::PKey::EC.generate('prime256v1')
23
+ File.write(file_path, key.to_pem) if file_path
24
+ PrivateKey.new(key)
25
+ end
26
+
27
+ def sign(payload)
28
+ asn1_to_raw(@key.sign(OpenSSL::Digest.new('SHA256'), payload), self)
29
+ end
30
+
31
+ def public_key
32
+ return @public_key if @public_key
33
+
34
+ pub = OpenSSL::PKey::EC.new('prime256v1')
35
+ pub.public_key = @key.public_key
36
+ @public_key = PublicKey.new(pub)
37
+ end
38
+
39
+ private
40
+
41
+ # Convert the ASN.1 Representation into the raw signature
42
+ #
43
+ # Adapted from ruby-jwt and json-jwt gems. More info here:
44
+ # https://github.com/nov/json-jwt/issues/21
45
+ # https://github.com/jwt/ruby-jwt/pull/87
46
+ # https://github.com/jwt/ruby-jwt/issues/84
47
+ def asn1_to_raw(signature, private_key)
48
+ byte_size = (private_key.group.degree + 7) / 8
49
+ OpenSSL::ASN1.decode(signature).value.map { |value| value.value.to_s(2).rjust(byte_size, "\x00") }.join
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmartHealthCardsTestKit
4
+ module Utils
5
+ # A key used for verifying JWS
6
+ class PublicKey < Key
7
+ def self.from_json(json)
8
+ # TODO
9
+ end
10
+
11
+ def verify(payload, signature)
12
+ @key.verify(OpenSSL::Digest.new('SHA256'), raw_to_asn1(signature, self), payload)
13
+ end
14
+
15
+ private
16
+
17
+ # Convert the raw signature into the ASN.1 Representation
18
+ #
19
+ # Adapted from ruby-jwt and json-jwt gems. More info here:
20
+ # https://github.com/nov/json-jwt/issues/21
21
+ # https://github.com/jwt/ruby-jwt/pull/87
22
+ # https://github.com/jwt/ruby-jwt/issues/84
23
+ def raw_to_asn1(signature, key)
24
+ byte_size = (key.group.degree + 7) / 8
25
+ sig_bytes = signature[0..(byte_size - 1)]
26
+ sig_char = signature[byte_size..] || ''
27
+ OpenSSL::ASN1::Sequence.new([sig_bytes, sig_char].map do |int|
28
+ OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2))
29
+ end).to_der
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SmartHealthCardsTestKit
4
+ module Utils
5
+ # Logic for verifying a Payload JWS
6
+ module Verification
7
+ # Verify Health Card with given KeySet
8
+ #
9
+ # @param verifiable [SmartHealthCardsTestKit::Utils::JWS, String] the health card to verify
10
+ # @param key_set [SmartHealthCardsTestKit::Utils::KeySet, nil] the KeySet from which keys should be taken or added
11
+ # @param resolve_keys [Boolean] if keys should be resolved
12
+ # @return [Boolean]
13
+ def verify_using_key_set(verifiable, key_set = nil, resolve_keys: true)
14
+ jws = JWS.from_jws(verifiable)
15
+ key_set ||= SmartHealthCardsTestKit::Utils::KeySet.new
16
+ key_set.add_keys(resolve_key(jws)) if resolve_keys && key_set.find_key(jws.kid).nil?
17
+
18
+ key = key_set.find_key(jws.kid)
19
+ unless key
20
+ raise MissingPublicKeyError,
21
+ 'Verifier does not contain public key that is able to verify this signature'
22
+ end
23
+
24
+ jws.public_key = key
25
+ jws.verify
26
+ end
27
+
28
+ # Resolve a key
29
+ # @param jws [SmartHealthCardsTestKit::Utils::JWS, String] The JWS for which to resolve keys
30
+ # @return [SmartHealthCardsTestKit::Utils::KeySet]
31
+ def resolve_key(jws)
32
+ jwks_uri = URI("#{HealthCard.new(jws.to_s).issuer}/.well-known/jwks.json")
33
+ res = Net::HTTP.get(jwks_uri)
34
+ SmartHealthCardsTestKit::Utils::KeySet.from_jwks(res)
35
+ # Handle response if key is malformed or unreachable
36
+ rescue StandardError => e
37
+ raise ArgumentError, "Unable resolve a valid key from uri #{jwks_uri}: #{e.message}"
38
+ end
39
+ end
40
+ end
41
+ end