/* * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ import java.io.*; import java.math.BigInteger; import java.nio.file.Path; import java.security.*; import java.security.spec.*; import java.util.*; import sun.security.ec.*; import sun.security.ec.point.*; import sun.security.util.ArrayUtil; import sun.security.util.math.*; import jdk.test.lib.Asserts; /* * @test * @bug 8189189 8147502 8295010 * @summary Test ECDSA primitive operations * @library /test/lib * @modules java.base/sun.security.ec java.base/sun.security.ec.point * java.base/sun.security.util java.base/sun.security.util.math * @run main ECDSAPrimitive */ public class ECDSAPrimitive { private static final Map CURVE_NAME_MAP = Map.ofEntries( Map.entry("P-256", "secp256r1"), Map.entry("P-384", "secp384r1"), Map.entry("P-521", "secp521r1") ); private static final Set DIGEST_NAME_SET = Set.of( "SHA-224", "SHA-256", "SHA-384", "SHA-512" ); public static void main(String[] args) throws Exception { Path siggenFile = Path.of(System.getProperty("test.src"), "SigGen-1.txt"); ECParameterSpec ecParams = null; String digestAlg = null; try (BufferedReader in = new BufferedReader(new FileReader( siggenFile.toFile()))) { Map values = new HashMap<>(); String line = in.readLine(); while (line != null) { line = line.trim(); if (line.startsWith("#") || line.length() == 0) { // ignore } else if (line.startsWith("[")) { // change curve and hash StringTokenizer tok = new StringTokenizer(line, "[,]"); String name = tok.nextToken(); String curveName = lookUpCurveName(name); String digestName = tok.nextToken(); digestAlg = lookUpDigestName(digestName); if (curveName == null) { System.out.println("Unknown curve: " + name + ". Skipping test"); ecParams = null; digestAlg = null; } if (digestAlg == null) { System.out.println("Unknown digest: " + digestName + ". Skipping test"); ecParams = null; digestAlg = null; } else { AlgorithmParameters params = AlgorithmParameters.getInstance("EC", System.getProperty("test.provider.name", "SunEC")); params.init(new ECGenParameterSpec(curveName)); ecParams = params.getParameterSpec( ECParameterSpec.class); System.out.println("Testing curve/digest: " + curveName + "/" + digestAlg); } } else if (line.startsWith("S")) { addKeyValue(line, values); if (ecParams != null) { runTest(ecParams, digestAlg, values); } } else { addKeyValue(line, values); } line = in.readLine(); } } } private static void runTest(ECParameterSpec ecParams, String digestAlg, Map values) throws Exception { Optional opsOpt = ECDSAOperations.forParameters(ecParams); Optional signerOpt = opsOpt.map(OpsSigner::new); Signer signer = signerOpt.orElseGet(() -> new JCASigner(ecParams)); byte[] msg = values.get("Msg"); MessageDigest md = MessageDigest.getInstance(digestAlg); byte[] digest = md.digest(msg); // all operations accept little endian private key and nonce byte[] privateKey = values.get("d"); byte[] k = values.get("k"); byte[] computedSig = signer.sign(privateKey, digest, k); int valueLength = computedSig.length / 2; byte[] computedR = Arrays.copyOf(computedSig, valueLength); byte[] expectedR = values.get("R"); Asserts.assertEquals(new BigInteger(1, expectedR), new BigInteger(1, computedR), "R"); byte[] computedS = Arrays.copyOfRange(computedSig, valueLength, 2 * valueLength); byte[] expectedS = values.get("S"); Asserts.assertEquals(new BigInteger(1, expectedS), new BigInteger(1, computedS), "S"); // ensure public key is correct byte[] expectedQx = values.get("Qx"); byte[] expectedQy = values.get("Qy"); ECPoint ecPublicKey = signer.checkPublicKey(privateKey, expectedQx, expectedQy); // ensure the verification works if (!signer.verify(ecPublicKey, digest, computedSig)) { throw new RuntimeException("Signature did not verify"); } // ensure incorrect signature does not verify int length = k.length; computedSig[length / 2] ^= (byte) 1; if (signer.verify(ecPublicKey, digest, computedSig)) { throw new RuntimeException("Incorrect signature verified"); } computedSig[length / 2] ^= (byte) 1; computedSig[length + length / 2] ^= (byte) 1; if (signer.verify(ecPublicKey, digest, computedSig)) { throw new RuntimeException("Incorrect signature verified"); } System.out.println("Test case passed"); } private static void addKeyValue(String line, Map values) { StringTokenizer tok = new StringTokenizer(line, " ="); String key = tok.nextToken(); String value = tok.nextToken(); byte[] valueArr; if (value.length() <= 2) { valueArr = new byte[1]; valueArr[0] = Byte.parseByte(value, 10); } else { // some values are odd-length big-endian integers if (value.length() % 2 == 1) { if (key.equals("Msg")) { throw new RuntimeException("message length may not be odd"); } value = "0" + value; } valueArr = HexFormat.of().parseHex(value); } values.put(key, valueArr); } private static String lookUpCurveName(String name) { return CURVE_NAME_MAP.get(name); } private static String lookUpDigestName(String name) { return DIGEST_NAME_SET.contains(name) ? name : null; } public static boolean verifySignedDigest(ECDSAOperations ops, ECPoint publicKey, byte[] digest, byte[] signature) { try { return verifySignedDigestImpl(ops, publicKey, digest, signature); } catch (ImproperSignatureException ex) { return false; } } private static boolean verifySignedDigestImpl(ECDSAOperations ops, ECPoint publicKey, byte[] digest, byte[] signature) throws ImproperSignatureException { ECOperations ecOps = ops.getEcOperations(); IntegerFieldModuloP orderField = ecOps.getOrderField(); int orderBits = orderField.getSize().bitLength(); if (orderBits % 8 != 0 && orderBits < digest.length * 8) { // This implementation does not support truncating digests to // a length that is not a multiple of 8. throw new ProviderException("Invalid digest length"); } // decode signature as (r, s) byte[] rBytes = Arrays.copyOf(signature, signature.length / 2); ArrayUtil.reverse(rBytes); byte[] sBytes = Arrays.copyOfRange(signature, signature.length / 2, signature.length); ArrayUtil.reverse(sBytes); // convert r and s to field elements // TODO: reject non-canonical values IntegerModuloP s = orderField.getElement(sBytes); IntegerModuloP r = orderField.getElement(rBytes); // truncate the digest and interpret as a field element int length = (orderBits + 7) / 8; int lengthE = Math.min(length, digest.length); byte[] E = new byte[lengthE]; System.arraycopy(digest, 0, E, 0, lengthE); ArrayUtil.reverse(E); IntegerModuloP e = orderField.getElement(E); // perform the calculation IntegerModuloP sInverse = s.multiplicativeInverse(); IntegerModuloP u1 = e.multiply(sInverse); IntegerModuloP u2 = r.multiply(sInverse); byte[] u1Bytes = u1.asByteArray(length); byte[] u2Bytes = u2.asByteArray(length); AffinePoint publicKeyPoint = ECDSAOperations.toAffinePoint(publicKey, ecOps.getField()); MutablePoint R = ecOps.multiply(publicKeyPoint, u2Bytes); AffinePoint a1 = ops.basePointMultiply(u1Bytes); MutablePoint p2 = new ProjectivePoint.Mutable( a1.getX(false).mutable(), a1.getY(false).mutable(), ecOps.getField().get1().mutable()); ecOps.setSum(R, p2); // can't continue if R is neutral if (ecOps.isNeutral(R)) { throw new ImproperSignatureException(); } IntegerModuloP xr = R.asAffine().getX(); byte[] temp = new byte[length]; xr.asByteArray(temp); IntegerModuloP v = orderField.getElement(temp); // Check that v==r by subtracting and comparing result to 0 v.subtract(r).mutable().asByteArray(temp); return ECOperations.allZero(temp); } private interface Signer { byte[] sign(byte[] privateKey, byte[] digest, byte[] k); ECPoint checkPublicKey(byte[] privateKey, byte[] expectedQx, byte[] expectedQy); boolean verify(ECPoint ecPublicKey, byte[] digest, byte[] sig); } private static class FixedRandom extends SecureRandom { private final byte[] val; public FixedRandom(byte[] val) { BigInteger biVal = new BigInteger(1, val); biVal = biVal.subtract(BigInteger.ONE); byte[] temp = biVal.toByteArray(); this.val = new byte[val.length]; int inStartPos = Math.max(0, temp.length - val.length); int outStartPos = Math.max(0, val.length - temp.length); System.arraycopy(temp, inStartPos, this.val, outStartPos, temp.length - inStartPos); } @Override public void nextBytes(byte[] bytes) { Arrays.fill(bytes, (byte) 0); int copyLength = Math.min(val.length, bytes.length - 2); System.arraycopy(val, 0, bytes, bytes.length - copyLength - 2, copyLength); } } // The signature verification function lives here. It is not used in the // JDK, but it is working, and the performance is roughly as good as the // native implementation in the JDK. private static class JCASigner implements Signer { private static final String SIG_ALG = "NONEwithECDSAinP1363Format"; private final ECParameterSpec ecParams; private JCASigner(ECParameterSpec ecParams) { this.ecParams = ecParams; } @Override public byte[] sign(byte[] privateKey, byte[] digest, byte[] k) { try { KeyFactory kf = KeyFactory.getInstance("EC", "SunEC"); BigInteger s = new BigInteger(1, privateKey); ECPrivateKeySpec privKeySpec = new ECPrivateKeySpec(s, ecParams); PrivateKey privKey = kf.generatePrivate(privKeySpec); Signature sig = Signature.getInstance(SIG_ALG, "SunEC"); sig.initSign(privKey, new FixedRandom(k)); sig.update(digest); return sig.sign(); } catch (Exception ex) { throw new RuntimeException(ex); } } @Override public ECPoint checkPublicKey(byte[] privateKey, byte[] expectedQx, byte[] expectedQy) { // no way to compute the public key using the API BigInteger x = new BigInteger(1, expectedQx); BigInteger y = new BigInteger(1, expectedQy); return new ECPoint(x, y); } @Override public boolean verify(ECPoint ecPublicKey, byte[] digest, byte[] providedSig) { try { KeyFactory kf = KeyFactory.getInstance("EC", "SunEC"); ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(ecPublicKey, ecParams); PublicKey pubKey = kf.generatePublic(pubKeySpec); Signature sig = Signature.getInstance(SIG_ALG, "SunEC"); sig.initVerify(pubKey); sig.update(digest); return sig.verify(providedSig); } catch (Exception ex) { throw new RuntimeException(ex); } } } private static class OpsSigner implements Signer { private final ECDSAOperations ops; public OpsSigner(ECDSAOperations ops) { this.ops = ops; } @Override public byte[] sign(byte[] privateKey, byte[] digest, byte[] k) { privateKey = privateKey.clone(); ArrayUtil.reverse(privateKey); k = k.clone(); ArrayUtil.reverse(k); ECDSAOperations.Nonce nonce = new ECDSAOperations.Nonce(k); try { return ops.signDigest(privateKey, digest, nonce); } catch (Exception ex) { throw new RuntimeException(ex); } } @Override public ECPoint checkPublicKey(byte[] privateKey, byte[] expectedQx, byte[] expectedQy) { privateKey = privateKey.clone(); ArrayUtil.reverse(privateKey); AffinePoint publicKey = ops.basePointMultiply(privateKey); int length = privateKey.length; byte[] computedQx = new byte[length]; byte[] computedQy = new byte[length]; publicKey.getX().asByteArray(computedQx); ArrayUtil.reverse(computedQx); Asserts.assertEqualsByteArray(expectedQx, computedQx, "Qx"); publicKey.getY().asByteArray(computedQy); ArrayUtil.reverse(computedQy); Asserts.assertEqualsByteArray(expectedQy, computedQy, "Qy"); BigInteger bigX = publicKey.getX().asBigInteger(); BigInteger bigY = publicKey.getY().asBigInteger(); return new ECPoint(bigX, bigY); } @Override public boolean verify(ECPoint publicKey, byte[] digest, byte[] sig) { return verifySignedDigest(ops, publicKey, digest, sig); } } /* * An exception indicating that a signature is not formed correctly. */ private static class ImproperSignatureException extends Exception { private static final long serialVersionUID = 1; } }