Search code examples
c#bouncycastleelliptic-curvediffie-hellmanecdh

PACE PIN Generic Mapping implementation in c#


I'm trying to implement PACE PIN authentication with a generic mapping using elliptic curves. Here is the beginning of my method, I have a 6984 error here : • Send PKPCD,map and Receive PKPICC,map from card. 6984 error mean DATA_INVALID : Given data or data in the card (e.g., saved challenge) is invalid. I tried to refer to this code https://github.com/PersoApp/docs/blob/main/PACE.md](https://github.com/PersoApp/docs/blob/main/PACE.md) and also to the ICAO manual where they explain the PACE generic Mapping https://www.icao.int/publications/Documents/9303_p11_cons_en.pdf

enter image description here

I customized the card with the following curve information:

Using ECC-NIST 256 curve for the domain parameters

# Private ECC key (Generated from a RNG)
d_pace_ecdh_card_private=0029482318BE67844AE13D6C2CD672AE69525F9016496DF15AF141BB26E901EB
prime=FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
a=FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC
order=FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551
b=5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B
G_X=
basepoint=6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C2964FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5
# Calculate Ecc public key (Q=d_pace_ecdh_card_private*basepoint)
pace_ecdh_public=046CEC918BFBE1198005718DEA22D2139C856B66326B66A8AF769DAADB4D4F439B56A161713158746F8D3117F097972B1F9CB2382E54A3AF2D61FAA30ED1FEF436

Here is my function

public void paceGenericMapping()
  {
      // Paramètres du domaine pour la courbe ECC-NIST 256
      X9ECParameters curve = SecNamedCurves.GetByName("secp256r1");
      // Utilisation des paramètres déjà connus

      Org.BouncyCastle.Math.BigInteger prime = new Org.BouncyCastle.Math.BigInteger("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF", 16);
      Org.BouncyCastle.Math.BigInteger a = new Org.BouncyCastle.Math.BigInteger("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC", 16);
      Org.BouncyCastle.Math.BigInteger order = new Org.BouncyCastle.Math.BigInteger("FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", 16);
      Org.BouncyCastle.Math.BigInteger b = new Org.BouncyCastle.Math.BigInteger("5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B", 16);
      Org.BouncyCastle.Math.BigInteger G_X = new Org.BouncyCastle.Math.BigInteger("6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296", 16);
      Org.BouncyCastle.Math.BigInteger G_Y = new Org.BouncyCastle.Math.BigInteger("4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5", 16);
      Org.BouncyCastle.Math.EC.ECPoint basePoint = curve.Curve.CreatePoint(G_X, G_Y);

      
      ECDomainParameters domainParams = new ECDomainParameters(curve.Curve, basePoint, order, curve.H, curve.GetSeed());

      //  • Read EF.DIR and EF.CardAccess (applet capabilities)
      string selectEF_F000 = executeCommande("00A40000023F00");
      string selectEF_CardAccess = executeCommande("00a4020c02011c");
      string read_binary_1 = executeCommande("00b0000005");
      string read_binary_2 = executeCommande("00b000055b");

      //  • Select algorithm (INS 22)
      string setMSE = executeCommande("0022c1a412800a04007f0007020204020283010384010c");

      //  • The host requests PACE for authentication, possibly giving a preference : 
      string paceAuthenticationREQUEST = executeCommande("10860000027c0000");

      //  •   The card responds with a 128-bit (16 byte) random number (nonce) encrypted with PACE_nonce_AES128key  : 7C 12 80 10 <encrypted_nonce>
      int startIndex = paceAuthenticationREQUEST.IndexOf("7C128010") + "7C128010".Length;

      // Trouver l'index de fin de la sous-chaîne
      int endIndex = paceAuthenticationREQUEST.IndexOf("9000", startIndex);

      // Extraire la sous-chaîne : representant Key: z - encrypted nonce (z [PACE])
      string encryptedNonce = paceAuthenticationREQUEST.Substring(startIndex, endIndex - startIndex);

      //  the host decrypts the 128-bit nonce with PACE_nonce_AES128key derived from user input as described above
      byte[] bytesEncryptedNonce = StringToByteArray(encryptedNonce);

      // Key: K - derived key from shared secret : trouver la clé dérivée du PIN
      string derivedKeySH = calculerSHA1("3132333400000003");
      byte[] bytesDerivedKeySH = StringToByteArray(derivedKeySH);

      //  • [decrypt nonce nonce]
      byte[] bytesDecryptedNonce = decryptAES(bytesEncryptedNonce, bytesDerivedKeySH);
      string decryptedNonce = byteToHexStr(bytesDecryptedNonce);
      Org.BouncyCastle.Math.BigInteger nonce = new Org.BouncyCastle.Math.BigInteger(decryptedNonce, 16);

      //  • [generate random number as private key SKPCD for DHKA]           
      SecureRandom random = new SecureRandom();
      Org.BouncyCastle.Math.BigInteger SKPCD = new Org.BouncyCastle.Math.BigInteger(domainParams.Curve.Order.BitLength, random).Mod(domainParams.Curve.Order);

      // •  [calculate ephermeral PKPCD = BasePoint G * SKPCD]
      Org.BouncyCastle.Math.EC.ECPoint PKPCD = domainParams.G.Multiply(SKPCD);
      // Convertir les coordonnées X et Y en hexadécimal
      string PKPCDXHex = PKPCD.XCoord.ToBigInteger().ToString(16);
      string PKPCDYHex = PKPCD.YCoord.ToBigInteger().ToString(16);         

      //  • Send PKPCD and Receive PKPICC from card
      string requestPKicc = "10860000457C43814104" + PKPCDXHex + PKPCDYHex + "00";
      string reponsePKicc = executeCommande(requestPKicc);

      // Définir les positions de début et de fin de la sous-chaîne
         int entete = 10; // Après les 4 premiers octets
         int sw = reponsePKicc.Length - 4; // Avant les 4 derniers octets

      // Extraire la sous-chaîne
       reponsePKicc = reponsePKicc.Substring(entete, sw - entete);         

      string PKICCXHex = reponsePKicc.Substring(0, reponsePKicc.Length/2);
      string PKICCYHex = reponsePKicc.Substring(reponsePKicc.Length / 2);
     
      Org.BouncyCastle.Math.EC.ECPoint PKPICC = curve.Curve.CreatePoint(new Org.BouncyCastle.Math.BigInteger(PKICCXHex, 16), new Org.BouncyCastle.Math.BigInteger(PKICCYHex, 16));

      //  • [build shared secret: H = PKPICC * SKPCD = G * SKPICC * SKPCD]
      Org.BouncyCastle.Math.EC.ECPoint H = PKPICC.Multiply(SKPCD);

      //  • [build new base point:] :  Gmap = G * s + H = G * s + G * SKPICC * SKPCD = G * (s + SKPICC * SKPCD)
      Org.BouncyCastle.Math.EC.ECPoint Gmap = basePoint.Multiply(nonce);           Gmap = Gmap.Add(H);

      //  • [generate random number as private key SKPCD,map for DHKA]            
      Org.BouncyCastle.Math.BigInteger SKPCDMap = new Org.BouncyCastle.Math.BigInteger(Gmap.Curve.Order.BitLength, random).Mod(Gmap.Curve.Order);

      //  • [calculate ephermeral PKPCD,map = BasePoint Gmap * SKPCD,map]
      
      Org.BouncyCastle.Math.EC.ECPoint PKPCDMap = Gmap.Multiply(SKPCDMap);
      // Convertir les coordonnées X et Y en hexadécimal
      string PKPCDMapXHex = PKPCDMap.XCoord.ToBigInteger().ToString(16);
      string PKPCDMapYHex = PKPCDMap.YCoord.ToBigInteger().ToString(16);

      //  • Send PKPCD,map and Receive PKPICC,map from card
      string requestPKpcdmap = "10860000457c43834104" + PKPCDMapXHex + PKPCDMapYHex + "00";
      string responsePKpcdmap = executeCommande(requestPKpcdmap);

     }

Solution

  • The actual problem in your current question is the determination of the public key from the private key.

    In order to have a reference value, the private key from the PACE log (which is available to you according to your old posts) is used, DH_PCD_SK (in your code SKPCD) and the value calculated with your code is verified with the corresponding public key from the PACE log, DH_PCD_PK (in your code PKPCD):

    Values from the PACE log:
    DH_PCD_SK/SKPCD 0xe7d872d42157246e0e243e5d96934b798aa82b39d9d2b6c6a82435a6b233b1e5
    DH_PCD_PK/PKPCD 0x044aef83454ddd59b48124dfcfad73979f671591bde3d812f103a54a5202f49f50850e5db442944a449ad24a50e71c5bc7f27d1034c732d4ea3b9c6f96207417bc00 
    request         0x10860000457c438141044aef83454ddd59b48124dfcfad73979f671591bde3d812f103a54a5202f49f50850e5db442944a449ad24a50e71c5bc7f27d1034c732d4ea3b9c6f96207417bc00 
    

    With your code, the public key is indeed calculated incorrectly, which produces the following incorrect request:

    request         0x10860000457c43814104d22ee3fd544730aebad66cea40125828561ef31fc2257000611cabc8d98b03de7fc6f52bbe7d39a7ef2bfbaf95b8a0012bf950fd5948e04844a204dda543b69b00
    

    This is caused by a bug in the implementation, which is that no normalization was performed after the multiplication with the base point.
    Most implementations use special coordinates to increase the performance and perform coordinate transformations, so that normalization is necessary at the end.
    If this normalization is carried out, e.g.:

    Org.BouncyCastle.Math.EC.ECPoint PKPCD = domainParams.G.Multiply(SKPCD).Normalize();
    

    the request becomes:

    request         0x10860000457c438141044aef83454ddd59b48124dfcfad73979f671591bde3d812f103a54a5202f49f50850e5db442944a449ad24a50e71c5bc7f27d1034c732d4ea3b9c6f96207417bc00
    

    and therefore matches the reference value.


    Note the following:

    • The request requires the uncompressed key. This can be easily generated with PKPCD.GetEncoded(false), i.e. a simpler alternative to your implementation is

      string requestPKicc = "10860000457c438141" + Convert.ToHexString(PKPCD.GetEncoded(false)).ToLower() + "00";
      

      Incidentally, when generating the uncompressed key, an implicit normalization is performed under the hood.

    • Since your domain parameters correspond to the default values, the code can be simplified with:

      ECDomainParameters domainParams = new ECDomainParameters(curve.Curve, curve.G, curve.N, curve.H, curve.GetSeed());
      

      However, if the domain parameters are changed later during processing, the explicit values must of course be used (as you have done).
      For your information, the domain parameters are ASN.1/DER encoded in the PACE Log and can therefore be loaded in an ASN.1 parser, e.g. here, which makes them easier to read.

    • The posted code is not executable and there is no test data available, so troubleshooting is hard. For further problems you should provide an MCVE that focuses on the actual problem and uses test data from the PACE log, as I demonstrated in my answer.