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