Skip to content

ETSI JSON Signatures

Aleksandar Gyonov edited this page Feb 16, 2024 · 11 revisions

Preface

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.

How to use

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:

  1. Get the signing key and/or certificate (from somewhere)

  2. Create the ETSISigner instance

  3. Attach the certificate to the JWS

  4. Sign the payload

  5. Encode the JWS

  6. 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.

Some notes on Async methods

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.

B64 header - Unencoded Payload Option

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.