1
2
3
4
5
6
7
8
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
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
65
66
67
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
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
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
159
160
161 try {
162 privateKey = secretKey.extractPrivateKey(
163 decryptorBuilder.build(new char[0]));
164 } catch (PGPException e) {
165
166 }
167 }
168 if (privateKey == null) {
169
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
186
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
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 }