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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  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. 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.GpgObjectSigner;
  41. import org.eclipse.jgit.lib.GpgSignature;
  42. import org.eclipse.jgit.lib.GpgSigner;
  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 the 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 (CanceledException e) {
  94. throw e;
  95. } catch (Exception e) {
  96. return false;
  97. }
  98. }
  99. private BouncyCastleGpgKey locateSigningKey(@Nullable String gpgSigningKey,
  100. PersonIdent committer,
  101. BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt)
  102. throws CanceledException, UnsupportedCredentialItem, IOException,
  103. NoSuchAlgorithmException, NoSuchProviderException, PGPException,
  104. URISyntaxException {
  105. if (gpgSigningKey == null || gpgSigningKey.isEmpty()) {
  106. gpgSigningKey = '<' + committer.getEmailAddress() + '>';
  107. }
  108. BouncyCastleGpgKeyLocator keyHelper = new BouncyCastleGpgKeyLocator(
  109. gpgSigningKey, passphrasePrompt);
  110. return keyHelper.findSecretKey();
  111. }
  112. @Override
  113. public void sign(@NonNull CommitBuilder commit,
  114. @Nullable String gpgSigningKey, @NonNull PersonIdent committer,
  115. CredentialsProvider credentialsProvider) throws CanceledException {
  116. try {
  117. signObject(commit, gpgSigningKey, committer, credentialsProvider,
  118. null);
  119. } catch (UnsupportedSigningFormatException e) {
  120. // Cannot occur with a null config
  121. }
  122. }
  123. @Override
  124. public void signObject(@NonNull ObjectBuilder object,
  125. @Nullable String gpgSigningKey, @NonNull PersonIdent committer,
  126. CredentialsProvider credentialsProvider, GpgConfig config)
  127. throws CanceledException, UnsupportedSigningFormatException {
  128. if (config != null && config.getKeyFormat() != GpgFormat.OPENPGP) {
  129. throw new UnsupportedSigningFormatException(
  130. JGitText.get().onlyOpenPgpSupportedForSigning);
  131. }
  132. try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt(
  133. credentialsProvider)) {
  134. BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey,
  135. committer,
  136. passphrasePrompt);
  137. PGPSecretKey secretKey = gpgKey.getSecretKey();
  138. if (secretKey == null) {
  139. throw new JGitInternalException(
  140. BCText.get().unableToSignCommitNoSecretKey);
  141. }
  142. JcePBESecretKeyDecryptorBuilder decryptorBuilder = new JcePBESecretKeyDecryptorBuilder()
  143. .setProvider(BouncyCastleProvider.PROVIDER_NAME);
  144. PGPPrivateKey privateKey = null;
  145. if (!passphrasePrompt.hasPassphrase()) {
  146. // Either the key is not encrypted, or it was read from the
  147. // legacy secring.gpg. Try getting the private key without
  148. // passphrase first.
  149. try {
  150. privateKey = secretKey.extractPrivateKey(
  151. decryptorBuilder.build(new char[0]));
  152. } catch (PGPException e) {
  153. // Ignore and try again with passphrase below
  154. }
  155. }
  156. if (privateKey == null) {
  157. // Try using a passphrase
  158. char[] passphrase = passphrasePrompt.getPassphrase(
  159. secretKey.getPublicKey().getFingerprint(),
  160. gpgKey.getOrigin());
  161. privateKey = secretKey
  162. .extractPrivateKey(decryptorBuilder.build(passphrase));
  163. }
  164. PGPPublicKey publicKey = secretKey.getPublicKey();
  165. PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(
  166. new JcaPGPContentSignerBuilder(
  167. publicKey.getAlgorithm(),
  168. HashAlgorithmTags.SHA256).setProvider(
  169. BouncyCastleProvider.PROVIDER_NAME));
  170. signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey);
  171. PGPSignatureSubpacketGenerator subpackets = new PGPSignatureSubpacketGenerator();
  172. subpackets.setIssuerFingerprint(false, publicKey);
  173. // Also add the signer's user ID. Note that GPG uses only the e-mail
  174. // address part.
  175. String userId = committer.getEmailAddress();
  176. Iterator<String> userIds = publicKey.getUserIDs();
  177. if (userIds.hasNext()) {
  178. String keyUserId = userIds.next();
  179. if (!StringUtils.isEmptyOrNull(keyUserId)
  180. && (userId == null || !keyUserId.contains(userId))) {
  181. // Not the committer's key?
  182. userId = extractSignerId(keyUserId);
  183. }
  184. }
  185. if (userId != null) {
  186. subpackets.setSignerUserID(false, userId);
  187. }
  188. signatureGenerator
  189. .setHashedSubpackets(subpackets.generate());
  190. ByteArrayOutputStream buffer = new ByteArrayOutputStream();
  191. try (BCPGOutputStream out = new BCPGOutputStream(
  192. new ArmoredOutputStream(buffer))) {
  193. signatureGenerator.update(object.build());
  194. signatureGenerator.generate().encode(out);
  195. }
  196. object.setGpgSignature(new GpgSignature(buffer.toByteArray()));
  197. } catch (PGPException | IOException | NoSuchAlgorithmException
  198. | NoSuchProviderException | URISyntaxException e) {
  199. throw new JGitInternalException(e.getMessage(), e);
  200. }
  201. }
  202. static String extractSignerId(String pgpUserId) {
  203. int from = pgpUserId.indexOf('<');
  204. if (from >= 0) {
  205. int to = pgpUserId.indexOf('>', from + 1);
  206. if (to > from + 1) {
  207. return pgpUserId.substring(from + 1, to);
  208. }
  209. }
  210. return pgpUserId;
  211. }
  212. }