licel / jcardsim

https://jcardsim.org
222 stars 123 forks source link

Spordically wrong XY coordinates for secp256r1 #209

Open Molyna opened 10 months ago

Molyna commented 10 months ago

We are having some issue with the simulator sometimes generating what seems to be the incorrect X and Y coordinates for a given ECPrivateKey running on JCardSim jcardsim-3.0.5-20230313.131323-6.jar.

Have seen this for example when the card generates private key as E99DC32000E3936ED3FF5CD60353C7224F02CF66282C934EA9DB0148C118D8BA it would return us 35CF191F5ED586D83DE772F73D5B09B9CE3CC4A23B2C87286956F32E87092B8B as X and 4DB9989CD0FFA714521F34660ACF9150DA1E9983A1A680EC466ECE03E2F78C70 as Y when the expected values would be 80FE67B5FB825A10769DFDF4A39164E45E92AF58E0F28C527296E042BACD9696 for X and 7390798A7C2FD02F343D021224DF629758A2CE6F00069F135BC538038BE5FF3A for Y. I've set up the following code to check if there is issue with thing like BouncyCastle implementation that is shaded with the simulator but this generates the correct coordinates.

        com.licel.jcardsim.bouncycastle.asn1.x9.X9ECParameters par = com.licel.jcardsim.bouncycastle.asn1.sec.SECNamedCurves.getByName("secp256r1");
        byte[] privateKey = new byte[]{-23, -99, -61, 32, 0, -29, -109, 110, -45, -1, 92, -42, 3, 83, -57, 34, 79, 2, -49, 102, 40, 44, -109, 78, -87, -37, 1, 72, -63, 24, -40, -70};
        BigInteger wrappedPrivate= new BigInteger(1, privateKey);
        com.licel.jcardsim.bouncycastle.math.ec.ECPoint xyPoint = new com.licel.jcardsim.bouncycastle.math.ec.FixedPointCombMultiplier().multiply(par.getG(), wrappedPrivate);
        com.licel.jcardsim.bouncycastle.crypto.params.ECDomainParameters params = new com.licel.jcardsim.bouncycastle.crypto.params.ECDomainParameters(par.getCurve(), par.getG(),  par.getN(), par.getH(), par.getSeed());
        com.licel.jcardsim.bouncycastle.crypto.AsymmetricCipherKeyPair keyPair = new com.licel.jcardsim.bouncycastle.crypto.AsymmetricCipherKeyPair(
                new com.licel.jcardsim.bouncycastle.crypto.params.ECPublicKeyParameters(xyPoint, params),
                new com.licel.jcardsim.bouncycastle.crypto.params.ECPrivateKeyParameters(wrappedPrivate, params));
        byte[] generatedW = ((com.licel.jcardsim.bouncycastle.crypto.params.ECPublicKeyParameters)keyPair.getPublic()).getQ().getEncoded(false);

The above code is basically a copy of the actions taken through the simulator code to see if there is something there. However this generates the correct XY.

The weird part here is that the XY that are incorrectly generated are on the curve though so it seems the coordinates might be generated with a different private key? It gets a bit hard debugging at that level with the BouncyCastle being shaded.

I've created the following POC applet to assist with reproduction of the issue. In general it takes me about 30 iterations before it happens.

package com.poc.bug;

import javacard.framework.*;
import javacard.security.*;

public class PoCApplet extends Applet {
    public final KeyPair authenticatorKeyAgreementKeyPair;
    private final KeyAgreement ecAuthenticatorKeyAgreement;

    byte[] tempBuffer;

    public static void install(byte[] bArray, short bOffset, byte bLength) {
        new PoCApplet(bArray, bOffset, bLength);
    }

    public boolean select() {
        return true;
    }
    private PoCApplet(byte[] bArray, short bOffset, byte bLength) {
        authenticatorKeyAgreementKeyPair = new KeyPair(
                (ECPublicKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PUBLIC, KeyBuilder.LENGTH_EC_FP_256, false),
                (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE, KeyBuilder.LENGTH_EC_FP_256, false));
        ecAuthenticatorKeyAgreement = KeyAgreement.getInstance(KeyAgreement.ALG_EC_SVDP_DH_PLAIN, false);
        tempBuffer = new byte[256];
        register(bArray, (short) (bOffset + 1), bArray[bOffset]);
    }

    public void process(APDU apdu) throws ISOException {
        byte[] temp = apdu.getBuffer();
        if (selectingApplet()) {
            temp[0] = 'O';
            temp[1] = 'K';
            apdu.setOutgoingAndSend((short) 0, (short) 2);
            return;
        }
        apdu.setIncomingAndReceive();
        short offset = apdu.getOffsetCdata();
        switch(temp[offset++]) {
            case 0:
                ecAuthenticatorKeyAgreement.init(authenticatorKeyAgreementKeyPair.getPrivate());
                ecAuthenticatorKeyAgreement.generateSecret(temp, offset, (short) 65, tempBuffer, (short) 0);
                Util.arrayCopyNonAtomic(tempBuffer, (short) 0, temp, (short) 0, (short) 32);
                ((ECPrivateKey) authenticatorKeyAgreementKeyPair.getPrivate()).getS(temp, (short) 32);
                apdu.setOutgoingAndSend((short) 0, (short) 64);
                break;
            case 1:
                authenticatorKeyAgreementKeyPair.genKeyPair();
                ((ECPublicKey) authenticatorKeyAgreementKeyPair.getPublic()).getW(apdu.getBuffer(), (short) 0);
                apdu.setOutgoingAndSend((short) 0, (short) 65);
                break;
        }
    }
}

As said, takes quite some runs for it to actually trigger. Please note that this is a very simple PoC and it has no error handling or input checking and as such will cause exceptions if the input isn't as expected above.

The code itself has 2 different logic paths. One is where it generates a new keyPair and returns the XY coordinates generated for this. This can be triggered with 801000000101 and it will then return the public key as a 65 byte response, with the first byte being 04 for Uncompressed ECPoint, followed by the X coordinate at byte index 1 through 32 and the Y coordinates on byte index 33 through 64.

The other path is the generation of the actual secret. This expects that the first byte of the request is 00 and then followed by a public key in the same format as the response to getting the public data from the card. So for example 8010000042010435CF191F5ED586D83DE772F73D5B09B9CE3CC4A23B2C87286956F32E87092B8B4DB9989CD0FFA714521F34660ACF9150DA1E9983A1A680EC466ECE03E2F78C70. It will then return the secret on the first 32 bytes and then the private key it used for the next 32 bytes.