Created
July 22, 2020 05:02
-
-
Save sdorra/b139ea8725b8aaaf98e9c07408cdbbc0 to your computer and use it in GitHub Desktop.
Sign and verify commits with JGit
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import org.bouncycastle.bcpg.ArmoredInputStream; | |
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.PGPKeyPair; | |
import org.bouncycastle.openpgp.PGPObjectFactory; | |
import org.bouncycastle.openpgp.PGPPrivateKey; | |
import org.bouncycastle.openpgp.PGPPublicKey; | |
import org.bouncycastle.openpgp.PGPSignature; | |
import org.bouncycastle.openpgp.PGPSignatureGenerator; | |
import org.bouncycastle.openpgp.PGPSignatureList; | |
import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; | |
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; | |
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider; | |
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair; | |
import org.eclipse.jgit.annotations.NonNull; | |
import org.eclipse.jgit.annotations.Nullable; | |
import org.eclipse.jgit.api.Git; | |
import org.eclipse.jgit.api.errors.GitAPIException; | |
import org.eclipse.jgit.api.errors.JGitInternalException; | |
import org.eclipse.jgit.internal.JGitText; | |
import org.eclipse.jgit.lib.CommitBuilder; | |
import org.eclipse.jgit.lib.GpgSignature; | |
import org.eclipse.jgit.lib.GpgSigner; | |
import org.eclipse.jgit.lib.PersonIdent; | |
import org.eclipse.jgit.revwalk.RevCommit; | |
import org.eclipse.jgit.transport.CredentialsProvider; | |
import org.eclipse.jgit.util.RawParseUtils; | |
import org.junit.jupiter.api.Test; | |
import org.junit.jupiter.api.io.TempDir; | |
import java.io.ByteArrayInputStream; | |
import java.io.ByteArrayOutputStream; | |
import java.io.IOException; | |
import java.nio.charset.StandardCharsets; | |
import java.nio.file.Files; | |
import java.nio.file.Path; | |
import java.nio.file.Paths; | |
import java.security.KeyPair; | |
import java.security.KeyPairGenerator; | |
import java.security.NoSuchAlgorithmException; | |
import java.security.NoSuchProviderException; | |
import java.security.Security; | |
import java.util.Arrays; | |
import java.util.Date; | |
import static org.assertj.core.api.Assertions.assertThat; | |
class JGitTest { | |
private static void registerBouncyCastleProviderIfNecessary() { | |
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { | |
Security.addProvider(new BouncyCastleProvider()); | |
} | |
} | |
@Test | |
void signAndVerify(@TempDir Path workdir) throws IOException, GitAPIException, PGPException, NoSuchProviderException, NoSuchAlgorithmException { | |
registerBouncyCastleProviderIfNecessary(); | |
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC"); | |
keyPairGenerator.initialize(2048); | |
KeyPair pair = keyPairGenerator.generateKeyPair(); | |
String identity = "0xAWESOMExBOB"; | |
PGPKeyPair keyPair = new JcaPGPKeyPair(PGPPublicKey.RSA_GENERAL, pair, new Date()); | |
ScmGpgSigner signer = new ScmGpgSigner(keyPair); | |
GpgSigner.setDefault(signer); | |
Path repositoryPath = workdir.resolve("repository"); | |
try (Git git = Git.init().setDirectory(repositoryPath.toFile()).call()) { | |
Files.write(repositoryPath.resolve("README.md"), "# Hello".getBytes(StandardCharsets.UTF_8)); | |
git.add().addFilepattern("README.md").call(); | |
git.commit() | |
.setAuthor("Bob The Signer", "[email protected]") | |
.setMessage("Signed from Bob") | |
.setSign(true) | |
.setSigningKey(identity) | |
.call(); | |
RevCommit commit = git.log().setMaxCount(1).call().iterator().next(); | |
byte[] raw = commit.getRawBuffer(); | |
ByteArrayOutputStream baos = new ByteArrayOutputStream(); | |
final byte[] header = {'g', 'p', 'g', 's', 'i', 'g'}; | |
final int start = RawParseUtils.headerStart(header, raw, 0); | |
final int end = RawParseUtils.headerEnd(raw, start); | |
byte[] headerPrefix = Arrays.copyOfRange(raw, 0, start - header.length - 1); | |
baos.write(headerPrefix); | |
byte[] headerSuffix = Arrays.copyOfRange(raw, end + 1, raw.length); | |
baos.write(headerSuffix); | |
System.out.println("-----BEGIN COMMIT-----"); | |
System.out.println(baos); | |
System.out.println("-----END COMMIT-----"); | |
byte[] signature = Arrays.copyOfRange(raw, start, end); | |
System.out.println(new String(signature, commit.getEncoding())); | |
ArmoredInputStream armoredInputStream = new ArmoredInputStream(new ByteArrayInputStream(signature)); | |
PGPObjectFactory pgpObjectFactory = new PGPObjectFactory(armoredInputStream, null); | |
PGPSignatureList list = (PGPSignatureList) pgpObjectFactory.nextObject(); | |
for (PGPSignature s : list) { | |
PGPContentVerifierBuilderProvider provider = new JcaPGPContentVerifierBuilderProvider(); | |
s.init(provider, keyPair.getPublicKey()); | |
s.update(baos.toByteArray()); | |
assertThat(s.verify()).isTrue(); | |
} | |
} | |
} | |
private static class ScmGpgSigner extends GpgSigner { | |
private final PGPKeyPair keyPair; | |
ScmGpgSigner(PGPKeyPair keyPair) { | |
this.keyPair = keyPair; | |
} | |
@Override | |
public boolean canLocateSigningKey(@Nullable String gpgSigningKey, PersonIdent committer, CredentialsProvider credentialsProvider) { | |
return true; | |
} | |
@Override | |
public void sign(@NonNull CommitBuilder commit, @Nullable String gpgSigningKey, | |
@NonNull PersonIdent committer, CredentialsProvider credentialsProvider) { | |
try { | |
if (keyPair == null) { | |
throw new JGitInternalException(JGitText.get().unableToSignCommitNoSecretKey); | |
} | |
PGPPrivateKey privateKey = keyPair.getPrivateKey(); | |
PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator( | |
new JcaPGPContentSignerBuilder( | |
keyPair.getPublicKey().getAlgorithm(), | |
HashAlgorithmTags.SHA256).setProvider(BouncyCastleProvider.PROVIDER_NAME) | |
); | |
signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey); | |
ByteArrayOutputStream buffer = new ByteArrayOutputStream(); | |
try (BCPGOutputStream out = new BCPGOutputStream(new ArmoredOutputStream(buffer))) { | |
signatureGenerator.update(commit.build()); | |
signatureGenerator.generate().encode(out); | |
} | |
commit.setGpgSignature(new GpgSignature(buffer.toByteArray())); | |
} catch (PGPException | IOException e) { | |
throw new JGitInternalException(e.getMessage(), e); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment