123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225 |
- /*
- * Copyright (C) 2018, 2021, Salesforce and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
- package org.eclipse.jgit.gpg.bc.internal;
-
- import java.io.ByteArrayOutputStream;
- import java.io.IOException;
- import java.net.URISyntaxException;
- import java.security.NoSuchAlgorithmException;
- import java.security.NoSuchProviderException;
- import java.security.Security;
- import java.util.Iterator;
-
- import org.bouncycastle.bcpg.ArmoredOutputStream;
- import org.bouncycastle.bcpg.BCPGOutputStream;
- import org.bouncycastle.bcpg.HashAlgorithmTags;
- import org.bouncycastle.jce.provider.BouncyCastleProvider;
- import org.bouncycastle.openpgp.PGPException;
- import org.bouncycastle.openpgp.PGPPrivateKey;
- import org.bouncycastle.openpgp.PGPPublicKey;
- import org.bouncycastle.openpgp.PGPSecretKey;
- import org.bouncycastle.openpgp.PGPSignature;
- import org.bouncycastle.openpgp.PGPSignatureGenerator;
- import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator;
- import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
- import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
- import org.eclipse.jgit.annotations.NonNull;
- import org.eclipse.jgit.annotations.Nullable;
- import org.eclipse.jgit.api.errors.CanceledException;
- import org.eclipse.jgit.api.errors.JGitInternalException;
- import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
- import org.eclipse.jgit.errors.UnsupportedCredentialItem;
- import org.eclipse.jgit.internal.JGitText;
- import org.eclipse.jgit.lib.CommitBuilder;
- import org.eclipse.jgit.lib.GpgConfig;
- import org.eclipse.jgit.lib.GpgObjectSigner;
- import org.eclipse.jgit.lib.GpgSignature;
- import org.eclipse.jgit.lib.GpgSigner;
- import org.eclipse.jgit.lib.ObjectBuilder;
- import org.eclipse.jgit.lib.PersonIdent;
- import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
- import org.eclipse.jgit.transport.CredentialsProvider;
- import org.eclipse.jgit.util.StringUtils;
-
- /**
- * GPG Signer using the BouncyCastle library.
- */
- public class BouncyCastleGpgSigner extends GpgSigner
- implements GpgObjectSigner {
-
- private static void registerBouncyCastleProviderIfNecessary() {
- if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
- Security.addProvider(new BouncyCastleProvider());
- }
- }
-
- /**
- * Create a new instance.
- * <p>
- * The BounceCastleProvider will be registered if necessary.
- * </p>
- */
- public BouncyCastleGpgSigner() {
- registerBouncyCastleProviderIfNecessary();
- }
-
- @Override
- public boolean canLocateSigningKey(@Nullable String gpgSigningKey,
- PersonIdent committer, CredentialsProvider credentialsProvider)
- throws CanceledException {
- try {
- return canLocateSigningKey(gpgSigningKey, committer,
- credentialsProvider, null);
- } catch (UnsupportedSigningFormatException e) {
- // Cannot occur with a null config
- return false;
- }
- }
-
- @Override
- public boolean canLocateSigningKey(@Nullable String gpgSigningKey,
- PersonIdent committer, CredentialsProvider credentialsProvider,
- GpgConfig config)
- throws CanceledException, UnsupportedSigningFormatException {
- if (config != null && config.getKeyFormat() != GpgFormat.OPENPGP) {
- throw new UnsupportedSigningFormatException(
- JGitText.get().onlyOpenPgpSupportedForSigning);
- }
- try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt(
- credentialsProvider)) {
- BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey,
- committer, passphrasePrompt);
- return gpgKey != null;
- } catch (CanceledException e) {
- throw e;
- } catch (Exception e) {
- return false;
- }
- }
-
- private BouncyCastleGpgKey locateSigningKey(@Nullable String gpgSigningKey,
- PersonIdent committer,
- BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt)
- throws CanceledException, UnsupportedCredentialItem, IOException,
- NoSuchAlgorithmException, NoSuchProviderException, PGPException,
- URISyntaxException {
- if (gpgSigningKey == null || gpgSigningKey.isEmpty()) {
- gpgSigningKey = '<' + committer.getEmailAddress() + '>';
- }
-
- BouncyCastleGpgKeyLocator keyHelper = new BouncyCastleGpgKeyLocator(
- gpgSigningKey, passphrasePrompt);
-
- return keyHelper.findSecretKey();
- }
-
- @Override
- public void sign(@NonNull CommitBuilder commit,
- @Nullable String gpgSigningKey, @NonNull PersonIdent committer,
- CredentialsProvider credentialsProvider) throws CanceledException {
- try {
- signObject(commit, gpgSigningKey, committer, credentialsProvider,
- null);
- } catch (UnsupportedSigningFormatException e) {
- // Cannot occur with a null config
- }
- }
-
- @Override
- public void signObject(@NonNull ObjectBuilder object,
- @Nullable String gpgSigningKey, @NonNull PersonIdent committer,
- CredentialsProvider credentialsProvider, GpgConfig config)
- throws CanceledException, UnsupportedSigningFormatException {
- if (config != null && config.getKeyFormat() != GpgFormat.OPENPGP) {
- throw new UnsupportedSigningFormatException(
- JGitText.get().onlyOpenPgpSupportedForSigning);
- }
- try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt(
- credentialsProvider)) {
- BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey,
- committer,
- passphrasePrompt);
- PGPSecretKey secretKey = gpgKey.getSecretKey();
- if (secretKey == null) {
- throw new JGitInternalException(
- BCText.get().unableToSignCommitNoSecretKey);
- }
- JcePBESecretKeyDecryptorBuilder decryptorBuilder = new JcePBESecretKeyDecryptorBuilder()
- .setProvider(BouncyCastleProvider.PROVIDER_NAME);
- PGPPrivateKey privateKey = null;
- if (!passphrasePrompt.hasPassphrase()) {
- // Either the key is not encrypted, or it was read from the
- // legacy secring.gpg. Try getting the private key without
- // passphrase first.
- try {
- privateKey = secretKey.extractPrivateKey(
- decryptorBuilder.build(new char[0]));
- } catch (PGPException e) {
- // Ignore and try again with passphrase below
- }
- }
- if (privateKey == null) {
- // Try using a passphrase
- char[] passphrase = passphrasePrompt.getPassphrase(
- secretKey.getPublicKey().getFingerprint(),
- gpgKey.getOrigin());
- privateKey = secretKey
- .extractPrivateKey(decryptorBuilder.build(passphrase));
- }
- PGPPublicKey publicKey = secretKey.getPublicKey();
- PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(
- new JcaPGPContentSignerBuilder(
- publicKey.getAlgorithm(),
- HashAlgorithmTags.SHA256).setProvider(
- BouncyCastleProvider.PROVIDER_NAME));
- signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey);
- PGPSignatureSubpacketGenerator subpackets = new PGPSignatureSubpacketGenerator();
- subpackets.setIssuerFingerprint(false, publicKey);
- // Also add the signer's user ID. Note that GPG uses only the e-mail
- // address part.
- String userId = committer.getEmailAddress();
- Iterator<String> userIds = publicKey.getUserIDs();
- if (userIds.hasNext()) {
- String keyUserId = userIds.next();
- if (!StringUtils.isEmptyOrNull(keyUserId)
- && (userId == null || !keyUserId.contains(userId))) {
- // Not the committer's key?
- userId = extractSignerId(keyUserId);
- }
- }
- if (userId != null) {
- subpackets.setSignerUserID(false, userId);
- }
- signatureGenerator
- .setHashedSubpackets(subpackets.generate());
- ByteArrayOutputStream buffer = new ByteArrayOutputStream();
- try (BCPGOutputStream out = new BCPGOutputStream(
- new ArmoredOutputStream(buffer))) {
- signatureGenerator.update(object.build());
- signatureGenerator.generate().encode(out);
- }
- object.setGpgSignature(new GpgSignature(buffer.toByteArray()));
- } catch (PGPException | IOException | NoSuchAlgorithmException
- | NoSuchProviderException | URISyntaxException e) {
- throw new JGitInternalException(e.getMessage(), e);
- }
- }
-
- static String extractSignerId(String pgpUserId) {
- int from = pgpUserId.indexOf('<');
- if (from >= 0) {
- int to = pgpUserId.indexOf('>', from + 1);
- if (to > from + 1) {
- return pgpUserId.substring(from + 1, to);
- }
- }
- return pgpUserId;
- }
- }
|