-
Notifications
You must be signed in to change notification settings - Fork 2
/
CovidCertificateVerifier.js
171 lines (137 loc) · 6.19 KB
/
CovidCertificateVerifier.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
const fetch = require('node-fetch')
const base45 = require('base45-js')
const pako = require('pako')
const cbor = require('cbor')
const zlib = require('zlib')
const rs = require('jsrsasign');
const cose = require('cose-js');
const x509 = require('@fidm/x509')
const certLogicJs = require('certlogic-js');
const CertificateVerificationException = require('../lib/CertificateVerificationException')
class CovidCertificateVerifier {
keysURL = 'https://de.dscg.ubirch.com/trustList/DSC/'
rulesURL = 'https://distribution.dcc-rules.de/rules'
valueSetURL = 'https://distribution.dcc-rules.de/valuesets'
keys = {}
ruleSetCountries = new Array()
availableCountries = new Set()
valueSets = new Object()
EXPIRES_AT = 6
ISSUED_AT = 4
PAYLOAD = -260
async init(certificateString) {
await this.loadKeys()
await this.loadBusinessRules()
await this.loadValueSets()
}
async checkCertificate(certificateString) {
const base45data = certificateString.slice(4)
const decodedb45 = base45.decode(base45data)
if (decodedb45.length == 0) {
console.error("cannot base45decode string")
throw new CertificateVerificationException("technically invalid certificate")
}
const coseRaw = pako.inflate(decodedb45)
if (!coseRaw) {
console.error("cannot inflate string")
throw new CertificateVerificationException("technically invalid certificate")
}
const message = cbor.decodeFirstSync(coseRaw)
const [protected_header, unprotected_header, cbor_data, signature] = message.value
const greenpassData = cbor.decodeAllSync(cbor_data)
let kid = cbor.decodeFirstSync(protected_header).get(cose.common.HeaderParameters.kid);
if (kid === undefined) {
kid = unprotected_header.get(cose.common.HeaderParameters.kid)
}
let kidb64 = Buffer.from(kid).toString('base64')
let result = await this.verifyCertificate(coseRaw, kidb64)
const payload = cbor.decodeFirstSync(cbor_data)
this.checkDates(payload)
return payload;
}
async checkRules(payload, countryOfOffice, date, logEnabled) {
this.executeRules(payload, countryOfOffice, date, logEnabled)
return payload.get(this.PAYLOAD).get(1)
}
checkDates(payload) {
const today = new Date()
let startDate = new Date()
let endDate = new Date()
try {
startDate = new Date(payload.get(this.EXPIRES_AT) * 1000)
endDate = new Date(payload.get(this.ISSUED_AT) * 1000)
} catch (error) {
console.error(error)
}
if (startDate > today) throw new CertificateVerificationException("certificate not valid yet")
if (endDate < today) throw new CertificateVerificationException("certificate not valid anymore")
}
executeRules(payload, countryOfOffice, date, logEnabled) {
let options = {
validationClock: date.toISOString(),
valueSets: this.valueSets
}
let countryRules = this.ruleSetCountries.filter(rule => rule.Country == countryOfOffice)
//TODO: add additional rules via file
let actualPayload = payload.get(this.PAYLOAD).get(1)
countryRules.forEach(rule => {
let rulePassed = certLogicJs.evaluate(rule.Logic, { payload: actualPayload, external: options })
if (!rulePassed) {
if (logEnabled) console.error(rule.Description.find((element) => element.lang === 'en').desc)
throw new CertificateVerificationException(rule.Description.find((element) => element.lang === 'en').desc)
}
})
}
async loadKeys() {
const response = await fetch(this.keysURL);
const certWithChecksum = await response.text();
const certWithoutChecksum = certWithChecksum.substring(certWithChecksum.indexOf('\n') + 1, certWithChecksum.length)
this.keys = JSON.parse(certWithoutChecksum).certificates
}
async loadBusinessRules() {
//url as a property in the mta.yaml
let response = await fetch(this.rulesURL);
const ruleSet = await response.json();
let ruleURLs = new Array()
ruleSet.forEach(element => {
ruleURLs.push(this.rulesURL + '/' + element.country + '/' + element.hash)
});
this.availableCountries = [... new Set(ruleSet.map(rule => rule.country))]
this.ruleSetCountries = await Promise.all(ruleURLs.map(async url => {
const resp = await fetch(url);
return resp.json();
}));
}
async loadValueSets() {
//url as a property in the mta.yaml
let response = await fetch(this.valueSetURL);
const valueSetMetadata = await response.json();
let valueSetURLS = new Array()
valueSetMetadata.forEach(element => {
valueSetURLS.push(this.valueSetURL + '/' + element.hash)
});
await Promise.all(valueSetURLS.map(async url => {
const resp = await fetch(url)
const json = await resp.json()
this.valueSets[json.valueSetId] = Object.keys(json.valueSetValues)
}));
}
splitIntoArray(string) {
return string.match(new RegExp('.{1,' + 64 + '}', 'g'))
}
async verifyCertificate(coseRaw, kid) {
const keyByKid = this.keys.find(cert => cert.kid == kid)
const certList = keyByKid ? [keyByKid] : this.keys
for (const cert of certList) {
const rawDataSplitted = this.splitIntoArray(cert.rawData).join('\n')
const certBuff = Buffer.from(`-----BEGIN CERTIFICATE-----\n${rawDataSplitted}\n-----END CERTIFICATE-----`)
const publicKeySplitted = this.splitIntoArray(x509.Certificate.fromPEM(certBuff).publicKeyRaw.toString('base64')).join('\n')
const pubkeyBuff = Buffer.from(`-----BEGIN PUBLIC KEY-----\n${publicKeySplitted}\n-----END PUBLIC KEY-----`)
const publicKey = x509.PublicKey.fromPEM(pubkeyBuff).keyRaw
await cose.sign.verify(coseRaw, { key: { x: publicKey.slice(1, 33), y: publicKey.slice(33, 65) } })
return true
}
return false
}
}
module.exports = CovidCertificateVerifier