Skip to content

Commit 921523f

Browse files
committed
SLCORE-2239 Add on-demand plugin signature verification
1 parent f5fc710 commit 921523f

File tree

14 files changed

+493
-2
lines changed

14 files changed

+493
-2
lines changed

backend/core/pom.xml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,14 @@
9494
<groupId>com.google.guava</groupId>
9595
<artifactId>guava</artifactId>
9696
</dependency>
97+
<dependency>
98+
<groupId>org.bouncycastle</groupId>
99+
<artifactId>bcpg-jdk18on</artifactId>
100+
</dependency>
101+
<dependency>
102+
<groupId>org.bouncycastle</groupId>
103+
<artifactId>bcprov-jdk18on</artifactId>
104+
</dependency>
97105
<dependency>
98106
<groupId>org.apache.httpcomponents.client5</groupId>
99107
<artifactId>httpclient5</artifactId>
@@ -157,7 +165,59 @@
157165
</dependency>
158166
</dependencies>
159167

168+
<build>
169+
<resources>
170+
<resource>
171+
<directory>src/main/resources</directory>
172+
<filtering>true</filtering>
173+
<includes>
174+
<include>ondemand-plugins.properties</include>
175+
</includes>
176+
</resource>
177+
<resource>
178+
<directory>src/main/resources</directory>
179+
<filtering>false</filtering>
180+
<excludes>
181+
<exclude>ondemand-plugins.properties</exclude>
182+
</excludes>
183+
</resource>
184+
</resources>
160185

186+
<plugins>
187+
<plugin>
188+
<groupId>com.googlecode.maven-download-plugin</groupId>
189+
<artifactId>download-maven-plugin</artifactId>
190+
<executions>
191+
<execution>
192+
<id>download-cfamily-signature</id>
193+
<phase>generate-resources</phase>
194+
<goals>
195+
<goal>wget</goal>
196+
</goals>
197+
<configuration>
198+
<url>https://binaries.sonarsource.com/CommercialDistribution/sonar-cfamily-plugin/sonar-cfamily-plugin-${cfamily.version}.jar.asc</url>
199+
<outputDirectory>${project.build.outputDirectory}</outputDirectory>
200+
<outputFileName>sonar-cpp-plugin.jar.asc</outputFileName>
201+
<skipCache>false</skipCache>
202+
</configuration>
203+
</execution>
204+
<execution>
205+
<id>download-csharp-signature</id>
206+
<phase>generate-resources</phase>
207+
<goals>
208+
<goal>wget</goal>
209+
</goals>
210+
<configuration>
211+
<url>https://binaries.sonarsource.com/Distribution/sonar-csharp-plugin/sonar-csharp-plugin-${csharp.version}.jar.asc</url>
212+
<outputDirectory>${project.build.outputDirectory}</outputDirectory>
213+
<outputFileName>sonar-cs-plugin.jar.asc</outputFileName>
214+
<skipCache>false</skipCache>
215+
</configuration>
216+
</execution>
217+
</executions>
218+
</plugin>
219+
</plugins>
220+
</build>
161221

162222
<profiles>
163223
<!-- Workaround for https://issues.apache.org/jira/projects/MJAR/issues/MJAR-138 -->
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*
2+
* SonarLint Core - Implementation
3+
* Copyright (C) 2016-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonarsource.sonarlint.core.plugin.ondemand;
21+
22+
import java.io.BufferedInputStream;
23+
import java.io.FileInputStream;
24+
import java.io.FileNotFoundException;
25+
import java.io.IOException;
26+
import java.io.InputStream;
27+
import java.nio.file.Path;
28+
import org.bouncycastle.jce.provider.BouncyCastleProvider;
29+
import org.bouncycastle.openpgp.PGPCompressedData;
30+
import org.bouncycastle.openpgp.PGPException;
31+
import org.bouncycastle.openpgp.PGPObjectFactory;
32+
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
33+
import org.bouncycastle.openpgp.PGPSignatureList;
34+
import org.bouncycastle.openpgp.PGPUtil;
35+
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
36+
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
37+
import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger;
38+
39+
/**
40+
* Verifies the PGP signature of downloaded plugins using the SonarSource public key.
41+
*/
42+
class OnDemandPluginSignatureVerifier {
43+
44+
private static final SonarLintLogger LOG = SonarLintLogger.get();
45+
private static final String SONAR_PUBLIC_KEY = "sonarsource-public.key";
46+
private static final BouncyCastleProvider BOUNCY_CASTLE_PROVIDER = new BouncyCastleProvider();
47+
48+
boolean verify(Path jarFile, String pluginKey) {
49+
var keyRing = loadPublicKeyRing();
50+
if (keyRing == null) {
51+
return false;
52+
}
53+
var isValid = verifyPgpSignature(jarFile, pluginKey, keyRing);
54+
if (isValid) {
55+
LOG.debug("Plugin file signature verified successfully");
56+
}
57+
return isValid;
58+
}
59+
60+
private PGPPublicKeyRingCollection loadPublicKeyRing() {
61+
try (var keyStream = getClass().getClassLoader().getResourceAsStream(SONAR_PUBLIC_KEY)) {
62+
if (keyStream == null) {
63+
throw new FileNotFoundException("PGP key not found in resources: " + SONAR_PUBLIC_KEY);
64+
}
65+
66+
var decoder = PGPUtil.getDecoderStream(new BufferedInputStream(keyStream));
67+
return new PGPPublicKeyRingCollection(decoder, new JcaKeyFingerprintCalculator());
68+
} catch (IOException | PGPException e) {
69+
LOG.error("Error loading public key ring", e);
70+
return null;
71+
}
72+
}
73+
74+
private InputStream loadBundledSignature(String pluginKey) {
75+
var signatureFileName = String.format("sonar-%s-plugin.jar.asc", pluginKey);
76+
return getClass().getClassLoader().getResourceAsStream(signatureFileName);
77+
}
78+
79+
private boolean verifyPgpSignature(Path dataFile, String pluginKey, PGPPublicKeyRingCollection keyRing) {
80+
try (var signatureStream = loadBundledSignature(pluginKey)) {
81+
if (signatureStream == null) {
82+
LOG.error("Could not find bundled signature for plugin: {}", pluginKey);
83+
return false;
84+
}
85+
86+
try (var decoderStream = PGPUtil.getDecoderStream(new BufferedInputStream(signatureStream))) {
87+
var pgpFact = new PGPObjectFactory(decoderStream, new JcaKeyFingerprintCalculator());
88+
89+
// Handle both compressed and uncompressed signature formats
90+
var signatureList = extractSignatureList(pgpFact);
91+
if (signatureList == null || signatureList.isEmpty()) {
92+
LOG.error("No signatures found in signature file");
93+
return false;
94+
}
95+
96+
var signature = signatureList.get(0);
97+
var publicKey = keyRing.getPublicKey(signature.getKeyID());
98+
if (publicKey == null) {
99+
LOG.error("Public key not found for signature keyID={}", signature.getKeyID());
100+
return false;
101+
}
102+
103+
signature.init(new JcaPGPContentVerifierBuilderProvider().setProvider(BOUNCY_CASTLE_PROVIDER), publicKey);
104+
105+
try (var dataIn = new FileInputStream(dataFile.toFile())) {
106+
var buffer = new byte[8192];
107+
int bytesRead;
108+
while ((bytesRead = dataIn.read(buffer)) != -1) {
109+
signature.update(buffer, 0, bytesRead);
110+
}
111+
}
112+
113+
return signature.verify();
114+
}
115+
} catch (IOException | PGPException e) {
116+
LOG.error("Error verifying PGP signature", e);
117+
return false;
118+
}
119+
}
120+
121+
private static PGPSignatureList extractSignatureList(PGPObjectFactory pgpFact) {
122+
try {
123+
var obj = pgpFact.nextObject();
124+
if (obj instanceof PGPCompressedData compressedData) {
125+
var innerFactory = new PGPObjectFactory(compressedData.getDataStream(), new JcaKeyFingerprintCalculator());
126+
var innerObj = innerFactory.nextObject();
127+
if (innerObj instanceof PGPSignatureList signatureList) {
128+
return signatureList;
129+
}
130+
} else if (obj instanceof PGPSignatureList signatureList) {
131+
return signatureList;
132+
}
133+
} catch (IOException | PGPException e) {
134+
LOG.error("Error extracting signature list", e);
135+
}
136+
return null;
137+
}
138+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
cfamily.version=${cfamily.version}
2+
cs.version=${csharp.version}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
-----BEGIN PGP PUBLIC KEY BLOCK-----
2+
Version: Hockeypuck 2.2
3+
Comment: Hostname:
4+
5+
xsFNBGCGrYsBEAC/Ws37TXMujQ4z2ioXlh5SlrWaCzdN5RSBAQEKaiuuQeuwdWku
6+
bsnhI2f7YgxfJh2if6hCsGeWx3Wd2paLT9IqJbnIltOzHQkYXajIJrJVDep31wQD
7+
FsjQS8DWdRGkrldc2ClWZs1PAGC4Snp9bNYrnlE8Z1uHVnmN2R0aQ3v7PGw2qpQ9
8+
XxsQl9m30hMDb4IZBOKy92PC+xNpb6dgee3HJ8uJ2t/nTUCuP1FsMPGP3crbK9po
9+
UOUigIWMKNnYTyHbx+p22EQIn3iKQU4DQTeZm1/rUnfuULp2Zhl+fTs6U/czCrdr
10+
7DN4MCzthK7DMhDHH7/uVk53+e0oe0FJZSxYE1ppjvLz4Ox7xMHrlOMFIqb9JOgn
11+
exUDV34KcPByHqY4ff7IL94Tx7YAwEplnJYBEfb0sYfmjai4PCFj74gjjCmhQUm8
12+
5Cbm23JvDGck9W75wc6qj7wcFpZrFtfpOsz10YsprM5TcmK9rEIV+o+bRqoNs5hS
13+
+heZmdz7LoWJgarJnlkPjDDOXW54bA5kS8ARlkxllzZ+f0BwaN/HBNbVv3gkBHUX
14+
YOxphjESdv/WByNQMgzoIBiUt02RqAJg9PECLJSjSfFzd2F9g7Lmc0TUdA/kLEZm
15+
DqgrDjPkfkwnSqCglI38Z/gcVoSDN2iYhEIfuGoZXbjG4IDVuFYyGZjimQARAQAB
16+
zShTb25hclNvdXJjZSBTLkEuIDxpbmZyYUBzb25hcnNvdXJjZS5jb20+wsGUBBMB
17+
CgA+FiEEZ58e6SsZYJ3oFv3oHbGY+TUl7BoFAmCGrYsCGwEFCQlmAYAFCwkIBwIG
18+
FQoJCAsCBBYCAwECHgECF4AACgkQHbGY+TUl7Bpn+w/8DZjbw5SqguIMnIN1lmZC
19+
DCNSKk7CJNpkO7ZjXYZo9ZzGlULse4wlqoW5cVH3NiOATV4BnQQotSoeBr8RFdh0
20+
TI+Zbt2wKv3j4+LxIlalfnYrj77SRh43qqmAKxVS5HAdEXfHNfBtNV88CJTTByX/
21+
PAw7vIbI+6YwwIP/ps33GrESjDZNefdLuTvq3FwrTNicoWnXrIFbs01lNfy6NTfk
22+
5ZrVHjmTQxHrh0VY4vNZNQYnTzET3fMmhudlIxXPuuNSPl2X1UaTVFNHSwK/IsOr
23+
m8oWZfG++HgbVmR9YG1Ci7tYTBc+gbp8xel5FjzKcBLQfZwqsnz/Gn3PlPCwKXNI
24+
uq0Gp925P86scOlCz73Wfy8vde3rc6j+hzlgKuwgunJvl+cyWAyTdvTkcpCN6QJk
25+
R6ZuXrNkqCzbxT0NNoWEHSDJmJ8ECqJRfza6ag7lReWaT/dGZ/R9a19pbGmGXuqq
26+
qcwE9hRognxejhAn7mfVpLEsGJwrQEeVQCKQVFIZkFpUr3oYOIPppGxguM97ZNvY
27+
uZnHq9UwufRMR83h0XWWdTqurYoAcHkjeXH0DKXkM9kQg86FSf/KSWj9cI8/q3en
28+
VM+HboxrzY8Cc91IwXLOgV1ipowwy8fcnyU8GD+P3bvh1J/nVgzm+NTJ4RIfbDDq
29+
4Q6vWIDIAfqnRK3aTr2atSTOwU0EaSxelAEQAMjFbyhLN7gPxyvuKGcBP5L7b4UK
30+
kDSPPqBXHmv1KiEspKSF2cy84F6xdcbfuk3/V0wieu7/1S4Ro7CawBAS8U0VcXEw
31+
X0MdbBaB0nlVV1QwAsF2e9bDbVpFZb9kOiBWRwXLANJu0NnH0CW3IB4ba23JS+72
32+
0P6d3Jf3ylvF/I/7HvNGyjxRQ4B7wGX+IzK9rVf/UjQ9ce8oDVIv6gki1z28fCGm
33+
nv05fIvWTcU1+yUWP4cnDtfOGjHh/ISps6cfPM2xruhCVGQ5UQD5Jabswx5ZXCsa
34+
HLRRBinhlElfPFBlc5e123RWRjgfrOGQtnkom7agCHmUlbWL2QEFrw3Jab/xspXW
35+
8oGhMyfKHFlpC/Tni5b17AL22r2v5XFe//uSJGh70zRLIk+xjg3YmW6/jfxbESTF
36+
dHdi8f5l08ALveuJ4I8sIMct42+HMkqiZVuIeg1IlyVzQv8FAZAuiGSMUlomNsLj
37+
HO9yk9Y6We7dVG9Fben0oAh7R4b+ZqyWy0rbh8SJeu3v+CT/hLmO/Ag0xo3zBv+X
38+
wgB5RYRhr7fVCEUMOkUO7yYxvlt/r+HRzMgV9lTIlVF9UZCQIpluvVWWiE9DXpbR
39+
lYVRy+FTaQmDKJBO3+QBR5jEzI5EKUuRFeBdzT0SBudBdE3r3AZl5uOwzTcSqLAp
40+
mryAdW+/vZAlS2tJABEBAAHCw7IEGAEKACYWIQRnnx7pKxlgnegW/egdsZj5NSXs
41+
GgUCaSxelAIbAgUJAeEzgAJACRAdsZj5NSXsGsF0IAQZAQoAHRYhBNFDbA26zqSH
42+
Aq+Xw2Px3XdTuLMVBQJpLF6UAAoJEGPx3XdTuLMVX2gP/jxQKk8JHjosAhpd1eyv
43+
/99x255Fx46KNpSTus2VKg0ffB+kDKqN5plbPExa6MEC/oxnGBio14ennnYLOIap
44+
ecx3WBJYh59wsjlDUwgOLsA+tfzyo6nBW/UIgYVZEATNkhbCuw3lQFZHW5e+be+K
45+
xuGBZqDH5IKJGL3XodyVNesND1phusLhMco34zVVc5LvRyJ5CzZgjXTIOzx1qbX0
46+
uJalpYsd4LnChQNuzpRv31zO1d8FG5kE39osU6VNJh7fnDCnbXuj3FvZnPos26rT
47+
HNafdg+e+/8dOOvwt4UJ2tFAlLhiF/wQxFixhz1b4zXEOYRKuRn1XKm6XDaAT1Kp
48+
/zlmTQ2F0ObftNjaO9l6mTlunGey5CHUAZ2tAsdDwYIqewIrjjF0o+geMjlKmflI
49+
h5gkkcxNDXEagjkJXMzL8pzs8+g1B1cg1NHL9hsjp5VqhLD10J48n6C3nhwP1noB
50+
eM3GtKtcK6gK6IYxn3Fou7ABCbRhQXQVAci7iPJ4nW2ySQeQmeLl4lGxWPWZct9I
51+
LtH8ly+m2+7h7srMwDjRPeA3Owc7JgdLXdWdG7F4zppW4JqWvXIJ3wAJ+KFof6p6
52+
WrEPWfHVM1qieTdchenPufH3CA474vYH/+l4ie+URXT9v2gukJ4LMVfrTxqJhUPw
53+
S4k6EOF/2sAi3EcXKDF4o+iEkqUQALGe/o8fng1biW6wD4z2fDu8zp3iU4ldNpst
54+
QMTX4QsTzsZee6lQoQQ6G+m3IOgQo7EKFEV6rxpMamyQbg2YW8WfypowlfGXAAJ8
55+
x5mm+tdN4D6uvJidX2MyKAiAspP12Jc2T+Bap+e5TLi955Lk/RzcXgn7MUAZOD74
56+
HND3/SFOg6mrZT1FPj4Qe5bArjONogYJoHo4/sNJMxKJ2g7WrBAILpgQBAHUts1f
57+
B+AZoShaFZ9UqAhnspzvI03XU9PUhPN50djdv1NachiRo6KNKCadvdWBo1ZAZsos
58+
cWLLFnx4miVcgRrmrErkQ/8tidtT3zRcEQE0DvWUat7nXJ7VVwbafhbTwA9v8O+U
59+
3b9pDjlPBcy3hN3NRd8043XVQOzhTxPFwAof6g6ChqT6ANrZ8JN8DyKwUKCje8SX
60+
r96otE0K2jEtvJiqXeFTNxMlJ9aO2kdwpnN+58fYAp6FGwXtIU7pNAOCEfaCTmo1
61+
LMuvW4WmjH6uvRlw8erhrUzqbikEzMwsxec02iZjfC8fhBBJxOZMDvPArjDKsYjl
62+
uBxYw6MBaBH4mkLQJWHubPQ6lUSEzEEtz76vfCQijh6JTI4kKH3MQfug11d3+NfP
63+
wfUCvYAaXxJlaY2kt9zY8GzUOy8AUuQN48cIlm70lGYYBE6lT+xgKqQMJ4Nn+m4t
64+
decWOnlnzsFNBGCGrk4BEACTD/+Nk/tDzN3viBmw0GvgWWyeyfVKuhXTYgp1NA2Z
65+
ugcsz9ZFjzQegH+jwekWc4JFSQTFHpxqog94eQ7UKzk3LaYeCMiPpuxyxsY8MSZo
66+
oAOcysRabkvVHNLFhCKiiTu7E8NkOlCT9v2+f/1aatFnM+D///1/RTR0MJ7lz3Eu
67+
QWtC6gC0MQBydHoN9Ofov07j8RSVXBBf7TfZjl+uYfpYEkP5++bnWLw1WMv8Acea
68+
XyCjoJ/3L5GfrIHoNmpRujj8FLAZV0YOdpQCEwMn6gfJrcWXcPLcg3vmmYLhOWqj
69+
9kZoqE7Npejtzp9S4Yi9wM0ZTG+TTk2zec7dw7RstxTLEEJ8dx9IyXAkoNf8etlC
70+
9f9KuTnLK23lsi3cvjs58WzYxtl6MQS9x8U9QBlb86K8GMDYiwRrPyDusVvzwe0l
71+
Zgrt7SboQP5+hD+wY92tJde9JQbYSVcIQwgRGPZGYIZ+DEo5g4SWBVp/y+pFTVd2
72+
dFmbu8D2RLunI+hy7zjBEXbdRCxhyI16/lGG5wecg6Y4N26w3trUHymeTdAPQ+5s
73+
wE9F2MTz1D/FQrrb/pGa/6FcgusLvAvTJNCK/NAQNWx9ZJ1/teGCO8n2vhPi2995
74+
0id4V93HdLcCy2PBAL4ltAp4gCBjXXRXZuou2jC+syfB/o8kln0/1sblBVlheopM
75+
bQARAQABwsF2BCgBCgAgFiEEZ58e6SsZYJ3oFv3oHbGY+TUl7BoFAmksXlcCHQIA
76+
CgkQHbGY+TUl7Br/gQ//dL3MGWJo5mjTCsZ+GG/faFGtzO2k6CbwDQooH4fq4ZUf
77+
I3yEFWDqm7lrKRvt40MnYmP6wDyObjcRXbbHoyXTZriDfz88u4tayVxLXa/t2hVB
78+
2WxUQ8pjobZrq2HXnRGyFZcQjaKhS1u6qKovp45nTuPgVHCr8d7tZYYnY5EGkNz9
79+
zUokkCc9yJNuS6VftyEZ7Lbv7kVluAz48Q5lJ2RBBOPa+a6SEI/Vlz431ZUCxnz8
80+
W/m6u4NgpvSFHjDvpr7N+NGNZM7tdjZy3HTG/k7vnxUqAYR2NNd/xXOFT6LUTuAK
81+
DlO4n08lPW+/DOlqynVJXamHjXvMKlMlVNRANb9C2xt9yEsIrl0+6jMM/IFdaONX
82+
B5uqDUciCgEYR032MAg7L88kgOC3pjUjNkOZQB6YColoRhmhKiA1f46AxLObUWVe
83+
XwDueyIbhPdFie91F02gGwvsXF+Gp4RmcbG1G98oCVMR5Qb/eklL1Xr4wr9geRaO
84+
R9mMX/L1HEWykMX/bmapa+fuXGlOxG+RnJuyFvUVnZmbqCyOmVCRSS55ykUyu5wf
85+
SoxqJrcmGclvlPvXBr6vmwtfLYUFbqudMULZAWqGI5TWxZlRQqEJmmAD3t5cHhWU
86+
IMP50VMrn8SuYMhviOkcKzdkB4qYjeebMbCLvWu9rhupeW4ysa3psWxSbE1Sa7fC
87+
w7IEGAEKACYWIQRnnx7pKxlgnegW/egdsZj5NSXsGgUCYIauTgIbAgUJCWYBgAJA
88+
CRAdsZj5NSXsGsF0IAQZAQoAHRYhBCsQQmd/2BkMe5/A3CFh1y59zUJYBQJghq5O
89+
AAoJECFh1y59zUJYd/YP/idnBZt7ClccnTBIf4xXqEfLY9kWU3Xk5B8iPd/piBhP
90+
JM5/kLqEi1FzxrD6TRP/clApBnqGX3wciUSN9PgGvX/vP2gPl4BfJVn7h9i7SsJ+
91+
RzwZ+10eiVv/sp0Nl35Ie+2ToXSAKOR8reC7VSseYIKCIZ3d0OnrjpuaB+PRf8Zg
92+
BtrZjFOM5Us+xHx0gDSWuk94hraJsF98IIWkj3LeS7WG6CFVoTN8jMbGv8V/+GyY
93+
J4UenPw0yFIJvGa4BWaxPQBHf+zFs01tg5LIiZ1AFHhn95mnaYLi8L2xguqo4faT
94+
oPqisiXysjlHTAASzRfhShc0MqbQV3hM8ZsM2xezcIng2p9lsuIj7PBagh0tdc7R
95+
usNwSDKx9VhxsaaRpz6ecxTUtvqQZxVkrZCcdpHvwOcIjbyGwm55qSL5txnpUI7I
96+
pv9a5DYxWWI5fvAA/Vb7y4Rta76HYLw9BC+ktMAJ9+Hye5s0rTWfxtUZQqKewl7J
97+
Q+W/f14tWxB/8fqRTwzLiVQF25QFx+2SMAflZ0QDIJ09awrjQLD82xY7N1A3RI/H
98+
Oba/Jwr7GxZfejxUVL3W+/bBKnSkXadZPPbmM2ZhEcObpjhbfHerRc/CdiekJ9O4
99+
bWSD6X/w9P4TJYFGTjk3UM6kA5JIJhBVvOOQb6bNO2xA/xwW+pN/olV5t0qCJNxG
100+
jP8QAJ0nQTG8RSEsx3yUduU2kEHVqTzvLfceH3dMTIxpcFvyiydXRwk2RkcubXqW
101+
pXpaRWbINBERPsKykIdgYYf98r8T4imyF8CBcIP5Qrth4nVYTEjw3NwIfrIyJn0m
102+
t9K/A/MQHfaXK7Fh1h4rpFwA5ehHLKtmpMe5s/m2Z0/3VI0Xo0Ls6xRX3jn5mWf6
103+
O/hnve1dDwxMapCChQxrvvp7JBA7NYJcW6duC90sMZpU83SVT//ysOe6UOl1JSWM
104+
AcosfYhKBHRQBqOwhNCcUB6vMTmlDYf5KPgIYamaYoGwiTWv9ZaW2Zo0QWPpBvp5
105+
Qi4dk/69y1XFnDwj73B9OLW4Nu1irVlivsNUVvhgP6zp8/4e1GgQQ4t87iQ5BBQT
106+
5IYMfZFHEPvb+5gS67i5FeUxNJZ7Dk33tUiPWCEH+kwS4AoM5A5AqZTw9ZslDwQC
107+
adz7WfP3h3ZeHKrwUuTrYgV/jKlgI0N9+iDRIkMiqwvyFegBJuHKuWzD5p3aO7Rx
108+
N7xJOf101r7BtYfg8SZWrmWOP3OlhV7NjC3F0Y2Rnk1Yvo3769So4hdutmRo/BXv
109+
hquGBJz8qYrboUe6QwdrYF/ycAmX5SSfNKZws3vsF4A49i94TOMkX8COXxx2tLsF
110+
+iqdj/MS4Y81F1vz0NQPPIOvu1bQOEU27GDEm44+94lprE3g
111+
=1ETd
112+
-----END PGP PUBLIC KEY BLOCK-----

0 commit comments

Comments
 (0)