Skip to content
This repository was archived by the owner on Mar 8, 2024. It is now read-only.

Commit 5bf69bd

Browse files
authored
logic to parse a chain of certificates from the x5c section of a JWS (#20)
* functions and types for signing and validating a VerifiablePayID * get pem and x5c working simplify verify call to use Embedded option * lint fixes and refactored SigningParams types from interface to classes * exports * packages.json cleanup * WIP: get PKI with certificate chain validation workiing * lint fixes use generated Root CA, Intermediate CA, and server cert in tests * add 'name' to crit section * splitting web-pki into multiple branches * logic to parse a chain of certificates from the x5c section of a JWS * add test to verify self-signed certs pass signature check but fail chain of certificate check * add TODO comment on types
1 parent 441a744 commit 5bf69bd

8 files changed

Lines changed: 293 additions & 3 deletions

File tree

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { promises } from 'fs'
2+
import * as tls from 'tls'
3+
4+
import { JWS } from 'jose'
5+
import * as forge from 'node-forge'
6+
7+
import { getJwkFromRecipient, isX5C } from './keys'
8+
9+
import certificateFromPem = forge.pki.certificateFromPem
10+
11+
/**
12+
* Service to validate the certificate chain (using web PKI) for Verifiable PayIDs
13+
* signed with a a server key.
14+
*/
15+
export default class CertificateChainValidator {
16+
public caStore: forge.pki.CAStore
17+
18+
public constructor() {
19+
this.caStore = forge.pki.createCaStore([])
20+
this.importNodeRootCertficates()
21+
}
22+
23+
/**
24+
* Verifies the certificate chain for any x5c certificates inside the JWS protected section.
25+
*
26+
* @param jws - The JWS to verify.
27+
* @returns True if verified.
28+
*/
29+
public verifyCertificateChainJWS(jws: JWS.GeneralJWS): boolean {
30+
return jws.signatures
31+
.map((recipient) => this.verifyCertificateChainRecipient(recipient))
32+
.every((val) => val)
33+
}
34+
35+
/**
36+
* Verifies the chain within the recipient.
37+
*
38+
* @param recipient - The recipient to verify.
39+
* @returns True if verified.
40+
*/
41+
public verifyCertificateChainRecipient(recipient: JWS.JWSRecipient): boolean {
42+
return this.verifyCertificateChain(extractX5CCertificates(recipient))
43+
}
44+
45+
/**
46+
* Verifies the chain within the list of certificates.
47+
*
48+
* @param chain - The list of certificates.
49+
* @returns True if verified.
50+
*/
51+
public verifyCertificateChain(chain: forge.pki.Certificate[]): boolean {
52+
if (chain.length === 0) {
53+
return true
54+
}
55+
try {
56+
return forge.pki.verifyCertificateChain(this.caStore, chain)
57+
} catch {
58+
return false
59+
}
60+
}
61+
62+
/**
63+
* Adds a root certificate to be included in certificate chain validation.
64+
*
65+
* @param certificate - The certificate text in PEM format.
66+
*/
67+
public addRootCertificate(certificate: string): void {
68+
const cert = forge.pki.certificateFromPem(certificate)
69+
this.caStore.addCertificate(cert)
70+
}
71+
72+
/**
73+
* Adds a root certificate to be included in certificate chain validation.
74+
*
75+
* @param path - Path to certificate file in PEM format.
76+
*/
77+
public async addRootCertificateFile(path: string): Promise<void> {
78+
this.addRootCertificate(await promises.readFile(path, 'ascii'))
79+
}
80+
81+
/**
82+
* Imports the Root certificates from Node into the CA store used for certificate chain validation.
83+
*/
84+
private importNodeRootCertficates(): void {
85+
for (const rootCert of tls.rootCertificates) {
86+
try {
87+
this.addRootCertificate(rootCert)
88+
} catch {
89+
// unsupported cert. just skip.
90+
}
91+
}
92+
}
93+
}
94+
95+
/**
96+
* Extract X5C certificates from the JWS protected section.
97+
*
98+
* @param recipient - The recipient to parse.
99+
* @returns List of certificates.
100+
*/
101+
export function extractX5CCertificates(
102+
recipient: JWS.JWSRecipient,
103+
): forge.pki.Certificate[] {
104+
const jwk = getJwkFromRecipient(recipient)
105+
if (jwk && isX5C(jwk) && jwk.x5c) {
106+
return jwk.x5c.map((pem) =>
107+
certificateFromPem(
108+
`-----BEGIN CERTIFICATE-----${pem}-----END CERTIFICATE-----`,
109+
),
110+
)
111+
}
112+
return []
113+
}

src/verifiable/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './keys'
22
export { default as IdentityKeySigningParams } from './identity-key-signing-params'
3+
export { default as CertificateChainValidator } from './certificate-chain-validator'
34
export { default as ServerKeySigningParams } from './server-key-signing-params'
45
export * from './signatures'
56
export * from './verifiable-payid'

src/verifiable/signatures.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { JWS, JWK } from 'jose'
22

3+
import CertificateChainValidator from './certificate-chain-validator'
34
import IdentityKeySigningParams from './identity-key-signing-params'
45
import ServerKeySigningParams from './server-key-signing-params'
56
import { Address, PaymentInformation } from './verifiable-payid'
67

78
import GeneralJWS = JWS.GeneralJWS
89

10+
export const certificateChainValidator = new CertificateChainValidator()
11+
912
/**
1013
* Creates a signed JWS.
1114
*
@@ -149,11 +152,13 @@ export function verifyPayId(toVerify: string | PaymentInformation): boolean {
149152
*
150153
* @param expectedPayId - The expected payid.
151154
* @param verifiedAddress - JWS representing a verified address.
155+
* @param checkCertificateChain - Flag to enable/disable validation of x5c certificate chain.
152156
* @returns Returns true if any signature is invalid, returns false. Otherwise true.
153157
*/
154158
export function verifySignedAddress(
155159
expectedPayId: string,
156160
verifiedAddress: GeneralJWS | string,
161+
checkCertificateChain = true,
157162
): boolean {
158163
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- because JSON
159164
const jws: GeneralJWS =
@@ -162,18 +167,18 @@ export function verifySignedAddress(
162167
: verifiedAddress
163168
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- because JSON
164169
const address: UnsignedVerifiedAddress = JSON.parse(jws.payload)
165-
166170
if (expectedPayId !== address.payId) {
167171
// payId does not match what was inside the signed payload
168172
return false
169173
}
170-
171174
try {
172-
// verifies signatures
173175
JWS.verify(jws, JWK.EmbeddedJWK, {
174176
crit: ['b64', 'name'],
175177
complete: true,
176178
})
179+
if (checkCertificateChain) {
180+
return certificateChainValidator.verifyCertificateChainJWS(jws)
181+
}
177182
return true
178183
} catch {
179184
return false

test/certs/self-signed.cert

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIC1TCCAb2gAwIBAgIJAJd9RRWtqliiMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNV
3+
BAMTD3d3dy5leGFtcGxlLmNvbTAeFw0yMDA3MzEyMzU2MjNaFw0zMDA3MjkyMzU2
4+
MjNaMBoxGDAWBgNVBAMTD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEB
5+
BQADggEPADCCAQoCggEBAKomwXmr2JbchPHQv06fCw+mVibx719qyaPi6rYVCPkq
6+
Q9wuDBuA5zINcE/VqPNOgJ2eOYul3dZd5vXR83vq09azA1aig1AukLtcgqUGurG+
7+
UEHLZ0ylS5OFDfm/mxPty3Pw5pwsSl4d4Wn8PMkbxVqjsAkprkSiW6796+RNyc4O
8+
09+bjSG7EX1qLf+ekkutGTxAms80UOgT+s3xEQKGuCkFv1I1u1VX9uzTrM5YH1sj
9+
0GVXRz6aeT+8qEHciwecnGVR9nL53kLRch0VmSeIVBXYbKoEEZJhY9y9CfDpt3r0
10+
kJDWVdPjaCBYlD2aB/E/ZG996Fwaz9ruV58MLgy1KqkCAwEAAaMeMBwwGgYDVR0R
11+
BBMwEYIPd3d3LmV4YW1wbGUuY29tMA0GCSqGSIb3DQEBBQUAA4IBAQABpAlNIt3G
12+
Fuy9CuG12wWSp4Vlg805CxmV2OTgvW1XTeRHLbWiMfvm4nLiFRuOvjehthTUEkkt
13+
3/m5urEdrEknW27uL3etq5CYnkEVuGioQMQqtXFZGdQg+pwi3eT/VlzLDvmq0hcH
14+
oR133ik5xwLgwlw6kxe1nFMslLxg7/bf7EYavAtWzBP9FRu1ty+467Z8rCf360MN
15+
0A7S1LYLzsIJ6hjTrUKIwhac+bZrGdDhBDvm8TaDD7YU54A32arKYdCuGKqaLYK5
16+
YCQ1BdhSCVRS/0C8G0il0mEQqcBZHvX+zQ4lZ+eWLreqdv8IX4BI2+iTbDbQjTvH
17+
i+Mf2hijqhIi
18+
-----END CERTIFICATE-----

test/certs/self-signed.key

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
MIIEowIBAAKCAQEAqibBeavYltyE8dC/Tp8LD6ZWJvHvX2rJo+LqthUI+SpD3C4M
3+
G4DnMg1wT9Wo806AnZ45i6Xd1l3m9dHze+rT1rMDVqKDUC6Qu1yCpQa6sb5QQctn
4+
TKVLk4UN+b+bE+3Lc/DmnCxKXh3hafw8yRvFWqOwCSmuRKJbrv3r5E3Jzg7T35uN
5+
IbsRfWot/56SS60ZPECazzRQ6BP6zfERAoa4KQW/UjW7VVf27NOszlgfWyPQZVdH
6+
Ppp5P7yoQdyLB5ycZVH2cvneQtFyHRWZJ4hUFdhsqgQRkmFj3L0J8Om3evSQkNZV
7+
0+NoIFiUPZoH8T9kb33oXBrP2u5XnwwuDLUqqQIDAQABAoIBADSUXV1X+UpFQt/m
8+
/fcxtp1TbXQDd1EpEr6ONLGntmoo4Wd840jsgIU7GeXRxK/LJnuOlYHN88t2oRR9
9+
mJxGaMgD8ZgoCCQS/66mW8jbV33tradnT7ijq8Mebr8qsqVp6mEdpGXGWgTTfwDd
10+
bXtIFah4xMFQHAYhletxlB+s2hvOgAqCX1rnkjkRO79aidRmjtwWatTuirD+BL2a
11+
1gyC2t052PPH0FK0yH9yx/ez27mSMxqP4pod45r0/7N3t1wR0vDI6whsLMacJrWJ
12+
hf7aBKwoLILwjx6lcEZPnOuj7ozBpfm0cFPZfRG5c0IGkibnGkDEqzpLcz3MEBEt
13+
pwvcIgECgYEA25nwS/U54o7ihKJyBY5NnQXpGvBYJCd74s4q24ExQrQt2ua3ia1u
14+
HifpBrQk7UfPNVwrzco+A4fpvchGSR6vwhg9FvhGQTqwdR6/fMYqcX/vLR/nQEU/
15+
DKvXFK0dWAKhwP2dBBTobeJdVNXCV7K2jNHp5RsNzM+NeH8sojF+zNkCgYEAxlqS
16+
TbKCiMYjFOdiVW/0zou59YnlooTrwOJNKnCv7HsAxSbErWbtF8SFbeVDSCdHTI10
17+
hesToKKhYZhjGkVwu7pme9p6T3vOGVaD4E6Ooj34tkW87J2FqOdhmBY6+hUqx/LB
18+
3THjDLUgWTMIIB8RjLvHIcGzF3TCov1RKIgY6lECgYEAv7S3Ldg6XCnYXWlimK8N
19+
2lJamQXQLF+7qtfIWi+CTXT1wu8+spYQV4sHxq5kvi++GBsKsnAnivWPe/nmQdbk
20+
IFEAo5jB3BfcC6J4D/j+/G5u4bnEKztIO0uYS5iE0Vwa0VuVQwbtkV/XkkO5kM2W
21+
x4BI65Sei3l1Swfacw06YKECgYAGDyo8+WEHcJYNw2u7lGn0DUym9YlwR4M0JzWY
22+
QEz/elpxq1eCvIwtl7FDxCckAx8odYHDvYSh+ZXYd2E/ojNpaK5MxkXKO8v19jCd
23+
H4k355C7cLHuwHkeycKvdK5kiVT/Oqk1apq2/ql4UBjFcm2E0Q+qNlKUOtrfQ8HA
24+
7TdloQKBgG0hUZmuEfbF03UkZ360d6Ui2tyc2D/Txx4Zkoy7WUZe4VZ1vrHVBb/j
25+
dCl7MASpwySN4i+5bAAg/bpa92dhnrhrVuF9WXXqbhRTyJ+cX7/pv8xgEMu/1uGF
26+
uGcNkHwoP1EsuxQQBai5WGJPI4gp7B4COKbU+2f6etGy2WxmTD8m
27+
-----END RSA PRIVATE KEY-----
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import 'mocha'
2+
3+
import { assert } from 'chai'
4+
5+
import { extractX5CCertificates } from '../../../src/verifiable/certificate-chain-validator'
6+
7+
describe('extractX5CCertificates()', function () {
8+
const nakedCert =
9+
'MIIFNzCCBB+gAwIBAgIVALitpIaiMG63BEIyEPDvVd+wrmGOMA0GCSqGSIb3DQEB' +
10+
'DQUAMG0xCzAJBgNVBAYTAkdCMQ8wDQYDVQQHDAZMb25kb24xEjAQBgNVBAoMCU15' +
11+
'Q29tcGFueTEVMBMGA1UEAwwMTXlDb21wYW55IENBMSIwIAYJKoZIhvcNAQkBFhNh' +
12+
'ZG1pbkBteWNvbXBhbnkuY29tMB4XDTIwMDgwNTIwMTM1MFoXDTI1MDgwNDIwMTM1' +
13+
'MFowOzELMAkGA1UEBhMCVVMxEzARBgNVBAoMClBheUlEIFRlc3QxFzAVBgNVBAMM' +
14+
'DnRlc3QucGF5aWQub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA' +
15+
'nq5zAkTi9dldHeBx5L3BvI8uvd/YOjHdcJz+7GB0OW/dN7J8mcchPN6YGmJZajJa' +
16+
'v7ZeqOXSrQZoqfrt+QN/YogiqRs4Dj6LnVIQ29bXW1OyPp4ZpfwNLBettcehP+Nc' +
17+
'mwBNgvEecx1ap5z5qZym0qgJsjv2S6LeFrGwPfnvx4leLvA6ZuD90VtXxYwOVakY' +
18+
'eJpPqmHZbPDxVz673izi3g9UUWh+Iz6GSdpj7ZNf2m7C5F1numYkrAEG3ZjnuTbt' +
19+
'/scQij/9M+3wyJOY/Hac5SDnZDLXtequvL51wEYpSOumtR1GwBQ6NjmUudtgbpwq' +
20+
'85zWC7VUHjKvuFxKD7HMXAAcHZ3aQ0PsDqy7qBeX7T8qAH6yTK2Uu+hj/h3Ua5Tz' +
21+
'05xboXuTbgPyzFJRa3kv535NHa55nu6L0Vpx1NUFt7pRz9V4usnSW6ElwYbqIxdN' +
22+
'3DRzqoKlTcIkgS/0YJku6+94diX8CUvu9rbACLDEQqxzfdt3TGyos3JzheQVtgYQ' +
23+
'n9IHNDYBrrCMrKYKgZCAhp97Sba6Vi1IXVHZggQ1e7zAhxMsPb+YlxsMzqf70T3D' +
24+
'NQTHYxhHSV4/SHIKh/8knCu/+bFg9fb4anXCXzD+C9GSZS+DFmYpdTkgE6t5DEH1' +
25+
'mPC5scdOljPDWQaoJQq1cgwjR1xOHZOthwy0QOSVf2kCAwEAAaOB/zCB/DAJBgNV' +
26+
'HRMEAjAAMB0GA1UdDgQWBBTNPOnIjDLnEdv51dGQXNa4GyOckjCBqgYDVR0jBIGi' +
27+
'MIGfgBSFCeuSHVQE+yYW86SL1IjwF66DrKFxpG8wbTELMAkGA1UEBhMCR0IxDzAN' +
28+
'BgNVBAcMBkxvbmRvbjESMBAGA1UECgwJTXlDb21wYW55MRUwEwYDVQQDDAxNeUNv' +
29+
'bXBhbnkgQ0ExIjAgBgkqhkiG9w0BCQEWE2FkbWluQG15Y29tcGFueS5jb22CFE4x' +
30+
'B5M+twm35qBIXdskVZaI9GZUMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggr' +
31+
'BgEFBQcDATANBgkqhkiG9w0BAQ0FAAOCAQEAim7034Mpl0CZYDamnR67++qsGkZo' +
32+
'lhdfjozAilZXPzKdTuOIe6LtPCpxx1Vdut44ZvZMZWfnz+vYbqC5TptmWTlHtK3R' +
33+
'GgCZNPMnaDxbH/vIWOQvPxYLRALbo0TA9PuMO8CWuWxp8+Qd6ob12dhKSTbr4KdX' +
34+
'RKS4iUVlj3d+y7UwS0eAtUINRDDnzapAnU86f3qUGnqAN9KV4sPGlYWA9F7e+uZd' +
35+
'C6c+yXG8VmcMYBM4wChkPb+Yr3eNoKPSxDiPKh9NO3A69AaR2zpv4cu5CEn4wjK0' +
36+
'rZIYsYKJwkFxikvDwmyH+BZ8jaDSS6ut/ftY2Sssia7uH6KisWD20Y6jkQ=='
37+
38+
it('succeeds verification of chain if Root CA configured', async function () {
39+
const jwk = {
40+
kty: 'RSA',
41+
// eslint-disable-next-line id-length,id-blacklist -- JWK dictates
42+
e: 'AQAB',
43+
use: 'sig',
44+
// eslint-disable-next-line id-length -- JWK dictates this
45+
n:
46+
'nq5zAkTi9dldHeBx5L3BvI8uvd_YOjHdcJz-7GB0OW_dN7J8mcchPN6YGmJZajJav7ZeqOXSrQZoqfrt-QN_YogiqRs4Dj6LnVIQ29bXW1OyPp4ZpfwNLBettcehP-NcmwBNgvEecx1ap5z5qZym0qgJsjv2S6LeFrGwPfnvx4leLvA6ZuD90VtXxYwOVakYeJpPqmHZbPDxVz673izi3g9UUWh-Iz6GSdpj7ZNf2m7C5F1numYkrAEG3ZjnuTbt_scQij_9M-3wyJOY_Hac5SDnZDLXtequvL51wEYpSOumtR1GwBQ6NjmUudtgbpwq85zWC7VUHjKvuFxKD7HMXAAcHZ3aQ0PsDqy7qBeX7T8qAH6yTK2Uu-hj_h3Ua5Tz05xboXuTbgPyzFJRa3kv535NHa55nu6L0Vpx1NUFt7pRz9V4usnSW6ElwYbqIxdN3DRzqoKlTcIkgS_0YJku6-94diX8CUvu9rbACLDEQqxzfdt3TGyos3JzheQVtgYQn9IHNDYBrrCMrKYKgZCAhp97Sba6Vi1IXVHZggQ1e7zAhxMsPb-YlxsMzqf70T3DNQTHYxhHSV4_SHIKh_8knCu_-bFg9fb4anXCXzD-C9GSZS-DFmYpdTkgE6t5DEH1mPC5scdOljPDWQaoJQq1cgwjR1xOHZOthwy0QOSVf2k',
47+
x5c: [nakedCert, nakedCert],
48+
}
49+
const protectedHeaders = { jwk }
50+
const protectedBase64 = Buffer.from(
51+
JSON.stringify(protectedHeaders),
52+
).toString('base64')
53+
54+
const x5c = extractX5CCertificates({
55+
protected: protectedBase64,
56+
signature: 'not-a-real-signature',
57+
})
58+
assert.lengthOf(x5c, 2)
59+
})
60+
})

test/unit/verifiable/signatures.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from '../../../src/verifiable/keys'
1111
import ServerKeySigningParams from '../../../src/verifiable/server-key-signing-params'
1212
import {
13+
certificateChainValidator,
1314
sign,
1415
signWithKeys,
1516
signWithServerKey,
@@ -38,6 +39,7 @@ describe('sign()', function () {
3839
let address: Address
3940

4041
beforeEach(function () {
42+
certificateChainValidator.addRootCertificateFile('test/certs/root.crt')
4143
address = {
4244
environment: 'TESTNET',
4345
paymentNetwork: 'XRPL',
@@ -130,6 +132,21 @@ describe('sign()', function () {
130132
assert.isFalse(verifySignedAddress('hacked$payid.example', jws))
131133
})
132134

135+
it('verification fails for untrusted self-signed certificate', async function () {
136+
const jws = signWithServerKey(
137+
payId,
138+
address,
139+
new ServerKeySigningParams(
140+
await getSigningKeyFromFile('test/certs/self-signed.key'),
141+
'RS256',
142+
await getJwkFromFile('test/certs/self-signed.cert'),
143+
),
144+
)
145+
// should validate if certificate chain validation is skipped
146+
assert.isTrue(verifySignedAddress(payId, jws, false))
147+
assert.isFalse(verifySignedAddress(payId, jws))
148+
})
149+
133150
it('verifySignedAddress - verifies jws with multiple x5c', async function () {
134151
const jws = `{"payload":"{\\"payId\\":\\"alice$payid.example\\",\\"payIdAddress\\":{\\"environment\\":\\"TESTNET\\",\\"paymentNetwork\\":\\"XRPL\\",\\"addressDetailsType\\":\\"CryptoAddressDetails\\",\\"addressDetails\\":{\\"address\\":\\"rP3t3JStqWPYd8H88WfBYh3v84qqYzbHQ6\\"}}}","signatures":[{"protected":"eyJuYW1lIjoic2VydmVyS2V5IiwiYWxnIjoiUlMyNTYiLCJ0eXAiOiJKT1NFK0pTT04iLCJiNjQiOmZhbHNlLCJjcml0IjpbImI2NCJdLCJqd2siOnsiZSI6IkFRQUIiLCJuIjoieVFDU0FwdVRCX3BSclJyT1h6aVVTcElvZ0JlckZjM09ZTzVWR0tsYXFJWFBpQkt1LW0tT3lPZEJCclh6eHdCR1lJSWZIbnhjdVFlN3REU3FzY1p4M2w5NklPSFJUU3p3NXdaV0tlVjdfZzAydlJWNEtBS1Z2TkpiUlBON05tSjdiQkNsUWgyZEZkRmZNRlZtUk5NN3A2SlVGeGt0VHhhREEtY0ZkWVNxTDlBYXd5N0RXTUNXT1pGYjR4SXNHbDF3MGs0Z2tEQThYWHE4UHVQYURkXzRxRFVRdjExdWZxQU9kQU1YWDdudy1LV3k4QURGRWVtWnZVcHo5RkVaMk9hWUM3UFJ0NVMzNmR4aS1JajJpOTZuVE5xVmlrbXhrN24zWkE2NlVSbXRHc3RKSFNtRS1oZWs0YzU4Y0ZJbXZSSl9FaFN1QjlHWVRFajBKS2dOaHlKWnlRIiwia3R5IjoiUlNBIiwia2lkIjoiNmNuRDY1NURnXzRLZkphRVY1N3ZGYTV6UkEyT0FuWHBNQXlVbmpuaEpJcyIsIng1YyI6WyJNSUlGU0RDQ0JEQ2dBd0lCQWdJU0JFLzRzVlJSYTBkNVJYM0c1ZUFOUWhoQU1BMEdDU3FHU0liM0RRRUJDd1VBTUVveEN6QUpCZ05WQkFZVEFsVlRNUll3RkFZRFZRUUtFdzFNWlhRbmN5QkZibU55ZVhCME1TTXdJUVlEVlFRREV4cE1aWFFuY3lCRmJtTnllWEIwSUVGMWRHaHZjbWwwZVNCWU16QWVGdzB5TURBM01qQXlNVEEyTUROYUZ3MHlNREV3TVRneU1UQTJNRE5hTUJNeEVUQVBCZ05WQkFNVENIQmhlV2xrTG0xc01JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeVFDU0FwdVRCL3BSclJyT1h6aVVTcElvZ0JlckZjM09ZTzVWR0tsYXFJWFBpQkt1K20rT3lPZEJCclh6eHdCR1lJSWZIbnhjdVFlN3REU3FzY1p4M2w5NklPSFJUU3p3NXdaV0tlVjcvZzAydlJWNEtBS1Z2TkpiUlBON05tSjdiQkNsUWgyZEZkRmZNRlZtUk5NN3A2SlVGeGt0VHhhREErY0ZkWVNxTDlBYXd5N0RXTUNXT1pGYjR4SXNHbDF3MGs0Z2tEQThYWHE4UHVQYURkLzRxRFVRdjExdWZxQU9kQU1YWDdudytLV3k4QURGRWVtWnZVcHo5RkVaMk9hWUM3UFJ0NVMzNmR4aStJajJpOTZuVE5xVmlrbXhrN24zWkE2NlVSbXRHc3RKSFNtRStoZWs0YzU4Y0ZJbXZSSi9FaFN1QjlHWVRFajBKS2dOaHlKWnlRSURBUUFCbzRJQ1hUQ0NBbGt3RGdZRFZSMFBBUUgvQkFRREFnV2dNQjBHQTFVZEpRUVdNQlFHQ0NzR0FRVUZCd01CQmdnckJnRUZCUWNEQWpBTUJnTlZIUk1CQWY4RUFqQUFNQjBHQTFVZERnUVdCQlJxMlNza3FiRVVtRG5Hc0ZmekdjMms1c3hSRFRBZkJnTlZIU01FR0RBV2dCU29TbXBqQkgzZHV1YlJPYmVtUldYdjg2anNvVEJ2QmdnckJnRUZCUWNCQVFSak1HRXdMZ1lJS3dZQkJRVUhNQUdHSW1oMGRIQTZMeTl2WTNOd0xtbHVkQzE0TXk1c1pYUnpaVzVqY25sd2RDNXZjbWN3THdZSUt3WUJCUVVITUFLR0kyaDBkSEE2THk5alpYSjBMbWx1ZEMxNE15NXNaWFJ6Wlc1amNubHdkQzV2Y21jdk1CTUdBMVVkRVFRTU1BcUNDSEJoZVdsa0xtMXNNRXdHQTFVZElBUkZNRU13Q0FZR1o0RU1BUUlCTURjR0N5c0dBUVFCZ3Q4VEFRRUJNQ2d3SmdZSUt3WUJCUVVIQWdFV0dtaDBkSEE2THk5amNITXViR1YwYzJWdVkzSjVjSFF1YjNKbk1JSUJCQVlLS3dZQkJBSFdlUUlFQWdTQjlRU0I4Z0R3QUhZQTV4THlzRGQrR21MN2pza01ZWVR4Nm5zM3kxWWRFU1piOCtEelMvSkJWRzRBQUFGemJrTXlMQUFBQkFNQVJ6QkZBaUIzVllqM2ExZThYQkJUa3QyREx3dG8rZWFGNGFYaWR3MnlIblpTY3NRRXhRSWhBSjZkMDRPV0ljaEdYRjdEdzBCcUxEVXFlWWpFQ3pPOTJ6STdqWHlsdkh2Q0FIWUFCN2RjRytWOWFQL3hzTVlkSXhYSHV1WlhmRmVVdDJydXZHRTZHbW5Ub2h3QUFBRnpia015VmdBQUJBTUFSekJGQWlFQXhFbC85b012S2x6WWFWMTQwWDJjUHhRd2h2WDJpOGs3MVZ4M29kZXdCSzRDSURzYVQrWFVxdU5xdS90SDVwRFBrMEVRSGViSUVScXpiVXVKMW9rTXVVZU5NQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUFqNUk3ZnZ0RkRhNnpsRHFMajJwYVVLMUpzYW9WSzFIMlJNVzY5YXdPbTNQV1laVG83RFF4cmNrenc0bjBlaElRb1V2UlpGT09BNmJKQmFKU1owaU9JY0hsNlhvVm1KZ0lEL2FHMDNDUHJYMWoreEkwMzdmMlBhOSs3QVpQNUE1Q1BrSWx2dnZQb1Z6cGM3eEZMRitWZHk1QThva1VlOTRobDhYKy80WjN6QXYyL0JaY3dENUxZWGxDK0YzQ09Yci9QNWFRVHFqOTlvWXU3ZlMzSHp2RENzOTlnM0dPQ28xWGgzWFg2UTVqZ1FtSTR0S1F0K1JPN0Z3cnhVTnFKWDg3WnNwNTF0azBGUS92NEEydE9kTHlnTzh2V2drR3J2NU1VZFprdUUvbm5SYWZYeTY3VWZWNFhYZ2F5YXAxTWRoeUcwaXh1SURWaXExTmk2eGpjeXVBZCIsIk1JSUVrakNDQTNxZ0F3SUJBZ0lRQ2dGQlFnQUFBVk9GYzJvTGhleW5DREFOQmdrcWhraUc5dzBCQVFzRkFEQS9NU1F3SWdZRFZRUUtFeHRFYVdkcGRHRnNJRk5wWjI1aGRIVnlaU0JVY25WemRDQkRieTR4RnpBVkJnTlZCQU1URGtSVFZDQlNiMjkwSUVOQklGZ3pNQjRYRFRFMk1ETXhOekUyTkRBME5sb1hEVEl4TURNeE56RTJOREEwTmxvd1NqRUxNQWtHQTFVRUJoTUNWVk14RmpBVUJnTlZCQW9URFV4bGRDZHpJRVZ1WTNKNWNIUXhJekFoQmdOVkJBTVRHa3hsZENkeklFVnVZM0o1Y0hRZ1FYVjBhRzl5YVhSNUlGZ3pNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQW5OTU04RnJsTGtlM2NsMDNnN05vWXpEcTF6VW1HU1hodmI0MThYQ1NMN2U0UzBFRnE2bWVOUWhZN0xFcXhHaUhDNlBqZGVUbTg2ZGljYnA1Z1dBZjE1R2FuL1BRZUdkeHlHa09sWkhQL3VhWjZXQThTTXgreWsxM0VpU2RSeHRhNjduc0hqY0FISnlzZTZjRjZzNUs2NzFCNVRhWXVjdjliVHlXYU44aktrS1FESVowWjhoL3BacTRVbUVVRXo5bDZZS0h5OXY2RGxiMmhvbnpoVCtYaHErdzNCcnZhdzJWRm4zRUs2QmxzcGtFTm5XQWE2eEs4eHVRU1hndm9wWlBLaUFsS1FUR2RNRFFNYzJQTVRpVkZycW9NN2hEOGJFZnd6Qi9vbmt4RXowdE52amovUEl6YXJrNU1jV3Z4STBOSFdRV002cjZoQ20yMUF2QTJIM0Rrd0lEQVFBQm80SUJmVENDQVhrd0VnWURWUjBUQVFIL0JBZ3dCZ0VCL3dJQkFEQU9CZ05WSFE4QkFmOEVCQU1DQVlZd2Z3WUlLd1lCQlFVSEFRRUVjekJ4TURJR0NDc0dBUVVGQnpBQmhpWm9kSFJ3T2k4dmFYTnlaeTUwY25WemRHbGtMbTlqYzNBdWFXUmxiblJ5ZFhOMExtTnZiVEE3QmdnckJnRUZCUWN3QW9ZdmFIUjBjRG92TDJGd2NITXVhV1JsYm5SeWRYTjBMbU52YlM5eWIyOTBjeTlrYzNSeWIyOTBZMkY0TXk1d04yTXdId1lEVlIwakJCZ3dGb0FVeEtleHBIc3NjZnJiNFV1UWRmL0VGV0NGaVJBd1ZBWURWUjBnQkUwd1N6QUlCZ1puZ1F3QkFnRXdQd1lMS3dZQkJBR0MzeE1CQVFFd01EQXVCZ2dyQmdFRkJRY0NBUllpYUhSMGNEb3ZMMk53Y3k1eWIyOTBMWGd4TG14bGRITmxibU55ZVhCMExtOXlaekE4QmdOVkhSOEVOVEF6TURHZ0w2QXRoaXRvZEhSd09pOHZZM0pzTG1sa1pXNTBjblZ6ZEM1amIyMHZSRk5VVWs5UFZFTkJXRE5EVWt3dVkzSnNNQjBHQTFVZERnUVdCQlNvU21wakJIM2R1dWJST2JlbVJXWHY4Nmpzb1RBTkJna3Foa2lHOXcwQkFRc0ZBQU9DQVFFQTNUUFhFZk5qV0RqZEdCWDdDVlcrZGxhNWNFaWxhVWNuZThJa0NKTHhXaDlLRWlrM0pIUlJIR0pvdU0yVmNHZmw5NlM4VGloUnpadm9yb2VkNnRpNldxRUJtdHp3M1dvZGF0ZytWeU9lcGg0RVlwci8xd1hLdHg4L3dBcEl2SlN3dG1WaTRNRlU1YU1xclNERTZlYTczTWoydGNNeW81ak1kNmptZVdVSEs4c28vam9XVW9IT1Vnd3VYNFBvMVFZeiszZHN6a0RxTXA0ZmtseEJ3WFJzVzEwS1h6UE1UWitzT1BBdmV5eGluZG1qa1c4bEd5K1FzUmxHUGZaK0c2WjZoN21qZW0wWStpV2xrWWNWNFBJV0wxaXdCaThzYUNiR1M1ak4ycDhNK1grUTdVTktFa1JPYjNONktPcWtxbTU3VEgySDNlREpBa1NuaDYvRE5GdTBRZz09Il0sIng1dCI6Ino3QUpKOXNUUERvNEJ0Q3ZsN1U4T25pM3JPdyIsIng1dCNTMjU2IjoiVlVXQmNNRVltejU1Um9oWWxFYXpnemIxaFllRTVYVTc0cUJQR2cxZ0JhcyJ9fQ","signature":"c2N2Qe-iYnqsP5cr4f6Whx8Vc1wi1D6KgIawQk9m5-a0XCI_tXHzsxGyc01z2OCoH-P7apAxzEVpo11QBiPXaI3uHmHIS9nypQLqH61bVjH6P5cZXC-A_hWwNij65CrZKpwkUUPAOB1gVbbnS3yAyXyiXezvlHj_AMEKjvhXn4Okp3B2E0k_YgChlInvHw11x9DHxKouV_hb1bZT_pJKc74v4Z0l5i94bK4u8U22lZ6C25tBwH-BSg41bwdKiT29D9CDOhgUNc2saREo5T-BvwVvS_-92t_UBP5tso9c9sWpe871ShUaMK4jT1HT3NqHSTWde1q8MWIxIFBN3rVeow"}]}`
135152

0 commit comments

Comments
 (0)