GPG: compute the keygrip to find a secret key

The gpg-agent stores secret keys in individual files in the secret
key directory private-keys-v1.d. The files have the key's keygrip
(in upper case) as name and extension ".key".

A keygrip is a SHA1 hash over the parameters of the public key. By
computing this keygrip, we can pre-compute the expected file name and
then check only that one file instead of having to iterate over all
keys stored in that directory.

This file naming scheme is actually an implementation detail of
gpg-agent. It is unlikely to change, though. The keygrip itself is
computed via libgcrypt and will remain stable according to the GPG
main author.[1]

Add an implementation for calculating the keygrip and include tests.
Do not iterate over files in BouncyCastleGpgKeyLocator but only check
the single file identified by the keygrip.

Ideally upstream BouncyCastle would provide such a getKeyGrip() method.
But as it re-builds GPG and libgcrypt internals, it's doubtful it would
be included there, and since BouncyCastle even lacks a number of curve
OIDs for ed25519/curve25519 and uses the short-Weierstrass parameters
instead of the more common Montgomery parameters, including it there
might be quite a bit of work.

[1] http://gnupg.10057.n7.nabble.com/GnuPG-2-1-x-and-2-2-x-keyring-formats-tp54146p54154.html

Bug: 547536
Change-Id: I30022a0e7b33b1bf35aec1222f84591f0c30ddfd
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
This commit is contained in:
Thomas Wolf 2021-01-17 16:21:28 +01:00 committed by Matthias Sohn
parent 3774fcc848
commit 64cbea8a97
25 changed files with 650 additions and 67 deletions

View File

@ -162,6 +162,7 @@ java_library(
"//org.eclipse.jgit:__pkg__",
"//org.eclipse.jgit.gpg.bc:__pkg__",
"//org.eclipse.jgit.test:__pkg__",
"//org.eclipse.jgit.gpg.bc.test:__pkg__",
],
exports = ["@bcpg//jar"],
)
@ -172,6 +173,7 @@ java_library(
"//org.eclipse.jgit:__pkg__",
"//org.eclipse.jgit.gpg.bc:__pkg__",
"//org.eclipse.jgit.test:__pkg__",
"//org.eclipse.jgit.gpg.bc.test:__pkg__",
],
exports = ["@bcprov//jar"],
)

View File

@ -2,10 +2,15 @@
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>
<classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
<classpathentry kind="src" path="tst">
<classpathentry kind="src" output="bin-tst" path="tst">
<attributes>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="bin"/>
<classpathentry kind="src" output="bin-tst" path="tst-rsrc">
<attributes>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="bin-tst"/>
</classpath>

View File

@ -1,2 +1,3 @@
/bin
/bin-tst
/target

View File

@ -1,3 +1,8 @@
load(
"@com_googlesource_gerrit_bazlets//tools:genrule2.bzl",
"genrule2",
)
load("@rules_java//java:defs.bzl", "java_import")
load(
"@com_googlesource_gerrit_bazlets//tools:junit.bzl",
"junit_tests",
@ -8,7 +13,22 @@ junit_tests(
srcs = glob(["tst/**/*.java"]),
tags = ["bc"],
deps = [
"//lib:bcpg",
"//lib:bcprov",
"//lib:junit",
"//org.eclipse.jgit.gpg.bc:gpg-bc",
"//org.eclipse.jgit.gpg.bc.test:tst_rsrc",
],
)
java_import(
name = "tst_rsrc",
jars = [":tst_rsrc_jar"],
)
genrule2(
name = "tst_rsrc_jar",
srcs = glob(["tst-rsrc/**"]),
outs = ["tst_rsrc.jar"],
cmd = "o=$$PWD/$@ && tar cf - $(SRCS) | tar -C $$TMP --strip-components=2 -xf - && cd $$TMP && zip -qr $$o .",
)

View File

@ -7,8 +7,16 @@ Bundle-Version: 5.11.0.qualifier
Bundle-Vendor: %Bundle-Vendor
Bundle-Localization: plugin
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Import-Package: org.eclipse.jgit.gpg.bc.internal;version="[5.11.0,5.12.0)",
org.junit;version="[4.13,5.0.0)"
Import-Package: org.bouncycastle.jce.provider;version="[1.65.0,2.0.0)",
org.bouncycastle.openpgp;version="[1.65.0,2.0.0)",
org.bouncycastle.openpgp.operator.jcajce;version="[1.65.0,2.0.0)",
org.bouncycastle.util.encoders;version="[1.65.0,2.0.0)",
org.eclipse.jgit.gpg.bc.internal;version="[5.11.0,5.12.0)",
org.eclipse.jgit.gpg.bc.internal.keys;version="[5.11.0,5.12.0)",
org.eclipse.jgit.util.sha1;version="[5.11.0,5.12.0)",
org.junit;version="[4.13,5.0.0)",
org.junit.runner;version="[4.13,5.0.0)",
org.junit.runners;version="[4.13,5.0.0)"
Export-Package: org.eclipse.jgit.gpg.bc.internal;x-internal:=true
Require-Bundle: org.hamcrest.core;bundle-version="[1.1.0,2.0.0)",
org.hamcrest.library;bundle-version="[1.1.0,2.0.0)"

View File

@ -1,5 +1,5 @@
source.. = tst/
output.. = bin/
output.. = bin-tst/
bin.includes = META-INF/,\
.,\
plugin.properties

View File

@ -85,6 +85,12 @@
<sourceDirectory>src/</sourceDirectory>
<testSourceDirectory>tst/</testSourceDirectory>
<testResources>
<testResource>
<directory>tst-rsrc/</directory>
</testResource>
</testResources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>

View File

@ -0,0 +1,61 @@
/*
* Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> 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.keys;
import static org.junit.Assert.assertEquals;
import java.math.BigInteger;
import java.util.Locale;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.util.encoders.Hex;
import org.eclipse.jgit.util.sha1.SHA1;
import org.junit.Test;
public class KeyGrip25519Test {
interface Hash {
byte[] hash(SHA1 sha, BigInteger q) throws PGPException;
}
private void assertKeyGrip(String key, String expectedKeyGrip, Hash hash)
throws Exception {
SHA1 grip = SHA1.newInstance();
grip.setDetectCollision(false);
BigInteger pk = new BigInteger(key, 16);
byte[] keyGrip = hash.hash(grip, pk);
assertEquals("Keygrip should match", expectedKeyGrip,
Hex.toHexString(keyGrip).toUpperCase(Locale.ROOT));
}
@Test
public void testCompressed() throws Exception {
assertKeyGrip("40"
+ "773E72848C1FD5F9652B29E2E7AF79571A04990E96F2016BF4E0EC1890C2B7DB",
"9DB6C64A38830F4960701789475520BE8C821F47",
KeyGrip::hashEd25519);
}
@Test
public void testCompressedNoPrefix() throws Exception {
assertKeyGrip(
"773E72848C1FD5F9652B29E2E7AF79571A04990E96F2016BF4E0EC1890C2B7DB",
"9DB6C64A38830F4960701789475520BE8C821F47",
KeyGrip::hashEd25519);
}
@Test
public void testCurve25519() throws Exception {
assertKeyGrip("40"
+ "918C1733127F6BF2646FAE3D081A18AE77111C903B906310B077505EFFF12740",
"0F89A565D3EA187CE839332398F5D480677DF49C",
KeyGrip::hashCurve25519);
}
}

View File

@ -0,0 +1,143 @@
/*
* Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> 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.keys;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.io.InputStream;
import java.security.Security;
import java.util.Iterator;
import java.util.Locale;
import java.util.function.Consumer;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
import org.bouncycastle.util.encoders.Hex;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
@RunWith(Parameterized.class)
public class KeyGripTest {
@BeforeClass
public static void ensureBC() {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
protected static class TestData {
String filename;
String[] expectedKeyGrips;
TestData(String filename, String... keyGrips) {
this.filename = filename;
this.expectedKeyGrips = keyGrips;
}
@Override
public String toString() {
return filename;
}
}
@Parameters(name = "{0}")
public static TestData[] initTestData() {
return new TestData[] {
new TestData("rsa.asc",
"D148210FAF36468055B83D0F5A6DEB83FBC8E864",
"A5E4CD2CBBE44A16E4D6EC05C2E3C3A599DC763C"),
new TestData("dsa-elgamal.asc",
"552286BEB2999F0A9E26A50385B90D9724001187",
"CED7034A8EB5F4CE90DF99147EC33D86FCD3296C"),
new TestData("brainpool256.asc",
"A01BAA22A72F09A0FF0A1D4CBCE70844DD52DDD7",
"C1678B7DE5F144C93B89468D5F9764ACE182ED36"),
new TestData("brainpool384.asc",
"2F25DB025DEBF3EA2715350209B985829B04F50A",
"B6BD8B81F75AF914163D97DF8DE8F6FC64C283F8"),
new TestData("brainpool512.asc",
"5A484F56AB4B8B6583B6365034999F6543FAE1AE",
"9133E4A7E8FC8515518DF444C3F2F247EEBBADEC"),
new TestData("nistp256.asc",
"FC81AECE90BCE6E54D0D637D266109783AC8DAC0",
"A56DC8DB8355747A809037459B4258B8A743EAB5"),
new TestData("nistp384.asc",
"A1338230AED1C9C125663518470B49056C9D1733",
"797A83FE041FFE06A7F4B1D32C6F4AE0F6D87ADF"),
new TestData("nistp521.asc",
"D91B789603EC9138AA20342A2B6DC86C81B70F5D",
"FD048B2CA1919CB241DC8A2C7FA3E742EF343DCA"),
new TestData("secp256k1.asc",
"498B89C485489BA16B40755C0EBA580166393074",
"48FFED40D018747363BDEFFDD404D1F4870F8064"),
new TestData("ed25519.asc",
"940D97D75C306D737A59A98EAFF1272832CEDC0B"),
new TestData("x25519.asc",
"A77DC8173DA6BEE126F5BD6F5A14E01200B52FCE",
"636C983EDB558527BA82780B52CB5DAE011BE46B")
};
}
// Injected by JUnit
@Parameter
public TestData data;
private void readAsc(InputStream in, Consumer<PGPPublicKey> process)
throws IOException, PGPException {
PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(
PGPUtil.getDecoderStream(in), new JcaKeyFingerprintCalculator());
Iterator<PGPPublicKeyRing> keyRings = pgpPub.getKeyRings();
while (keyRings.hasNext()) {
PGPPublicKeyRing keyRing = keyRings.next();
Iterator<PGPPublicKey> keys = keyRing.getPublicKeys();
while (keys.hasNext()) {
process.accept(keys.next());
}
}
}
@Test
public void testGrip() throws Exception {
try (InputStream in = this.getClass()
.getResourceAsStream(data.filename)) {
int index[] = { 0 };
readAsc(in, key -> {
byte[] keyGrip = null;
try {
keyGrip = KeyGrip.getKeyGrip(key);
} catch (PGPException e) {
throw new RuntimeException(e);
}
assertTrue("More keys than expected",
index[0] < data.expectedKeyGrips.length);
assertEquals("Wrong keygrip", data.expectedKeyGrips[index[0]++],
Hex.toHexString(keyGrip).toUpperCase(Locale.ROOT));
});
assertEquals("Missing keys", data.expectedKeyGrips.length,
index[0]);
}
}
}

View File

@ -8,12 +8,19 @@ Bundle-Vendor: %Bundle-Vendor
Bundle-Localization: plugin
Bundle-Version: 5.11.0.qualifier
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Import-Package: org.bouncycastle.bcpg;version="[1.65.0,2.0.0)",
Import-Package: org.bouncycastle.asn1;version="[1.65.0,2.0.0)",
org.bouncycastle.asn1.cryptlib;version="[1.65.0,2.0.0)",
org.bouncycastle.asn1.x9;version="[1.65.0,2.0.0)",
org.bouncycastle.bcpg;version="[1.65.0,2.0.0)",
org.bouncycastle.bcpg.sig;version="[1.65.0,2.0.0)",
org.bouncycastle.crypto.ec;version="[1.65.0,2.0.0)",
org.bouncycastle.gpg;version="[1.65.0,2.0.0)",
org.bouncycastle.gpg.keybox;version="[1.65.0,2.0.0)",
org.bouncycastle.gpg.keybox.jcajce;version="[1.65.0,2.0.0)",
org.bouncycastle.jcajce.interfaces;version="[1.65.0,2.0.0)",
org.bouncycastle.jce.provider;version="[1.65.0,2.0.0)",
org.bouncycastle.math.ec;version="[1.65.0,2.0.0)",
org.bouncycastle.math.field;version="[1.65.0,2.0.0)",
org.bouncycastle.openpgp;version="[1.65.0,2.0.0)",
org.bouncycastle.openpgp.jcajce;version="[1.65.0,2.0.0)",
org.bouncycastle.openpgp.operator;version="[1.65.0,2.0.0)",
@ -27,5 +34,5 @@ Import-Package: org.bouncycastle.bcpg;version="[1.65.0,2.0.0)",
org.eclipse.jgit.transport;version="[5.11.0,5.12.0)",
org.eclipse.jgit.util;version="[5.11.0,5.12.0)",
org.slf4j;version="[1.7.0,2.0.0)"
Export-Package: org.eclipse.jgit.gpg.bc.internal;version="5.11.0";
x-friends:="org.eclipse.jgit.gpg.bc.test"
Export-Package: org.eclipse.jgit.gpg.bc.internal;version="5.11.0";x-friends:="org.eclipse.jgit.gpg.bc.test",
org.eclipse.jgit.gpg.bc.internal.keys;version="5.11.0";x-friends:="org.eclipse.jgit.gpg.bc.test"

View File

@ -1,6 +1,8 @@
corrupt25519Key=Ed25519/Curve25519 public key has wrong length: {0}
credentialPassphrase=Passphrase
gpgFailedToParseSecretKey=Failed to parse secret key file in directory: {0}. Is the entered passphrase correct?
gpgFailedToParseSecretKey=Failed to parse secret key file {0}. Is the entered passphrase correct?
gpgNoCredentialsProvider=missing credentials provider
gpgNoKeygrip=Cannot find key {0}: cannot determine key grip
gpgNoKeyring=neither pubring.kbx nor secring.gpg files found
gpgNoKeyInLegacySecring=no matching secret key found in legacy secring.gpg for key or user id: {0}
gpgNoPublicKeyFound=Unable to find a public-key with key or user id: {0}
@ -16,3 +18,7 @@ signatureNoPublicKey=No public key found to verify the signature
signatureParseError=Signature cannot be parsed
signatureVerificationError=Signature verification failed
unableToSignCommitNoSecretKey=Unable to sign commit. Signing key not available.
uncompressed25519Key=Cannot handle ed25519 public key with uncompressed data: {0}
unknownCurve=Unknown curve {0}
unknownCurveParameters=Curve {0} does not have a prime field
unknownKeyType=Unknown key type {0}

View File

@ -27,9 +27,11 @@ public static BCText get() {
}
// @formatter:off
/***/ public String corrupt25519Key;
/***/ public String credentialPassphrase;
/***/ public String gpgFailedToParseSecretKey;
/***/ public String gpgNoCredentialsProvider;
/***/ public String gpgNoKeygrip;
/***/ public String gpgNoKeyring;
/***/ public String gpgNoKeyInLegacySecring;
/***/ public String gpgNoPublicKeyFound;
@ -45,5 +47,9 @@ public static BCText get() {
/***/ public String signatureParseError;
/***/ public String signatureVerificationError;
/***/ public String unableToSignCommitNoSecretKey;
/***/ public String uncompressed25519Key;
/***/ public String unknownCurve;
/***/ public String unknownCurveParameters;
/***/ public String unknownKeyType;
}

View File

@ -27,12 +27,8 @@
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.bouncycastle.gpg.SExprParser;
import org.bouncycastle.gpg.keybox.BlobType;
@ -61,6 +57,7 @@
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.api.errors.CanceledException;
import org.eclipse.jgit.errors.UnsupportedCredentialItem;
import org.eclipse.jgit.gpg.bc.internal.keys.KeyGrip;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.StringUtils;
import org.eclipse.jgit.util.SystemReader;
@ -158,15 +155,10 @@ public BouncyCastleGpgKeyLocator(String signingKey,
private PGPSecretKey attemptParseSecretKey(Path keyFile,
PGPDigestCalculatorProvider calculatorProvider,
PBEProtectionRemoverFactory passphraseProvider,
PGPPublicKey publicKey) {
PGPPublicKey publicKey) throws IOException, PGPException {
try (InputStream in = newInputStream(keyFile)) {
return new SExprParser(calculatorProvider).parseSecretKey(
new BufferedInputStream(in), passphraseProvider, publicKey);
} catch (IOException | PGPException | ClassCastException e) {
if (log.isDebugEnabled())
log.debug("Ignoring unreadable file '{}': {}", keyFile, //$NON-NLS-1$
e.getMessage(), e);
return null;
}
}
@ -472,67 +464,71 @@ private BouncyCastleGpgKey findSecretKeyForKeyBoxPublicKey(
PGPPublicKey publicKey, Path userKeyboxPath)
throws PGPException, CanceledException, UnsupportedCredentialItem,
URISyntaxException {
/*
* this is somewhat brute-force but there doesn't seem to be another
* way; we have to walk all private key files we find and try to open
* them
*/
PGPDigestCalculatorProvider calculatorProvider = new JcaPGPDigestCalculatorProviderBuilder()
.build();
try (Stream<Path> keyFiles = Files.walk(USER_SECRET_KEY_DIR)) {
List<Path> allPaths = keyFiles.filter(Files::isRegularFile)
.collect(Collectors.toCollection(ArrayList::new));
if (allPaths.isEmpty()) {
return null;
}
byte[] keyGrip = null;
try {
keyGrip = KeyGrip.getKeyGrip(publicKey);
} catch (PGPException e) {
throw new PGPException(
MessageFormat.format(BCText.get().gpgNoKeygrip,
Hex.toHexString(publicKey.getFingerprint())),
e);
}
String filename = Hex.toHexString(keyGrip).toUpperCase(Locale.ROOT)
+ ".key"; //$NON-NLS-1$
Path keyFile = USER_SECRET_KEY_DIR.resolve(filename);
if (!Files.exists(keyFile)) {
return null;
}
boolean clearPrompt = false;
try {
PGPDigestCalculatorProvider calculatorProvider = new JcaPGPDigestCalculatorProviderBuilder()
.build();
PBEProtectionRemoverFactory passphraseProvider = p -> {
throw new EncryptedPgpKeyException();
};
for (int attempts = 0; attempts < 2; attempts++) {
// Second pass will traverse only the encrypted keys with a real
// passphrase provider.
Iterator<Path> pathIterator = allPaths.iterator();
while (pathIterator.hasNext()) {
Path keyFile = pathIterator.next();
try {
PGPSecretKey secretKey = attemptParseSecretKey(keyFile,
calculatorProvider, passphraseProvider,
publicKey);
pathIterator.remove();
if (secretKey != null) {
if (!secretKey.isSigningKey()) {
throw new PGPException(MessageFormat.format(
BCText.get().gpgNotASigningKey,
signingKey));
}
return new BouncyCastleGpgKey(secretKey,
userKeyboxPath);
}
} catch (EncryptedPgpKeyException e) {
// Ignore; we'll try again.
}
}
if (attempts > 0 || allPaths.isEmpty()) {
break;
}
// allPaths contains only the encrypted keys now.
PGPSecretKey secretKey = null;
try {
// Try without passphrase
secretKey = attemptParseSecretKey(keyFile, calculatorProvider,
passphraseProvider, publicKey);
} catch (EncryptedPgpKeyException e) {
// Let's try again with a passphrase
passphraseProvider = new JcePBEProtectionRemoverFactory(
passphrasePrompt.getPassphrase(
publicKey.getFingerprint(), userKeyboxPath));
}
clearPrompt = true;
try {
secretKey = attemptParseSecretKey(keyFile, calculatorProvider,
passphraseProvider, publicKey);
} catch (PGPException e1) {
throw new PGPException(MessageFormat.format(
BCText.get().gpgFailedToParseSecretKey,
keyFile.toAbsolutePath()), e);
passphrasePrompt.clear();
}
}
if (secretKey != null) {
if (!secretKey.isSigningKey()) {
throw new PGPException(MessageFormat.format(
BCText.get().gpgNotASigningKey, signingKey));
}
clearPrompt = false;
return new BouncyCastleGpgKey(secretKey, userKeyboxPath);
}
return null;
} catch (RuntimeException e) {
passphrasePrompt.clear();
throw e;
} catch (FileNotFoundException | NoSuchFileException e) {
clearPrompt = false;
return null;
} catch (IOException e) {
passphrasePrompt.clear();
throw new PGPException(MessageFormat.format(
BCText.get().gpgFailedToParseSecretKey,
USER_SECRET_KEY_DIR.toAbsolutePath()), e);
keyFile.toAbsolutePath()), e);
} finally {
if (clearPrompt) {
passphrasePrompt.clear();
}
}
}

View File

@ -0,0 +1,322 @@
/*
* Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> 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.keys;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.Arrays;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.cryptlib.CryptlibObjectIdentifiers;
import org.bouncycastle.asn1.x9.ECNamedCurveTable;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.bcpg.DSAPublicBCPGKey;
import org.bouncycastle.bcpg.ECPublicBCPGKey;
import org.bouncycastle.bcpg.ElGamalPublicBCPGKey;
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
import org.bouncycastle.bcpg.RSAPublicBCPGKey;
import org.bouncycastle.crypto.ec.CustomNamedCurves;
import org.bouncycastle.math.ec.ECAlgorithms;
import org.bouncycastle.math.field.FiniteField;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.util.encoders.Hex;
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.gpg.bc.internal.BCText;
import org.eclipse.jgit.util.sha1.SHA1;
/**
* Utilities to compute the <em>keygrip</em> of a key. A keygrip is a SHA1 hash
* over the public key parameters and is used internally by the gpg-agent to
* find the secret key belonging to a public key: the secret key is stored in a
* file under ~/.gnupg/private-keys-v1.d/ with a name "&lt;keygrip>.key". While
* this storage organization is an implementation detail of GPG, the way
* keygrips are computed is not; they are computed by libgcrypt and their
* definition is stable.
*/
public final class KeyGrip {
// Some OIDs apparently unknown to BouncyCastle.
private static String OID_OPENPGP_ED25519 = "1.3.6.1.4.1.11591.15.1"; //$NON-NLS-1$
private static String OID_RFC8410_CURVE25519 = "1.3.101.110"; //$NON-NLS-1$
private static String OID_RFC8410_ED25519 = "1.3.101.112"; //$NON-NLS-1$
private KeyGrip() {
// No instantiation
}
/**
* Computes the keygrip for a {@link PGPPublicKey}.
*
* @param publicKey
* to get the keygrip of
* @return the keygrip
* @throws PGPException
* if an unknown key type is encountered.
*/
@NonNull
public static byte[] getKeyGrip(PGPPublicKey publicKey)
throws PGPException {
SHA1 grip = SHA1.newInstance();
grip.setDetectCollision(false);
switch (publicKey.getAlgorithm()) {
case PublicKeyAlgorithmTags.RSA_GENERAL:
case PublicKeyAlgorithmTags.RSA_ENCRYPT:
case PublicKeyAlgorithmTags.RSA_SIGN:
BigInteger modulus = ((RSAPublicBCPGKey) publicKey
.getPublicKeyPacket().getKey()).getModulus();
hash(grip, modulus.toByteArray());
break;
case PublicKeyAlgorithmTags.DSA:
DSAPublicBCPGKey dsa = (DSAPublicBCPGKey) publicKey
.getPublicKeyPacket().getKey();
hash(grip, dsa.getP().toByteArray(), 'p', true);
hash(grip, dsa.getQ().toByteArray(), 'q', true);
hash(grip, dsa.getG().toByteArray(), 'g', true);
hash(grip, dsa.getY().toByteArray(), 'y', true);
break;
case PublicKeyAlgorithmTags.ELGAMAL_GENERAL:
case PublicKeyAlgorithmTags.ELGAMAL_ENCRYPT:
ElGamalPublicBCPGKey eg = (ElGamalPublicBCPGKey) publicKey
.getPublicKeyPacket().getKey();
hash(grip, eg.getP().toByteArray(), 'p', true);
hash(grip, eg.getG().toByteArray(), 'g', true);
hash(grip, eg.getY().toByteArray(), 'y', true);
break;
case PublicKeyAlgorithmTags.ECDH:
case PublicKeyAlgorithmTags.ECDSA:
case PublicKeyAlgorithmTags.EDDSA:
ECPublicBCPGKey ec = (ECPublicBCPGKey) publicKey
.getPublicKeyPacket().getKey();
ASN1ObjectIdentifier curveOID = ec.getCurveOID();
// BC doesn't know these OIDs.
if (OID_OPENPGP_ED25519.equals(curveOID.getId())
|| OID_RFC8410_ED25519.equals(curveOID.getId())) {
return hashEd25519(grip, ec.getEncodedPoint());
} else if (CryptlibObjectIdentifiers.curvey25519.equals(curveOID)
|| OID_RFC8410_CURVE25519.equals(curveOID.getId())) {
// curvey25519 actually is the OpenPGP OID for Curve25519 and is
// known to BC, but the parameters are for the short Weierstrass
// form. See https://github.com/bcgit/bc-java/issues/399 .
// libgcrypt uses Montgomery form.
return hashCurve25519(grip, ec.getEncodedPoint());
}
X9ECParameters params = getX9Parameters(curveOID);
if (params == null) {
throw new PGPException(MessageFormat
.format(BCText.get().unknownCurve, curveOID.getId()));
}
// Need to write p, a, b, g, n, q
BigInteger q = ec.getEncodedPoint();
byte[] g = params.getG().getEncoded(false);
BigInteger a = params.getCurve().getA().toBigInteger();
BigInteger b = params.getCurve().getB().toBigInteger();
BigInteger n = params.getN();
BigInteger p = null;
FiniteField field = params.getCurve().getField();
if (ECAlgorithms.isFpField(field)) {
p = field.getCharacteristic();
}
if (p == null) {
// Don't know...
throw new PGPException(MessageFormat.format(
BCText.get().unknownCurveParameters, curveOID.getId()));
}
hash(grip, p.toByteArray(), 'p', false);
hash(grip, a.toByteArray(), 'a', false);
hash(grip, b.toByteArray(), 'b', false);
hash(grip, g, 'g', false);
hash(grip, n.toByteArray(), 'n', false);
if (publicKey.getAlgorithm() == PublicKeyAlgorithmTags.EDDSA) {
hashQ25519(grip, q);
} else {
hash(grip, q.toByteArray(), 'q', false);
}
break;
default:
throw new PGPException(
MessageFormat.format(BCText.get().unknownKeyType,
Integer.toString(publicKey.getAlgorithm())));
}
return grip.digest();
}
private static void hash(SHA1 grip, byte[] data) {
// Need to skip leading zero bytes
int i = 0;
while (i < data.length && data[i] == 0) {
i++;
}
int length = data.length - i;
if (i < data.length) {
if ((data[i] & 0x80) != 0) {
grip.update((byte) 0);
}
grip.update(data, i, length);
}
}
private static void hash(SHA1 grip, byte[] data, char id, boolean zeroPad) {
// Need to skip leading zero bytes
int i = 0;
while (i < data.length && data[i] == 0) {
i++;
}
int length = data.length - i;
boolean addZero = false;
if (i < data.length && zeroPad && (data[i] & 0x80) != 0) {
addZero = true;
}
// libgcrypt includes an SExp in the hash
String prefix = "(1:" + id + (addZero ? length + 1 : length) + ':'; //$NON-NLS-1$
grip.update(prefix.getBytes(StandardCharsets.US_ASCII));
// For some items, gcrypt prepends a zero byte if the high bit is set
if (addZero) {
grip.update((byte) 0);
}
if (i < data.length) {
grip.update(data, i, length);
}
grip.update((byte) ')');
}
private static void hashQ25519(SHA1 grip, BigInteger q)
throws PGPException {
byte[] data = q.toByteArray();
switch (data[0]) {
case 0x04:
if (data.length != 65) {
throw new PGPException(MessageFormat.format(
BCText.get().corrupt25519Key, Hex.toHexString(data)));
}
// Uncompressed: should not occur with ed25519 or curve25519
throw new PGPException(MessageFormat.format(
BCText.get().uncompressed25519Key, Hex.toHexString(data)));
case 0x40:
if (data.length != 33) {
throw new PGPException(MessageFormat.format(
BCText.get().corrupt25519Key, Hex.toHexString(data)));
}
// Compressed; normal case. Skip prefix.
hash(grip, Arrays.copyOfRange(data, 1, data.length), 'q', false);
break;
default:
if (data.length != 32) {
throw new PGPException(MessageFormat.format(
BCText.get().corrupt25519Key, Hex.toHexString(data)));
}
// Compressed format without prefix. Should not occur?
hash(grip, data, 'q', false);
break;
}
}
/**
* Computes the keygrip for an ed25519 public key.
* <p>
* Package-visible for tests only.
* </p>
*
* @param grip
* initialized {@link SHA1}
* @param q
* the public key's EC point
* @return the keygrip
* @throws PGPException
* if q indicates uncompressed format
*/
@SuppressWarnings("nls")
static byte[] hashEd25519(SHA1 grip, BigInteger q) throws PGPException {
// For the values, see RFC 7748: https://tools.ietf.org/html/rfc7748
// p = 2^255 - 19
hash(grip, Hex.decodeStrict(
"7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED"),
'p', false);
// Field: a = 1
hash(grip, new byte[] { 0x01 }, 'a', false);
// Field: b = 121665/121666 (mod p)
// See Berstein et.al., "Twisted Edwards Curves",
// https://doi.org/10.1007/978-3-540-68164-9_26
hash(grip, Hex.decodeStrict(
"2DFC9311D490018C7338BF8688861767FF8FF5B2BEBE27548A14B235ECA6874A"),
'b', false);
// Generator point with affine X,Y
// @formatter:off
// X(P) = 15112221349535400772501151409588531511454012693041857206046113283949847762202
// Y(P) = 46316835694926478169428394003475163141307993866256225615783033603165251855960
// the "04" signifies uncompressed format.
// @formatter:on
hash(grip, Hex.decodeStrict("04"
+ "216936D3CD6E53FEC0A4E231FDD6DC5C692CC7609525A7B2C9562D608F25D51A"
+ "6666666666666666666666666666666666666666666666666666666666666658"),
'g', false);
// order = 2^252 + 0x14def9dea2f79cd65812631a5cf5d3ed
hash(grip, Hex.decodeStrict(
"1000000000000000000000000000000014DEF9DEA2F79CD65812631A5CF5D3ED"),
'n', false);
hashQ25519(grip, q);
return grip.digest();
}
/**
* Computes the keygrip for a curve25519 public key.
* <p>
* Package-visible for tests only.
* </p>
*
* @param grip
* initialized {@link SHA1}
* @param q
* the public key's EC point
* @return the keygrip
* @throws PGPException
* if q indicates uncompressed format
*/
@SuppressWarnings("nls")
static byte[] hashCurve25519(SHA1 grip, BigInteger q) throws PGPException {
hash(grip, Hex.decodeStrict(
"7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED"),
'p', false);
// Unclear: RFC 7748 says A = 486662. This value here is (A-2)/4 =
// 121665. Compare ecc-curves.c in libgcrypt:
// https://github.com/gpg/libgcrypt/blob/361a058/cipher/ecc-curves.c#L146
hash(grip, new byte[] { 0x01, (byte) 0xDB, 0x41 }, 'a', false);
hash(grip, new byte[] { 0x01 }, 'b', false);
// libgcrypt uses the old g.y value before the erratum to RFC 7748 for
// the keygrip. The new value would be
// 5F51E65E475F794B1FE122D388B72EB36DC2B28192839E4DD6163A5D81312C14. See
// https://www.rfc-editor.org/errata/eid4730 and
// https://github.com/gpg/libgcrypt/commit/f67b6492e0b0
hash(grip, Hex.decodeStrict("04"
+ "0000000000000000000000000000000000000000000000000000000000000009"
+ "20AE19A1B8A086B4E01EDD2C7748D14C923D4D7E6D7C61B229E9C5A27ECED3D9"),
'g', false);
hash(grip, Hex.decodeStrict(
"1000000000000000000000000000000014DEF9DEA2F79CD65812631A5CF5D3ED"),
'n', false);
hashQ25519(grip, q);
return grip.digest();
}
private static X9ECParameters getX9Parameters(
ASN1ObjectIdentifier curveOID) {
X9ECParameters params = CustomNamedCurves.getByOID(curveOID);
if (params == null) {
params = ECNamedCurveTable.getByOID(curveOID);
}
return params;
}
}