После установки аутентификации 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
Да, я знаю! Извините, я уточню. Вот почему я думаю, что допустил ошибку в формуле. Мне нужна вся цепочка, я понял, что полученный результат — это всего лишь кодирование данных. Сам заголовок оставляем как есть, меняя CLA на CLA = 0x0c и адаптируя Le на Lc' на основе того, что попало в зашифрованные данные. Заголовок Lc' '87' L '01' Зашифрованные данные' Le Где заголовок: 0c a4 04 0c
Справочные данные, похоже, имеют некоторое сходство с данными в описании (заголовок, DO87, DO8E). Последний связан с MAC, который, похоже, реализован в коде Java, а не в коде C#, поэтому, возможно, в вашем случае эта часть отсутствует. Я не знаком с протоколом, так что это всего лишь предположение.
Возможно, вы захотите проверить реализацию StringToByteArray, чтобы убедиться, что она использует правильную кодировку. Кроме того, используются ли заметки @param инструментом? Если нет, рассмотрите возможность использования стандартных XML-комментариев C# (которые являются расходными).
Мне кажется, что перенос кода Java в код C# не завершен. Помимо MAC, например. реализация класса TLV, выполняющего кодировку ASN.1, отсутствует. Это может привести к ошибкам, например. отсутствуют спецификации длины и, следовательно, к неправильному MAC (даже если он был реализован).
Да, я вижу это, я пытаюсь реализовать недостающий код в методе шифрования и классе TLV, Tag и TagLengthValue.





Разница между результатом, сгенерированным кодом 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 должен быть полностью перенесен!
Спасибо вам большое, ребята. Я реализовал то, чего не хватало (этот код в ответе), и теперь все работает нормально.
Я сталкиваюсь с ошибками в моем методе шифрования. Я не могу выполнить две последовательные команды; второй всегда возвращает ошибку. Однако, если я выполню только второй, я не получу ошибки. APDU SM 1: Отправить=>0CA4040C1D8711013DF573A640BFC090E005821A7A63AFC88E083ABF695BFA47D8A700 Получить<=990290008E08841BDC5AA2AFFD749000 APDU SM 2: Отправить=>0CA4 020C1D871101502DD053E2B81B61567E3E98A3F7EC878E0890F909AE6E5F2E8B00 Get<=6988 6988 означает неверный объект данных безопасного обмена сообщениями (SM).
@Bouls - Код, опубликованный в ответе, не является полным портом кода Java на C# (что должно быть интуитивно понятно только по длине кода). Ответ должен просто продемонстрировать, что справочные данные могут быть воспроизведены, если будут перенесены все функции, необходимые для вашего варианта использования. На это прямо указано в последнем разделе.
@Bouls — вам могут понадобиться дополнительные функции для вашего нового варианта использования. Проверьте, содержит ли код C# все функции, необходимые для этого варианта использования. Сравните код C# с кодом Java, чтобы выявить недостающие функции. Если возможно, попробуйте выполнить обработку с помощью кода Java, что упростит устранение неполадок, а не простую проверку кода.
@Bouls — Если вам нужна дополнительная помощь, опубликуйте новый вопрос со всей необходимой информацией (код, тестовые данные и т. д.). В комментариях вообще невозможно решить такой (сложный) вопрос. При необходимости вы можете сослаться на этот ответ.
Знаете ли вы, что ваш результат включен в справочные данные 0ca4040c1d871101a948e4f1a82356c11cf1a6a0b18d0c8f8e0871b60bb93b7af10800? Я спрашиваю только потому, что ты вообще об этом не упоминаешь.