C#- How to sign certificate XML document without URI attribute on <Reference> tag?

1.6k views Asked by At

It is possible to sign a XML document without passing the URI attribute to the <Reference> tag?

I was able to sign the entire XML document of type <Invoice> using KeyInfoX509Data certificate and XAdEs format using C#. The signature is valid, SignedXml.CheckSignature() returns true. The problem is that the broker who is responsible to validate the signature doesn't support empty attributes, such as:

<ds:Reference URI=""> // Here is the problem
            <ds:Transforms>
                <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
            </ds:Transforms>
            <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
            <ds:DigestValue>rDCBSbPc1QFATbQeP+77oskLJ3Tw6aog61bqGtyglXs=</ds:DigestValue>
        </ds:Reference>

I decided to remove the URI attribute before sending the document to broker, but SignedXml.CheckSignature() returns false without it. The signature is not valid without the URI.

I tried to remove the URI attribute before calling the SignedXml.ComputeSignature(), but I got the following Exception:

An XmlDocument context is required for enveloped transforms.

Which means that the URI attribute is mandatory when using the XmlDsigEnvelopedSignatureTransform class to generate the signature.

Then, I tried to set a Id attribute to the <Invoice> tag:

<Invoice Id="id-dsig-123456"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../oasis/maindoc/UBL-Invoice-2.1.xsd"
     xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
     xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
     xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">

This means that the signature will still validate the entire <Invoice> document, by searching for the Id attribute on the <Reference>tag:

<ds:Reference URI="#id-dsig-123456">

The signature is generated and the SignedXml.CheckSignature() returns true, but the broker doesn't allow any attributes on <Invoice> tag and invalidates the signature.

I need to generate the signature like this

<ds:Signature Id="id-a086e4d3-ee02-458c-8429-eafe54eb6f6b"
              xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
    <ds:SignedInfo>
        <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
        <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
        <ds:Reference> ***// Without URI***
            <ds:Transforms>
                <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
            </ds:Transforms>
            <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
            <ds:DigestValue>rDCBSbPc1QFATbQeP+77oskLJ3Tw6aog61bqGtyglXs=</ds:DigestValue>
        </ds:Reference>
        <ds:Reference URI="#xades-id-a086e4d3-ee02-458c-8429-eafe54eb6f6b"
                      Type="http://uri.etsi.org/01903#SignedProperties">
            <ds:Transforms>
                <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
            </ds:Transforms>
            <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
            <ds:DigestValue>xBW/yL+uQJa3KrCXjdEPlFEJns7ZnM2pHs0Y5nLNvMM=</ds:DigestValue>
        </ds:Reference>
    </ds:SignedInfo>
    <ds:SignatureValue>XDTgRB3qyzLPMBPzfYeuwGSOz5JN52cdstIMHo3IXc8jGWp5JWFbOTD7Nj7QHZB0z5mULXgL7/eWfR5KkrT12aRHWPHocCbLfBgAi1xjNYGW1aNNxhnn4fSZSg4KrWS9oMoxkEKM2IrbJ++PgIM4+89MZA4kEtyih4WKkENnlE+w4RDnstjirA5viLZQEBDegPtGqS9ybrVej5QkF5/fy8HpZKsEl4oWPIWSrTQk7G3d4oDo/d2AU8XQgvcht+LkttJF4PJRq3AiyqwlJjoNSbt1R7NWYEJp+IulWEU0pdvW7LcVjrLO8yIkoHTWtyhbP20Zi3zLHxP9uPpMD37tzQ==</ds:SignatureValue>
    <ds:KeyInfo>
        <ds:X509Data>
            <ds:X509Certificate>Certificate raw content...</ds:X509Certificate>
        </ds:X509Data>
    </ds:KeyInfo>
    <ds:Object>
        <xades:QualifyingProperties Target="#id-a086e4d3-ee02-458c-8429-eafe54eb6f6b"
                                    xmlns:xades="http://uri.etsi.org/01903/v1.3.2#">
            <xades:SignedProperties Id="xades-id-a086e4d3-ee02-458c-8429-eafe54eb6f6b">
                <xades:SignedSignatureProperties>
                    <xades:SigningTime>2021-06-05T02:52:49Z</xades:SigningTime>
                    <xades:SigningCertificate>
                        <xades:Cert>
                            <xades:CertDigest>
                                <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
                                <ds:DigestValue>LgjnhFUgsN4oTt4wjtzF+/7GYHEkTLVijMpjlMjfZ2E=</ds:DigestValue>
                            </xades:CertDigest>
                            <xades:IssuerSerial>
                                <ds:X509IssuerName>Certificate data....</ds:X509IssuerName>
                                <ds:X509SerialNumber>Serial number...</ds:X509SerialNumber>
                            </xades:IssuerSerial>
                        </xades:Cert>
                    </xades:SigningCertificate>
                </xades:SignedSignatureProperties>
            </xades:SignedProperties>
        </xades:QualifyingProperties>
    </ds:Object>
</ds:Signature>

To generate the signature, I'm using the following code. The PrefixedSignedXml class inherits from SignedXml and is used to set the "ds:" prefix when using the SignedXmlmethods:

private PrefixedSignedXml GeneratePrefixedSignedXml(XmlDocument xmlDocument, X509Certificate2 certificate)
    {
        Log.Information("[Begin] - GeneratePrefixedSignedXml");

        Sha256SignatureDescription.Register();

        var keyInfo = new KeyInfo();
        var cspParams = new CspParameters(24) { KeyContainerName = PrefixedSignedXml.XmlKeyContainerName };
        var rsaKey = new RSACryptoServiceProvider(cspParams) { PersistKeyInCsp = false };

        keyInfo.AddClause(new KeyInfoX509Data(certificate));

        var signedXml = new PrefixedSignedXml(xmlDocument) { SigningKey = rsaKey };

        signedXml.KeyInfo = keyInfo;
        signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl;
        signedXml.SignedInfo.SignatureMethod = PrefixedSignedXml.XmlRsaSignatureMethod;

        var signatureReference = new Reference
        {
            Uri = "", // This needs to be null
            DigestMethod = PrefixedSignedXml.XmlRsaDigestMethod,
        };

        signatureReference.AddTransform(new XmlDsigEnvelopedSignatureTransform());
        signedXml.AddReference(signatureReference);

        Log.Information("[End] - GeneratePrefixedSignedXml");

        return signedXml;
    }

This is the PrefixedSignedXml:

public class PrefixedSignedXml : SignedXml
{
    public const string XmlDsigSignatureProperties = "http://uri.etsi.org/01903#SignedProperties";
    public const string XadesProofOfApproval = "http://uri.etsi.org/01903/v1.2.2#ProofOfApproval";
    public const string XadesNamespaceUrl = "http://uri.etsi.org/01903/v1.3.2#";
    public const string XmlRsaSignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
    public const string XmlRsaDigestMethod = "http://www.w3.org/2001/04/xmlenc#sha256";
    public const string DsaSignatureMethod = "http://www.w3.org/2000/09/xmldsig#dsa-sha1";
    public const string XmlKeyContainerName = "XML_DSIG_RSA_KEY";

    private const string CryptographicXmlLoadKeyFailedException = "Cryptography_Xml_LoadKeyFailed";
    private const string CryptographicXmlCreatedKeyFailedException = "Cryptography_Xml_CreatedKeyFailed";
    private const string CryptographicXmlSignatureDescriptionNotCreatedException = "Cryptography_Xml_SignatureDescriptionNotCreated";
    private const string CryptographicExceptionXmlCreateHashAlgorithmFailed = "Cryptography_Xml_CreateHashAlgorithmFailed";
    private const string MethodInfoName = "BuildDigestedReferences";

    public XmlElement PropertiesNode { get; set; }

    private readonly List<DataObject> _dataObjects = new List<DataObject>();

    public PrefixedSignedXml(XmlDocument document)
        : base(document) { }

    public override XmlElement GetIdElement(XmlDocument document, string idValue)
    {
        if (string.IsNullOrEmpty(idValue))
            return null;

        var xmlElement = base.GetIdElement(document, idValue);

        if (xmlElement == null)
        {
            XmlNamespaceManager nsManager = new XmlNamespaceManager(document.NameTable);
            nsManager.AddNamespace("wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd");

            xmlElement = document.SelectSingleNode("//*[@Id=\"" + idValue + "\"]", nsManager) as XmlElement;
        }

        if (xmlElement != null)
            return xmlElement;

        if (_dataObjects.Count == 0)
            return null;

        foreach (var dataObject in _dataObjects)
        {
            var nodeWithSameId = XmlHelper.FindNodeWithAttributeValueIn(dataObject.Data, "Id", idValue);

            if (nodeWithSameId != null)
                return nodeWithSameId;
        }

        return null;
    }

    public new void AddObject(DataObject dataObject)
    {
        base.AddObject(dataObject);
        _dataObjects.Add(dataObject);
    }

    public void ComputeSignature(string prefix)
    {
        this.BuildDigestedReferences();

        var signingKey = this.SigningKey;

        if (signingKey == null)
            throw new CryptographicException(CryptographicXmlLoadKeyFailedException);

        if (this.SignedInfo.SignatureMethod == null)
        {
            if ((signingKey is DSA))
                this.SignedInfo.SignatureMethod = DsaSignatureMethod;

            if (!(signingKey is RSA))
                throw new CryptographicException(CryptographicXmlCreatedKeyFailedException);

            if (this.SignedInfo.SignatureMethod == null)
                this.SignedInfo.SignatureMethod = XmlRsaSignatureMethod;
        }

        if (!(CryptoConfig.CreateFromName(this.SignedInfo.SignatureMethod) is SignatureDescription description))
            throw new CryptographicException(CryptographicXmlSignatureDescriptionNotCreatedException);

        var hash = description.CreateDigest();

        if (hash == null)
            throw new CryptographicException(CryptographicExceptionXmlCreateHashAlgorithmFailed);

        this.GetC14NDigest(hash, prefix);
        this.m_signature.SignatureValue = description.CreateFormatter(signingKey).CreateSignature(hash);
    }

    public XmlElement GetXml(string prefix)
    {
        var xmlElement = this.GetXml();

        XmlHelper.SetPrefix(prefix, xmlElement);

        return xmlElement;
    }

    private void BuildDigestedReferences()
    {
        var type = typeof(SignedXml);

        var methodInfo = type.GetMethod(MethodInfoName, BindingFlags.NonPublic | BindingFlags.Instance);

        methodInfo.Invoke(this, new object[] { });
    }

    private byte[] GetC14NDigest(HashAlgorithm hash, string prefix)
    {
        var canonicalizationMethodObject = this.SignedInfo.CanonicalizationMethodObject;

        var document = new XmlDocument
        {
            PreserveWhitespace = true
        };

        document.AppendChild(document.ImportNode(this.SignedInfo.GetXml(), true));

        XmlHelper.SetPrefix(prefix, document.DocumentElement);

        canonicalizationMethodObject.LoadInput(document);

        return canonicalizationMethodObject.GetDigestedOutput(hash);
    }
}

And this is the method used to add the XAdEs attributes:

private void AddXadesProperties(XmlDocument document, PrefixedSignedXml xadesSignedXml, X509Certificate2 signingCertificate)
    {
        Log.Information("[Begin] - AddXadesProperties");

        var parametersSignature = new Reference
        {
            Uri = $"#{SignaturePropertiesId}{SignatureId}",
            Type = PrefixedSignedXml.XmlDsigSignatureProperties,
            DigestMethod = PrefixedSignedXml.XmlRsaDigestMethod
        };

        parametersSignature.AddTransform(new XmlDsigExcC14NTransform());
        xadesSignedXml.AddReference(parametersSignature);

        // <Object>
        var objectNode = document.CreateElement(XmlDsPrefix, "Object", SignedXml.XmlDsigNamespaceUrl);

        // <Object><QualifyingProperties>
        var qualifyingPropertiesNode = document.CreateElement(XadesPrefix, "QualifyingProperties", PrefixedSignedXml.XadesNamespaceUrl);

        qualifyingPropertiesNode.SetAttribute("Target", $"#{SignatureId}");
        objectNode.AppendChild(qualifyingPropertiesNode);

        // <Object><QualifyingProperties><SignedProperties>
        var signedPropertiesNode = document.CreateElement(XadesPrefix, "SignedProperties", PrefixedSignedXml.XadesNamespaceUrl);

        signedPropertiesNode.SetAttribute("Id", $"{SignaturePropertiesId}{SignatureId}");
        qualifyingPropertiesNode.AppendChild(signedPropertiesNode);

        // <Object><QualifyingProperties><SignedProperties><SignedSignatureProperties>
        var signedSignaturePropertiesNode = document.CreateElement(XadesPrefix, "SignedSignatureProperties", PrefixedSignedXml.XadesNamespaceUrl);

        signedPropertiesNode.AppendChild(signedSignaturePropertiesNode);

        // <Object><QualifyingProperties><SignedProperties><SignedSignatureProperties></SigningTime>
        var signingTime = document.CreateElement(XadesPrefix, "SigningTime", PrefixedSignedXml.XadesNamespaceUrl);

        signingTime.InnerText = $"{DateTime.UtcNow:s}Z";
        signedSignaturePropertiesNode.AppendChild(signingTime);

        // <Object><QualifyingProperties><SignedProperties><SignedSignatureProperties><SigningCertificate>
        var signingCertificateNode = document.CreateElement(XadesPrefix, "SigningCertificate", PrefixedSignedXml.XadesNamespaceUrl);

        signedSignaturePropertiesNode.AppendChild(signingCertificateNode);

        // <Object><QualifyingProperties><SignedProperties><SignedSignatureProperties><SigningCertificate><Cert>
        var certNode = document.CreateElement(XadesPrefix, "Cert", PrefixedSignedXml.XadesNamespaceUrl);

        signingCertificateNode.AppendChild(certNode);

        // <Object><QualifyingProperties><SignedProperties><SignedSignatureProperties><SigningCertificate><Cert><CertDigest>
        var certDigestNode = document.CreateElement(XadesPrefix, "CertDigest", PrefixedSignedXml.XadesNamespaceUrl);

        certNode.AppendChild(certDigestNode);

        // <Object><QualifyingProperties><SignedProperties><SignedSignatureProperties><SigningCertificate><Cert><CertDigest></DigestMethod>
        var digestMethod = document.CreateElement(XmlDsPrefix, "DigestMethod", SignedXml.XmlDsigNamespaceUrl);
        var digestMethodAlgorithmAtribute = document.CreateAttribute("Algorithm");

        digestMethodAlgorithmAtribute.InnerText = PrefixedSignedXml.XmlRsaDigestMethod;
        digestMethod.Attributes.Append(digestMethodAlgorithmAtribute);
        certDigestNode.AppendChild(digestMethod);

        // <Object><QualifyingProperties><SignedProperties><SignedSignatureProperties><SigningCertificate><Cert><CertDigest></DigestMethod>
        var digestValue = document.CreateElement(XmlDsPrefix, "DigestValue", SignedXml.XmlDsigNamespaceUrl);

        digestValue.InnerText = GenerateCertificateHash256Value(signingCertificate);
        certDigestNode.AppendChild(digestValue);

        // <Object><QualifyingProperties><SignedProperties><SignedSignatureProperties><SigningCertificate><Cert><IssuerSerial>
        var issuerSerialNode = document.CreateElement(XadesPrefix, "IssuerSerial", PrefixedSignedXml.XadesNamespaceUrl);

        certNode.AppendChild(issuerSerialNode);

        // <Object><QualifyingProperties><SignedProperties><SignedSignatureProperties><SigningCertificate><Cert><IssuerSerial></X509IssuerName>
        var x509IssuerName = document.CreateElement(XmlDsPrefix, "X509IssuerName", SignedXml.XmlDsigNamespaceUrl);

        x509IssuerName.InnerText = signingCertificate.IssuerName.Name;
        issuerSerialNode.AppendChild(x509IssuerName);

        // <Object><QualifyingProperties><SignedProperties><SignedSignatureProperties><SigningCertificate><Cert><IssuerSerial></X509SerialNumber>
        var x509SerialNumber = document.CreateElement(XmlDsPrefix, "X509SerialNumber", SignedXml.XmlDsigNamespaceUrl);

        x509SerialNumber.InnerText = ToDecimalSerialNumberString(signingCertificate.SerialNumber);
        issuerSerialNode.AppendChild(x509SerialNumber);

        var dataObject = new DataObject { Data = qualifyingPropertiesNode.SelectNodes(".") };

        xadesSignedXml.AddObject(dataObject);

        Log.Information("[End] - AddXadesProperties");
    }

After that, the ComputeSignature() generates the digests and the initial signature, then I pass the <ds:SignedInfo> tag to another server who generates the final <ds:SignatureValue>. Finally, I pass the <SignatureValue> to the <ds:Signature> itself, append to the XmlDocument and send to the broker.

Does anyone knows if it's possible to sign a XML document without passing the URI attribute to the <Reference> tag?

Project is a ClassLibrary using .NetFramework 4.6.

I'm facing this problem for a while, any help would be grateful.

2

There are 2 answers

0
Danilo Souza On

Turns out that I was right this whole time. The *<Reference URI="#id-value">* tag is necessary on this case and the broker was updated to support this format.

Thanks to @jdweng for the support.

0
Nikolai MD On

I got Reference without URI attribute, but in my case, resulted signed xml still could be validated on the other side. May be my code can help.

  1. Create a transform class, that is inherited from XmlDsigExcC14NTransform

    public class MyXmlDsigExcC14NTransform : XmlDsigExcC14NTransform
    {
    
      public override void LoadInput(object obj)
      {
        if(obj == null)
            obj = this.Context.OwnerDocument;
    
        base.LoadInput(obj);
      }
    }
    
  2. Add this transform to reference instance:

    var documentReference = new Reference();
    documentReference.DigestMethod = "http://www.w3.org/2000/09/xmldsig#sha1";
    documentReference.AddTransform(new MyXmlDsigExcC14NTransform());
    references.Add(documentReference);
    

After I had signed xml, I received needed Reference tag in signature.

<ds:Reference>
    <ds:Transforms>
        <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
    </ds:Transforms>
    <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
    <ds:DigestValue>DigestValue</ds:DigestValue>
</ds:Reference>

Hope it will help