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 5.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  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 org.bouncycastle.bcpg.ArmoredOutputStream;
  18. import org.bouncycastle.bcpg.BCPGOutputStream;
  19. import org.bouncycastle.bcpg.HashAlgorithmTags;
  20. import org.bouncycastle.jce.provider.BouncyCastleProvider;
  21. import org.bouncycastle.openpgp.PGPException;
  22. import org.bouncycastle.openpgp.PGPPrivateKey;
  23. import org.bouncycastle.openpgp.PGPSecretKey;
  24. import org.bouncycastle.openpgp.PGPSignature;
  25. import org.bouncycastle.openpgp.PGPSignatureGenerator;
  26. import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator;
  27. import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
  28. import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
  29. import org.eclipse.jgit.annotations.NonNull;
  30. import org.eclipse.jgit.annotations.Nullable;
  31. import org.eclipse.jgit.api.errors.CanceledException;
  32. import org.eclipse.jgit.api.errors.JGitInternalException;
  33. import org.eclipse.jgit.errors.UnsupportedCredentialItem;
  34. import org.eclipse.jgit.lib.CommitBuilder;
  35. import org.eclipse.jgit.lib.GpgSignature;
  36. import org.eclipse.jgit.lib.GpgSigner;
  37. import org.eclipse.jgit.lib.PersonIdent;
  38. import org.eclipse.jgit.transport.CredentialsProvider;
  39. /**
  40. * GPG Signer using BouncyCastle library
  41. */
  42. public class BouncyCastleGpgSigner extends GpgSigner {
  43. private static void registerBouncyCastleProviderIfNecessary() {
  44. if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
  45. Security.addProvider(new BouncyCastleProvider());
  46. }
  47. }
  48. /**
  49. * Create a new instance.
  50. * <p>
  51. * The BounceCastleProvider will be registered if necessary.
  52. * </p>
  53. */
  54. public BouncyCastleGpgSigner() {
  55. registerBouncyCastleProviderIfNecessary();
  56. }
  57. @Override
  58. public boolean canLocateSigningKey(@Nullable String gpgSigningKey,
  59. PersonIdent committer, CredentialsProvider credentialsProvider)
  60. throws CanceledException {
  61. try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt(
  62. credentialsProvider)) {
  63. BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey,
  64. committer, passphrasePrompt);
  65. return gpgKey != null;
  66. } catch (PGPException | IOException | NoSuchAlgorithmException
  67. | NoSuchProviderException | URISyntaxException e) {
  68. return false;
  69. }
  70. }
  71. private BouncyCastleGpgKey locateSigningKey(@Nullable String gpgSigningKey,
  72. PersonIdent committer,
  73. BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt)
  74. throws CanceledException, UnsupportedCredentialItem, IOException,
  75. NoSuchAlgorithmException, NoSuchProviderException, PGPException,
  76. URISyntaxException {
  77. if (gpgSigningKey == null || gpgSigningKey.isEmpty()) {
  78. gpgSigningKey = '<' + committer.getEmailAddress() + '>';
  79. }
  80. BouncyCastleGpgKeyLocator keyHelper = new BouncyCastleGpgKeyLocator(
  81. gpgSigningKey, passphrasePrompt);
  82. return keyHelper.findSecretKey();
  83. }
  84. @Override
  85. public void sign(@NonNull CommitBuilder commit,
  86. @Nullable String gpgSigningKey, @NonNull PersonIdent committer,
  87. CredentialsProvider credentialsProvider) throws CanceledException {
  88. try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt(
  89. credentialsProvider)) {
  90. BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey,
  91. committer, passphrasePrompt);
  92. PGPSecretKey secretKey = gpgKey.getSecretKey();
  93. if (secretKey == null) {
  94. throw new JGitInternalException(
  95. BCText.get().unableToSignCommitNoSecretKey);
  96. }
  97. JcePBESecretKeyDecryptorBuilder decryptorBuilder = new JcePBESecretKeyDecryptorBuilder()
  98. .setProvider(BouncyCastleProvider.PROVIDER_NAME);
  99. PGPPrivateKey privateKey = null;
  100. if (!passphrasePrompt.hasPassphrase()) {
  101. // Either the key is not encrypted, or it was read from the
  102. // legacy secring.gpg. Try getting the private key without
  103. // passphrase first.
  104. try {
  105. privateKey = secretKey.extractPrivateKey(
  106. decryptorBuilder.build(new char[0]));
  107. } catch (PGPException e) {
  108. // Ignore and try again with passphrase below
  109. }
  110. }
  111. if (privateKey == null) {
  112. // Try using a passphrase
  113. char[] passphrase = passphrasePrompt.getPassphrase(
  114. secretKey.getPublicKey().getFingerprint(),
  115. gpgKey.getOrigin());
  116. privateKey = secretKey
  117. .extractPrivateKey(decryptorBuilder.build(passphrase));
  118. }
  119. PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(
  120. new JcaPGPContentSignerBuilder(
  121. secretKey.getPublicKey().getAlgorithm(),
  122. HashAlgorithmTags.SHA256).setProvider(
  123. BouncyCastleProvider.PROVIDER_NAME));
  124. signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey);
  125. PGPSignatureSubpacketGenerator subpacketGenerator = new PGPSignatureSubpacketGenerator();
  126. subpacketGenerator.setIssuerFingerprint(false,
  127. secretKey.getPublicKey());
  128. signatureGenerator
  129. .setHashedSubpackets(subpacketGenerator.generate());
  130. ByteArrayOutputStream buffer = new ByteArrayOutputStream();
  131. try (BCPGOutputStream out = new BCPGOutputStream(
  132. new ArmoredOutputStream(buffer))) {
  133. signatureGenerator.update(commit.build());
  134. signatureGenerator.generate().encode(out);
  135. }
  136. commit.setGpgSignature(new GpgSignature(buffer.toByteArray()));
  137. } catch (PGPException | IOException | NoSuchAlgorithmException
  138. | NoSuchProviderException | URISyntaxException e) {
  139. throw new JGitInternalException(e.getMessage(), e);
  140. }
  141. }
  142. }