Я создаю пустой контейнер для цифровой подписи, затем получаю хэш файла и отправляю его на подпись в сервис цифровой подписи. После этого получаю ответ — цифровую подпись в виде строки. Далее я встраиваю эту строку в контейнер, созданный в файле. Но я не могу пройти авторизацию на сайте https://ec.europa.eu/digital-building-blocks/DSS/webapp-demo/validation И я получаю ошибку в этом сервисе введите сюда описание изображения
public HashInfo[] ConvertHashToBase64(string[] filePaths, JsonData identityInfo, string uniqueId)
{
SPWeb cWeb = SPContext.Current.Web;
List<HashInfo> hashInfos = new List<HashInfo>();
foreach (string filePath in filePaths)
{
var signInstance = new PAdESSignerClass();
signInstance.EmptySignature(signedPath, aWeb, identityInfo, uniqueId);
SPFile file = aWeb.GetFile(filePath);
var fileStream = signedFile.OpenBinary();
using (SHA256 sha256Hash = SHA256.Create())
{
byte[] hashBytes = sha256Hash.ComputeHash(fileStream);
string base64Hash = Convert.ToBase64String(hashBytes);
hashInfos.Add(new HashInfo
{
Hash = base64Hash,
UniqueId = file.UniqueId,
FileName = file.Name
});
}
}
return hashInfos.ToArray();
}
public class PAdESSignerClass
{
public void EmptySignature(string containerFilePath, SPWeb web, JsonData jsonData, string guidString)
{
SPFile file = web.GetFile(containerFilePath);
var cert = jsonData.cert;
string certificate = cert.certificates[0].ToString();
byte[] certificateBytes = Convert.FromBase64String(certificate);
X509Certificate2 xcertificate = new X509Certificate2(certificateBytes);
IBouncyCastleFactory FACTORY = BouncyCastleFactoryCreator.GetFactory();
IX509Certificate[] iTextCertificates2 = FACTORY.CreateX509CertificateParser().ReadAllCerts(xcertificate.GetRawCertData()).ToArray();
using (Stream fileStream = file.OpenBinaryStream())
{
using (MemoryStream memoryStream = new MemoryStream())
{
iText.Kernel.Pdf.PdfReader reader = new iText.Kernel.Pdf.PdfReader(fileStream);
PdfSigner signer = new PdfSigner(reader, memoryStream, new StampingProperties());
signer.GetSignatureAppearance().SetCertificate(iTextCertificates2[0]);
signer.SetFieldName(guidString);
iText.Signatures.IExternalSignatureContainer external = new ExternalBlankSignatureContainer(iText.Kernel.Pdf.PdfName.Adobe_PPKLite,
iText.Kernel.Pdf.PdfName.Adbe_pkcs7_detached);
signer.SignExternalContainer(external, 8192);
byte[] pdfBytes = memoryStream.ToArray();
web.Files.Add(containerFilePath, pdfBytes, true);
}
}
}
public void AddSignatureToPdf(string certPath, JsonData jsonData, string containerFilePath, byte[] signatureHash, SPWeb web, string uniqueId, string timestamp)
{
var cert = jsonData.cert;
string certificate = cert.certificates[0].ToString();
byte[] certificateBytes = Convert.FromBase64String(certificate);
SPFile file = web.GetFile(containerFilePath);
using (Stream fileStream = file.OpenBinaryStream())
{
using (MemoryStream memoryStream = new MemoryStream())
{
iText.Kernel.Pdf.PdfReader pdfDoc = new iText.Kernel.Pdf.PdfReader(fileStream);
PdfSigner signer = new PdfSigner(pdfDoc, memoryStream, new iText.Kernel.Pdf.StampingProperties());
X509Certificate2 xcertificate = new X509Certificate2(certificateBytes);
IBouncyCastleFactory FACTORY = BouncyCastleFactoryCreator.GetFactory();
IX509Certificate[] iTextCertificates2 = FACTORY.CreateX509CertificateParser().ReadAllCerts(xcertificate.GetRawCertData()).ToArray();
IPrivateKey iTextPrivateKey = FACTORY.CreateRsa2048KeyPairGenerator().GenerateKeyPair().GetPrivateKey();
signer.GetSignatureAppearance();
iText.Signatures.IExternalSignatureContainer external = new MyExternalSignatureContainer(iTextPrivateKey, iTextCertificates2, signatureHash, certificateBytes, timestamp);
PdfSigner.SignDeferred(signer.GetDocument(), uniqueId, memoryStream, external);
byte[] signedBytes = memoryStream.ToArray();
web.Files.Add(containerFilePath, signedBytes, true);
}
}
}
}
public class MyExternalSignatureContainer : iText.Signatures.IExternalSignatureContainer
{
private readonly byte[] signedBytes;
protected IPrivateKey pk;
protected IX509Certificate[] chain;
protected byte[] signatureHash;
protected byte[] certificateBytes;
protected string timestamp;
public MyExternalSignatureContainer(IPrivateKey pk, IX509Certificate[] chain, byte[]signatureHash, byte[] certificateBytes, string timestamp)
{
this.pk = pk;
this.chain = chain;
this.signatureHash = signatureHash;
this.certificateBytes = certificateBytes;
this.timestamp = timestamp;
}
public void ModifySigningDictionary(iText.Kernel.Pdf.PdfDictionary signDic)
{
}
public byte[] Sign(Stream inputStream)
{
byte[] encodedSig = null;
iText.Signatures.PrivateKeySignature signature = new iText.Signatures.PrivateKeySignature(pk, "SHA256");
string digestAlgorithmName = signature.GetDigestAlgorithmName();
iText.Signatures.PdfPKCS7 sgn = new iText.Signatures.PdfPKCS7(null, chain, digestAlgorithmName, false);
byte[] hash = iText.Signatures.DigestAlgorithms.Digest(inputStream, digestAlgorithmName);
byte[] sh = sgn.GetAuthenticatedAttributeBytes(hash, PdfSigner.CryptoStandard.CMS, null, null);
byte[] extSignature = signature.Sign(sh);
sgn.SetExternalSignatureValue(signatureHash, null, signature.GetSignatureAlgorithmName());
encodedSig = sgn.GetEncodedPKCS7(hash, PdfSigner.CryptoStandard.CMS, null, null, null);
return encodedSig;
}
}
Пример цифрового кода ответа на запрос «Получить идентификационную информацию»
{
"key": {
"status": "enabled",
"algo": [
"1.2.840.113549.1.1.11"
],
"len": 2048
},
"cert": {
"status": "valid",
"certificates": [
"MIIG1TCCBL2gAwIBAgI...V2yYRGjwk/GQsq67A= = "
],
"issuerDN": "C=NL,O=Digidentity B.V.,organizationIdentifier=NTRNL-27322631,CN=TEST Digidentity Personal Qualified CA",
"serialNumber": "7867EX1A9M6PLE3A8C4K6751Q05ADD3C",
"subjectDN": "C=NL,SN=Doe,GN=John,serialNumber=4exa3441-m601-44p6-9416-ls695cid3775,CN=John Doe",
"validFrom": "20220209095106Z",
"validTo": "20230209095106Z"
},
"authMode": "implicit",
"multisign": 1,
"lang": "en-US",
"SCAL": "2"
}
Обновлено 24 мая 24 г. Итак, если мне не нужно создавать пустой контейнер и добавлять его в файл, то мне тоже не следует использовать метод SignDeferred? Я получаю хэш:
string digestAlgorithmName = signature.GetDigestAlgorithmName();
byte[] hash = iText.Signatures.DigestAlgorithms.Digest(inputStream, digestAlgorithmName);
byte[] sh = sgn.GetAuthenticatedAttributeBytes(hash, PdfSigner.CryptoStandard.CMS, null, null);
Я использую этот sh для создания хэша для CSC API:
using (SHA256 sha256Hash = SHA256.Create())
{
byte[] hashBytes = sha256Hash.ComputeHash(sh);
string base64Hash = Convert.ToBase64String(hashBytes);
hashInfos.Add(new HashInfo
{
Hash = base64Hash,
UniqueId = file.UniqueId,
FileName = file.Name
});
}
И после этого я выполняю:
byte[] extSignature = signature.Sign(sh);
и устанавливаем подпись, полученную от Digidentity:
sgn.SetExternalSignatureValue(signatureHash, null, signature.GetSignatureAlgorithmName());
@mkl Я загрузил файл на Google Диск drive.google.com/file/d/1-BBQ5q-FjoEzGZYPs2BfuSPjB47UTPPW/… Не знаю, как прикрепил файл к комментарию. Это файл, подписанный по схеме, указанной я описал в вопросе
@mkl Я добавил новый файл с данными продукта. drive.google.com/file/d/1RWG9czxEBUdzem2-Ya0Aa3iXV-17Krvw/…
Подпись первого файла примера выглядит зашифрованной несовпадающим ключом. По крайней мере, подпись второго файла примера зашифрована ключом, соответствующим вашему сертификату, но хэш, подписанный им, не является хешем подписанных атрибутов вашей подписи. Таким образом, второй вариант лучше (сохраните внесенные вами изменения, чтобы получить его), но проблема все равно остается. Я посмотрю на ваш код, у меня есть идея, что искать.
Учитывая код в вашем вопросе, похоже, что он подписал ваш первый файл, но не второй: AddSignatureToPdf
добавляет контейнер подписи, подписанный произвольным ключом RSA, не связанным с сертификатом, который вы установили в качестве сертификата подписывающего лица. Что касается вашего второго файла, генерирующий код был улучшен и теперь использует правильный закрытый ключ. Обновите свой вопрос, чтобы отобразить этот код.
@mkl Я обновил код. Теперь он соответствует коду, использованному для подписи второго файла, и были использованы данные продукта из Digidentity.
«Я обновил код. Теперь он соответствует коду, использованному для подписи второго файла, и использовались данные прода от Digidentity» — Это маловероятно. Ваш код по-прежнему создает подпись с использованием закрытого ключа, который генерируется случайным образом независимо от сертификата. Однако второй файл был подписан с использованием закрытого ключа, соответствующего сертификату. Шансы на то, что случайно сгенерированный закрытый ключ соответствует правильному закрытому ключу, практически равны нулю.
При этом, как Digidentity дает вам доступ к правильному закрытому ключу? Создается ли сертификат на основе запроса сертификата, который вы сгенерировали самостоятельно (и, следовательно, у вас все еще есть закрытый ключ)? Или они вам прислали какой-то магазин ключей? Или это на каком-то внешнем устройстве? Или даже веб-сервис?
@mkl Я взаимодействую с Digidentity, используя их API: docs.digidentity.com В частности: Для аутентификации: Аутентификация -> Код авторизации -> Вход без пароля Для подписи: Подписание -> CSC API Я получаю сертификат в ответ на Получить удостоверение запрос информации в следующем виде(этот код я добавил в пост) Они же предоставили данные необходимые для авторизации в их системе. Но они не предоставили никакого закрытого ключа
«CSC API [...] Они также предоставили данные, необходимые для авторизации в своей системе. Но никакого закрытого ключа они не предоставили» — Вы упоминаете CSC API. CSC — это консорциум облачных подписей, который опубликовал API CSC для подписи с использованием служб подписи в облаке. Таким образом, у вас никогда не будет настоящего закрытого ключа для вашего сертификата. Вместо этого вам придется отправить им данные для подписи (т. е. ваш GetAuthenticatedAttributeBytes
или их хеш) в веб-запросе, чтобы получить ответ, содержащий байты подписи. На странице docs.digidentity.com объясняется, как это сделать.
@mkl Я не понимаю, что я делаю не так? Отправляю хеш файла, предварительно создав в нем контейнер для ЭЦП, на подпись. После этого я получаю ответ от Digidentity в виде строки подписи и пытаюсь добавить эту подпись в существующий контейнер. Подскажите, пожалуйста, что мне нужно сделать, чтобы пройти валидацию?
«Не понимаю, что я делаю не так? Я отправляю хеш файла, предварительно создав в нем контейнер для ЭЦП, на подпись». - Я не вижу, чтобы вы вообще что-либо отправляли или получали в публикуемом вами коде. Но если вы имеете в виду хеши, которые вы собираете в ConvertHashToBase64
, то они неверны вдвойне. Они были бы полезны для внешних файлов подписей, но с помощью iText вы пытаетесь создать интегрированные подписи, которые работают по-другому: для интегрированных подписей хэш документа рассчитывается не по всему подготовленному PDF-файлу, а только по нему за вычетом заполнителя для подписи!
... На самом деле вы уже вычислили этот хэш документа в своем методе MyExternalSignatureContainer.Sign
как byte[] hash
. Но даже этот хеш не подлежит прямой подписи. Вместо этого это одно значение среди многих в наборе так называемых аутентифицированных (или подписанных) атрибутов. Вы извлекаете байты этих атрибутов в MyExternalSignatureContainer.Sign
как byte[] sh
. Поэтому вам нужно отправить хеш-значение sh
для подписи и использовать результат как byte[] extSignature
.
@mkl Итак, в функции EmptySignature после создания контейнера мне также нужно создать хеш с помощью GetAuthenticatedAttributeBytes и передать этот хеш в функцию ConvertHashToBase64, откуда он потом будет отправлен в Digidentity. После этого мне следует использовать строку, полученную от Digidentity, для метода SignDeferred?
Ну, строго говоря, самым простым изменением было бы просто внутри MyExternalSignatureContainer.Sign
вычислить хэш sh
, отправить его в CSC API, получить результат и использовать его. Есть ли конкретная причина, по которой вы хотите иметь хэши в другом месте?
Нет конкретной причины хранить хэш в другом месте. Я просто не понимаю, вы говорите, что самым простым способом было бы отправить хеш непосредственно в методе MyExternalSignatureContainer.Sign: byte[] sh = sgn.GetAuthenticatedAttributeBytes(hash, PdfSigner.CryptoStandard.CMS, null, null); а затем используйте строку из Digidentity для sgn.SetExternalSignatureValue.
Но создание пустого контейнера необходимо? и этот хэш sh должен формироваться после добавления пустого контейнера, верно? Формирование хэша необходимо сначала для запроса авторизации подписи (Digidentity), а затем тот же хэш для подписи в запросе Sign hash (Digidentity). Поэтому мне нужно его сформировать и сохранить для использования в обоих методах API.
Я попытался отправить этот хэш в Digidentity: byte[] sh = sgn.GetAuthenticatedAttributeBytes(hash, PdfSigner.CryptoStandard.CMS, null, null); и получил ошибку от службы Digidentity — подготовить_подпись_фаилед — невозможно вызвать сертификат. В большинстве случаев это указывает на то, что указанное значение хеш-функции недействительно. Итак, хеш недействителен. Извините, но не могли бы вы еще раз объяснить, какую последовательность мне следует использовать?
«Я попытался отправить этот хэш в Digidentity: byte[] sh = sgn.GetAuthenticatedAttributeBytes(hash, PdfSigner.CryptoStandard.CMS, null, null);» - это не хеш! Сначала вам нужно вычислить хэш этого, как я предлагал в своем предыдущем комментарии.
«Но создание пустого контейнера необходимо?» - нет. Весь опубликованный вами код излишне сложен. То, что я предложил, было просто самым простым способом сделать его работоспособным с учетом кода, который вы показываете. После этого идет оптимизация цели.
@mkl Не могли бы вы взглянуть? Я обновил сообщение — в разделе «Обновлено 24.05.24». Подскажите пожалуйста, правильно ли я понял логику?
«Я обновил сообщение — в разделе «Обновлено 24.05.24». - Я не совсем уверен, что правильно понимаю, о чем вы там говорите. В частности, вы только готовите некоторые данные для API CSC, но не указываете, отправляете ли вы их в API и когда, и используете ли вы результат. И затем вы вызываете signature.Sign(sh)
, но signature
в вашем коде является объектом PrivateKeySignature
, основанным на случайном закрытом ключе, поэтому эта подпись не имеет никакого смысла.
@mkl Могу ли я написать вам на почту или любой другой удобный для вас мессенджер? В комментариях есть ограничения на количество символов. Это возможно? Когда решение заработает, я обязательно его добавлю и отмечу ваш ответ как правильный.
Я получил ваше письмо. Я отвечу позже.
В комментариях мы попытались исправить существующий код для правильной подписи PDF-файлов с помощью службы подписи API CSC. Это не сработало. Поэтому для другого подхода я показываю в этом ответе более простой кодовый фрейм, в который вы можете включить фактическую связь CSC. К сожалению, вы не предоставили этот код, поэтому я не могу включить его сюда в код ответа.
При подписании PDF-файла с помощью iText вам, как правило, следует использовать одноэтапный подход (да, ниже еще необходимо несколько шагов, но iText сделает это за вас).
Только при особых обстоятельствах имеет смысл рассмотреть многоэтапный подход, при котором сначала PDF-файл только подготавливается к подписанию, вычисляется его хэш и где-то сохраняется, а затем вставляется каким-то образом полученный за это время контейнер подписи в этот подготовленный PDF-файл с помощью средства отсроченного подписания.
Таким образом, следующий код использует одноэтапный подход, который запрашивает каждую подпись отдельно от вашего сервиса. Более того, поскольку использование CSC API v1 возвращает голые подписанные хеши (не полноценные контейнеры подписей PKCS#7/CMS), это позволяет iText создавать контейнер подписей.
При таком подходе весь код, взаимодействующий с вашим сервисом, заключен в одну IExternalSignature
реализацию:
class ServiceSignature : IExternalSignature
{
public string GetDigestAlgorithmName()
{
return "SHA-256";
}
public string GetSignatureAlgorithmName()
{
return "RSA";
}
public ISignatureMechanismParams? GetSignatureMechanismParameters()
{
return null;
}
public X509CertificateBC[] GetChain()
{
// Insert your code that retrieves your certificate chain from your signature
// service and returns it as X509CertificateBC[].
}
public byte[] Sign(byte[] message)
{
byte[] hash = SHA256.HashData(message);
// Insert your code that retrieves a signature for that hash value
// from your signature service and returns it.
}
}
(SignWithService вспомогательный класс)
Затем вы можете использовать этот класс ServiceSignature
, чтобы просто подписать один PDF-файл за один шаг:
ServiceSignature signature = new ServiceSignature();
using (PdfReader reader = new PdfReader(pdfFile))
using (MemoryStream mem = new MemoryStream())
{
PdfSigner signer = new PdfSigner(reader, mem, new StampingProperties().UseAppendMode());
signer.SignDetached(signature, signature.GetChain(), null, null, null, 0, PdfSigner.CryptoStandard.CMS);
File.WriteAllBytes(resultFile, mem.ToArray());
}
(SignWithService тест SignWithIExternalSignature
)
(На самом деле здесь не нужен MemoryStream
, вы можете напрямую использовать файловый поток.)
Чтобы подписать несколько файлов, просто поместите структуру using
в цикл.
Очевидно, вы можете немного оптимизировать класс ServiceSignature
. Например, вы можете получить цепочку сертификатов из своего сервиса только один раз и кэшировать результат. Также вы можете сначала запросить у своего сервиса поддерживаемые алгоритмы хеширования и подписи и сделать так, чтобы возвращаемые значения GetDigestAlgorithmName
, GetSignatureAlgorithmName
и GetSignatureMechanismParameters
зависели от вашего выбора.
Большое спасибо за ваш ответ! Как проверю вместе с запросами на цифру, обязательно напишу о результате
Благодаря вашему ответу я успешно прошел проверку. Я безмерно благодарен за вашу помощь!
Поделитесь примером PDF-файла, подписанного вашим кодом, для анализа.