使用新的比利时身份证(eid 中间件和 itext)签署 PDF

Sign PDF with new Belgian id card (eid middleware and itext)

我尝试用比利时身份证签署一些 pdf 文件。 为实现这一目标,我使用比利时开斋节中间件对数据进行签名,并使用 itext7 为 pdf 加盖签名。

我使用 PdfSigner (itext) 并且实现了 IExternalSignature 来调用 eid 中间件来签署消息。

使用加密 RSA 和哈希 SHA256 的比利时身份证 1.7 一切正常。

但是当我尝试使用加密 ECDSA 和散列 SHA384 使用新的比利时身份证 1.8 签名时,adobe reader(或其他 reader)无法验证签名。 “文档已被更改或损坏”。

这似乎是散列中某处不匹配或...

我搜索了几天,但我没有更多的想法来解决这个问题...

有人知道出了什么问题吗?

在此先感谢您的帮助。

这里有一些额外的信息。

外部签名class:

    internal sealed class BeIDSignature : IExternalSignature
    {
        public string GetEncryptionAlgorithm()
        {
            return eidWrapper.Instance.GetEncryptionAlgorithm().ToString();
        }

        public string GetHashAlgorithm()
        {
            switch (EidWrapper.Instance.GetEncryptionAlgorithm())
            {
                case EidWrapper.EncryptionAmgorithm.RSA:
                    return DigestAlgorithms.SHA256;
                case EidWrapper.EncryptionAmgorithm.ECDSA:
                    return DigestAlgorithms.SHA384;
                default:
                    return null;
            }
        }
    
        public byte[] Sign(byte[] message)
        {
            return EidWrapper.Instance.SignData(message);
        }
    }

GetEncryptionAlgorithm 将 return RSA 或 ECDSA,具体取决于芯片。 sign 方法将使用 eid-mw packege 来生成签名。

EidWrapper sign方法的一小段代码:

    if (key.KeyType.KeyType == CKK.EC)
    {
        session.SignInit(new Mechanism(CKM.ECDSA_SHA384), key);
        return session.Sign(data);
    }
    else if (key.KeyType.KeyType == CKK.RSA)
    {
        session.SignInit(new Mechanism(CKM.SHA256_RSA_PKCS), key);
        return session.Sign(data);
    }

您可以在这里找到包含 3 个 pdf 文件的 zip:

https://easyupload.io/yzscsu

再次感谢您的宝贵时间。

一些想法:

  • 您确定芯片上有可用的 ECDSA 密钥吗?我快速浏览了文档(不确定它是否是最新的 - 参见 eid-mw github),其中只提到了 RSA。此外,如果您可以使用 RSA/SHA256 进行签名,那么支持 ECDSA 就意味着卡上还有第二个密钥对 - 我对此有些怀疑;
  • 尝试使用您的 eID 在 Adob​​e Reader 中使用 ECDSA/SHA384 进行签名 - 检查您是否可以验证签名;
  • 使用 SD-DSS tool 在线验证签名:诊断数据可能会帮助您 pin-pointing 哪里出了问题(例如生成了 sha384 摘要,但签名结构提到 sha256 作为摘要算法).

这个答案总结了对问题的评论。

iText 和 ECDSA 签名

首先必须认识到,目前(即对于 iText 7.2.2)ECDSA 并非被 iText 签名的所有部分支持 API。

此限制主要是由于 iText PdfPKCS7 class 用于创建和验证嵌入在 PDF 中的 CMS 签名容器。当它用于创建签名容器时,它存储在 signatureAlgorithm 字段中的标识符不考虑使用的哈希算法。例如。它对 RSA(使用 PKCS1 v1.5 填充)签名使用相同的值,无论它实际上是 SHA1withRSA 还是 SHA256withRSA 或任何组合。

对于 RSA,这没问题,因为确实有一个标识符可用于所有这些情况。对于 DSA,它还可以,因为在许多情况下,DSA 仅限于与 SHA1 一起使用。

对于 ECDSA,这 不是 好的,只有标识符考虑了哈希算法。 iText 在所有这些情况下都使用 EC public 密钥标识符,这是完全错误的。几乎没有人注意到这个错误的原因是 Adob​​e Acrobat 验证显然 忽略 这个 signatureAlgorithm 字段的内容:你甚至可以将 RSA 标识符写入ECDSA 签名的这个字段并且验证成功,没有任何问题的迹象。

因此,要创建正确的 ECDSA 签名,目前不应使用 PdfPKCS7 class。由于所有 PdfSigner.signDetached 方法在内部都使用 PdfPKCS7 class,这反过来意味着不能使用它们,而是 PdfSigner.signExternalContainer。因此,不得使用 IExternalSignature 实现来检索签名值,而应使用 IExternalSignatureContainer 实现,其中以不同方式构建 CMS 签名容器,例如使用 BouncyCastle classes。

在手头的案例中 IExternalSignatureBeIDSignature 实现,因此,必须相应地替换。

有关详细信息,请阅读 Which Interface to Use of the iText knowledge base article Digital Signing with iText 7 部分。

ECDSA 签名格式

有两种主要格式可以存储 ECDSA 签名值,作为两个整数的 TLV (DER) 编码序列或(普通编码)作为这两个整数的固定长度表示的串联。

根据使用的格式,必须分别为 ECDSA 和 PLAIN-ECDSA 使用特定的算法标识符。如果需要特定标识符,可以将签名值从一种格式转换为另一种格式。

在手头的案例中 比利时身份证 returns 纯格式的 ECDSA 签名值。要使用更常见的 non-PLAIN ECDSA 标识符,必须将该值转换为 DER 格式。这可以使用以下方法完成:

byte[] PlainToDer(byte[] plain)
{
    int valueLength = plain.Length / 2;
    BigInteger r = new BigInteger(1, plain, 0, valueLength);
    BigInteger s = new BigInteger(1, plain, valueLength, valueLength);
    return new DerSequence(new DerInteger(r), new DerInteger(s)).GetEncoded(Asn1Encodable.Der);
}

(X509Certificate2Signature helper method from the code examples of Digital Signing with iText 7)

这是比利时开斋节智能卡的外部容器示例。

代码未完全实现,但您有一个基础可以在 ECDSA/SHA384 中正确地签名。

希望对某人有所帮助:)


internal sealed class BeIdExternalSignatureContainer : IExternalSignatureContainer
    {
        private IX509Store _crls = null;
        private readonly IHttpClientFactory _httpClientFactory;
        private int _crlsSize = 0;

        public BeIdExternalSignatureContainer(IHttpClientFactory httpClientFactory)
        {
            this._httpClientFactory = httpClientFactory;
        }

        public void ModifySigningDictionary(PdfDictionary signDic)
        {
            signDic.Put(PdfName.Filter, PdfName.Adobe_PPKLite);
            signDic.Put(PdfName.SubFilter, PdfName.ETSI_CAdES_DETACHED);
        }

        public int ComputeEstimateSize()
        {
            // Base on itext estimation
            if (this._crlsSize == 0)
            {
                this.InitializeCrls();
            }

            return (8192 + this._crlsSize + 4192) * 2 + 2;
        }

        public byte[] Sign(Stream data)
        {
            IReadOnlyDictionary<string, byte[]> certificatesBinaries = EidWrapper.Instance.GetCertificateFiles(new string[] {
                        Constants.SIGNATURE_CERTIFICATE_NAME,
                        Constants.CA_CERTIFICATE_NAME,
                        Constants.ROOT_CERTIFICATE_NAME
                    });

            X509Certificate signCertificate = new(X509CertificateStructure.GetInstance(certificatesBinaries[Constants.SIGNATURE_CERTIFICATE_NAME]));
            EidWrapper.EncryptionAlgorithms encryptionAlgorithm = EidWrapper.Instance.GetEncryptionAlgorithm();

            SignerInfoGenerator signerGenerator = new SignerInfoGeneratorBuilder()
                .WithSignedAttributeGenerator(new PadesSignedAttributeGenerator { SigningCertificate = signCertificate })
                .WithUnsignedAttributeGenerator(new PadesUnsignedAttributeGenerator(this._httpClientFactory))
                .Build(new BeIdSignatureFactory(encryptionAlgorithm), signCertificate);

            CmsSignedDataGenerator gen = new();
            gen.AddSignerInfoGenerator(signerGenerator);

            IX509Store x509CertStore = X509StoreFactory.Create(
                "Certificate/Collection",
                new X509CollectionStoreParameters(certificatesBinaries.Values.Select(b => new X509Certificate(X509CertificateStructure.GetInstance(b))).ToList()));

            gen.AddCertificates(x509CertStore);
            gen.AddCrls(this.Crls);

            CmsProcessableInputStream cmsProcessableInputStream = new(data);
            CmsSignedData signedData = gen.Generate(cmsProcessableInputStream, false);
            return signedData.GetEncoded();
        }

        private IX509Store Crls
        {
            get
            {
                if (this._crls == null)
                {
                    this.InitializeCrls();
                }

                return this._crls;
            }
        }

        private void InitializeCrls()
        {
            this._crlsSize = 0;
            List<X509Crl> crls = new();
            X509CrlParser crlParser = new();
            HttpClient httpClient = this._httpClientFactory.CreateClient();
            foreach (var crlUrl in BeIdSignatureConstants.CRL_URL_COLLECTION)
            {
                using HttpResponseMessage response = httpClient.Send(new HttpRequestMessage { Method = HttpMethod.Get, RequestUri = new Uri(crlUrl) });
                if (response.IsSuccessStatusCode)
                {
                    using MemoryStream ms = new MemoryStream();
                    response.Content.ReadAsStream().CopyTo(ms);
                    byte[] crlBytes = ms.ToArray();
                    this._crlsSize += crlBytes.Length;
                    crls.Add(crlParser.ReadCrl(crlBytes));
                }
            }

            this._crls = X509StoreFactory.Create(
                "CRL/Collection",
                new X509CollectionStoreParameters(crls));
        }
    }

internal class BeIdSignatureFactory : ISignatureFactory
    {
        private readonly EidWrapper.EncryptionAlgorithms _encryptionAlgorithm;
        public BeIdSignatureFactory(EidWrapper.EncryptionAlgorithms encryptionAlgorithm)
        {
            this._encryptionAlgorithm = encryptionAlgorithm;
        }

        public IStreamCalculator CreateCalculator()
        {
            BeIdSigner signer = new(this._encryptionAlgorithm);
            signer.Init(true, null);
            return new DefaultSignatureCalculator(signer);
        }

        public object AlgorithmDetails
        {
            get
            {
                return MapAlgorithm(this._encryptionAlgorithm);
            }
        }

        public static AlgorithmIdentifier MapAlgorithm(EidWrapper.EncryptionAlgorithms encryptionAlgorithm)
        {
            switch (encryptionAlgorithm)
            {
                case EidWrapper.EncryptionAlgorithms.RSA:
                    return new AlgorithmIdentifier(PkcsObjectIdentifiers.Sha256WithRsaEncryption, DerNull.Instance);
                case EidWrapper.EncryptionAlgorithms.ECDSA:
                    return new AlgorithmIdentifier(X9ObjectIdentifiers.ECDsaWithSha384, DerNull.Instance);
                default:
                    throw new ArgumentException($"Unsupported encryption algorithm: {encryptionAlgorithm}");
            }
        }
    }

internal class BeIdSigner : ISigner
    {
        private byte[] _input;
        private readonly EidWrapper.EncryptionAlgorithms _encryptionAlgorithm;

        public BeIdSigner(EidWrapper.EncryptionAlgorithms encryptionAlgorithm)
        {
            this._encryptionAlgorithm = encryptionAlgorithm;
        }

        public void Init(bool forSigning, ICipherParameters parameters)
        {
            this.Reset();
        }

        public void Update(byte input)
        {
            throw new NotImplementedException("The \"ISigner.Update\" method is not implemented for the \"BeIdSigner\" class.");
        }

        public void BlockUpdate(byte[] input, int inOff, int length)
        {
            this._input = input.Skip(inOff).Take(length).ToArray();
        }

        public byte[] GenerateSignature()
        {
            return this._encryptionAlgorithm == EidWrapper.EncryptionAlgorithms.ECDSA
                ? PlainToDer(EidWrapper.Instance.SignData(this._input))
                : EidWrapper.Instance.SignData(this._input);
        }

        public bool VerifySignature(byte[] signature)
        {
            throw new NotImplementedException("The \"ISigner.VerifySignature\" method is not implemented for the \"BeIdSigner\" class.");
        }

        public void Reset()
        {
            this._input = null;
        }

        public string AlgorithmName => throw new NotImplementedException("The \"ISigner.AlgorithmName\" property is not implemented for the \"BeIdSigner\" class.");

        private static byte[] PlainToDer(byte[] plain)
        {
            int valueLength = plain.Length / 2;
            BigInteger r = new(1, plain, 0, valueLength);
            BigInteger s = new(1, plain, valueLength, valueLength);
            return new DerSequence(new DerInteger(r), new DerInteger(s)).GetEncoded(Asn1Encodable.Der);
        }
    }