Ручная проверка подписи XML
Я могу успешно выполнить ручную проверку ссылок (канонизировать каждый ссылочный элемент -> SHA1 -> Base64 -> проверить, совпадает ли это с содержимым DigestValue), но мне не удается проверить SignatureValue. Вот SignedInfo для канонизации и хэширования:
<ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:CanonicalizationMethod>
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"></ds:SignatureMethod>
<ds:Reference URI="#element-1-1291739860070-11803898">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:Transform>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></ds:DigestMethod>
<ds:DigestValue>d2cIarD4atw3HFADamfO9YTKkKs=</ds:DigestValue>
</ds:Reference>
<ds:Reference URI="#timestamp">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:Transform>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></ds:DigestMethod>
<ds:DigestValue>YR/fZlwJdw+KbyP24UYiyDv8/Dc=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
После удаления всех пробелов между тегами (и, таким образом, получая весь элемент в одной строке), я получаю этот дайджест sha1 (в Base64):
6l26iBH7il / yrCQW6eEfv / VqAVo =
Теперь я ожидаю найти тот же дайджест после расшифровки контента SignatureValue, но получаю другое и более длинное значение:
MCEwCQYFKw4DAhoFAAQU3M24VwKG02yUu6jlEH + u6R4N8Ig =
Вот некоторый Java-код для расшифровки:
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = dbf.newDocumentBuilder();
Document doc = builder.parse(new File(inputFilePath));
NodeList nl = doc.getElementsByTagName("ds:SignatureValue");
if (nl.getLength() == 0) {
throw new Exception("Cannot find SignatureValue element");
}
String signature = "OZg96GMrGh0cEwbpHwv3KDhFtFcnzPxbwp9Xv0pgw8Mr9+NIjRlg/G1OyIZ3SdcOYqqzF4/TVLDi5VclwnjBAFl3SEdkyUbbjXVAGkSsxPQcC4un9UYcecESETlAgV8UrHV3zTrjAWQvDg/YBKveoH90FIhfAthslqeFu3h9U20=";
X509Certificate cert = X509Certificate.getInstance(new FileInputStream(<a file path>));
PublicKey pubkey = cert.getPublicKey();
Cipher cipher = Cipher.getInstance("RSA","SunJCE");
cipher.init(Cipher.DECRYPT_MODE, pubkey);
byte[] decodedSignature = Base64Coder.decode(signature);
cipher.update(decodedSignature);
byte[] sha1 = cipher.doFinal();
System.out.println(Base64Coder.encode(sha1));
Меня сильно смущает то, что два дайджеста имеют разный размер, но, конечно, мне также нужно получить одно и то же значение из двух вычислений. Какие-либо предложения? Спасибо.
2 ответа
MCEwCQYFKw4DAhoFAAQU3M24VwKG02yUu6jlEH+u6R4N8Ig=
кодирование Base64 для структуры ASN.1 в кодировке DER: SEQUENCE
содержащий сначала AlgorithmIdentifier
(в котором говорится, что это SHA-1, без параметров, поскольку SHA-1 не принимает ни одного), затем OCTET STRING
который содержит фактическое 20-байтовое значение. В шестнадцатеричном формате это значение: dccdb8570286d36c94bba8e5107faee91e0df088
,
Эта структура ASN.1 является частью стандартного механизма подписи RSA. Вы используете расшифровку RSA для доступа к этой структуре, которая является нестандартной. На самом деле вам повезло что-либо получить, поскольку шифрование RSA и подпись RSA - это два разных алгоритма. Бывает так, что они оба используют одинаковые пары ключей, и что схемы подписи и шифрования старого стиля (PKCS#1 v1.5) используют аналогичные методы заполнения (похожие, но не идентичные; уже немного удивительно, что реализация RSA на Java не подавляет заполнение подписи при использовании в режиме дешифрования).
Тем не мение, 6l26iBH7il/yrCQW6eEfv/VqAVo=
это кодировка Base64 для 20-байтового значения, которое в шестнадцатеричном виде: ea5dba8811fb8a5ff2ac2416e9e11fbff56a015a
, Это то, что вы получаете, хэшируя структуру XML, показанную выше, после удаления всех пробелов между тегами. Удаление всех пробелов не является правильной каноникой. На самом деле, насколько я знаю, пробелы затрагиваются только между атрибутами внутри тегов, но внешний пробел должен оставаться неизменным (за исключением нормализации конца строки [вещь LF / CR+LF]).
Значение, которое использовалось для генерации подписи (dccdb85...
) можно получить, используя показанный вами объект XML и удалив начальные пробелы. Для ясности: вы копируете + вставляете XML в файл, затем удаляете начальные пробелы (от 0 до 3 пробелов) в каждой строке. Вы убедитесь, что все конец строки используют один LF (0x0A байт), и вы удаляете последний LF (тот, что сразу после </ds:SignedInfo>
). Полученный файл должен иметь длину 930 байт, и его хэш SHA-1 является ожидаемым dccdb85...
значение.
Глядя на ваш конкретный XML-токен, я могу рассказать вам несколько вещей.
Вы используете метод канонизации Исключительная XML Канонизация Версия 1.0. Это очень важный фактор, гарантирующий, что вы создадите правильные значения дайджеста и подпись.
Вы используете один и тот же метод канонизации как для вычисления эталонных дайджестов, так и для канонизации SignedInfo перед созданием подписи.
Спецификация для эксклюзивного XML Canonicalizaiton версии 1.0 производится W3C и может быть найдена в соответствующей Рекомендации W3C. Если вы вычисляете свои значения вручную, убедитесь, что вы точно соответствуете спецификации, потому что канонизацию трудно сделать правильно, и очень важно сделать это правильно, иначе ваши значения будут неправильными.
Я только что написал обширную статью, описывающую процесс проверки подписи XML. Статья находится в моем блоге. Он описывает процесс гораздо более подробно, чем мой ответ, поскольку в XML Signature много сложностей. Он также содержит ссылки на распространенные спецификации и RFC.