View Javadoc
1   /*
2    * Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  package org.eclipse.jgit.gpg.bc.internal;
11  
12  import java.io.ByteArrayInputStream;
13  import java.io.IOException;
14  import java.io.InputStream;
15  import java.security.Security;
16  import java.text.MessageFormat;
17  import java.time.Instant;
18  import java.util.Arrays;
19  import java.util.Date;
20  import java.util.Iterator;
21  import java.util.Locale;
22  
23  import org.bouncycastle.bcpg.sig.IssuerFingerprint;
24  import org.bouncycastle.jce.provider.BouncyCastleProvider;
25  import org.bouncycastle.openpgp.PGPCompressedData;
26  import org.bouncycastle.openpgp.PGPException;
27  import org.bouncycastle.openpgp.PGPPublicKey;
28  import org.bouncycastle.openpgp.PGPSignature;
29  import org.bouncycastle.openpgp.PGPSignatureList;
30  import org.bouncycastle.openpgp.PGPSignatureSubpacketVector;
31  import org.bouncycastle.openpgp.PGPUtil;
32  import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory;
33  import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
34  import org.bouncycastle.util.encoders.Hex;
35  import org.eclipse.jgit.annotations.NonNull;
36  import org.eclipse.jgit.annotations.Nullable;
37  import org.eclipse.jgit.api.errors.JGitInternalException;
38  import org.eclipse.jgit.lib.GpgConfig;
39  import org.eclipse.jgit.lib.GpgSignatureVerifier;
40  import org.eclipse.jgit.revwalk.RevCommit;
41  import org.eclipse.jgit.revwalk.RevObject;
42  import org.eclipse.jgit.revwalk.RevTag;
43  import org.eclipse.jgit.util.LRUMap;
44  import org.eclipse.jgit.util.RawParseUtils;
45  import org.eclipse.jgit.util.StringUtils;
46  
47  /**
48   * A {@link GpgSignatureVerifier} to verify GPG signatures using BouncyCastle.
49   */
50  public class BouncyCastleGpgSignatureVerifier implements GpgSignatureVerifier {
51  
52  	private static void registerBouncyCastleProviderIfNecessary() {
53  		if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
54  			Security.addProvider(new BouncyCastleProvider());
55  		}
56  	}
57  
58  	/**
59  	 * Creates a new instance and registers the BouncyCastle security provider
60  	 * if needed.
61  	 */
62  	public BouncyCastleGpgSignatureVerifier() {
63  		registerBouncyCastleProviderIfNecessary();
64  	}
65  
66  	// To support more efficient signature verification of multiple objects we
67  	// cache public keys once found in a LRU cache.
68  
69  	private static final Object NO_KEY = new Object();
70  
71  	private LRUMap<String, Object> byFingerprint = new LRUMap<>(16, 200);
72  
73  	private LRUMap<String, Object> bySigner = new LRUMap<>(16, 200);
74  
75  	@Override
76  	public String getName() {
77  		return "bc"; //$NON-NLS-1$
78  	}
79  
80  	@Override
81  	@Nullable
82  	public SignatureVerification verifySignature(@NonNull RevObject object,
83  			@NonNull GpgConfig config) throws IOException {
84  		if (object instanceof RevCommit) {
85  			RevCommit commit = (RevCommit) object;
86  			byte[] signatureData = commit.getRawGpgSignature();
87  			if (signatureData == null) {
88  				return null;
89  			}
90  			byte[] raw = commit.getRawBuffer();
91  			// Now remove the GPG signature
92  			byte[] header = { 'g', 'p', 'g', 's', 'i', 'g' };
93  			int start = RawParseUtils.headerStart(header, raw, 0);
94  			if (start < 0) {
95  				return null;
96  			}
97  			int end = RawParseUtils.headerEnd(raw, start);
98  			// start is at the beginning of the header's content
99  			start -= header.length + 1;
100 			// end is on the terminating LF; we need to skip that, too
101 			if (end < raw.length) {
102 				end++;
103 			}
104 			byte[] data = new byte[raw.length - (end - start)];
105 			System.arraycopy(raw, 0, data, 0, start);
106 			System.arraycopy(raw, end, data, start, raw.length - end);
107 			return verify(data, signatureData);
108 		} else if (object instanceof RevTag) {
109 			RevTag tag = (RevTag) object;
110 			byte[] signatureData = tag.getRawGpgSignature();
111 			if (signatureData == null) {
112 				return null;
113 			}
114 			byte[] raw = tag.getRawBuffer();
115 			// The signature is just tacked onto the end of the message, which
116 			// is last in the buffer.
117 			byte[] data = Arrays.copyOfRange(raw, 0,
118 					raw.length - signatureData.length);
119 			return verify(data, signatureData);
120 		}
121 		return null;
122 	}
123 
124 	static PGPSignature parseSignature(InputStream in)
125 			throws IOException, PGPException {
126 		try (InputStream sigIn = PGPUtil.getDecoderStream(in)) {
127 			JcaPGPObjectFactory pgpFactory = new JcaPGPObjectFactory(sigIn);
128 			Object obj = pgpFactory.nextObject();
129 			if (obj instanceof PGPCompressedData) {
130 				obj = new JcaPGPObjectFactory(
131 						((PGPCompressedData) obj).getDataStream()).nextObject();
132 			}
133 			if (obj instanceof PGPSignatureList) {
134 				return ((PGPSignatureList) obj).get(0);
135 			}
136 			return null;
137 		}
138 	}
139 
140 	@Override
141 	public SignatureVerification verify(byte[] data, byte[] signatureData)
142 			throws IOException {
143 		PGPSignature signature = null;
144 		String fingerprint = null;
145 		String signer = null;
146 		String keyId = null;
147 		try (InputStream sigIn = new ByteArrayInputStream(signatureData)) {
148 			signature = parseSignature(sigIn);
149 			if (signature != null) {
150 				// Try to figure out something to find the public key with.
151 				if (signature.hasSubpackets()) {
152 					PGPSignatureSubpacketVector packets = signature
153 							.getHashedSubPackets();
154 					IssuerFingerprint fingerprintPacket = packets
155 							.getIssuerFingerprint();
156 					if (fingerprintPacket != null) {
157 						fingerprint = Hex
158 								.toHexString(fingerprintPacket.getFingerprint())
159 								.toLowerCase(Locale.ROOT);
160 					}
161 					signer = packets.getSignerUserID();
162 					if (signer != null) {
163 						signer = BouncyCastleGpgSigner.extractSignerId(signer);
164 					}
165 				}
166 				keyId = Long.toUnsignedString(signature.getKeyID(), 16)
167 						.toLowerCase(Locale.ROOT);
168 			} else {
169 				throw new JGitInternalException(BCText.get().nonSignatureError);
170 			}
171 		} catch (PGPException e) {
172 			throw new JGitInternalException(BCText.get().signatureParseError,
173 					e);
174 		}
175 		Date signatureCreatedAt = signature.getCreationTime();
176 		if (fingerprint == null && signer == null && keyId == null) {
177 			return new VerificationResult(signatureCreatedAt, null, null, null,
178 					false, false, TrustLevel.UNKNOWN,
179 					BCText.get().signatureNoKeyInfo);
180 		}
181 		if (fingerprint != null && keyId != null
182 				&& !fingerprint.endsWith(keyId)) {
183 			return new VerificationResult(signatureCreatedAt, signer, fingerprint,
184 					null, false, false, TrustLevel.UNKNOWN,
185 					MessageFormat.format(BCText.get().signatureInconsistent,
186 							keyId, fingerprint));
187 		}
188 		if (fingerprint == null && keyId != null) {
189 			fingerprint = keyId;
190 		}
191 		// Try to find the public key
192 		String keySpec = '<' + signer + '>';
193 		Object cached = null;
194 		PGPPublicKey publicKey = null;
195 		try {
196 			cached = byFingerprint.get(fingerprint);
197 			if (cached != null) {
198 				if (cached instanceof PGPPublicKey) {
199 					publicKey = (PGPPublicKey) cached;
200 				}
201 			} else if (!StringUtils.isEmptyOrNull(signer)) {
202 				cached = bySigner.get(signer);
203 				if (cached != null) {
204 					if (cached instanceof PGPPublicKey) {
205 						publicKey = (PGPPublicKey) cached;
206 					}
207 				}
208 			}
209 			if (cached == null) {
210 				publicKey = BouncyCastleGpgKeyLocator.findPublicKey(fingerprint,
211 						keySpec);
212 			}
213 		} catch (IOException | PGPException e) {
214 			throw new JGitInternalException(
215 					BCText.get().signatureKeyLookupError, e);
216 		}
217 		if (publicKey == null) {
218 			if (cached == null) {
219 				byFingerprint.put(fingerprint, NO_KEY);
220 				byFingerprint.put(keyId, NO_KEY);
221 				if (signer != null) {
222 					bySigner.put(signer, NO_KEY);
223 				}
224 			}
225 			return new VerificationResult(signatureCreatedAt, signer,
226 					fingerprint, null, false, false, TrustLevel.UNKNOWN,
227 					BCText.get().signatureNoPublicKey);
228 		}
229 		if (cached == null) {
230 			byFingerprint.put(fingerprint, publicKey);
231 			byFingerprint.put(keyId, publicKey);
232 			if (signer != null) {
233 				bySigner.put(signer, publicKey);
234 			}
235 		}
236 		String user = null;
237 		Iterator<String> userIds = publicKey.getUserIDs();
238 		if (!StringUtils.isEmptyOrNull(signer)) {
239 			while (userIds.hasNext()) {
240 				String userId = userIds.next();
241 				if (BouncyCastleGpgKeyLocator.containsSigningKey(userId,
242 						keySpec)) {
243 					user = userId;
244 					break;
245 				}
246 			}
247 		}
248 		if (user == null) {
249 			userIds = publicKey.getUserIDs();
250 			if (userIds.hasNext()) {
251 				user = userIds.next();
252 			}
253 		}
254 		boolean expired = false;
255 		long validFor = publicKey.getValidSeconds();
256 		if (validFor > 0 && signatureCreatedAt != null) {
257 			Instant expiredAt = publicKey.getCreationTime().toInstant()
258 					.plusSeconds(validFor);
259 			expired = expiredAt.isBefore(signatureCreatedAt.toInstant());
260 		}
261 		// Trust data is not defined in OpenPGP; the format is implementation
262 		// specific. We don't use the GPG trustdb but simply the trust packet
263 		// on the public key, if present. Even if present, it may or may not
264 		// be set.
265 		byte[] trustData = publicKey.getTrustData();
266 		TrustLevel trust = parseGpgTrustPacket(trustData);
267 		boolean verified = false;
268 		try {
269 			signature.init(
270 					new JcaPGPContentVerifierBuilderProvider()
271 							.setProvider(BouncyCastleProvider.PROVIDER_NAME),
272 					publicKey);
273 			signature.update(data);
274 			verified = signature.verify();
275 		} catch (PGPException e) {
276 			throw new JGitInternalException(
277 					BCText.get().signatureVerificationError, e);
278 		}
279 		return new VerificationResult(signatureCreatedAt, signer, fingerprint, user,
280 				verified, expired, trust, null);
281 	}
282 
283 	private TrustLevel parseGpgTrustPacket(byte[] packet) {
284 		if (packet == null || packet.length < 6) {
285 			// A GPG trust packet has at least 6 bytes.
286 			return TrustLevel.UNKNOWN;
287 		}
288 		if (packet[2] != 'g' || packet[3] != 'p' || packet[4] != 'g') {
289 			// Not a GPG trust packet
290 			return TrustLevel.UNKNOWN;
291 		}
292 		int trust = packet[0] & 0x0F;
293 		switch (trust) {
294 		case 0: // No determined/set
295 		case 1: // Trust expired; i.e., calculation outdated or key expired
296 		case 2: // Undefined: not enough information to set
297 			return TrustLevel.UNKNOWN;
298 		case 3:
299 			return TrustLevel.NEVER;
300 		case 4:
301 			return TrustLevel.MARGINAL;
302 		case 5:
303 			return TrustLevel.FULL;
304 		case 6:
305 			return TrustLevel.ULTIMATE;
306 		default:
307 			return TrustLevel.UNKNOWN;
308 		}
309 	}
310 
311 	@Override
312 	public void clear() {
313 		byFingerprint.clear();
314 		bySigner.clear();
315 	}
316 
317 	private static class VerificationResult implements SignatureVerification {
318 
319 		private final Date creationDate;
320 
321 		private final String signer;
322 
323 		private final String keyUser;
324 
325 		private final String fingerprint;
326 
327 		private final boolean verified;
328 
329 		private final boolean expired;
330 
331 		private final @NonNull TrustLevel trustLevel;
332 
333 		private final String message;
334 
335 		public VerificationResult(Date creationDate, String signer,
336 				String fingerprint, String user, boolean verified,
337 				boolean expired, @NonNull TrustLevel trust, String message) {
338 			this.creationDate = creationDate;
339 			this.signer = signer;
340 			this.fingerprint = fingerprint;
341 			this.keyUser = user;
342 			this.verified = verified;
343 			this.expired = expired;
344 			this.trustLevel = trust;
345 			this.message = message;
346 		}
347 
348 		@Override
349 		public Date getCreationDate() {
350 			return creationDate;
351 		}
352 
353 		@Override
354 		public String getSigner() {
355 			return signer;
356 		}
357 
358 		@Override
359 		public String getKeyUser() {
360 			return keyUser;
361 		}
362 
363 		@Override
364 		public String getKeyFingerprint() {
365 			return fingerprint;
366 		}
367 
368 		@Override
369 		public boolean isExpired() {
370 			return expired;
371 		}
372 
373 		@Override
374 		public TrustLevel getTrustLevel() {
375 			return trustLevel;
376 		}
377 
378 		@Override
379 		public String getMessage() {
380 			return message;
381 		}
382 
383 		@Override
384 		public boolean getVerified() {
385 			return verified;
386 		}
387 	}
388 }