-
Notifications
You must be signed in to change notification settings - Fork 2
ETSI JSON Signatures
EU has standardized the JSON Signature format in ETSI TS 119 182-1. It is based on the JSON Web Signature (JWS) standard. The ETSI standard adds a few additional requirements to the JWS standard and is also knows as JSON advanced digital signatures - JAdES.
This library implements a class CryptoEx.JWS.ETSI.ETSISigner that may be used to produce and verify JAdES compliant signatures.
The class is ready to use for basic and most common scenarios. And in the current Wiki topic it is shown how to use it.
However, it is not a complete implementation of the, whole ETSI standards with all of it's particularities!
BUT, it is a good starting point for your own implementation, if you need some more specific or edge case. You can extend the class and override the methods that you need to change. Or you can write your own logic and use the class as a reference / example.
Tested & verified with DSS Demonstration WebApp.
The class CryptoEx.JWS.ETSI.ETSISigner:
public class ETSISigner : JWSSigner
{
/// <summary>
/// A constructor without a private key, used for verification
/// </summary>
public ETSISigner();
/// <summary>
/// A constructiror with an private key - RSA or ECDSA, used for signing
/// </summary>
/// <param name="signer">The private key</param>
/// <exception cref="ArgumentException">Invalid private key type</exception>
public ETSISigner(AsymmetricAlgorithm signer);
/// <summary>
/// A constructiror with an private key - RSA or ECDSA, used for signing and hash algorithm
/// </summary>
/// <param name="signer">The private key</param>
/// <param name="hashAlgorithm">Hash algorithm, mainly for RSA</param>
/// <param name="useRSAPSS">In case of RSA, whether to use RSA-PSS</param>
/// <exception cref="ArgumentException">Invalid private key type</exception>
public ETSISigner(AsymmetricAlgorithm signer, HashAlgorithmName hashAlgorithm, bool useRSAPSS = false);
/// <summary>
/// A constructiror with an private key - HMAC, used for signing.
/// I doubt that for ETSI you can use HMAC, but still here is it
/// </summary>
/// <param name="signer">The private key</param>
/// <exception cref="ArgumentException">Invalid private key type</exception>
public ETSISigner(HMAC signer);
/// <summary>
/// Clear some data.
/// Every thing except the signer and the HashAlgorithmName!
/// After calling 'Decode' and before calling 'Sign' you MUST call this method! 'Veryfy...' calls this method internally.
/// </summary>
public override void Clear();
/// <summary>
/// Digitally sign the attachement, optional payload and protected header in detached mode
/// </summary>
/// <param name="attachement">The attached data (file) </param>
/// <param name="optionalPayload">The optional payload. SHOUD BE JSON STRING.</param>
/// <param name="mimeTypeAttachement">Optionally mimeType. Defaults to "octet-stream"</param>
/// <param name="mimeType">Optionally mimeType of the payload</param>
/// <param name="typHeaderparameter">Optionally the 'typ' header parameter https://www.rfc-editor.org/rfc/rfc7515#section-4.1.9,
/// to put in the header.
/// </param>
/// <param name="b64">Wheter to use an Unencoded Payload Option - https://www.rfc-editor.org/rfc/rfc7797.
/// By defult it is not used, so the payload is encoded.
/// If you want to use it, set it to FALSE. And use it carefully and with understanding.
/// </param>
public void SignDetached(Stream attachement, string? optionalPayload = null, string mimeTypeAttachement = "octet-stream",
string? mimeType = null, string? typHeaderparameter = null, bool? b64 = null);
/// <summary>
/// Digitally sign the attachement, optional payload and protected header in detached mode
/// Async version, for when the attachement is a network stream or some other stream that may be
/// good to be read async.
/// </summary>
/// <param name="attachement">The attached data (file) </param>
/// <param name="optionalPayload">The optional payload. SHOUD BE JSON STRING.</param>
/// <param name="mimeTypeAttachement">Optionally mimeType. Defaults to "octet-stream"</param>
/// <param name="mimeType">Optionally mimeType of the payload</param>
/// <param name="typHeaderparameter">Optionally the 'typ' header parameter https://www.rfc-editor.org/rfc/rfc7515#section-4.1.9,
/// to put in the header.
/// </param>
/// <param name="b64">Wheter to use an Unencoded Payload Option - https://www.rfc-editor.org/rfc/rfc7797.
/// By defult it is not used, so the payload is encoded.
/// If you want to use it, set it to FALSE. And use it carefully and with understanding.
/// </param>
public async Task SignDetachedAsync(Stream attachement, string? optionalPayload = null, string mimeTypeAttachement = "octet-stream",
string? mimeType = null, string? typHeaderparameter = null, bool? b64 = null);
/// <summary>
/// Verify the signature of an enveloped JWS
/// </summary>
/// <param name="signature">The JWS signature</param>
/// <param name="payload">The payload in the signature document</param>
/// <param name="cInfo">returns the context info about the signature</param>
/// <returns>True signature is valid. False - no it is invalid</returns>
/// <exception cref="NotSupportedException">Some more advanced ETSI detached signatures, that are not yet implemented</exception>
public bool Verify(ReadOnlySpan<char> signature, out byte[] payload, out ETSIContextInfo cInfo);
/// <summary>
/// Verify the detached signature
/// </summary>
/// <param name="attachement">The dettached file</param>
/// <param name="signature">The JWS signature</param>
/// <param name="payload">Public keys to use for verification. MUST correspond to each of the JWS headers in the JWS, returned by te Decode method!</param>
/// <param name="cInfo">Etsi headers returnd by Decode method</param>
/// <returns>True / false = valid / invalid signature check</returns>
/// <exception cref="NotSupportedException">Some more advanced ETSI detached signatures, that are not yet implemented</exception>
public bool VerifyDetached(Stream attachement, ReadOnlySpan<char> signature,
out byte[] payload, out ETSIContextInfo cInfo);
/// <summary>
/// Verify the detached signature.
/// Async version, for when the attachement is a network stream or some other stream that may be
/// good to be read async.
///
/// NB. Unfortunatelly Async methods can not have out parameters, so the payload and cInfo, are not provided
/// out of the box. They are however available if you call the Decode method. So ypu can call Decode,
/// after successfull check of the signature, by this method, to get the payload and cInfo.
/// </summary>
/// <param name="attachement">The dettached file</param>
/// <param name="signature">The JWS signature</param>
/// <param name="payload">Public keys to use for verification. MUST correspond to each of the JWS headers in the JWS, returned by te Decode method!</param>
/// <param name="cInfo">Etsi headers returnd by Decode method</param>
/// <returns>True / false = valid / invalid signature check</returns>
/// <exception cref="NotSupportedException">Some more advanced ETSI detached signatures, that are not yet implemented</exception>
public async Task<bool> VerifyDetachedAsync(Stream attachement, string signature);
/// <summary>
/// Extract only the ETSI context info and payload, from the signature.
/// To be used mainly with VerifyAsync
/// </summary>
/// <param name="signature">The signature</param>
/// <param name="payload">The payload</param>
/// <returns>The ETSI context info</returns>
public ETSIContextInfo ExtractContextInfo(string signature, out byte[] payload);
}
As you may see it extends the base class CryptoEx.JWS.JWSSigner, the same way as JAdES standard extends the JWS standard.
Basically you can use the class with steps like these:
// Try get certificate
X509Certificate2? cert = GetCertificate(CertType.EC);
// Get RSA private key
ECDsa? ecKey = cert.GetECDsaPrivateKey();
if (ecKey != null) {
// Create signer
ETSISigner signer = new ETSISigner(ecKey, HashAlgorithmName.SHA512);
// Get payload
signer.AttachSignersCertificate(cert);
signer.Sign(Encoding.UTF8.GetBytes(message), "text/json", JWSConstants.JOSE_JSON);
// Encode - produce JWS
var jSign = signer.Encode(JWSEncodeTypeEnum.Flattened);
// Decode & verify
bool result = signer.Verify(jSign, out byte[] _, out ETSIContextInfo _);
// Here you don not need to get the public key for verification by your own logic !
// The 'Verify' method does it. It is possible for the 'Verify' method
// To get the public key, because as per ETSI standard - the signer MUST
// place the signing certificate inside the 'ETSIHeader' JWS header!
}
Basically you:
-
Get the signing key and/or certificate (from somewhere)
-
Create the ETSISigner instance
-
Attach the certificate to the JWS
-
Sign the payload
-
Encode the JWS
-
Optionally - Verify the JWS. The method returns true / false, based only on cryptographic verification of the digest and the digital signature.
BUT, the method also returns a ETSIContextInfo object, that contains additional information about the signature and the signing certificate, optionally timestamp. You can use it to further check if the signing certificate is valid, if the signing time is in the validity period, etc...
NB Please, refer to the test methods in the class CryptoEx.Tests.TestETSI for more examples and check out the implementation code in the library itself for insights.
The class ETSISigner provides 2 (two) async methods for signing and verifying detached signatures. These are especially useful when the attachement is a big file, network stream and etc.
To understand why it is so important to use these, you may also have a look to the topic Base64Url Encode and Decode in this Wiki. And more specifically to the section Interesting sample with large files and pipes.
Another point of interest might be the so-called "Unencoded Payload Option" and related to it b64 JWS signed header
parameter.
The Sign
methods has a optional parameter - b64
. If you do set it to false the produced jAdES signature will be in an Unencoded Payload. If you do not set the
b64
parameter or set it to true the Sign
method will produce jAdES signature in common (normal), base64Url encoded format.
The Verify
methods processes the signature in a correct way - it's logic understands the b64
header parameter and tries to verify the signatures accordingly.
There are some test methods, for the b64
option. They are at the CryptoEx.Tests.Test_B64_JWS_And_ETSI class and you may look at them for example usage.
For background information (specification) on the b64
stuff, please have a look at: RFC 7797.
CryptoEx