diff --git a/lib/BUILD b/lib/BUILD index b56ba2f66..8ad49d450 100644 --- a/lib/BUILD +++ b/lib/BUILD @@ -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"], ) diff --git a/org.eclipse.jgit.gpg.bc.test/.classpath b/org.eclipse.jgit.gpg.bc.test/.classpath index f08af0a4e..0acccbaae 100644 --- a/org.eclipse.jgit.gpg.bc.test/.classpath +++ b/org.eclipse.jgit.gpg.bc.test/.classpath @@ -2,10 +2,15 @@ - + - + + + + + + diff --git a/org.eclipse.jgit.gpg.bc.test/.gitignore b/org.eclipse.jgit.gpg.bc.test/.gitignore index 934e0e06f..8b6760c93 100644 --- a/org.eclipse.jgit.gpg.bc.test/.gitignore +++ b/org.eclipse.jgit.gpg.bc.test/.gitignore @@ -1,2 +1,3 @@ /bin +/bin-tst /target diff --git a/org.eclipse.jgit.gpg.bc.test/BUILD b/org.eclipse.jgit.gpg.bc.test/BUILD index 1e3677d92..59859b283 100644 --- a/org.eclipse.jgit.gpg.bc.test/BUILD +++ b/org.eclipse.jgit.gpg.bc.test/BUILD @@ -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 .", +) diff --git a/org.eclipse.jgit.gpg.bc.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.gpg.bc.test/META-INF/MANIFEST.MF index 35a418c5a..39ece1fcf 100644 --- a/org.eclipse.jgit.gpg.bc.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.gpg.bc.test/META-INF/MANIFEST.MF @@ -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)" diff --git a/org.eclipse.jgit.gpg.bc.test/build.properties b/org.eclipse.jgit.gpg.bc.test/build.properties index 9ffa0caf7..e36d6667e 100644 --- a/org.eclipse.jgit.gpg.bc.test/build.properties +++ b/org.eclipse.jgit.gpg.bc.test/build.properties @@ -1,5 +1,5 @@ source.. = tst/ -output.. = bin/ +output.. = bin-tst/ bin.includes = META-INF/,\ .,\ plugin.properties diff --git a/org.eclipse.jgit.gpg.bc.test/pom.xml b/org.eclipse.jgit.gpg.bc.test/pom.xml index f244fb476..cac7e151e 100644 --- a/org.eclipse.jgit.gpg.bc.test/pom.xml +++ b/org.eclipse.jgit.gpg.bc.test/pom.xml @@ -85,6 +85,12 @@ src/ tst/ + + + tst-rsrc/ + + + org.apache.maven.plugins diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/brainpool256.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/brainpool256.asc new file mode 100644 index 000000000..8427cfcc0 Binary files /dev/null and b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/brainpool256.asc differ diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/brainpool384.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/brainpool384.asc new file mode 100644 index 000000000..bdb20fe93 Binary files /dev/null and b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/brainpool384.asc differ diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/brainpool512.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/brainpool512.asc new file mode 100644 index 000000000..5b4bca2c6 Binary files /dev/null and b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/brainpool512.asc differ diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/dsa-elgamal.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/dsa-elgamal.asc new file mode 100644 index 000000000..db067325c Binary files /dev/null and b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/dsa-elgamal.asc differ diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/ed25519.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/ed25519.asc new file mode 100644 index 000000000..636a5a95f Binary files /dev/null and b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/ed25519.asc differ diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/nistp256.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/nistp256.asc new file mode 100644 index 000000000..fd1509e9b Binary files /dev/null and b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/nistp256.asc differ diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/nistp384.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/nistp384.asc new file mode 100644 index 000000000..b2b59959e Binary files /dev/null and b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/nistp384.asc differ diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/nistp521.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/nistp521.asc new file mode 100644 index 000000000..db18f81a2 Binary files /dev/null and b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/nistp521.asc differ diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/rsa.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/rsa.asc new file mode 100644 index 000000000..e74df7a2a Binary files /dev/null and b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/rsa.asc differ diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/secp256k1.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/secp256k1.asc new file mode 100644 index 000000000..837f8a867 Binary files /dev/null and b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/secp256k1.asc differ diff --git a/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/x25519.asc b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/x25519.asc new file mode 100644 index 000000000..d531d7a8b Binary files /dev/null and b/org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/x25519.asc differ diff --git a/org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/keys/KeyGrip25519Test.java b/org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/keys/KeyGrip25519Test.java new file mode 100644 index 000000000..e30080258 --- /dev/null +++ b/org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/keys/KeyGrip25519Test.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2021, Thomas Wolf 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); + } +} diff --git a/org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/keys/KeyGripTest.java b/org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/keys/KeyGripTest.java new file mode 100644 index 000000000..a4aaf40d9 --- /dev/null +++ b/org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/keys/KeyGripTest.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2021, Thomas Wolf 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 process) + throws IOException, PGPException { + PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection( + PGPUtil.getDecoderStream(in), new JcaKeyFingerprintCalculator()); + + Iterator keyRings = pgpPub.getKeyRings(); + while (keyRings.hasNext()) { + PGPPublicKeyRing keyRing = keyRings.next(); + + Iterator 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]); + } + } +} diff --git a/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF b/org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF index e5f432dc5..b379a2b47 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 83ed9059e..f2aa014d6 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 9403ba235..4753ac138 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 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; } 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 13655c0d5..7f0f32a2a 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.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 keyFiles = Files.walk(USER_SECRET_KEY_DIR)) { - List 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 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 000000000..b1d444601 --- /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 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 keygrip 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 "<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. + *

+ * Package-visible for tests only. + *

+ * + * @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. + *

+ * Package-visible for tests only. + *

+ * + * @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; + } + +}