'Sign PDF with new Belgian id card (eid middleware and itext)
I try to sign some pdf's with a Belgian id card. To acheive that, I'm using the belgium eid middleware to sign the data and itext7 to stamp the pdf with the signature.
I use a PdfSigner (itext) and I have implement a IExternalSignature to call the eid middleware to sign the message.
All work well for the Belgian id card 1.7 with encryption RSA and hash SHA256.
But when I try to sign with the new Belgian id card 1.8 with encryption ECDSA and hash SHA384, the signature can't be validated by adobe reader (or other reader). "The document has been altered or corrupted".
It seems to be a mismatch somewhere in the hashing or ...
I search for some days but I have no more idea to fix that ...
Someone have an idea about what is going wrong?
Thanks in advance for your help.
Here some additional informations.
The external signature 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 will return RSA or ECDSA depending of the chip. The sign method will use the eid-mw packege to generate the signature.
A little piece of code of the sign method of the EidWrapper:
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);
}
You can find here a zip with 3 pdf files:
- The original file
- One signed directly with adobe (siganture is ok)
- One signed with eid-mw and itext (signature is NOT ok). But remember that is working for RSA/SHA256 siganture.
Thanks again for your time.
Solution 1:[1]
This answer sums up the comments to the question.
iText and ECDSA signatures
First of all one has to realize that currently (i.e. for iText 7.2.2) ECDSA is not supported by all parts of the iText signing API.
This limitation is mostly due to the iText PdfPKCS7
class used to create and validate CMS signature containers embedded in PDFs. When it is used to create a signature container, the identifier it stores in the signatureAlgorithm field does not take the used hash algorithm into account. E.g. it uses the same value for RSA (with PKCS1 v1.5 padding) signatures, no matter if it's actually SHA1withRSA or SHA256withRSA or whichever combination.
For RSA this is ok because there indeed is an identifier that can be used for all these cases. For DSA it is somewhat ok because in many contexts DSA is limited to use with SHA1 only.
For ECDSA this is not ok, there only are identifiers taking the hash algorithm into account. iText uses the EC public key identifier in all these cases which is simply wrong. The reason why hardly anyone noticed this bug is that Adobe Acrobat validation apparently ignores the contents of this signatureAlgorithm field: You can even write the RSA identifier into this field of an ECDSA signature and the validation succeeds without an indication of a problem.
To create proper ECDSA signatures, therefore, one currently should not use the PdfPKCS7
class. As all the PdfSigner.signDetached
methods internally use the PdfPKCS7
class, this in turn means that one must not use them but instead PdfSigner.signExternalContainer
. As a consequence, one must not use an IExternalSignature
implementation to retrieve one's signature value but instead an IExternalSignatureContainer
implementation in which one builds the CMS signature container differently, for example using BouncyCastle classes.
In the case at hand the BeIDSignature
implementation of IExternalSignature
, therefore, must be replaced accordingly.
For further details please read the section Which Interface to Use of the iText knowledge base article Digital Signing with iText 7.
ECDSA signature formats
There are two major formats in which an ECDSA signature value can be stored, either as a TLV (DER) encoded sequence of two integers or (plain encoding) as the concatenation of fixed length representations of those two integers.
Depending on the used format one has to use specific algorithm identifiers for ECDSA and PLAIN-ECDSA respectively. If one needs a specific identifier, one can convert the signature value from one format to the other.
In the case at hand the Belgian ID card returns the ECDSA signature value in plain format. To use the more common non-PLAIN ECDSA identifiers, one has to convert that value to the DER format. This can be done using this method:
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)
Solution 2:[2]
Here is a sample of an external container for the belgian eid smartcard.
The code is not fully implemented but you have a base to make a siganture in ECDSA/SHA384 correctly.
Hope that will help someone :)
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);
}
}
Solution 3:[3]
Some thoughts:
- Are you sure that there is an ECDSA key available on the chip ? I had a short look at the documentation (not sure it's up to date - cf. eid-mw github), which only mentions RSA. Additionally if you can sign using RSA/SHA256, having ECDSA support as well would mean that there's a second key pair on the card - I have some doubts about this ;
- Try to sign with ECDSA / SHA384 in Adobe Reader using your eID - check whether you can validate the signature ;
- Validate the signature online, using the SD-DSS tool: the diagnostic data may help you in pin-pointing what is wrong (e.g. a sha384 digest was generated, but the signature structure mentions sha256 as digest algo).
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | mkl |
Solution 2 | Nicolas Cop |
Solution 3 | veebee |