Хорошо, наконец понял.
Прежде всего, сообщения PKCS представляют собой сложные структуры, закодированные в двоичном виде с использованием ASN1.
Во-вторых, их можно сериализовать в двоичные файлы (кодировка DER) или текстовые файлы PEM с использованием Кодировка Base64.
В-третьих, формат PKCS#7 определяет несколько типов пакетов, из которых my называется Signed Data. Эти форматы отличаются значением OBJECT IDENTIFIER в начале объекта ASN1 (1-й элемент последовательности обертки) — вы можете перейти к http://lapo.it/asn1js/ и вставьте текст пакета для полностью проанализированного структура.
Далее нам нужно разобрать пакет (Base64 -> ASN1 -> некоторое представление объекта). К сожалению, для этого нет пакета npm. Я нашел неплохой проект forge, который не опубликован в реестре npm (хотя и совместим с npm). Он анализировал формат PEM, но результирующее дерево довольно неприятно для обхода. Основываясь на их реализациях Encrypted Data и Enveloped Data, я создал частичную реализацию подписанных данных в своем собственном форке. UPD: мой запрос на включение позже был объединен с проектом forge.
Теперь, наконец, мы разобрали все это. В этот момент я нашел отличную (и, вероятно, единственную во всей сети) пояснительную статью о проверке подписанного PKCS#7: http://qistoph.blogspot.com/2012/01/manual-verify-pkcs7-signed-data-with.html
Мне удалось извлечь и успешно расшифровать подпись из файла, но хеш внутри отличался от хэша данных. Боже, благослови Криса, который объяснил, что на самом деле происходит.
Процесс подписи данных состоит из двух шагов:
- вычисляется хеш исходного контента
- строится набор «Авторизованных атрибутов», включающий: тип подписываемых данных, время подписи и хэш данных
Затем набор из шага 2 подписывается с использованием закрытого ключа подписывающей стороны.
Из-за особенностей PKCS#7 этот набор атрибутов хранится внутри сконструированного типа, зависящего от контекста (класс = 0x80, тип = 0), но должен быть подписан и проверен как обычный SET (класс = 0, тип = 17).
Как упоминает Крис (https://stackoverflow.com/a/16154756/108533), это только проверяет, что атрибуты в пакет действительны. Мы также должны проверить фактический хэш данных по атрибуту дайджеста.
Итак, наконец, вот код, выполняющий проверку (cert.pem
— это файл сертификата, который мне прислал провайдер, package
— это закодированное в PEM сообщение, которое я получил от них через HTTP POST):
var fs = require('fs');
var crypto = require('crypto');
var forge = require('forge');
var pkcs7 = forge.pkcs7;
var asn1 = forge.asn1;
var oids = forge.pki.oids;
var folder = '/a/path/to/files/';
var pkg = fs.readFileSync(folder + 'package').toString();
var cert = fs.readFileSync(folder + 'cert.pem').toString();
var res = true;
try {
var msg = pkcs7.messageFromPem(pkg);
var attrs = msg.rawCapture.authenticatedAttributes;
var set = asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SET, true, attrs);
var buf = Buffer.from(asn1.toDer(set).data, 'binary');
var sig = msg.rawCapture.signature;
var v = crypto.createVerify('RSA-SHA1');
v.update(buf);
if (!v.verify(cert, sig)) {
console.log('Wrong authorized attributes!');
res = false;
}
var h = crypto.createHash('SHA1');
var data = msg.rawCapture.content.value[0].value[0].value;
h.update(data);
var attrDigest = null;
for (var i = 0, l = attrs.length; i < l; ++i) {
if (asn1.derToOid(attrs[i].value[0].value) === oids.messageDigest) {
attrDigest = attrs[i].value[1].value[0].value;
}
}
var dataDigest = h.digest();
if (dataDigest !== attrDigest) {
console.log('Wrong content digest');
res = false;
}
}
catch (_e) {
console.dir(_e);
res = false;
}
if (res) {
console.log("It's OK");
}
person
Community
schedule
22.04.2013