You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

BouncyCastleGpgSigner.java 8.2KB

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