aboutsummaryrefslogtreecommitdiffstats
path: root/org.eclipse.jgit.gpg.bc
diff options
context:
space:
mode:
authorThomas Wolf <thomas.wolf@paranor.ch>2021-01-17 16:21:28 +0100
committerMatthias Sohn <matthias.sohn@sap.com>2021-02-16 00:37:01 +0100
commit64cbea8a9794047fe576d03ab8a46e4eaf7eabee (patch)
tree4dc95a2278643c1d8faa70875043301de94ebc6a /org.eclipse.jgit.gpg.bc
parent3774fcc848da7526ffa74211cbb2781df5731125 (diff)
downloadjgit-64cbea8a9794047fe576d03ab8a46e4eaf7eabee.tar.gz
jgit-64cbea8a9794047fe576d03ab8a46e4eaf7eabee.zip
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>
Diffstat (limited to 'org.eclipse.jgit.gpg.bc')
-rw-r--r--org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF13
-rw-r--r--org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties8
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java6
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java112
-rw-r--r--org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/KeyGrip.java322
5 files changed, 399 insertions, 62 deletions
diff --git a/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF b/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF
index e5f432dc57..b379a2b47e 100644
--- a/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF
@@ -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"
diff --git a/org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties b/org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties
index 83ed9059ec..f2aa014d6b 100644
--- a/org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties
+++ b/org.eclipse.jgit.gpg.bc/resources/org/eclipse/jgit/gpg/bc/internal/BCText.properties
@@ -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} \ No newline at end of file
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java
index 9403ba2352..4753ac138d 100644
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BCText.java
@@ -27,9 +27,11 @@ public final class BCText extends TranslationBundle {
}
// @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 final class BCText extends TranslationBundle {
/***/ public String signatureParseError;
/***/ public String signatureVerificationError;
/***/ public String unableToSignCommitNoSecretKey;
+ /***/ public String uncompressed25519Key;
+ /***/ public String unknownCurve;
+ /***/ public String unknownCurveParameters;
+ /***/ public String unknownKeyType;
}
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java
index 13655c0d55..7f0f32a2a7 100644
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgKeyLocator.java
@@ -27,12 +27,8 @@ import java.nio.file.Paths;
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.bouncycastle.util.encoders.Hex;
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 class BouncyCastleGpgKeyLocator {
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 @@ public class BouncyCastleGpgKeyLocator {
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();
+ }
}
}
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/KeyGrip.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/KeyGrip.java
new file mode 100644
index 0000000000..b1d4446010
--- /dev/null
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/KeyGrip.java
@@ -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;
+ }
+
+}