-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathxades.js
338 lines (295 loc) · 10.9 KB
/
xades.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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
var XadesXml = require("./lib/xades_xml")
var Crypto = require("./lib/crypto")
var Certificate = require("./lib/certificate")
var OcspResponse = require("./lib/ocsp").OcspResponse
var TimestampResponse = require("./lib/timestamp").TimestampResponse
// XML Security URLs: https://tools.ietf.org/html/rfc6931
var C14N_URL = "http://www.w3.org/2001/10/xml-exc-c14n#"
var SHA256_URL = "http://www.w3.org/2001/04/xmlenc#sha256"
var sha256 = Crypto.hash.bind(null, "sha256")
var concat = Array.prototype.concat.bind(Array.prototype)
module.exports = Xades
// https://www.w3.org/TR/xmldsig-core1/#sec-AlgID
var SIG_ALGORITHM_URLS = {
rsa: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
ecdsa: "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
dsa: "http://www.w3.org/2009/xmldsig11#dsa-sha256"
}
// curl https://www.sk.ee/repository/bdoc-spec21.pdf | sha256sum |
// awk '{ printf("%s", $1) }' | xxd -r -p | base64
var BDOC_2_1_0_OID = "1.3.6.1.4.1.10015.1000.3.2.1"
var BDOC_2_1_0_SHA256 = "3Tl1oILSvOAWomdI9VeWV6IA/32eSXRUri9kPEz1IVs="
//var BDOC_2_1_2_OID = "1.3.6.1.4.1.10015.1000.3.2.3"
var BDOC_POLICY = {
// The order of elements in <xades:SignaturePolicyIdentifier> is fixed
// according to libdigidocpp's schemas.
xades$SignaturePolicyId: {
xades$SigPolicyId: {
xades$Identifier: {
// Digidoc4j v3.3.0 fails if not given BDOC v2.1.0's OID. That
// is, it doesn't support BDOC v2.1.2.
Qualifier: "OIDAsURN",
$: `urn:oid:${BDOC_2_1_0_OID}`
}
},
// <xades:SigPolicyHash> is the hash of the <xades:SPURI> content,
// although BDOC v2.1.2 says it's not to be really validated.
xades$SigPolicyHash: {
ds$DigestMethod: {Algorithm: SHA256_URL},
ds$DigestValue: {$: BDOC_2_1_0_SHA256}
},
// Should move the specification to somewhere under our control?
xades$SigPolicyQualifiers: {
xades$SigPolicyQualifier: {
xades$SPURI: {$: "https://www.sk.ee/repository/bdoc-spec21.pdf"}
}
}
}
}
// Profile-wise BDOC v2.1.2 creates XAdES-LT signatures based on either
// XAdES-EPES or XAdES-BES. "LT" stands for "long-term". "T" for time, which
// can come from a time-mark (TM) or a time-stamp (TS). Time-mark is a method
// of getting a timestamp from the OCSP response. Time-stamp gets it from
// a dedicated server.
//
// The XAdES profiles are described in https://www.etsi.org/deliver/etsi_ts/103100_103199/103171/02.01.01_60/ts_103171v020101p.pdf.
function Xades(cert, files, opts) {
this.certificate = cert
var certChain = opts.certChain || [cert]
var signedProperties = {
Id: "signed-properties",
xades$SignedSignatureProperties: {
// BDOC v2.1.2 says the time of the signature creation is the time of the
// OCSP response. However it still requires a <xades:SigningTime>
// element.
//
// TODO: Confirm whether this can include milliseconds or not.
xades$SigningTime: {$: (opts && opts.timestamp) || formatIsoDateTime(new Date)},
xades$SigningCertificate: {
xades$Cert: {
xades$CertDigest: {
ds$DigestMethod: {Algorithm: SHA256_URL},
ds$DigestValue: {$: sha256(cert.toBuffer()).toString("base64")}
},
xades$IssuerSerial: {
// https://www.w3.org/TR/xmldsig-core1/ refers to RFC 4514 and its
// distinguished name encoding rules.
ds$X509IssuerName: {$: cert.issuerRfc4514Name},
ds$X509SerialNumber: {$: cert.serialNumber.toString()}
}
}
},
},
xades$SignedDataObjectProperties: {
xades$DataObjectFormat: files.map((file, i) => ({
ObjectReference: "#file-" + i,
xades$MimeType: {$: String(file.type)}
}))
}
}
if (opts) switch (opts.policy) {
case undefined:
case null: break
case "bdoc":
// Only used with BDOC and its time-mark variant.
// There's also <xades:SignaturePolicyImplied>.
var props = signedProperties.xades$SignedSignatureProperties
props.xades$SignaturePolicyIdentifier = BDOC_POLICY
break
default: throw new RangeError("Unknown policy: " + opts.policy)
}
var canonicalizedSignedProps = XadesXml.canonicalize({
asic$XAdESSignatures: {
ds$Signature: {
ds$Object: {
xades$QualifyingProperties: {
xades$SignedProperties: signedProperties
}
}
}
}
}, [
"asic$XAdESSignatures",
"ds$Signature",
"ds$Object",
"xades$QualifyingProperties",
"xades$SignedProperties"
])
var sigAlgorithmUrl = SIG_ALGORITHM_URLS[cert.publicKeyAlgorithmName]
if (sigAlgorithmUrl == null) throw new RangeError(
"Unsupported signature algorithm: " + cert.publicKeyAlgorithmName
)
var signedInfo = {
// While BDOC v2.1.2 seems to hint the use of
// http://www.w3.org/2006/12/xml-c14n11, libdigidocpp v3.14.1 supports
// Exclusive XML Canonicalization just fine. So does Digidoc4j v3.3.0. As
// the exclusive variant is a more robust canonicalization method for
// extracted subtrees, sticking to that. Libdigidocpp's source code even
// has comments and Digidoc4j has tests regarding explicit support for
// exclusive canonicalization. In the worst case we're just EIDAS and
// XADES LT-TM compatible.
ds$CanonicalizationMethod: {Algorithm: C14N_URL},
ds$SignatureMethod: {Algorithm: sigAlgorithmUrl},
ds$Reference: concat({
Type: "http://uri.etsi.org/01903#SignedProperties",
URI: "#signed-properties",
ds$Transforms: {ds$Transform: {Algorithm: C14N_URL}},
ds$DigestMethod: {Algorithm: SHA256_URL},
ds$DigestValue: {$: sha256(canonicalizedSignedProps).toString("base64")}
}, files.map((file, i) => ({
// An id is required to match the DataObjectFormat to this
// reference.
Id: "file-" + i,
URI: file.path,
ds$DigestMethod: {Algorithm: SHA256_URL},
ds$DigestValue: {$: file.hash.toString("base64")}
})))
}
this.obj = {
// https://www.w3.org/TR/XAdES/
asic$XAdESSignatures: {
ds$Signature: {
Id: "signature",
ds$SignedInfo: signedInfo,
ds$SignatureValue: { Id: 'signature-value' },
ds$KeyInfo: {
ds$X509Data: certChain.map((c) => ({ds$X509Certificate: {$: c.toString("base64")}}))
},
ds$Object: {
xades$QualifyingProperties: {
Target: "#signature",
xades$SignedProperties: signedProperties,
xades$UnsignedProperties: {
xades$UnsignedSignatureProperties: {
// TODO: According to BDOC v2.1.2 Section 6, based on XAdES LT,
// <xades:CertificateValues> needs to include the issuer chain
// certificates and the OCSP response certificate if the latter
// is not included in the OCSP response itself. That includes
// the time stamp's certificate if not included in the time
// stamp itself.
//
// libdigidocpp v3.14.1 doesn't seem to make use of any
// included certificates though.
//
// Digidoc4j v3.3.0 on the other hand throws "OCSP Responder
// does not meet TM requirements" if the OCSP response
// certificate is missing. It doesn't seem to depend on the
// issuer nor root CA certificate.
//xades$CertificateValues: {
// xades$EncapsulatedX509Certificate: []
//},
// Optionally insert <xades:RevocationValues> here later.
// Optionally insert <xades:SignatureTimeStamp> here later.
}
}
}
}
}
}
}
}
Xades.prototype.obj = null
Xades.prototype.certificate = null
Xades.prototype.signableHashAlgorithm = "sha256"
Xades.prototype.__defineGetter__("signable", function() {
return XadesXml.canonicalize(this.obj, [
"asic$XAdESSignatures",
"ds$Signature",
"ds$SignedInfo"
])
})
Xades.prototype.__defineGetter__("signableHash", function() {
return sha256(this.signable)
})
Xades.prototype.__defineGetter__("signature", function() {
return Buffer.from(
this.obj.asic$XAdESSignatures.ds$Signature.ds$SignatureValue.$,
"base64"
)
})
Xades.prototype.setSignature = function(signature) {
var basedSig = signature.toString("base64")
this.obj.asic$XAdESSignatures.ds$Signature.ds$SignatureValue.$ = basedSig
}
// The <ds:SignatureVale> element is hashed for time stamping as per XAdES
// v1.4.1 Specification.
Xades.prototype.__defineGetter__("signatureElement", function() {
return XadesXml.canonicalize(this.obj, [
"asic$XAdESSignatures",
"ds$Signature",
"ds$SignatureValue"
])
})
// BDOC v2.1.0 required that the time stamp be queried before the OCSP response.
// http://open-eid.github.io/libdigidocpp/manual.html#signature-notes:
//
// «An exception is thrown if the OCSP confirmation's time is earlier than
// time-stamp's time. If the OCSP confirmation's time is later than
// time-stamp's time by more than 15 minutes then a warning is returned. If the
// difference is more than 24 hours then exception is thrown.»
//
// BDOC v2.1.2, however, removed that requirement as can be seen in the
// specification and from the CHANGELOG: https://www.id.ee/?id=36110.
//
// BDOC v2.1.2 says only OCSP resposnes with status "good" are allowed.
Xades.prototype.setOcspResponse = function(ocsp) {
var obj = this.obj.asic$XAdESSignatures.ds$Signature.ds$Object
var props = obj.xades$QualifyingProperties.xades$UnsignedProperties
props = props.xades$UnsignedSignatureProperties
props.xades$RevocationValues = {
xades$OCSPValues: {
xades$EncapsulatedOCSPValue: {$: serializeOcsp(ocsp).toString("base64")}
}
}
}
// http://open-eid.github.io/libdigidocpp/manual.html#signature-notes
// «In case of BDOC-TS signature, the time-stamping authority's (TSA's)
// certificate is not added to the <CertificateValues> element (differently
// from the requirements of BDOC specification, chap 6) to avoid duplication of
// the certificate in the signature. It is expected that the TSA certificate is
// present in the time-stamp token itself.»
Xades.prototype.setTimestamp = function(stamp) {
var obj = this.obj.asic$XAdESSignatures.ds$Signature.ds$Object
var props = obj.xades$QualifyingProperties.xades$UnsignedProperties
props = props.xades$UnsignedSignatureProperties
props.xades$SignatureTimeStamp = {
xades$Include: {
URI: '#signature-value',
},
ds$CanonicalizationMethod: {
Algorithm: C14N_URL
},
xades$EncapsulatedTimeStamp: {
$: serializeTimestamp(stamp).toString("base64")
}
}
}
Xades.prototype.toString = function() {
return XadesXml.stringify(this.obj)
}
Xades.prototype.valueOf = function() {
return this.obj
}
Xades.parse = function(xml) {
var obj = XadesXml.parse(xml)
var keyInfo = obj.asic$XAdESSignatures.ds$Signature.ds$KeyInfo
var der = Buffer.from(keyInfo.ds$X509Data.ds$X509Certificate.$, "base64")
var cert = new Certificate(der)
var xades = Object.create(Xades.prototype)
xades.obj = obj
xades.certificate = cert
return xades
}
function serializeOcsp(ocsp) {
if (ocsp instanceof OcspResponse) return ocsp.toBuffer()
if (ocsp instanceof Buffer) return ocsp
throw new TypeError("Invalid OCSP response type: " + ocsp)
}
function serializeTimestamp(stamp) {
if (stamp instanceof TimestampResponse) return stamp.token
if (stamp instanceof Buffer) return stamp
throw new TypeError("Invalid time stamp type: " + stamp)
}
function formatIsoDateTime(time) {
return time.toISOString().replace(/\.\d\d\dZ$/, "Z")
}