blob: c9317247106b4be08cf57ac76ff158654d08a9a4 [file] [log] [blame]
/*
* 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.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 static ASN1ObjectIdentifier CURVE25519 = ECNamedCurveTable
.getOID("curve25519"); //$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 (CURVE25519.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;
}
}