Реализация безопасного обмена сообщениями на C#

После установки аутентификации PACE PIN я пытаюсь реализовать безопасный обмен сообщениями, чтобы иметь возможность отправлять безопасные команды APDU. Я основывал свою реализацию на руководстве ICAO 9303, часть 11, глава 9.8, стр. 63: https://www.icao.int/publications/Documents/9303_p11_cons_en.pdf,

и в этом коде Java, который я использовал: https://github.com/ecsec/open-ecard/blob/master/ifd/ifd-protocols/pace/src/main/java/org/openecard/ifd/protocol /pace/SecureMessaging.java, а также в журнале PlatinumReader, где уже реализован безопасный обмен сообщениями. Вот мой код: метод расчета 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;
   }

класс 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);
              }
          }
  }

класс 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;
      }

  }

Вот данные PlatinumReader, которые я использовал, где они работают нормально, и я пытаюсь получить тот же результат:

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

Вот тест, который я провожу, используя те же значения:

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

И вот мой результат:

ADPU SM : 01A948E4F1A82356C11CF1A6A0B18D0C8F

Используемые ключи являются эфемерными, поэтому я могу опубликовать их, и это всего лишь этап тестирования:

Ключ: K_MAC — производный MAC-ключ (K_MAC [PACE]) b5 fe b9 48 8f 17 be 03 e5 4c 7d 80 90 7e 8a 1f

Ключ: K_Enc — производный ключ шифрования (K_Enc[PACE]) 3d 71 55 f3 79 1c 31 3c 29 24 f5 1a e6 0f 1a c9

А вот тест: https://dotnetfiddle.net/l3z7P1

У меня есть часть зашифрованных данных, у меня должна быть такая структура: Заголовок Lc' '87' L '01' 'Зашифрованные данные' Le' где заголовок: 0c a4 04 0c

Знаете ли вы, что ваш результат включен в справочные данные 0ca4040c1d871101a948e4f1a82356c11cf1a6a0b18d0c8f8e0871b60bb9‌​3b7af10800? Я спрашиваю только потому, что ты вообще об этом не упоминаешь.

Topaco 30.03.2024 21:05

Да, я знаю! Извините, я уточню. Вот почему я думаю, что допустил ошибку в формуле. Мне нужна вся цепочка, я понял, что полученный результат — это всего лишь кодирование данных. Сам заголовок оставляем как есть, меняя CLA на CLA = 0x0c и адаптируя Le на Lc' на основе того, что попало в зашифрованные данные. Заголовок Lc' '87' L '01' Зашифрованные данные' Le Где заголовок: 0c a4 04 0c

Bouls 30.03.2024 21:25

Справочные данные, похоже, имеют некоторое сходство с данными в описании (заголовок, DO87, DO8E). Последний связан с MAC, который, похоже, реализован в коде Java, а не в коде C#, поэтому, возможно, в вашем случае эта часть отсутствует. Я не знаком с протоколом, так что это всего лишь предположение.

Topaco 30.03.2024 21:34

Возможно, вы захотите проверить реализацию StringToByteArray, чтобы убедиться, что она использует правильную кодировку. Кроме того, используются ли заметки @param инструментом? Если нет, рассмотрите возможность использования стандартных XML-комментариев C# (которые являются расходными).

Flydog57 30.03.2024 22:49

Мне кажется, что перенос кода Java в код C# не завершен. Помимо MAC, например. реализация класса TLV, выполняющего кодировку ASN.1, отсутствует. Это может привести к ошибкам, например. отсутствуют спецификации длины и, следовательно, к неправильному MAC (даже если он был реализован).

Topaco 31.03.2024 10:50

Да, я вижу это, я пытаюсь реализовать недостающий код в методе шифрования и классе TLV, Tag и TagLengthValue.

Bouls 31.03.2024 14:09
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
6
144
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Разница между результатом, сгенерированным кодом C#, и справочными данными связана с неполным портированием справочного кода Java на C#.
В частности, отсутствуют реализация MAC и реализация класса TLV, выполняющего кодирование ASN.1/DER. Если код Java полностью перенесен на C#, справочные данные можно воспроизвести.

В следующем коде C# метод encrypt(byte[], byte[]) дополнен функциями из кода Java, необходимыми для генерации справочных данных (соответствующая строка справочного кода Java указана в коде C#):

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;
}

Если эти методы заменить/добавить в опубликованном коде C#, справочные данные 0x0ca4040c1d871101a948e4f1a82356c11cf1a6a0b18d0c8f8e0871b60bb93b7af10800 можно будет воспроизвести.

Обратите внимание, что приведенные выше изменения являются неполными и реализуют только функции, необходимые для этих конкретных справочных данных. Чтобы код C# работал в целом, метод encrypt(byte[], byte[]) из кода Java должен быть полностью перенесен!

Спасибо вам большое, ребята. Я реализовал то, чего не хватало (этот код в ответе), и теперь все работает нормально.

Bouls 01.04.2024 22:36

Я сталкиваюсь с ошибками в моем методе шифрования. Я не могу выполнить две последовательные команды; второй всегда возвращает ошибку. Однако, если я выполню только второй, я не получу ошибки. APDU SM 1: Отправить=>0CA4040C1D8711013DF573A640BFC090E005821A7A63AFC88E083A‌​BF695BFA47D8A700 Получить<=990290008E08841BDC5AA2AFFD749000 APDU SM 2: Отправить=>0CA4 020C1D871101502DD053E2B81B61567E3E98A3F7EC878E0890‌​F909AE6E5F2E8B00 Get<=6988 6988 означает неверный объект данных безопасного обмена сообщениями (SM).

Bouls 03.04.2024 14:18

@Bouls - Код, опубликованный в ответе, не является полным портом кода Java на C# (что должно быть интуитивно понятно только по длине кода). Ответ должен просто продемонстрировать, что справочные данные могут быть воспроизведены, если будут перенесены все функции, необходимые для вашего варианта использования. На это прямо указано в последнем разделе.

Topaco 03.04.2024 15:37

@Bouls — вам могут понадобиться дополнительные функции для вашего нового варианта использования. Проверьте, содержит ли код C# все функции, необходимые для этого варианта использования. Сравните код C# с кодом Java, чтобы выявить недостающие функции. Если возможно, попробуйте выполнить обработку с помощью кода Java, что упростит устранение неполадок, а не простую проверку кода.

Topaco 03.04.2024 15:41

@Bouls — Если вам нужна дополнительная помощь, опубликуйте новый вопрос со всей необходимой информацией (код, тестовые данные и т. д.). В комментариях вообще невозможно решить такой (сложный) вопрос. При необходимости вы можете сослаться на этот ответ.

Topaco 03.04.2024 15:43

Другие вопросы по теме