From 64cbea8a9794047fe576d03ab8a46e4eaf7eabee Mon Sep 17 00:00:00 2001 From: Thomas Wolf Date: Sun, 17 Jan 2021 16:21:28 +0100 Subject: [PATCH] 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 --- lib/BUILD | 2 + org.eclipse.jgit.gpg.bc.test/.classpath | 9 +- org.eclipse.jgit.gpg.bc.test/.gitignore | 1 + org.eclipse.jgit.gpg.bc.test/BUILD | 20 ++ .../META-INF/MANIFEST.MF | 12 +- org.eclipse.jgit.gpg.bc.test/build.properties | 2 +- org.eclipse.jgit.gpg.bc.test/pom.xml | 6 + .../gpg/bc/internal/keys/brainpool256.asc | Bin 0 -> 701 bytes .../gpg/bc/internal/keys/brainpool384.asc | Bin 0 -> 872 bytes .../gpg/bc/internal/keys/brainpool512.asc | Bin 0 -> 1046 bytes .../jgit/gpg/bc/internal/keys/dsa-elgamal.asc | Bin 0 -> 2627 bytes .../jgit/gpg/bc/internal/keys/ed25519.asc | Bin 0 -> 372 bytes .../jgit/gpg/bc/internal/keys/nistp256.asc | Bin 0 -> 697 bytes .../jgit/gpg/bc/internal/keys/nistp384.asc | Bin 0 -> 859 bytes .../jgit/gpg/bc/internal/keys/nistp521.asc | Bin 0 -> 1050 bytes .../eclipse/jgit/gpg/bc/internal/keys/rsa.asc | Bin 0 -> 2407 bytes .../jgit/gpg/bc/internal/keys/secp256k1.asc | Bin 0 -> 689 bytes .../jgit/gpg/bc/internal/keys/x25519.asc | Bin 0 -> 612 bytes .../bc/internal/keys/KeyGrip25519Test.java | 61 ++++ .../gpg/bc/internal/keys/KeyGripTest.java | 143 ++++++++ org.eclipse.jgit.gpg.bc/META-INF/MANIFEST.MF | 13 +- .../jgit/gpg/bc/internal/BCText.properties | 8 +- .../eclipse/jgit/gpg/bc/internal/BCText.java | 6 + .../internal/BouncyCastleGpgKeyLocator.java | 112 +++--- .../jgit/gpg/bc/internal/keys/KeyGrip.java | 322 ++++++++++++++++++ 25 files changed, 650 insertions(+), 67 deletions(-) create mode 100644 org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/brainpool256.asc create mode 100644 org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/brainpool384.asc create mode 100644 org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/brainpool512.asc create mode 100644 org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/dsa-elgamal.asc create mode 100644 org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/ed25519.asc create mode 100644 org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/nistp256.asc create mode 100644 org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/nistp384.asc create mode 100644 org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/nistp521.asc create mode 100644 org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/rsa.asc create mode 100644 org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/secp256k1.asc create mode 100644 org.eclipse.jgit.gpg.bc.test/tst-rsrc/org/eclipse/jgit/gpg/bc/internal/keys/x25519.asc create mode 100644 org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/keys/KeyGrip25519Test.java create mode 100644 org.eclipse.jgit.gpg.bc.test/tst/org/eclipse/jgit/gpg/bc/internal/keys/KeyGripTest.java create mode 100644 org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/keys/KeyGrip.java 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 0000000000000000000000000000000000000000..8427cfcc0555a1385dad05283e3d21006ba94388 GIT binary patch literal 701 zcmaJ<$*!V60KD@R-pvc>h~mLRH{C$f>^?xesUVxm_Gp%`AK%O*bC_i6Sd~;&D)sU4 z2E;0{7LMvH93L7)UQiq@pkd4OTm9g;r()u;vk~)5olY(S10lf08c0p-8upmy+L;fw z>_^Gk?E3P)KI>&=J3-wuMD2XyQr(u%I~_0aXpq&xROF!~a3F}J1j5XxvYQ$pb;EZf2(k?V zSpt+CV^}!KeyYw~vC^KteQX6YFV^gpMna{4#~5^!068t)zkCfmfOxN(Rg}jXFI<$z znD|@}S~@M9{4#D?tTlZr#tl63UX~HSuR6znO@J_PIuW$$GrFw^^k}{db+IKocAU5t zdZPKT^j^9B8I$}oCf*0J6q7>J%cs}zl4;Bv+TMp*gp|wrSF`#Q##g-+ZG6^aK?p{$ z4E$4!eeHfhN%nPQyL^`GX7jk7RJp{eV(r|)gfd77mjE}yLkb8WR|KOBf@mJ^w?>Wj zp7=mzvqz-bx8g>%D0KB(aFuHSDji$DdG1>f%n0!7TD&!4_biT#A|9({)w#plsZ6G{ zb9-DPWF$E5n8bjyFP5H0PwTG-7YfpRy-cGD1-!h-T2Ey~$_*zWo20FB{yrQI+<|qM N4fpGM@8tjd@+ZKw-Q@rP literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..bdb20fe939744e44c9af55d5d4d45c777accf982 GIT binary patch literal 872 zcmaKrxpJdG07P@X!nw=EB^wAfm>C8%2*f4Ck%?;n36KQh`ueqZgUz&Tt@EgYMx zzR^_MnPfWh7;CdN(ayVo_~nk^c8djTj61N|s!vAX zp74AH4j?I*0~5J)&j4!GfMQ*9djo+1K^P-2W3;Ch(sm#uu9w#J*`m!hwnyoRZGMo) ztv@I2*wX+pkrvJ`3J%DF|3Jg9AW|GYI7?c5*8}44;>B^e{1YznKL1{-FP1#o#4uz+ z$7ChD@*5{F1ksgGB@I_w;4NW>CEtgQJ*a+Tf3~WLp3-^;n)XEuKo{w2^n*fS+;)`) zQ`y;}>h9ioHOsn8(e-+7jvkRJ5o%Ah(twZr;reX`A%G#8EfwNZaDC`cM!L(_NXT1J zZnkZSyo!IfO{r^R{!31ZURldoO}llGVP98{l29jX^r$V$lO*eYIWviRi766}nrml- zP2~%%L?>&W5lt={O47hnCPD4Y;EpsoNf*_a@nbHICtyM3wuEVc8~>;w-)dACS5SK$ zj9^8MEyUu$xl*{TqFtTai%>(?$j+&X0O$>@|LvI{KzRboy3Ajso^x}zA=AZf{*HdF7zANFBVV$ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5b4bca2c67c21ef74fc8980af295fc1a22345f71 GIT binary patch literal 1046 zcma*mJF}un6b0a%UvYBk6rg|*Owir5*dUMQrI-OKC~rX!e*K)8Xrfzpg?mN2zFObk zKY>^$_Lr->Uv3DG1bvw}{(?sbnSa`EhN%q;p9TjPAImTos)3mhK(P&hg~3KyDy?a@ zM{hUEn6|T|4J+#E5U|U;7<*lEls?zhEABCRkz3THD)zUr$Tv~ ziMpjkM^MNc*757bbn7h>ClN_onMbGS885;k)54F{K0^qt0Oddd#h^ht7zkttECc}> zM<_1|nhD~`*;G%Ww2-ty4bFma+pki zmam0xpE>Pp?u?jJ`Q`lCg?doBVzJ#*|FNLZD54HkQKTngHG9b{`UjksFUA0t2J##N z%40BlXv94&alTURW9#V2U*AtY(WmCXY6r7&#YpbdqY;rKAH4U%`@+E0<;C0Gn=Zj- zT2YWq(-m{NtaIZBUj~m;71ha#w2V%UYjG`#%CSrUzj;9`yWI<>RA*CT9Tjie1Gd=_ zj@zZEhl#g{r;@unKLi~OZ%OLJQ%E?DT?V)b_&A{!M0rh^_YW+dTR;Fwwf{$%|4YgI zF}~0rN>2>MB4(s^pXuAC*0SxUGKZ{EkXaU=)Vx8cZ`jfe`3jgTmcAbYDaIjRY=zibF*K# zbI+3o{ywO-HH0!hLIrwBYfEteJSj9$A%^re1ZLru z&&Qe@?29U*zSuCn>)4BMPE1XeEI}5ac$%bFB=z2cC%m3L72{00kgEOheJAZVe9OK{ zIYb!v#VGZf(CwN8E|q?H6B#&?8BG3HA?38)`bOsVAb++hL@ zm?Bo+l=kvY8`mV@6Ct+7LZgVIQ+I|YL9VzkCTYTzGm)6j_(xAo1RP;A@4aP{1V+G; zqQEU$I(c*`CLpij;s~Q^J%hoC_3%Zf#9Sj(4`-9@g1$Y`S*R)eo}GF`uY&|ZlXqiS zVWM#CkLiUxgO79ak$afp!>Y4*?+fwGQT}Zo3!cmA^b4=6UjfC%QM200Edv+Gp&FhY zrY%;9Xn95-&q9qvLp@>L*z#PqdSOmYdRq^hfsy@4ef>VJwA16{cM~Q(mB}DRU==9Q zM?8U?Lvy~4&$rY}pa}V~*xPfKebXDd_Wd~v%xKh<>L4*;H#^H__}eR5`CZXfO;03) z`J8pFo%rh4MS1RUVl8m*ApgoBHN+P#NiLKXMozWu!{+z(JedI43b~i>4=w0ypRH?GZ&&?V|9U4w!% zjtXJ=vYUuVlYuzq4B)AeObd~Gg7xjjXm8^wX>cJkCkV-sm2r{d)kgLW&@FY z#gwOGBuQ{j>7H)pz&P}zWAoA_y^N~Cu7M1` zXYhM0K&U$B|M>Z?5*fafa-^+!6#N0A$xOJg9N9eDzr6!ztaz4VxV4bH7;uj~(3&ap zw1rPd_{XMqkrBnx2p=S2&-iHMLBMOykjdwp0m4^?>#p_r�F5%jDFx1Xi58wiBD#OwKKY3uYUDY^F=hwUEY06~j##Ucxfdz0QT zH<1a9z;AR@?Xo3gXiOd(50q!pC*!QkNj4A?1DEN%D7%p`i-;;FCkzDAq;8-+YQyN` zCP&Nkz17S7NxkTi0dV*bYcZ9=IOEIMzAr@Mct?+b(ov*p=urCfA9RXL>?k;Mlece3 zCB8c{txg^8hu?7CBXzD|OE&Zk5Ih6gtL;#YvN2teX7gJ3c^x_b>MAuIbUCzRf4pC$ zyj~LyYr=S@CWA@BSpGdaHFYN;_+^&;fC94v9|46?<&7-PQUitIhb78B`_`E%7OgD} z)$WOb#eS)@N`b}n6^wIFAJSqP49aD);vkMod|kT`Y(0ZTvIu!VD%~QunqG!!7qIIc$vu6vxc-kUN5l;)js_LRV5n}g$KppFL=2YI1G=U z(T0IN!9~^k?w7c&L-=d*bL%&b0Q1v)l{OBAK}ZSWRP_35Fu$kPLFI!%Osc89g;mA9 zpFicWWOgE2uR6INu->EwIAY=asGAW1rchFo>kYFjxrE{UNk3RYCFkJjW8R!V!_2C- zhL<`kKMEFcp?8>r_dTL~zwIsWqy+Nuw^3p+9-$)h{qNu}W<(WOsSB-R=%v*Z&sTps z3RIw8jQ=GW@^4W_Q~wWTg7kJIF#WdBpM|4rQU}>D4YT60w(oEI-L7*p0L=_BBYc&~o=Xz`RXu^i=hb{mIwUXjwhl8pwaxgVG`YEdV7`{5R_G FKLM+=SMUG; literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..636a5a95f3f48270fb90b0dbe8b55d79a8b0a8c4 GIT binary patch literal 372 zcmaKoyLN&=6h-^_3VN3dd?6MVcP=Oc@-%>B2aG`wg@_6G_c3YGWtV5K)9ms11vR$J zmHe$Q`4QbQlx%}bbmyV{W5oK2Z)jlUaVon2jb%80JE=DS za^*Z`VH9PH;i1l~L^U`0oqs6dU5A`I-2wP=RvufwGja&@tDTwC$?noOSVv?P-Xm+z jk2j}I z0?6%i(~j6+c^cXlViZ1wcderc0%{nI%;>|hMm{Y40eV5|;Bi5d(BWE-Z`S=i@HCFU z^`4W~d$<{l>Kp|K^=9D8A7H^2s9fwAJ{TNg>I#5#639|f_p2dOw48ZQYf`QmcieEnU-dHy>L0c!6yJw5K39#Z%&gyi%hLa5u3?U5G&OfU(9EXf_eBjOey0G6l(g?iP_Mgy tHaVB%M3+l-%_cwTo?qvgNl-|4r-rGMwtRkapO*ODa(`}zJ>x&0`~|H}*+KvS literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b2b59959e6e5f73141cae12ccb68bb51b07950c6 GIT binary patch literal 859 zcmaKrxsszu00n)%qJ5%62xuW0V^kFd6i0K2+d~`$0)zmmOXBNycAXt-p83+ti2VHg zJ&1Mk{o(1}hZjMcpbraYAJBG@^)LP8xKTgiWOdOEI+`V83=mNP3xg2Z&=}^!ISRWp zZ<0XxxkT+wh{3SbUNIXn!Lp4lPKaRZA>zCGQ@3T4Je%?uZ7^v&$2Nscv4Sg1p3Umy zfygiOm4Dt`OC0Q{vX-Z5IdbU%DstO;5*a#C_NcCb8tA-$^!S>+_srR-(Ahr5FUJHb zLppOb#Cif5Q3)lGgKcCXpW1->37`%Ht*nB!23m*l{7&r&+g0DQ!=@^siKfH7p0yL6 zG%&p46=J|4+WUWi$@Db^dU-u8UCQ!ZZ*D~~lF@)w)!SOBRAO>@o*VUrbDwt_#zEjo zyq`sd_Gs!E+^^Z;1X7tD>^6)j5&J=Qg}795cbIcHFi;+m6AQ;MY+T$;B6s=PpULB= zwJUYe0#mf)Yv=NXT(Y2GvQ=UlE!6h`p^ggOAeq~&8O}?#X@Y#Vjx%?3M_$ds9~L~K zVUfTz%Q8>LuQx15bEKgZTFZTRd3~95x3P_Yvvv~-dU^G;b@yk3TaCINlJyJ&*Wcc! zqmfqVF5TjhwnQw{{si2UD1fS+O30|Ll}Q(@LZYaQMPh@{+f+?Dad z+n?Pzo!cg`pl_D(1>^usCPx4N6cE@*NzaAr9D%)=_MTopz-hf2bRYzM^J<=b^}NSI zGkO(?xGjC>nG($K&AN;uv^StYssm0Avf}X`#KeuVc6Xlwn?4ouj^!zstMMD~zI#W+ lc1X1cb=q9zMA!$GmK0WP@KW~Xeu>6T;Qk8(f2)6~<{xES55oWe literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..db18f81a2968cb18b3078982c9a31c3269314965 GIT binary patch literal 1050 zcmaKrxsszu07QMhqJ5$R5FmjuMimsOxIqX69+vfBys` z6U=p@Xkh6}N6{BFY)Su@eiKBinJAoXS=~)V=MVytiSXtE!Hy+Oz$@oYhu)j$ zW-LB*=UhT+we|?c-XLjWk_tobvSQiDhIW;V;mdu>Gs?WPXoO;R^gk+ufI4a>O15LQ=R za!Ui0ZU{;f5}@K_P!&L_5%N#=U0eqo953R14QZ?ydii!+`_L=kxd*e<0I6);zjmFF zH>}#H!>2#?ub@fK_X3MmxKn}x>|4c|J{P^@I#a0mA-L)JlcxxtGed%JBLdw}5ke63 z(mpsDG+OI&PObM;^v(Q0h?XEjJAC6zIh96DZtc|BUr44F?x%#~1k#!glR`LKj>VTf zmsR6)%-E-KvOXRieEQVB^743l=30a9Nl|ONbGG%>tinah{!@03q)?E2_}8&Uy5?_-(5k8)=1vS;PuH!NHQZ@ph=CcfSXc@wbj>-^y=diuIx%+ufW9%tl6*-JIsSO#vd5w$TR75+Gvz>o7S#IEPeJar(O>NXe*DW-1O z%%iMH(J3jf(oG3BqXfF-f#03@1=X1TR9N4lR}G&7pSPE&OEXUEaTI2|4_%VV)${v$ zjL}`<1NU=ir(7#uIzyxw%k-pN=KR1>0{(!}WDT;5#7a!BMmY6TFTsc3R~7v@pfn^a z#h$#;pqYN|o)JkflqwV)mp&n{jkAMAWwc}a`Z(gxd|d*@5=$*@8SlJM)n#b;Lw=lY XTxa{~_#*zWkM2qQ{u){G|D66GnrlwU literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e74df7a2a9da4086d78d218dd231b0085b8d7a6b GIT binary patch literal 2407 zcmaJ@w-T({0qgmS+cR&-sc=DFG6$Bl9bw5iFJJF#H|{KTQY&fn&p&@5$f{=laaH$^ z8wjQ*{xRg_k6=2Y@gMge2pWKD3(6{0A5xHb@vc~6w7cZg_=mPT(w<3S%Oh!BO=ABw z4e%fASBleqJM0teVGrrk>i3IxtaiaC0UqmS+F8pCUNRGgnx!F85sM-#t{@9cN4T{T zG1Uf?XedqW(N$2RCiZI1(wig&C%1BDv<(b>C*dvQFK^W z^;xkJutCxxRY=$XlSHxG>56mctW>%(zrOp5!JUBTf|f9c^w^j`u*HLiYqz+|zU+vC z;#K3>P%Yu=9doZR5j)Y4WlmBy0k$N=;~uJySB>!b%|wWLj}7GLqDuIZlBTrpI9s2} zg`x3?GIGQ0`|>5`*Y@KQO3d(itNdkcAQRjA(!L3~Q`5MKI*E|zGhnUuq2Bsd0wjZ| zif5XG>n!8B9W-+_yq|PgKnyN@G0XM?uWH{Jm4H=NUz^($f+^eAZ^DH*IO|=60O{~A z&!I-mNVN2k4&&sqJ3aOf8Xcz7Aomee`xP0Dst}*NVDMmc|S>oD?%|hH)%TI6Xbn)x5@&h>G&Orp|l; z_5ss35vn`W%zc~4x({e+{lZ0cCv9v%G$O+(K?LyWpI108sPsVcp^#&(ZAEaz9 zYX~QWY7ZwTRe-m2rzh?o+d!xDkojz+j*xBxi4)gKB@{h6@5fYZxVXo}WA|*?F0&K+ z!b|A#G%bnh?@dJNtFkF zGyrUFoY-vhrSDvXo^P_p+v{iF3OWfjp5yd9vE@X{}&*_ zMFo`RXOU05TIk%Jk@~ErNEyi(dSgnc6rZ4Sv^m09$rf)8@vKj-hz;cXm8|8f$@U#b zoQvmQ|WY6Zf1k6*rsozUx&E%5#_x?r@09RF31^S6>(kjh zeh_wH9jQ{aCb>I(7{5DSkk>PQJztH|a#$uYa)uaup3<>Yoa8bqww>Iy^xFBhuIEFo zCy(Q}XY=`Cr2rvVSXcH%3_ABZmIQRCDjMF`7n;&)R3hf^?<=#RcC$DG%Gg zMLH|kNUHUTS}JBzcCx-tJYfUZuFQsO*~68FhckqIw#IK4O*Sb}$D)&Ih#Ga0M)2FM zdlnwGN-`_L7la2CPUAZ2{z}>n%{-&H;`f(d$WmI+Qv|W4D|mVF05aty+L$iQj5>Gc z=y~sxs4{Hyc*MKv3)!vRRz|0?JVW46^o_;RVhQ;^HB!_?oeyP2gA!_;DTP7t%#jFh zJ&~u0#*!Jq5v%1Y8!-1tdc8JAdCh$@vDyhR(QS)xhZ%}7aUy9I2n?^Hqh&Tb)~TF*u@v3{OMU}iI-iY}GlU=!qtAp-ZCFWB zmX;*?{717&Ej{TZ)S*Cn-Xz)$K6-i)ok`kv_p@2BU{(6NNDF!yMZd#pbFAE_Nt@qG zlb6h`RywK*Tf!+QypaYcv<{!x%gWFoV{=2|uC`G54)%6@WeVJTeh%_C7qyWEj@p5k zPII3B@NMmGP@vvBR!CDj0Af_g9(RcuOM*Wq|Resncj;jY@Yxnm}HvS;Mt!D#robOP-O$TY2H_nKw|Hz>% literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..837f8a867e434456ea4171dc8dfb6814861383d2 GIT binary patch literal 689 zcmaLV%Z{Qz7zN-xPvPDe6`{q&EdDAAsD|RA&?W8=5D*cOtNrwI$s{xBOy(@l*?l>S z#g8DOQ+wv>?#vC4Me)oe;S5<0Hh-=QL8ypAcfUEH8Hhg=Qqr(`12Dj;>^;Uqh()-h^V&|#Ly_BqF8}*0xH;UHh zr?DbC;ZAtj)XGkDBM9ZFOt7;ob_4-yuSB-p3EgqK+Qjn@Lkq;!suZ`ylsx$(h~~zL zPyyue&}-~BB>fBW5b!bG;wW;$J=lerc49hMh0|B*g&O^^f4rQ7Ddewx65l9)pa26W z_bR%T#9AsbVe;kLQLDp_d2FzQT6d6S14crTluHO@J_g7H$XoUm=ASP6;EM|;ex@#k z@c4;Y$j+_~fmr)RcZGYcQfLr+F^eRG@P;5FutUK>1EZo6_1BL*kE)qo%-!BPi}Tf) z&0dEHwfsp)!%4V7u7xKK$tK7Ru=Dwy0nqCn*{;?JtuxyZ8A8x3BC$e{mGFJ-A| zvK#k|eW%7Jl2<5#^m{|v_d=0V{{l1ymAHK@begoe>DO+`AAY>K2U)`q^0pX4#b^kL z?Y(juooQU3)SZ3RI>eh}D0aYeHU{iQ(nUv1g=b*VT1Z&bU`HFn3q~lzP!Mc#f(->i z%v<&+{K_^!5(IMZ{FZ7*NegTg9SU@URS#w4&+=#$=wKKLkYFr^ 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; + } + +}