nz_covid_pass 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/nz_covid_pass.rb +164 -0
  3. metadata +114 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a15dcb5d048451461d684042f0b6c555816785696fc6b5878b4c482d52252f2d
4
+ data.tar.gz: 9d6ecccff2869460d6049fafa285551f179c40d9d318b4ccbecc7afaa3ee85d0
5
+ SHA512:
6
+ metadata.gz: f197842d3ddf735008a7e841c5bce7ba7d8da574084f7c1373ad74e804dd62b338b07a917c352841224af1d060190d28f81b0adc8a46157f0d649c09f56345b7
7
+ data.tar.gz: 433861af9fe0a5f01e2ee7318040766e45217ac7633c92f22a130accdfc6de02014798538060551105d7753c55ddf37ce3bdce2f9110d57f3a32adb2759284fc
@@ -0,0 +1,164 @@
1
+ require 'uri'
2
+ require 'date'
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'base32'
6
+ require 'cose'
7
+ require 'cbor'
8
+ require 'cwt'
9
+ require 'jwt'
10
+
11
+ class NZCovidPass
12
+ Error = Class.new(StandardError)
13
+ ParseError = Class.new(Error)
14
+ NetworkError = Class.new(Error)
15
+ NotYetValidError = Class.new(Error)
16
+ ExpiredError = Class.new(Error)
17
+
18
+ VERSION_IDENTIFIER = "1"
19
+
20
+ TRUSTED_ISSUER_IDENTIFIERS = [
21
+ "did:web:nzcp.identity.health.nz",
22
+ ]
23
+
24
+ TEST_TRUSTED_ISSUER_IDENTIFIERS = [
25
+ "did:web:nzcp.covid19.health.nz",
26
+ ]
27
+
28
+ def initialize(code, allow_test_issuers: false, time: Time.now, cache: nil)
29
+ @code = code
30
+ @allow_test_issuers = allow_test_issuers
31
+ @time = time
32
+ @cache = cache
33
+
34
+ verify!
35
+ end
36
+
37
+ def verify!
38
+ raise ParseError, "invalid URL format" unless url_components
39
+ raise ParseError, "scheme must be NZCP" unless url_components[:scheme] == "NZCP"
40
+ raise ParseError, "version must be #{VERSION_IDENTIFIER}" unless url_components[:version] == VERSION_IDENTIFIER
41
+ raise ParseError, "ALG must be ES256 (-7)" unless alg == -7
42
+ raise ParseError, "invalid issuer" unless trusted_issuer_identifiers.include?(cwt.iss)
43
+ raise ParseError, "no vc claim" unless vc
44
+ raise ParseError, "invalid vc @context" unless vc["@context"].first == "https://www.w3.org/2018/credentials/v1"
45
+ raise ParseError, "invalid vc type" unless vc["type"] == ["VerifiableCredential", "PublicCovidPass"]
46
+ raise NotYetValidError, "not yet valid" if cwt.nbf > @time.to_i
47
+ raise ExpiredError, "expired" if cwt.exp < @time.to_i
48
+ raise ParseError, "invalid jti" unless jti
49
+ raise ParseError, "credentialSubject missing" unless credential_subject
50
+ raise ParseError, "givenName missing" unless given_name
51
+ raise ParseError, "dob missing" unless dob
52
+
53
+ sign1.verify(retrieve_public_key)
54
+ end
55
+
56
+ def given_name
57
+ credential_subject["givenName"]
58
+ end
59
+
60
+ def family_name
61
+ credential_subject["familyName"]
62
+ end
63
+
64
+ def dob
65
+ credential_subject["dob"] && Date.parse(credential_subject["dob"])
66
+ end
67
+
68
+ def jti
69
+ cti = cwt.cti
70
+ if cti.length == 16
71
+ match = cti.unpack("H32").first.match(/^(.{8})(.{4})(.{4})(.{4})(.{12})$/)
72
+ "urn:uuid:#{match.captures.join("-")}"
73
+ end
74
+ end
75
+
76
+ def version
77
+ vc["version"]
78
+ end
79
+
80
+ private
81
+
82
+ def url_components
83
+ if match = @code.match(%r(\A([^:]+):/([^/]+)/([A-Z2-7]+)\z))
84
+ scheme, version, data = match.captures
85
+ {scheme: scheme, version: version, data: data}
86
+ end
87
+ end
88
+
89
+ def credential_subject
90
+ vc["credentialSubject"]
91
+ end
92
+
93
+ def kid
94
+ sign1.protected_headers.fetch(4)
95
+ end
96
+
97
+ def alg
98
+ sign1.protected_headers.fetch(1)
99
+ end
100
+
101
+ def sign1
102
+ @sign1 ||= begin
103
+ cbor_data = Base32.decode(url_components[:data])
104
+ COSE::Sign1.deserialize(cbor_data)
105
+ end
106
+ end
107
+
108
+ def sign1_payload
109
+ @sign1_payload ||= CBOR.decode(sign1.payload)
110
+ end
111
+
112
+ def cwt
113
+ @cwt ||= CWT::ClaimsSet.from_cbor(sign1.payload)
114
+ end
115
+
116
+ def vc
117
+ @vc ||= sign1_payload["vc"]
118
+ end
119
+
120
+ def retrieve_public_key
121
+ did_data = retrieve_did_document
122
+
123
+ key_reference = "#{cwt.iss}##{kid}"
124
+ verification_method = did_data["verificationMethod"].detect { |method| method["id"] == key_reference }
125
+
126
+ if verification_method.nil?
127
+ raise ParseError, "No matching verification method found in did document"
128
+ end
129
+
130
+ jwk_data = verification_method["publicKeyJwk"]
131
+ jwk = JWT::JWK.import(jwk_data)
132
+ key = COSE::Key.from_pkey(jwk.keypair)
133
+
134
+ key.kid = kid
135
+ key
136
+ end
137
+
138
+ def retrieve_did_document
139
+ host = cwt.iss.split(":").last
140
+
141
+ return @cache[host] if @cache && @cache[host]
142
+
143
+ http = Net::HTTP.new(host, 443)
144
+ http.use_ssl = true
145
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
146
+ request = Net::HTTP::Get.new("/.well-known/did.json", {"Accept" => "application/json"})
147
+
148
+ response = http.request(request)
149
+ raise NetworkError, "https request returned response code #{response.code}" unless response.code == "200"
150
+
151
+ document = JSON.parse(response.body)
152
+ @cache[host] = document if @cache
153
+
154
+ document
155
+ end
156
+
157
+ def trusted_issuer_identifiers
158
+ if @allow_test_issuers
159
+ TRUSTED_ISSUER_IDENTIFIERS + TEST_TRUSTED_ISSUER_IDENTIFIERS
160
+ else
161
+ TRUSTED_ISSUER_IDENTIFIERS
162
+ end
163
+ end
164
+ end
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nz_covid_pass
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Roger Nesbitt
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-11-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: base32
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.3.4
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.3.4
27
+ - !ruby/object:Gem::Dependency
28
+ name: cose
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 1.2.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 1.2.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: cbor
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.5.9.6
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.5.9.6
55
+ - !ruby/object:Gem::Dependency
56
+ name: cwt
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.5.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.5.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: jwt
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 2.3.0
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 2.3.0
83
+ description: This gem reads in the data contained in a NZ Covid Pass 2D barcode, confirms
84
+ the signature and outputs the signed data inside
85
+ email: roger@seriousorange.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - lib/nz_covid_pass.rb
91
+ homepage: https://github.com/mogest/nz_covid_pass
92
+ licenses:
93
+ - MIT
94
+ metadata: {}
95
+ post_install_message:
96
+ rdoc_options: []
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ requirements: []
110
+ rubygems_version: 3.1.6
111
+ signing_key:
112
+ specification_version: 4
113
+ summary: Reads and validates the signature of NZ Covid Pass passes
114
+ test_files: []