Search code examples
c#encryptioncryptographyapduecdh

Secure Messaging Implementation in C#


After establishing PACE PIN authentication, I am trying to implement secure messaging to be able to send secure APDU commands. I based my implementation on the ICAO 9303 part 11 manual, chapter 9.8, page 63 : https://www.icao.int/publications/Documents/9303_p11_cons_en.pdf,

and on this Java code that I used : https://github.com/ecsec/open-ecard/blob/master/ifd/ifd-protocols/pace/src/main/java/org/openecard/ifd/protocol/pace/SecureMessaging.java, as well as on a log from the PlatinumReader where secure messaging has already been implemented. Here is my code: the method for calculating the APDU:

 public string chipherAPDU(string apdu, string keyEnc, string keyMac)
   {
       string chipherApdu = "";

       byte[] apduBytes = StringToByteArray(apdu);
       byte[] KeyENC = StringToByteArray(keyEnc);
       byte[] KeyMAC = StringToByteArray(keyMac);
       byte[] secureMessagingSSC = StringToByteArray("00000000000000000000000000000001");
       SecureMessaging sm = new SecureMessaging(KeyMAC, KeyENC);
       chipherApdu = byteToHexStr ( sm.encrypt(apduBytes, secureMessagingSSC) );
       return chipherApdu;
   }

class SecureMessaging :

  internal class SecureMessaging
  {

          private readonly byte[] NULL = new byte[] { 0x00 };
          private const byte PAD = 0x80;
          private readonly byte[] secureMessagingSSC;
          private readonly byte[] keyMAC;
          private readonly byte[] keyENC;           

          public SecureMessaging(byte[] keyMAC, byte[] keyENC)
          {
              this.keyENC = keyENC;
              this.keyMAC = keyMAC;

              secureMessagingSSC = new byte[16];
          }

          public byte[] encrypt(byte[] apdu)
          {
              incrementSSC(secureMessagingSSC);
              byte[] commandAPDU = encrypt(apdu, secureMessagingSSC);
              incrementSSC(secureMessagingSSC);

              return commandAPDU;
          }

          public byte[] encrypt(byte[] apdu, byte[] secureMessagingSSC)
          {
              MemoryStream outputStream = new MemoryStream();
              CardCommandAPDU cAPDU = new CardCommandAPDU(apdu);

              if (cAPDU.isSecureMessaging())
              {
                  throw new ArgumentException("Malformed APDU.");
              }

              byte[] data = cAPDU.data;
              byte[] header = cAPDU.header;
              int lc = cAPDU.lc;
              int le = cAPDU.le;

              if (data.Length > 0)
              {
                  data = pad(data, 16);
                  using (Aes aes = Aes.Create())
                  {
                      aes.Key = keyENC;
                      aes.IV = getCipherIV(secureMessagingSSC);
                      aes.Mode = CipherMode.CBC;
                      aes.Padding = PaddingMode.None;

                      ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
                      byte[] encryptedData = encryptor.TransformFinalBlock(data, 0, data.Length);

                      outputStream.Write(new byte[] { 0x01 }, 0, 1);
                      outputStream.Write(encryptedData, 0, encryptedData.Length);
                  }
              }

              return outputStream.ToArray();
          }

          public byte[] decrypt(byte[] response)
          {
              if (response.Length < 12)
              {
                  throw new ArgumentException("Malformed Secure Messaging APDU.");
              }
              return decrypt(response, secureMessagingSSC);
          }

          private byte[] decrypt(byte[] response, byte[] secureMessagingSSC)
          {
              MemoryStream outputStream = new MemoryStream();
              byte[] statusBytes = new byte[2];
              byte[] dataObject = null;
              byte[] macObject = new byte[8];                

              return outputStream.ToArray();
          }

          public static void incrementSSC(byte[] ssc)
          {
              for (int i = ssc.Length - 1; i >= 0; i--)
              {
                  ssc[i]++;
                  if (ssc[i] != 0)
                  {
                      break;
                  }
              }
          }

          // Other methods omitted for brevity
          private byte[] pad(byte[] data, int blockSize)
          {
              byte[] result = new byte[data.Length + (blockSize - data.Length % blockSize)];
              Array.Copy(data, 0, result, 0, data.Length);
              result[data.Length] = PAD;

              return result;
          }

          private byte[] getCipherIV(byte[] smssc)
          {
              using (Aes aes = Aes.Create())
              {
                  aes.Key = keyENC;
                  aes.Mode = CipherMode.ECB;
                  aes.Padding = PaddingMode.None;

                  ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, null);
                  return encryptor.TransformFinalBlock(smssc, 0, smssc.Length);
              }
          }
  }

class CardCommandAPDU :

  public class CardCommandAPDU
  {
      public byte[] header = new byte[4];
      public int le = -1;
      public int lc = -1;
      public byte[] data;
      public CardCommandAPDU(byte[] commandAPDU)
      {
          Array.Copy(commandAPDU, 0, header, 0, 4);
          setBody(copy(commandAPDU, 4, commandAPDU.Length - 4));
      }



      /**
       * Définit le corps (LE, DATA, LC) de l'APDU.
       *
       * @param body Corps de l'APDU
       */
      public void setBody(byte[] body)
      {
          /*
           * Cas 1. : |CLA|INS|P1|P2|
           * Cas 2. : |CLA|INS|P1|P2|LE|
           * Cas 2.1: |CLA|INS|P1|P2|EXTLE|
           * Cas 3. : |CLA|INS|P1|P2|LC|DATA|
           * Cas 3.1: |CLA|INS|P1|P2|EXTLC|DATA|
           * Cas 4. : |CLA|INS|P1|P2|LC|DATA|LE|
           * Cas 4.1: |CLA|INS|P1|P2|EXTLC|DATA|LE|
           * Cas 4.2: |CLA|INS|P1|P2|LC|DATA|EXTLE|
           * Cas 4.3: |CLA|INS|P1|P2|EXTLC|DATA|EXTLE|
           */
          try
          {
              using (MemoryStream stream = new MemoryStream(body))
              {
                  int length = (int)stream.Length;

                  // Nettoyage
                  lc = -1;
                  le = -1;
                  data = new byte[0];

                  if (length == 0)
                  {
                      // Cas 1. : |CLA|INS|P1|P2|
                  }
                  else if (length == 1)
                  {
                      // Cas 2 |CLA|INS|P1|P2|LE|
                      le = stream.ReadByte() & 0xFF;
                  }
                  else if (length < 65536)
                  {
                      int tmp = stream.ReadByte();

                      if (tmp == 0)
                      {
                          // Cas 2.1, 3.1, 4.1, 4.3
                          if (stream.Length < 3)
                          {
                              // Cas 2.1 |CLA|INS|P1|P2|EXTLE|
                              le = (stream.ReadByte() & 0xFF) << 8 | stream.ReadByte() & 0xFF;
                          }
                          else
                          {
                              // Cas 3.1, 4.1, 4.3
                              lc = (stream.ReadByte() & 0xFF) << 8 | stream.ReadByte() & 0xFF;

                              data = new byte[lc];
                              stream.Read(data, 0, lc);

                              if (stream.Length == 1)
                              {
                                  // Cas 4.1 |CLA|INS|P1|P2|EXTLC|DATA|LE|
                                  le = stream.ReadByte() & 0xFF;
                              }
                              else if (stream.Length == 2)
                              {
                                  // Cas 4.3 |CLA|INS|P1|P2|EXTLC|DATA|EXTLE|
                                  le = (stream.ReadByte() & 0xFF) << 8 | stream.ReadByte() & 0xFF;
                              }
                              else if (stream.Length == 3)
                              {
                                  if (stream.ReadByte() == 0)
                                  {
                                      // Cas 4.3 |CLA|INS|P1|P2|EXTLC|DATA|EXTLE|
                                      le = (stream.ReadByte() & 0xFF) << 8 | stream.ReadByte() & 0xFF;
                                  }
                                  else
                                  {
                                      throw new ArgumentException("APDU malformée.");
                                  }
                              }
                              else if (stream.Length > 3)
                              {
                                  throw new ArgumentException("APDU malformée.");
                              }
                          }
                      }
                      else if (tmp > 0)
                      {
                          // Cas 3, 4, 4.2
                          lc = tmp & 0xFF;
                          data = new byte[lc];
                          stream.Read(data, 0, lc);

                          if (stream.Length == 1)
                          {
                              // Cas 4 |CLA|INS|P1|P2|LC|DATA|LE|
                              setLE((byte)stream.ReadByte());
                          }
                          else if (stream.Length == 3)
                          {
                              // Cas 4.2 |CLA|INS|P1|P2|LC|DATA|EXTLE|
                              stream.ReadByte(); // Ignorer le premier octet
                              setLE((short)((stream.ReadByte() & 0xFF) << 8 | stream.ReadByte() & 0xFF));
                          }
                          else if (stream.Length == 2 || stream.Length > 3)
                          {
                              throw new ArgumentException("APDU malformée.");
                          }
                      }
                      else
                      {
                          throw new ArgumentException("APDU malformée.");
                      }
                  }
                  else
                  {
                      throw new ArgumentException("APDU malformée.");
                  }
              }
          }
          catch (Exception e)
          {
              Console.WriteLine("Exception", e);
          }
      }

      /**
       * Définit le champ de longueur attendue (LE) de l'APDU.
       *
       * @param le Champ de longueur attendue (LE)
       */
      public void setLE(short le)
      {
          if (le == (short)0x0000)
          {
              setLE(65536);
          }
          else
          {
              setLE((int)le & 0xFFFF);
          }
      }

      /**
       * Définit le champ de longueur attendue (LE) de l'APDU.
       *
       * @param le Champ de longueur attendue (LE)
       */
      public void setLE(int le)
      {
          if (le < 0 || le > 65536)
          {
              throw new ArgumentException("La longueur doit être comprise entre '1' et '65535'.");
          }
          else
          {
              this.le = le;
          }
      }

              /**
       * Copie une plage de bytes.
       *
       * @param input l'entrée
       * @param offset l'offset
       * @param length la longueur
       * @return le tableau de bytes
       */
      public static byte[] copy(byte[] input, int offset, int length)
      {
          if (input == null)
          {
              return null;
          }
          byte[] tmp = new byte[length];
          Array.Copy(input, offset, tmp, 0, length);
          return tmp;
      }


      public  bool isSecureMessaging()
      {
          return (header[0] & 0x0F) == 0x0C;
      }

  }

Here are the data from the PlatinumReader that I used where it works fine, and I'm trying to get the same result:

APDU: Request: 00 a4 04 0c 07 a0 00 00 02 47 10 01          
GENERIC P7896 T36084 : APDU: SMRequest - 00000000000000000000000000000001
0c a4 04 0c 1d 87 11 01 a9 48 e4 f1 a8 23 56 c1
1c f1 a6 a0 b1 8d 0c 8f 8e 08 71 b6 0b b9 3b 7a 
f1 08 00

Here is the test i do by using the same values :

 string apduSM = chipherAPDU("00a4040c07a0000002471001", "3d7155f3791c313c2924f51ae60f1ac9" , "b5feb9488f17be03e54c7d80907e8a1f");
  addLogMsg("ADPU SM : " + apduSM);

And here is my result :

ADPU SM : 01A948E4F1A82356C11CF1A6A0B18D0C8F

The Keys used are ephemeral so i can post them and it just a testing phase:

Key: K_MAC - derived MAC key (K_MAC [PACE]) b5 fe b9 48 8f 17 be 03 e5 4c 7d 80 90 7e 8a 1f

Key: K_Enc - derived encryption key (K_Enc [PACE]) 3d 71 55 f3 79 1c 31 3c 29 24 f5 1a e6 0f 1a c9

And here is the test : https://dotnetfiddle.net/l3z7P1

I have part of the encrypted data, I should have a structure like: Header Lc' '87' L '01' 'Encrypted Data' Le' where header is : 0c a4 04 0c


Solution

  • The difference between the result generated by the C# code and the reference data is due to an incomplete porting of the Java reference code to C#.
    In particular, the implementation of the MAC and the implementation of the TLV class that performs ASN.1/DER encodings are missing. If the Java code is ported completely to C#, the reference data can be reproduced.

    In the following C# code, the encrypt(byte[], byte[]) method has been supplemented with the functionalities from the Java code that are required to generate the reference data (the corresponding line of the Java reference code is specified in the C# code):

    using Org.BouncyCastle.Crypto.Macs;
    using Org.BouncyCastle.Crypto.Engines;
    using Org.BouncyCastle.Crypto.Parameters;
    
    ...
    
    public byte[] encrypt(byte[] apdu, byte[] secureMessagingSSC)
    {
        CardCommandAPDU cAPDU = new CardCommandAPDU(apdu);
        
        if (cAPDU.isSecureMessaging())
        {
            throw new ArgumentException("Malformed APDU.");
        }
        
        byte[] data = cAPDU.data;
        byte[] header = cAPDU.header;
        int lc = cAPDU.lc;
        int le = cAPDU.le;
        
        byte[] dataEncrypted = [];
        if (data.Length > 0)
        {
            data = pad(data, 16);
            using (Aes aes = Aes.Create())
            {
                aes.Key = keyENC;
                aes.IV = getCipherIV(secureMessagingSSC);
                aes.Mode = CipherMode.CBC;
                aes.Padding = PaddingMode.None;
        
                ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
                dataEncrypted = encryptor.TransformFinalBlock(data, 0, data.Length);
        
                // Add padding indicator 0x01 
                // Java code: https://github.com/ecsec/open-ecard/blob/master/ifd/ifd-protocols/pace/src/main/java/org/openecard/ifd/protocol/pace/SecureMessaging.java#L114
                dataEncrypted = [0x01, ..dataEncrypted];
        
                // ASN.1/DER (TLV) - encrypted data
                // Java code: https://github.com/ecsec/open-ecard/blob/master/ifd/ifd-protocols/pace/src/main/java/org/openecard/ifd/protocol/pace/SecureMessaging.java#L117
                dataEncrypted = [0x87, (byte)dataEncrypted.Length, ..dataEncrypted];
            }
        }
        
        // Write protected LE: skipped as le < 0
        // Java code: https://github.com/ecsec/open-ecard/blob/master/ifd/ifd-protocols/pace/src/main/java/org/openecard/ifd/protocol/pace/SecureMessaging.java#L123
        if (le >= 0)
        {
            // ...
        }
        
        // Indicate Secure Messaging 
        // Java code:   https://github.com/ecsec/open-ecard/blob/master/ifd/ifd-protocols/pace/src/main/java/org/openecard/ifd/protocol/pace/SecureMessaging.java#L137
        header[0] |= 0x0C;
        
        // Calculate MAC
        // Java code: https://github.com/ecsec/open-ecard/blob/master/ifd/ifd-protocols/pace/src/main/java/org/openecard/ifd/protocol/pace/SecureMessaging.java#L142
        byte[] mac = new byte[16];
        CMac cmac = getCMAC(secureMessagingSSC);
        byte[] paddedHeader = pad(header, 16);
        cmac.BlockUpdate(paddedHeader, 0, paddedHeader.Length);
        if (dataEncrypted.Length > 0)
        {
            byte[] paddedData = pad(dataEncrypted, 16);
            cmac.BlockUpdate(paddedData, 0, paddedData.Length);
        }
        cmac.DoFinal(mac, 0);
        mac = mac[..8];
        
        // ASN.1/DER (TLV) - MAC
        // Java code: https://github.com/ecsec/open-ecard/blob/master/ifd/ifd-protocols/pace/src/main/java/org/openecard/ifd/protocol/pace/SecureMessaging.java#L160
        mac = [0x8e, (byte)mac.Length, ..mac];
        
        // Concate encrypted data and MAC 
        // Java code: https://github.com/ecsec/open-ecard/blob/master/ifd/ifd-protocols/pace/src/main/java/org/openecard/ifd/protocol/pace/SecureMessaging.java#L163 
        byte[] secureData = [..dataEncrypted, ..mac];
        
        // Add header and footer
        // Java code: https://github.com/ecsec/open-ecard/blob/master/ifd/ifd-protocols/pace/src/main/java/org/openecard/ifd/protocol/pace/SecureMessaging.java#L165
        byte[] secureCommand = [..header, (byte)secureData.Length, ..secureData, 0x00];
        
        return secureCommand;
    }
            
    private CMac getCMAC(byte[] smssc)
    {
        CMac cmac = new CMac(new AesEngine());
        cmac.Init(new KeyParameter(keyMAC));
        cmac.BlockUpdate(smssc, 0, smssc.Length);
    
        return cmac;
    }
    

    If these methods are replaced/added in the posted C# code, the reference data 0x0ca4040c1d871101a948e4f1a82356c11cf1a6a0b18d0c8f8e0871b60bb93b7af10800 can be reproduced.

    Please note that the above changes are incomplete and only implement the functionalities required for this specific reference data. For the C# code to work in general, the encrypt(byte[], byte[]) method from the Java code must be completely migrated!