org.jmrtd.Util.java Source code

Java tutorial

Introduction

Here is the source code for org.jmrtd.Util.java

Source

/*
 * JMRTD - A Java API for accessing machine readable travel documents.
 *
 * Copyright (C) 2006 - 2015  The JMRTD team
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 *
 * $Id$
 */

package org.jmrtd;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.Provider;
import java.security.PublicKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.ECField;
import java.security.spec.ECFieldF2m;
import java.security.spec.ECFieldFp;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.ECPublicKeySpec;
import java.security.spec.EllipticCurve;
import java.security.spec.KeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.logging.Logger;

import javax.crypto.BadPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.interfaces.DHPublicKey;
import javax.crypto.spec.DHParameterSpec;
import javax.crypto.spec.DHPublicKeySpec;
import javax.crypto.spec.SecretKeySpec;

import org.bouncycastle.asn1.ASN1Encoding;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.eac.EACObjectIdentifiers;
import org.bouncycastle.asn1.pkcs.DHParameter;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.asn1.x9.X962NamedCurves;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.crypto.params.DHParameters;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
import org.bouncycastle.math.ec.ECCurve;
import org.bouncycastle.math.ec.ECFieldElement;
import org.jmrtd.lds.SecurityInfo;
import org.jmrtd.lds.icao.MRZInfo;

import net.sf.scuba.tlv.TLVOutputStream;
import net.sf.scuba.util.Hex;

/* FIXME: Move some of these to specific protocol classes. */

/**
 * Some static helper functions. Mostly dealing with low-level crypto.
 *
 * @deprecated The visibility of this class will be changed to package.
 *
 * @author Wojciech Mostowski
 * @author Cees-Bart Breunesse (ceesb@cs.ru.nl)
 * @author Engelbert Hubbers (hubbers@cs.ru.nl)
 * @author Martijn Oostdijk (martijn.oostdijk@innovalor.com)
 * @author Ronny Wichers Schreur (ronny@cs.ru.nl)
 *
 * @version $Revision$
 */
public class Util {

    private static final Logger LOGGER = Logger.getLogger("org.jmrtd");

    /** Mode for KDF. */
    public static final int ENC_MODE = 1, MAC_MODE = 2, PACE_MODE = 3;

    private static final Provider BC_PROVIDER = JMRTDSecurityProvider.getBouncyCastleProvider();

    private Util() {
    }

    /**
     * Derives the ENC or MAC key for BAC from the keySeed.
     *
     * @param keySeed the key seed.
     * @param mode either <code>ENC_MODE</code> or <code>MAC_MODE</code>
     *
     * @return the key
     *
     * @throws GeneralSecurityException on security error
     */
    public static SecretKey deriveKey(byte[] keySeed, int mode) throws GeneralSecurityException {
        return deriveKey(keySeed, "DESede", 128, mode);
    }

    /**
     * Derives the ENC or MAC key for BAC or PACE
     *
     * @param keySeed the key seed.
     * @param cipherAlgName either AES or DESede
     * @param keyLength key length in bits
     * @param mode either {@code ENC_MODE}, {@code MAC_MODE}, or {@code PACE_MODE}
     *
     * @return the key.
     *
     * @throws GeneralSecurityException on security error
     */
    public static SecretKey deriveKey(byte[] keySeed, String cipherAlgName, int keyLength, int mode)
            throws GeneralSecurityException {
        return deriveKey(keySeed, cipherAlgName, keyLength, null, mode);
    }

    /**
     * Derives a shared key.
     *
     * @param keySeed the shared secret, as octets
     * @param cipherAlg in Java mnemonic notation (for example "DESede", "AES")
     * @param keyLength length in bits
     * @param nonce optional nonce or <code>null</code>
     * @param counter counter or mode
     *
     * @return the derived key
     *
     * @throws GeneralSecurityException if something went wrong
     */
    public static SecretKey deriveKey(byte[] keySeed, String cipherAlg, int keyLength, byte[] nonce, int counter)
            throws GeneralSecurityException {
        String digestAlg = inferDigestAlgorithmFromCipherAlgorithmForKeyDerivation(cipherAlg, keyLength);
        LOGGER.info("DEBUG: key derivation uses digestAlg = " + digestAlg);
        MessageDigest digest = MessageDigest.getInstance(digestAlg);
        digest.reset();
        digest.update(keySeed);
        if (nonce != null) {
            digest.update(nonce);
        }
        digest.update(new byte[] { 0x00, 0x00, 0x00, (byte) counter });
        byte[] hashResult = digest.digest();
        byte[] keyBytes = null;
        if ("DESede".equalsIgnoreCase(cipherAlg) || "3DES".equalsIgnoreCase(cipherAlg)) {
            /* TR-SAC 1.01, 4.2.1. */
            switch (keyLength) {
            case 112: /* Fall through. */
            case 128:
                keyBytes = new byte[24];
                System.arraycopy(hashResult, 0, keyBytes, 0, 8); /* E  (octets 1 to 8) */
                System.arraycopy(hashResult, 8, keyBytes, 8, 8); /* D  (octets 9 to 16) */
                System.arraycopy(hashResult, 0, keyBytes, 16,
                        8); /* E (again octets 1 to 8, i.e. 112-bit 3DES key) */
                break;
            default:
                throw new IllegalArgumentException("KDF can only use DESede with 128-bit key length");
            }
        } else if ("AES".equalsIgnoreCase(cipherAlg) || cipherAlg.startsWith("AES")) {
            /* TR-SAC 1.01, 4.2.2. */
            switch (keyLength) {
            case 128:
                keyBytes = new byte[16]; /* NOTE: 128 = 16 * 8 */
                System.arraycopy(hashResult, 0, keyBytes, 0, 16);
                break;
            case 192:
                keyBytes = new byte[24]; /* NOTE: 192 = 24 * 8 */
                System.arraycopy(hashResult, 0, keyBytes, 0, 24);
                break;
            case 256:
                keyBytes = new byte[32]; /* NOTE: 256 = 32 * 8 */
                System.arraycopy(hashResult, 0, keyBytes, 0, 32);
                break;
            default:
                throw new IllegalArgumentException(
                        "KDF can only use AES with 128-bit, 192-bit key or 256-bit length, found: " + keyLength
                                + "-bit key length");
            }
        }
        return new SecretKeySpec(keyBytes, cipherAlg);
    }

    /**
     * Computes the static key seed, based on information from the MRZ.
     *
     * @param documentNumber a string containing the document number
     * @param dateOfBirth a string containing the date of birth (YYMMDD)
     * @param dateOfExpiry a string containing the date of expiry (YYMMDD)
     * @param digestAlg a Java mnemonic algorithm string to indicate the digest algorithm (typically SHA-1)
     * @param doTruncate whether to truncate the resulting output to 16 bytes
     *
     * @return a byte array of length 16 containing the key seed
     *
     * @throws GeneralSecurityException on security error
     */
    public static byte[] computeKeySeed(String documentNumber, String dateOfBirth, String dateOfExpiry,
            String digestAlg, boolean doTruncate) throws GeneralSecurityException {

        /* Check digits... */
        byte[] documentNumberCheckDigit = { (byte) MRZInfo.checkDigit(documentNumber) };
        byte[] dateOfBirthCheckDigit = { (byte) MRZInfo.checkDigit(dateOfBirth) };
        byte[] dateOfExpiryCheckDigit = { (byte) MRZInfo.checkDigit(dateOfExpiry) };

        MessageDigest shaDigest = MessageDigest.getInstance(digestAlg);

        shaDigest.update(getBytes(documentNumber));
        shaDigest.update(documentNumberCheckDigit);
        shaDigest.update(getBytes(dateOfBirth));
        shaDigest.update(dateOfBirthCheckDigit);
        shaDigest.update(getBytes(dateOfExpiry));
        shaDigest.update(dateOfExpiryCheckDigit);

        byte[] hash = shaDigest.digest();

        if (doTruncate) {
            /* FIXME: truncate to 16 byte only for BAC with 3DES. Also for PACE and/or AES? -- MO */
            byte[] keySeed = new byte[16];
            System.arraycopy(hash, 0, keySeed, 0, 16);
            return keySeed;
        } else {
            return hash;
        }
    }

    /**
     * Pads the input <code>in</code> according to ISO9797-1 padding method 2.
     *
     * @param in input
     *
     * @return padded output
     */
    public static byte[] padWithMRZ(/*@ non_null */ byte[] in) {
        return padWithMRZ(in, 0, in.length);
    }

    public static byte[] padWithCAN(/*@ non_null */ byte[] in, int blockSize) {
        return padWithCAN(in, 0, in.length, blockSize);
    }

    /*@ requires 0 <= offset && offset < length;
       @ requires 0 <= length && length <= in.length;
     */
    public static byte[] padWithMRZ(/*@ non_null */ byte[] in, int offset, int length) {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        out.write(in, offset, length);
        out.write((byte) 0x80);
        while (out.size() % 8 != 0) {
            out.write((byte) 0x00);
        }
        return out.toByteArray();
    }

    public static byte[] padWithCAN(/*@ non_null */ byte[] in, int offset, int length, int blockSize) {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        out.write(in, offset, length);
        out.write((byte) 0x80);
        while (out.size() % blockSize != 0) {
            out.write((byte) 0x00);
        }
        return out.toByteArray();
    }

    public static long computeSendSequenceCounter(byte[] rndICC, byte[] rndIFD) {
        if (rndICC == null || rndICC.length != 8 || rndIFD == null || rndIFD.length != 8) {
            throw new IllegalStateException("Wrong length input");
        }
        long ssc = 0;
        for (int i = 4; i < 8; i++) {
            ssc <<= 8;
            ssc += (long) (rndICC[i] & 0x000000FF);
        }
        for (int i = 4; i < 8; i++) {
            ssc <<= 8;
            ssc += (long) (rndIFD[i] & 0x000000FF);
        }
        return ssc;
    }

    public static byte[] unpad(byte[] in) throws BadPaddingException {
        int i = in.length - 1;
        while (i >= 0 && in[i] == 0x00) {
            i--;
        }
        if ((in[i] & 0xFF) != 0x80) {
            throw new BadPaddingException(
                    "Expected constant 0x80, found 0x" + Integer.toHexString((in[i] & 0x000000FF))
                            + "\nDEBUG: in = " + Hex.bytesToHexString(in) + ", index = " + i);
        }
        byte[] out = new byte[i];
        System.arraycopy(in, 0, out, 0, i);
        return out;
    }

    /**
     * Recovers the M1 part of the message sent back by the AA protocol
     * (INTERNAL AUTHENTICATE command). The algorithm is described in
     * ISO 9796-2:2002 9.3.
     *
     * Based on code by Ronny (ronny@cs.ru.nl) who presumably ripped this
     * from Bouncy Castle.
     *
     * @param digestLength should be 20
     * @param plaintext response from card, already 'decrypted' (using the
     * AA public key)
     *
     * @return the m1 part of the message
     */
    public static byte[] recoverMessage(int digestLength, byte[] plaintext) {
        if (plaintext == null || plaintext.length < 1) {
            throw new IllegalArgumentException("Plaintext too short to recover message");
        }
        if (((plaintext[0] & 0xC0) ^ 0x40) != 0) {
            // 0xC0 = 1100 0000, 0x40 = 0100 0000
            throw new NumberFormatException("Could not get M1");
        }
        if (((plaintext[plaintext.length - 1] & 0xF) ^ 0xC) != 0) {
            // 0xF = 0000 1111, 0xC = 0000 1100
            throw new NumberFormatException("Could not get M1");
        }
        int delta = 0;
        if (((plaintext[plaintext.length - 1] & 0xFF) ^ 0xBC) == 0) {
            delta = 1;
        } else {
            // 0xBC = 1011 1100
            throw new NumberFormatException("Could not get M1");
        }

        /* find out how much padding we've got */
        int paddingLength = 0;
        for (; paddingLength < plaintext.length; paddingLength++) {
            // 0x0A = 0000 1010
            if (((plaintext[paddingLength] & 0x0F) ^ 0x0A) == 0) {
                break;
            }
        }
        int messageOffset = paddingLength + 1;

        int paddedMessageLength = plaintext.length - delta - digestLength;
        int messageLength = paddedMessageLength - messageOffset;

        /* there must be at least one byte of message string */
        if (messageLength <= 0) {
            throw new NumberFormatException("Could not get M1");
        }

        /* TODO: if we contain the whole message as well, check the hash of that. */
        if ((plaintext[0] & 0x20) == 0) {
            throw new NumberFormatException("Could not get M1");
        } else {
            byte[] recoveredMessage = new byte[messageLength];
            System.arraycopy(plaintext, messageOffset, recoveredMessage, 0, messageLength);
            return recoveredMessage;
        }
    }

    /**
     * For ECDSA the EAC 1.11 specification requires the signature to be stripped down from any ASN.1 wrappers, as so.
     *
     * @param signedData signed data
     * @param keySize key size
     *
     * @return signature without wrappers
     *
     * @throws IOException on error
     */
    public static byte[] getRawECDSASignature(byte[] signedData, int keySize) throws IOException {
        ASN1InputStream asn1In = new ASN1InputStream(signedData);
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try {
            ASN1Sequence obj = (ASN1Sequence) asn1In.readObject();
            Enumeration<ASN1Primitive> e = obj.getObjects();
            while (e.hasMoreElements()) {
                ASN1Integer i = (ASN1Integer) e.nextElement();
                byte[] t = i.getValue().toByteArray();
                t = alignKeyDataToSize(t, keySize);
                out.write(t);
            }
            out.flush();
            return out.toByteArray();
        } finally {
            asn1In.close();
            out.close();
        }
    }

    public static byte[] alignKeyDataToSize(byte[] keyData, int size) {
        byte[] result = new byte[size];
        if (keyData.length < size) {
            size = keyData.length;
        }
        System.arraycopy(keyData, keyData.length - size, result, result.length - size, size);
        return result;
    }

    /**
     * Converts an integer to an octet string.
     * Based on BSI TR 03111 Section 3.1.2.
     *
     * @param val positive integer
     * @param length length
     *
     * @return octet string
     */
    public static byte[] i2os(BigInteger val, int length) {
        BigInteger base = BigInteger.valueOf(256);
        byte[] result = new byte[length];
        for (int i = 0; i < length; i++) {
            BigInteger remainder = val.mod(base);
            val = val.divide(base);
            result[length - 1 - i] = (byte) remainder.intValue();
        }
        return result;
    }

    /**
     * Converts an integer to an octet string.
     *
     * @param val positive integer
     * @return octet string
     */
    public static byte[] i2os(BigInteger val) {
        /* FIXME: Quick hack. What if val < 0? -- MO */
        /* Do something with: int sizeInBytes = val.bitLength() / Byte.SIZE; */

        int sizeInNibbles = val.toString(16).length();
        if (sizeInNibbles % 2 != 0) {
            sizeInNibbles++;
        }
        int length = sizeInNibbles / 2;
        return i2os(val, length);
    }

    /**
     * Converts an octet string to an integer.
     * Based on BSI TR 03111 Section 3.1.2.
     *
     * @param bytes octet string
     *
     * @return positive integer
     */
    public static BigInteger os2i(byte[] bytes) {
        if (bytes == null) {
            throw new IllegalArgumentException();
        }
        return os2i(bytes, 0, bytes.length);
    }

    /**
     * Converts an octet string to an integer.
     * Based on BSI TR 03111 Section 3.1.2.
     *
     * @param bytes octet string
     * @param offset offset of octet string
     * @param length length of octet string
     *
     * @return positive integer
     */
    public static BigInteger os2i(byte[] bytes, int offset, int length) {
        if (bytes == null) {
            throw new IllegalArgumentException();
        }
        BigInteger result = BigInteger.ZERO;
        BigInteger base = BigInteger.valueOf(256);
        for (int i = offset; i < offset + length; i++) {
            result = result.multiply(base);
            result = result.add(BigInteger.valueOf(bytes[i] & 0xFF));
        }
        return result;
    }

    /**
     * Convert an octet string to field element via OS2FE as specified in BSI TR-03111.
     *
     * @param bytes octet string
     * @param p modulus
     *
     * @return positive integer
     */
    public static BigInteger os2fe(byte[] bytes, BigInteger p) {
        return Util.os2i(bytes).mod(p);
    }

    /**
     * Encode an EC public key point.
     * Prefixes a <code>0x04</code> (without a length).
     *
     * @param point public key point
     *
     * @return an octet string
     */
    public static byte[] publicKeyECPointToOS(ECPoint point) {
        ByteArrayOutputStream bOut = new ByteArrayOutputStream();
        BigInteger x = point.getAffineX();
        BigInteger y = point.getAffineY();
        try {
            bOut.write(0x04);
            bOut.write(i2os(x));
            bOut.write(i2os(y));
            bOut.close();
        } catch (IOException ioe) {
            throw new IllegalStateException(ioe.getMessage());
        }
        return bOut.toByteArray();
    }

    /* Best effort. FIXME: test and improve. -- MO */
    /**
     * Infers a digest algorithm mnemonic from a signature algorithm mnemonic.
     *
     * @param signatureAlgorithm a signature algorithm
     * @return a digest algorithm, or null if inference failed
     */
    public static String inferDigestAlgorithmFromSignatureAlgorithm(String signatureAlgorithm) {
        if (signatureAlgorithm == null) {
            throw new IllegalArgumentException();
        }
        String digestAlgorithm = null;
        String signatureAlgorithmToUppercase = signatureAlgorithm.toUpperCase();
        if (signatureAlgorithmToUppercase.contains("WITH")) {
            String[] components = signatureAlgorithmToUppercase.split("WITH");
            digestAlgorithm = components[0];
        }
        if ("SHA1".equalsIgnoreCase(digestAlgorithm)) {
            digestAlgorithm = "SHA-1";
        }
        if ("SHA224".equalsIgnoreCase(digestAlgorithm)) {
            digestAlgorithm = "SHA-224";
        }
        if ("SHA256".equalsIgnoreCase(digestAlgorithm)) {
            digestAlgorithm = "SHA-256";
        }
        if ("SHA384".equalsIgnoreCase(digestAlgorithm)) {
            digestAlgorithm = "SHA-384";
        }
        if ("SHA512".equalsIgnoreCase(digestAlgorithm)) {
            digestAlgorithm = "SHA-512";
        }
        return digestAlgorithm;
    }

    public static String inferDigestAlgorithmFromCipherAlgorithmForKeyDerivation(String cipherAlg, int keyLength) {
        if (cipherAlg == null) {
            throw new IllegalArgumentException();
        }
        if ("DESede".equals(cipherAlg) || "AES-128".equals(cipherAlg)) {
            return "SHA-1";
        }
        if ("AES".equals(cipherAlg) && keyLength == 128) {
            return "SHA-1";
        }
        if ("AES-256".equals(cipherAlg) || "AES-192".equals(cipherAlg)) {
            return "SHA-256";
        }
        if ("AES".equals(cipherAlg) && (keyLength == 192 || keyLength == 256)) {
            return "SHA-256";
        }
        throw new IllegalArgumentException(
                "Unsupported cipher algorithm or key length \"" + cipherAlg + "\", " + keyLength);
    }

    public static DHParameterSpec toExplicitDHParameterSpec(DHParameters params) {
        BigInteger p = params.getP();
        BigInteger generator = params.getG();
        int order = (int) params.getL();
        return new DHParameterSpec(p, generator, order);
    }

    /**
     * The public key algorithm (like RSA or) with some extra information (like 1024 bits).
     *
     * @param publicKey a public key
     *
     * @return the algorithm
     */
    public static String getDetailedPublicKeyAlgorithm(PublicKey publicKey) {
        String publicKeyAlgorithm = publicKey.getAlgorithm();
        if (publicKey instanceof RSAPublicKey) {
            RSAPublicKey rsaPublicKey = (RSAPublicKey) publicKey;
            int publicKeyBitLength = rsaPublicKey.getModulus().bitLength();
            publicKeyAlgorithm += " [" + publicKeyBitLength + " bit]";
        } else if (publicKey instanceof ECPublicKey) {
            ECPublicKey ecPublicKey = (ECPublicKey) publicKey;
            ECParameterSpec ecParams = ecPublicKey.getParams();
            String name = getCurveName(ecParams);
            if (name != null) {
                publicKeyAlgorithm += " [" + name + "]";
            }
        }
        return publicKeyAlgorithm;
    }

    /**
     * Gets the curve name if known (or null).
     *
     * @param params an specification of the curve
     *
     * @return the name
     */
    public static String getCurveName(ECParameterSpec params) {
        org.bouncycastle.jce.spec.ECNamedCurveSpec namedECParams = toNamedCurveSpec(params);
        if (namedECParams == null) {
            return null;
        }
        return namedECParams.getName();
    }

    public static ECParameterSpec toExplicitECParameterSpec(ECNamedCurveParameterSpec parameterSpec) {
        return toExplicitECParameterSpec(toECNamedCurveSpec(parameterSpec));
    }

    /**
     * Translates (named) curve spec to JCA compliant explicit param spec.
     *
     * @param params an EC parameter spec, possibly named
     *
     * @return another spec not name based
     */
    public static ECParameterSpec toExplicitECParameterSpec(ECParameterSpec params) {
        try {
            ECPoint g = params.getGenerator();
            BigInteger n = params.getOrder(); // Order, order
            int h = params.getCofactor(); // co-factor
            EllipticCurve curve = params.getCurve();
            BigInteger a = curve.getA();
            BigInteger b = curve.getB();
            ECField field = curve.getField();
            if (field instanceof ECFieldFp) {
                BigInteger p = ((ECFieldFp) field).getP();
                ECField resultField = new ECFieldFp(p);
                EllipticCurve resultCurve = new EllipticCurve(resultField, a, b);
                ECParameterSpec resultParams = new ECParameterSpec(resultCurve, g, n, h);
                return resultParams;
            } else if (field instanceof ECFieldF2m) {
                int m = ((ECFieldF2m) field).getM();
                ECField resultField = new ECFieldF2m(m);
                EllipticCurve resultCurve = new EllipticCurve(resultField, a, b);
                ECParameterSpec resultParams = new ECParameterSpec(resultCurve, g, n, h);
                return resultParams;
            } else {
                LOGGER.warning("Could not make named EC param spec explicit");
                return params;
            }
        } catch (Exception e) {
            LOGGER.warning("Could not make named EC param spec explicit");
            return params;
        }
    }

    private static org.bouncycastle.jce.spec.ECNamedCurveSpec toNamedCurveSpec(ECParameterSpec ecParamSpec) {
        if (ecParamSpec == null) {
            return null;
        }
        if (ecParamSpec instanceof org.bouncycastle.jce.spec.ECNamedCurveSpec) {
            return (org.bouncycastle.jce.spec.ECNamedCurveSpec) ecParamSpec;
        }
        @SuppressWarnings("unchecked")
        List<String> names = (List<String>) Collections.list(ECNamedCurveTable.getNames());
        List<org.bouncycastle.jce.spec.ECNamedCurveSpec> namedSpecs = new ArrayList<org.bouncycastle.jce.spec.ECNamedCurveSpec>();
        for (String name : names) {
            org.bouncycastle.jce.spec.ECNamedCurveSpec namedSpec = toECNamedCurveSpec(
                    ECNamedCurveTable.getParameterSpec(name));
            if (namedSpec.getCurve().equals(ecParamSpec.getCurve())
                    && namedSpec.getGenerator().equals(ecParamSpec.getGenerator())
                    && namedSpec.getOrder().equals(ecParamSpec.getOrder())
                    && namedSpec.getCofactor() == ecParamSpec.getCofactor()) {
                namedSpecs.add(namedSpec);
            }
        }
        if (namedSpecs.size() == 0) {
            //         throw new IllegalArgumentException("No named curve found");
            return null;
        } else if (namedSpecs.size() == 1) {
            return namedSpecs.get(0);
        } else {
            return namedSpecs.get(0);
        }
    }

    /**
     * Translates internal BC named curve spec to BC provided JCA compliant named curve spec.
     *
     * @param namedParamSpec a named EC parameter spec
     *
     * @return a JCA compliant named EC parameter spec
     */
    public static org.bouncycastle.jce.spec.ECNamedCurveSpec toECNamedCurveSpec(
            org.bouncycastle.jce.spec.ECNamedCurveParameterSpec namedParamSpec) {
        String name = namedParamSpec.getName();
        org.bouncycastle.math.ec.ECCurve curve = namedParamSpec.getCurve();
        org.bouncycastle.math.ec.ECPoint generator = namedParamSpec.getG();
        BigInteger order = namedParamSpec.getN();
        BigInteger coFactor = namedParamSpec.getH();
        byte[] seed = namedParamSpec.getSeed();
        return new org.bouncycastle.jce.spec.ECNamedCurveSpec(name, curve, generator, order, coFactor, seed);
    }

    /*
     * NOTE: Woj, I moved this here from DG14File, seemed more appropriate here. -- MO
     * FIXME: Do we still need this now that we have reconstructPublicKey? -- MO
     *
     * Woj says: Here we need to some hocus-pokus, the EAC specification require for
     * all the key information to include the domain parameters explicitly. This is
     * not what Bouncy Castle does by default. But we first have to check if this is
     * the case.
     */
    public static SubjectPublicKeyInfo toSubjectPublicKeyInfo(PublicKey publicKey) {
        try {
            String algorithm = publicKey.getAlgorithm();
            if ("EC".equals(algorithm) || "ECDH".equals(algorithm) || (publicKey instanceof ECPublicKey)) {
                ASN1InputStream asn1In = new ASN1InputStream(publicKey.getEncoded());
                SubjectPublicKeyInfo subjectPublicKeyInfo = new SubjectPublicKeyInfo(
                        (ASN1Sequence) asn1In.readObject());
                asn1In.close();
                AlgorithmIdentifier algorithmIdentifier = subjectPublicKeyInfo.getAlgorithm();
                String algOID = algorithmIdentifier.getAlgorithm().getId();
                if (!SecurityInfo.ID_EC_PUBLIC_KEY.equals(algOID)) {
                    throw new IllegalStateException("Was expecting id-ecPublicKey ("
                            + SecurityInfo.ID_EC_PUBLIC_KEY_TYPE + "), found " + algOID);
                }
                ASN1Primitive derEncodedParams = algorithmIdentifier.getParameters().toASN1Primitive();
                X9ECParameters params = null;
                if (derEncodedParams instanceof ASN1ObjectIdentifier) {
                    ASN1ObjectIdentifier paramsOID = (ASN1ObjectIdentifier) derEncodedParams;

                    /* It's a named curve from X9.62. */
                    params = X962NamedCurves.getByOID(paramsOID);
                    if (params == null) {
                        throw new IllegalStateException(
                                "Could not find X9.62 named curve for OID " + paramsOID.getId());
                    }

                    /* Reconstruct the parameters. */
                    org.bouncycastle.math.ec.ECPoint generator = params.getG();
                    org.bouncycastle.math.ec.ECCurve curve = generator.getCurve();
                    generator = curve.createPoint(generator.getX().toBigInteger(), generator.getY().toBigInteger(),
                            false);
                    params = new X9ECParameters(params.getCurve(), generator, params.getN(), params.getH(),
                            params.getSeed());
                } else {
                    /* It's not a named curve, we can just return the decoded public key info. */
                    return subjectPublicKeyInfo;
                }

                if (publicKey instanceof org.bouncycastle.jce.interfaces.ECPublicKey) {
                    org.bouncycastle.jce.interfaces.ECPublicKey ecPublicKey = (org.bouncycastle.jce.interfaces.ECPublicKey) publicKey;
                    AlgorithmIdentifier id = new AlgorithmIdentifier(
                            subjectPublicKeyInfo.getAlgorithm().getAlgorithm(), params.toASN1Primitive());
                    org.bouncycastle.math.ec.ECPoint q = ecPublicKey.getQ();
                    /* FIXME: investigate the compressed versus uncompressed point issue. What is allowed in TR03110? -- MO */
                    // In case we would like to compress the point:
                    // p = p.getCurve().createPoint(p.getX().toBigInteger(), p.getY().toBigInteger(), true);
                    subjectPublicKeyInfo = new SubjectPublicKeyInfo(id, q.getEncoded());
                    return subjectPublicKeyInfo;
                } else {
                    return subjectPublicKeyInfo;
                }
            } else if ("DH".equals(algorithm) || (publicKey instanceof DHPublicKey)) {
                DHPublicKey dhPublicKey = (DHPublicKey) publicKey;
                DHParameterSpec dhSpec = dhPublicKey.getParams();
                return new SubjectPublicKeyInfo(
                        new AlgorithmIdentifier(EACObjectIdentifiers.id_PK_DH,
                                new DHParameter(dhSpec.getP(), dhSpec.getG(), dhSpec.getL()).toASN1Primitive()),
                        new ASN1Integer(dhPublicKey.getY()));
            } else {
                throw new IllegalArgumentException(
                        "Unrecognized key type, found " + publicKey.getAlgorithm() + ", should be DH or ECDH");
            }
        } catch (Exception e) {
            LOGGER.severe("Exception: " + e.getMessage());
            return null;
        }
    }

    public static PublicKey toPublicKey(SubjectPublicKeyInfo subjectPublicKeyInfo) {
        try {
            byte[] encodedPublicKeyInfoBytes = subjectPublicKeyInfo.getEncoded(ASN1Encoding.DER);
            KeySpec keySpec = new X509EncodedKeySpec(encodedPublicKeyInfoBytes);
            try {
                KeyFactory factory = KeyFactory.getInstance("DH");
                return factory.generatePublic(keySpec);
            } catch (GeneralSecurityException gse) {
                KeyFactory factory = KeyFactory.getInstance("EC", BC_PROVIDER);
                return factory.generatePublic(keySpec);
            }
        } catch (GeneralSecurityException gse2) {
            LOGGER.severe("Exception: " + gse2.getMessage());
            return null;
        } catch (Exception ioe) {
            LOGGER.severe("Exception: " + ioe.getMessage());
            return null;
        }
    }

    /**
     * Reconstructs the public key to use explicit domain params for EC public keys
     *
     * @param publicKey the public key
     *
     * @return the same public key (if not EC or error), or a reconstructed one (if EC)
     */
    public static PublicKey reconstructPublicKey(PublicKey publicKey) {
        if (!(publicKey instanceof ECPublicKey)) {
            return publicKey;
        }
        try {
            ECPublicKey ecPublicKey = (ECPublicKey) publicKey;
            ECPoint w = ecPublicKey.getW();
            ECParameterSpec params = ecPublicKey.getParams();
            params = toExplicitECParameterSpec(params);
            if (params == null) {
                return publicKey;
            }
            ECPublicKeySpec explicitPublicKeySpec = new ECPublicKeySpec(w, params);
            return KeyFactory.getInstance("EC", BC_PROVIDER).generatePublic(explicitPublicKeySpec);
        } catch (Exception e) {
            LOGGER.warning("Could not make public key param spec explicit");
            return publicKey;
        }
    }

    /**
     * Based on TR-SAC 1.01 4.5.1 and 4.5.2.
     *
     * For signing authentication token, not for sending to smart card.
     * Assumes context is known.
     *
     * @param oid object identifier
     * @param publicKey public key
     *
     * @return encoded public key data object for signing as authentication token
     *
     * @throws InvalidKeyException when public key is not DH or EC
     */
    public static byte[] encodePublicKeyDataObject(String oid, PublicKey publicKey) throws InvalidKeyException {
        return encodePublicKeyDataObject(oid, publicKey, true);
    }

    /**
     * Based on TR-SAC 1.01 4.5.1 and 4.5.2.
     *
     * For signing authentication token, not for sending to smart card.
     *
     * @param oid object identifier
     * @param publicKey public key
     * @param isContextKnown whether context of public key is known to receiver (we will not include domain parameters in that case).
     *
     * @return encoded public key data object for signing as authentication token
     *
     * @throws InvalidKeyException when public key is not DH or EC
     */
    public static byte[] encodePublicKeyDataObject(String oid, PublicKey publicKey, boolean isContextKnown)
            throws InvalidKeyException {
        ByteArrayOutputStream bOut = new ByteArrayOutputStream();
        TLVOutputStream tlvOut = new TLVOutputStream(bOut);
        try {
            tlvOut.writeTag(0x7F49); // FIXME: constant for 7F49 */
            if (publicKey instanceof DHPublicKey) {
                DHPublicKey dhPublicKey = (DHPublicKey) publicKey;
                DHParameterSpec params = dhPublicKey.getParams();
                BigInteger p = params.getP();
                int l = params.getL();
                BigInteger generator = params.getG();
                BigInteger y = dhPublicKey.getY();

                tlvOut.write(new ASN1ObjectIdentifier(oid)
                        .getEncoded()); /* Object Identifier, NOTE: encoding already contains 0x06 tag  */
                if (!isContextKnown) {
                    tlvOut.writeTag(0x81);
                    tlvOut.writeValue(i2os(p)); /* p: Prime modulus */
                    tlvOut.writeTag(0x82);
                    tlvOut.writeValue(i2os(BigInteger.valueOf(l))); /* q: Order of the subgroup */
                    tlvOut.writeTag(0x83);
                    tlvOut.writeValue(i2os(generator)); /* Generator */
                }
                tlvOut.writeTag(0x84);
                tlvOut.writeValue(i2os(y)); /* y: Public value */
            } else if (publicKey instanceof ECPublicKey) {
                ECPublicKey ecPublicKey = (ECPublicKey) publicKey;
                ECParameterSpec params = ecPublicKey.getParams();
                BigInteger p = getPrime(params);
                EllipticCurve curve = params.getCurve();
                BigInteger a = curve.getA();
                BigInteger b = curve.getB();
                ECPoint generator = params.getGenerator();
                BigInteger order = params.getOrder();
                int coFactor = params.getCofactor();
                ECPoint publicPoint = ecPublicKey.getW();

                tlvOut.write(new ASN1ObjectIdentifier(oid)
                        .getEncoded()); /* Object Identifier, NOTE: encoding already contains 0x06 tag */
                if (!isContextKnown) {
                    tlvOut.writeTag(0x81);
                    tlvOut.writeValue(i2os(p)); /* Prime modulus */
                    tlvOut.writeTag(0x82);
                    tlvOut.writeValue(i2os(a)); /* First coefficient */
                    tlvOut.writeTag(0x83);
                    tlvOut.writeValue(i2os(b)); /* Second coefficient */
                    BigInteger affineX = generator.getAffineX();
                    BigInteger affineY = generator.getAffineY();
                    tlvOut.writeTag(0x84);
                    tlvOut.write(i2os(affineX));
                    tlvOut.write(i2os(affineY));
                    tlvOut.writeValueEnd(); /* Base point, FIXME: correct encoding? */
                    tlvOut.writeTag(0x85);
                    tlvOut.writeValue(i2os(order)); /* Order of the base point */
                }
                tlvOut.writeTag(0x86);
                tlvOut.writeValue(publicKeyECPointToOS(publicPoint)); /* Public point */
                if (!isContextKnown) {
                    tlvOut.writeTag(0x87);
                    tlvOut.writeValue(i2os(BigInteger.valueOf(coFactor))); /* Cofactor */
                }
            } else {
                throw new InvalidKeyException("Unsupported public key: " + publicKey.getClass().getCanonicalName());
            }
            tlvOut.writeValueEnd(); /* 7F49 */
            tlvOut.flush();
            tlvOut.close();
        } catch (IOException ioe) {
            LOGGER.severe("Exception: " + ioe.getMessage());
            throw new IllegalStateException("Error in encoding public key");
        }
        return bOut.toByteArray();
    }

    /*
     * FIXME: how can we be sure coords are uncompressed?
     */
    /**
     * Write uncompressed coordinates (for EC) or public value (DH).
     *
     * @param publicKey public key
     *
     * @return encoding for smart card
     *
     * @throws InvalidKeyException if the key type is not EC or DH
     */
    public static byte[] encodePublicKeyForSmartCard(PublicKey publicKey) throws InvalidKeyException {
        if (publicKey == null) {
            throw new IllegalArgumentException("Cannot encode null public key");
        }
        if (publicKey instanceof ECPublicKey) {
            ECPublicKey ecPublicKey = (ECPublicKey) publicKey;
            try {
                ByteArrayOutputStream bOut = new ByteArrayOutputStream();
                bOut.write(Util.publicKeyECPointToOS(ecPublicKey.getW()));
                byte[] encodedPublicKey = bOut.toByteArray();
                bOut.close();
                return encodedPublicKey;
            } catch (IOException ioe) {
                /* NOTE: Should never happen, we're writing to a ByteArrayOutputStream. */
                throw new IllegalStateException("Internal error writing to memory: " + ioe.getMessage());
            }
        } else if (publicKey instanceof DHPublicKey) {
            DHPublicKey dhPublicKey = (DHPublicKey) publicKey;
            return Util.i2os(dhPublicKey.getY());
        } else {
            throw new InvalidKeyException("Unsupported public key: " + publicKey.getClass().getCanonicalName());
        }
    }

    public static PublicKey decodePublicKeyFromSmartCard(byte[] encodedPublicKey, AlgorithmParameterSpec params) {
        if (params == null) {
            throw new IllegalArgumentException("Params cannot be null");
        }
        try {
            DataInputStream dataIn = new DataInputStream(new ByteArrayInputStream(encodedPublicKey));
            if (params instanceof ECParameterSpec) {
                int b = dataIn.read();
                if (b != 0x04) {
                    throw new IllegalArgumentException("Expected encoded public key to start with 0x04");
                }
                int length = (encodedPublicKey.length - 1) / 2;
                byte[] xCoordBytes = new byte[length];
                byte[] yCoordBytes = new byte[length];
                dataIn.readFully(xCoordBytes);
                dataIn.readFully(yCoordBytes);
                dataIn.close();

                BigInteger x = Util.os2i(xCoordBytes);
                BigInteger y = Util.os2i(yCoordBytes);
                ECPoint w = new ECPoint(x, y);

                ECParameterSpec ecParams = (ECParameterSpec) params;
                KeyFactory kf = KeyFactory.getInstance("EC");
                return kf.generatePublic(new ECPublicKeySpec(w, ecParams));
            } else if (params instanceof DHParameterSpec) {
                int b = dataIn.read();
                if (b != 0x04) {
                    throw new IllegalArgumentException("Expected encoded public key to start with 0x04");
                }
                int length = encodedPublicKey.length - 1;
                byte[] publicValue = new byte[length];
                dataIn.readFully(publicValue);
                dataIn.close();

                BigInteger y = Util.os2i(publicValue);

                KeyFactory kf = KeyFactory.getInstance("DH");
                DHParameterSpec dhParams = (DHParameterSpec) params;
                return kf.generatePublic(new DHPublicKeySpec(y, dhParams.getP(), dhParams.getG()));
            }
            throw new IllegalArgumentException(
                    "Expected ECParameterSpec or DHParameterSpec, found " + params.getClass().getCanonicalName());
        } catch (IOException ioe) {
            LOGGER.severe("Exception: " + ioe.getMessage());
            throw new IllegalArgumentException(ioe.getMessage());
        } catch (GeneralSecurityException gse) {
            LOGGER.severe("Exception: " + gse.getMessage());
            throw new IllegalArgumentException(gse.getMessage());
        }
    }

    /**
     * Infer an EAC object identifier for an EC or DH public key.
     *
     * @param publicKey a public key
     *
     * @return either ID_PK_ECDH or ID_PK_DH
     */
    public static String inferProtocolIdentifier(PublicKey publicKey) {
        String algorithm = publicKey.getAlgorithm();
        if ("EC".equals(algorithm) || "ECDH".equals(algorithm)) {
            return SecurityInfo.ID_PK_ECDH_OID;
        } else if ("DH".equals(algorithm)) {
            return SecurityInfo.ID_PK_DH_OID;
        } else {
            throw new IllegalArgumentException("Wrong key type. Was expecting ECDH or DH public key.");
        }
    }

    public static AlgorithmParameterSpec mapNonceGM(byte[] nonceS, byte[] sharedSecretH,
            AlgorithmParameterSpec params) {
        if (params == null) {
            throw new IllegalArgumentException("Unsupported parameters for mapping nonce");
        }
        if (params instanceof ECParameterSpec) {
            ECParameterSpec ecParams = (ECParameterSpec) params;
            BigInteger affineX = os2i(sharedSecretH);
            BigInteger affineY = computeAffineY(affineX, ecParams);
            ECPoint sharedSecretPointH = new ECPoint(affineX, affineY);
            return mapNonceGMWithECDH(os2i(nonceS), sharedSecretPointH, ecParams);
        } else if (params instanceof DHParameterSpec) {
            DHParameterSpec dhParams = (DHParameterSpec) params;
            return mapNonceGMWithDH(os2i(nonceS), os2i(sharedSecretH), dhParams);
        } else {
            throw new IllegalArgumentException(
                    "Unsupported parameters for mapping nonce, expected ECParameterSpec or DHParameterSpec, found "
                            + params.getClass().getCanonicalName());
        }
    }

    public static AlgorithmParameterSpec mapNonceIM(byte[] nonceS, byte[] nonceT, byte[] sharedSecretH,
            AlgorithmParameterSpec params) {
        /* FIXME: work in progress. */
        return null;
    }

    private static ECParameterSpec mapNonceGMWithECDH(BigInteger nonceS, ECPoint sharedSecretPointH,
            ECParameterSpec params) {
        /*
         * D~ = (p, a, b, G~, n, h) where G~ = [s]G + H
         */
        ECPoint generator = params.getGenerator();
        EllipticCurve curve = params.getCurve();
        BigInteger a = curve.getA();
        BigInteger b = curve.getB();
        ECFieldFp field = (ECFieldFp) curve.getField();
        BigInteger p = field.getP();
        BigInteger order = params.getOrder();
        int cofactor = params.getCofactor();
        ECPoint ephemeralGenerator = add(multiply(nonceS, generator, params), sharedSecretPointH, params);
        if (!toBouncyCastleECPoint(ephemeralGenerator, params).isValid()) {
            LOGGER.info("ephemeralGenerator is not a valid point");
        }
        return new ECParameterSpec(new EllipticCurve(new ECFieldFp(p), a, b), ephemeralGenerator, order, cofactor);
    }

    private static DHParameterSpec mapNonceGMWithDH(BigInteger nonceS, BigInteger sharedSecretH,
            DHParameterSpec params) {
        // g~ = g^s * h
        BigInteger p = params.getP();
        BigInteger generator = params.getG();
        BigInteger ephemeralGenerator = generator.modPow(nonceS, p).multiply(sharedSecretH).mod(p);
        return new DHParameterSpec(p, ephemeralGenerator, params.getL());
    }

    private static ECPoint add(ECPoint x, ECPoint y, ECParameterSpec params) {
        org.bouncycastle.math.ec.ECPoint bcX = toBouncyCastleECPoint(x, params);
        org.bouncycastle.math.ec.ECPoint bcY = toBouncyCastleECPoint(y, params);
        org.bouncycastle.math.ec.ECPoint bcSum = bcX.add(bcY);
        return fromBouncyCastleECPoint(bcSum);
    }

    public static ECPoint multiply(BigInteger s, ECPoint point, ECParameterSpec params) {
        org.bouncycastle.math.ec.ECPoint bcPoint = toBouncyCastleECPoint(point, params);
        org.bouncycastle.math.ec.ECPoint bcProd = bcPoint.multiply(s);
        return fromBouncyCastleECPoint(bcProd);
    }

    private static byte[] getBytes(String str) {
        byte[] bytes = str.getBytes();
        try {
            bytes = str.getBytes("UTF-8");
        } catch (UnsupportedEncodingException use) {
            /* NOTE: unlikely. */
            LOGGER.severe("Exception: " + use.getMessage());
        }
        return bytes;
    }

    public static BigInteger getPrime(AlgorithmParameterSpec params) {
        if (params == null) {
            throw new IllegalArgumentException("Parameters null");
        }
        if (params instanceof DHParameterSpec) {
            return ((DHParameterSpec) params).getP();
        } else if (params instanceof ECParameterSpec) {
            EllipticCurve curve = ((ECParameterSpec) params).getCurve();
            ECField field = curve.getField();
            if (!(field instanceof ECFieldFp)) {
                throw new IllegalStateException("Was expecting prime field of type ECFieldFp, found "
                        + field.getClass().getCanonicalName());
            }
            return ((ECFieldFp) field).getP();
        } else {
            throw new IllegalArgumentException(
                    "Unsupported agreement algorithm, was expecting DHParameterSpec or ECParameterSpec, found "
                            + params.getClass().getCanonicalName());
        }
    }

    public static byte[] wrapDO(byte tag, byte[] data) {
        if (data == null) {
            throw new IllegalArgumentException("Data to wrap is null");
        }
        byte[] result = new byte[data.length + 2];
        result[0] = tag;
        result[1] = (byte) data.length;
        System.arraycopy(data, 0, result, 2, data.length);
        return result;
    }

    public static byte[] unwrapDO(byte expectedTag, byte[] wrappedData) {
        if (wrappedData == null || wrappedData.length < 2) {
            throw new IllegalArgumentException("Wrapped data is null or length < 2");
        }
        byte actualTag = wrappedData[0];
        if (actualTag != expectedTag) {
            throw new IllegalArgumentException("Expected tag " + Integer.toHexString(expectedTag) + ", found tag "
                    + Integer.toHexString(actualTag));
        }
        byte[] result = new byte[wrappedData.length - 2];
        System.arraycopy(wrappedData, 2, result, 0, result.length);
        return result;
    }

    public static String inferKeyAgreementAlgorithm(PublicKey publicKey) {
        if (publicKey instanceof ECPublicKey) {
            return "ECDH";
        } else if (publicKey instanceof DHPublicKey) {
            return "DH";
        } else {
            throw new IllegalArgumentException("Unsupported public key: " + publicKey);
        }
    }

    /**
     * This just solves the curve equation for y.
     *
     * @param affineX the x coord of a point on the curve
     * @param params EC parameters for curve over Fp
     * @return the corresponding y coord
     */
    public static BigInteger computeAffineY(BigInteger affineX, ECParameterSpec params) {
        ECCurve bcCurve = toBouncyCastleECCurve(params);
        ECFieldElement a = bcCurve.getA();
        ECFieldElement b = bcCurve.getB();
        ECFieldElement x = bcCurve.fromBigInteger(affineX);
        ECFieldElement y = x.multiply(x).add(a).multiply(x).add(b).sqrt();
        return y.toBigInteger();
    }

    private static org.bouncycastle.math.ec.ECPoint toBouncyCastleECPoint(ECPoint point, ECParameterSpec params) {
        org.bouncycastle.math.ec.ECCurve bcCurve = toBouncyCastleECCurve(params);
        return bcCurve.createPoint(point.getAffineX(), point.getAffineY(), false);
        // return new org.bouncycastle.math.ec.ECPoint.Fp(bcCurve, bcCurve.fromBigInteger(point.getAffineX()), bcCurve.fromBigInteger(point.getAffineY()));
    }

    private static ECPoint fromBouncyCastleECPoint(org.bouncycastle.math.ec.ECPoint point) {
        point = point.normalize();
        if (!point.isValid()) {
            LOGGER.warning("point not valid");
        }
        return new ECPoint(point.getAffineXCoord().toBigInteger(), point.getAffineYCoord().toBigInteger());
    }

    public static boolean isValid(ECPoint ecPoint, ECParameterSpec params) {
        org.bouncycastle.math.ec.ECPoint bcPoint = toBouncyCastleECPoint(ecPoint, params);
        return bcPoint.isValid();
    }

    public static ECPoint normalize(ECPoint ecPoint, ECParameterSpec params) {
        org.bouncycastle.math.ec.ECPoint bcPoint = toBouncyCastleECPoint(ecPoint, params);
        bcPoint = bcPoint.normalize();
        return fromBouncyCastleECPoint(bcPoint);
    }

    private static ECCurve toBouncyCastleECCurve(ECParameterSpec params) {
        EllipticCurve curve = params.getCurve();
        ECField field = curve.getField();
        if (!(field instanceof ECFieldFp)) {
            throw new IllegalArgumentException(
                    "Only prime field supported (for now), found " + field.getClass().getCanonicalName());
        }
        int coFactor = params.getCofactor();
        BigInteger order = params.getOrder();
        BigInteger a = curve.getA();
        BigInteger b = curve.getB();
        BigInteger p = getPrime(params);
        return new ECCurve.Fp(p, a, b, order, BigInteger.valueOf(coFactor));
    }
}