View Javadoc
1   /*
2    * Copyright (C) 2018, 2021, Salesforce 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.ByteArrayOutputStream;
13  import java.io.IOException;
14  import java.net.URISyntaxException;
15  import java.security.NoSuchAlgorithmException;
16  import java.security.NoSuchProviderException;
17  import java.security.Security;
18  import java.util.Iterator;
19  
20  import org.bouncycastle.bcpg.ArmoredOutputStream;
21  import org.bouncycastle.bcpg.BCPGOutputStream;
22  import org.bouncycastle.bcpg.HashAlgorithmTags;
23  import org.bouncycastle.jce.provider.BouncyCastleProvider;
24  import org.bouncycastle.openpgp.PGPException;
25  import org.bouncycastle.openpgp.PGPPrivateKey;
26  import org.bouncycastle.openpgp.PGPPublicKey;
27  import org.bouncycastle.openpgp.PGPSecretKey;
28  import org.bouncycastle.openpgp.PGPSignature;
29  import org.bouncycastle.openpgp.PGPSignatureGenerator;
30  import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator;
31  import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
32  import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
33  import org.eclipse.jgit.annotations.NonNull;
34  import org.eclipse.jgit.annotations.Nullable;
35  import org.eclipse.jgit.api.errors.CanceledException;
36  import org.eclipse.jgit.api.errors.JGitInternalException;
37  import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
38  import org.eclipse.jgit.errors.UnsupportedCredentialItem;
39  import org.eclipse.jgit.internal.JGitText;
40  import org.eclipse.jgit.lib.CommitBuilder;
41  import org.eclipse.jgit.lib.GpgConfig;
42  import org.eclipse.jgit.lib.GpgObjectSigner;
43  import org.eclipse.jgit.lib.GpgSignature;
44  import org.eclipse.jgit.lib.GpgSigner;
45  import org.eclipse.jgit.lib.ObjectBuilder;
46  import org.eclipse.jgit.lib.PersonIdent;
47  import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
48  import org.eclipse.jgit.transport.CredentialsProvider;
49  import org.eclipse.jgit.util.StringUtils;
50  
51  /**
52   * GPG Signer using the BouncyCastle library.
53   */
54  public class BouncyCastleGpgSigner extends GpgSigner
55  		implements GpgObjectSigner {
56  
57  	private static void registerBouncyCastleProviderIfNecessary() {
58  		if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
59  			Security.addProvider(new BouncyCastleProvider());
60  		}
61  	}
62  
63  	/**
64  	 * Create a new instance.
65  	 * <p>
66  	 * The BounceCastleProvider will be registered if necessary.
67  	 * </p>
68  	 */
69  	public BouncyCastleGpgSigner() {
70  		registerBouncyCastleProviderIfNecessary();
71  	}
72  
73  	@Override
74  	public boolean canLocateSigningKey(@Nullable String gpgSigningKey,
75  			PersonIdent committer, CredentialsProvider credentialsProvider)
76  			throws CanceledException {
77  		try {
78  			return canLocateSigningKey(gpgSigningKey, committer,
79  					credentialsProvider, null);
80  		} catch (UnsupportedSigningFormatException e) {
81  			// Cannot occur with a null config
82  			return false;
83  		}
84  	}
85  
86  	@Override
87  	public boolean canLocateSigningKey(@Nullable String gpgSigningKey,
88  			PersonIdent committer, CredentialsProvider credentialsProvider,
89  			GpgConfig config)
90  			throws CanceledException, UnsupportedSigningFormatException {
91  		if (config != null && config.getKeyFormat() != GpgFormat.OPENPGP) {
92  			throw new UnsupportedSigningFormatException(
93  					JGitText.get().onlyOpenPgpSupportedForSigning);
94  		}
95  		try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt(
96  				credentialsProvider)) {
97  			BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey,
98  					committer, passphrasePrompt);
99  			return gpgKey != null;
100 		} catch (CanceledException e) {
101 			throw e;
102 		} catch (Exception e) {
103 			return false;
104 		}
105 	}
106 
107 	private BouncyCastleGpgKey locateSigningKey(@Nullable String gpgSigningKey,
108 			PersonIdent committer,
109 			BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt)
110 			throws CanceledException, UnsupportedCredentialItem, IOException,
111 			NoSuchAlgorithmException, NoSuchProviderException, PGPException,
112 			URISyntaxException {
113 		if (gpgSigningKey == null || gpgSigningKey.isEmpty()) {
114 			gpgSigningKey = '<' + committer.getEmailAddress() + '>';
115 		}
116 
117 		BouncyCastleGpgKeyLocator keyHelper = new BouncyCastleGpgKeyLocator(
118 				gpgSigningKey, passphrasePrompt);
119 
120 		return keyHelper.findSecretKey();
121 	}
122 
123 	@Override
124 	public void sign(@NonNull CommitBuilder commit,
125 			@Nullable String gpgSigningKey, @NonNull PersonIdent committer,
126 			CredentialsProvider credentialsProvider) throws CanceledException {
127 		try {
128 			signObject(commit, gpgSigningKey, committer, credentialsProvider,
129 					null);
130 		} catch (UnsupportedSigningFormatException e) {
131 			// Cannot occur with a null config
132 		}
133 	}
134 
135 	@Override
136 	public void signObject(@NonNull ObjectBuilder object,
137 			@Nullable String gpgSigningKey, @NonNull PersonIdent committer,
138 			CredentialsProvider credentialsProvider, GpgConfig config)
139 			throws CanceledException, UnsupportedSigningFormatException {
140 		if (config != null && config.getKeyFormat() != GpgFormat.OPENPGP) {
141 			throw new UnsupportedSigningFormatException(
142 					JGitText.get().onlyOpenPgpSupportedForSigning);
143 		}
144 		try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt(
145 				credentialsProvider)) {
146 			BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey,
147 					committer,
148 						passphrasePrompt);
149 			PGPSecretKey secretKey = gpgKey.getSecretKey();
150 			if (secretKey == null) {
151 				throw new JGitInternalException(
152 						BCText.get().unableToSignCommitNoSecretKey);
153 			}
154 			JcePBESecretKeyDecryptorBuilder decryptorBuilder = new JcePBESecretKeyDecryptorBuilder()
155 					.setProvider(BouncyCastleProvider.PROVIDER_NAME);
156 			PGPPrivateKey privateKey = null;
157 			if (!passphrasePrompt.hasPassphrase()) {
158 				// Either the key is not encrypted, or it was read from the
159 				// legacy secring.gpg. Try getting the private key without
160 				// passphrase first.
161 				try {
162 					privateKey = secretKey.extractPrivateKey(
163 							decryptorBuilder.build(new char[0]));
164 				} catch (PGPException e) {
165 					// Ignore and try again with passphrase below
166 				}
167 			}
168 			if (privateKey == null) {
169 				// Try using a passphrase
170 				char[] passphrase = passphrasePrompt.getPassphrase(
171 						secretKey.getPublicKey().getFingerprint(),
172 						gpgKey.getOrigin());
173 				privateKey = secretKey
174 						.extractPrivateKey(decryptorBuilder.build(passphrase));
175 			}
176 			PGPPublicKey publicKey = secretKey.getPublicKey();
177 			PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(
178 					new JcaPGPContentSignerBuilder(
179 							publicKey.getAlgorithm(),
180 							HashAlgorithmTags.SHA256).setProvider(
181 									BouncyCastleProvider.PROVIDER_NAME));
182 			signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey);
183 			PGPSignatureSubpacketGenerator subpackets = new PGPSignatureSubpacketGenerator();
184 			subpackets.setIssuerFingerprint(false, publicKey);
185 			// Also add the signer's user ID. Note that GPG uses only the e-mail
186 			// address part.
187 			String userId = committer.getEmailAddress();
188 			Iterator<String> userIds = publicKey.getUserIDs();
189 			if (userIds.hasNext()) {
190 				String keyUserId = userIds.next();
191 				if (!StringUtils.isEmptyOrNull(keyUserId)
192 						&& (userId == null || !keyUserId.contains(userId))) {
193 					// Not the committer's key?
194 					userId = extractSignerId(keyUserId);
195 				}
196 			}
197 			if (userId != null) {
198 				subpackets.addSignerUserID(false, userId);
199 			}
200 			signatureGenerator
201 					.setHashedSubpackets(subpackets.generate());
202 			ByteArrayOutputStream buffer = new ByteArrayOutputStream();
203 			try (BCPGOutputStream out = new BCPGOutputStream(
204 					new ArmoredOutputStream(buffer))) {
205 				signatureGenerator.update(object.build());
206 				signatureGenerator.generate().encode(out);
207 			}
208 			object.setGpgSignature(new GpgSignature(buffer.toByteArray()));
209 		} catch (PGPException | IOException | NoSuchAlgorithmException
210 				| NoSuchProviderException | URISyntaxException e) {
211 			throw new JGitInternalException(e.getMessage(), e);
212 		}
213 	}
214 
215 	static String extractSignerId(String pgpUserId) {
216 		int from = pgpUserId.indexOf('<');
217 		if (from >= 0) {
218 			int to = pgpUserId.indexOf('>', from + 1);
219 			if (to > from + 1) {
220 				return pgpUserId.substring(from + 1, to);
221 			}
222 		}
223 		return pgpUserId;
224 	}
225 }