Генерация цифровой подписи, но с определенным префиксом пространства имен ("ds:")

Я подписываю файлы XML цифровой подписью, но мне нужно, чтобы теги подписи содержали префикс пространства имен "ds". Я исследовал довольно Google и нашел много таких же вопросов, но не получил удовлетворительного ответа.

Я попытался поместить "ds" вручную в файл, но подпись становится недействительной. Тег "SignatureValue" подписывает тег "SignedInfo", поэтому подпись становится недействительной.

Может кто-нибудь показать мне, как я генерирую значение тега "SignatureValue", чтобы я мог заменить подпись после добавления префикса "ds"?

3 ответа

Решение

Видимо, много людей столкнулись с той же проблемой. Изучив исходный код класса Signature, я пришел к выводу, что Microsoft стремилась нам помочь. В методе LoadXml() есть жестко заданный префикс "ds". Таким образом, можно сгенерировать подпись, затем добавить к ней префикс пространства имен "ds", загрузить обратно измененную подпись и пересчитать "SignatureValue". К сожалению, ошибка в библиотеке делает вещи немного сложнее, чем они должны быть. Код с обходным решением и комментариями ниже.

public static void SignXml(XmlDocument xmlDoc, X509Certificate2 cert)
{
        // transformation cert -> key omitted
        RSACryptoServiceProvider key;

        // Create a SignedXml object. 
        SignedXml signedXml = new SignedXml(xmlDoc);

        // Add the key to the SignedXml document. 
        signedXml.SigningKey = key;
        signedXml.SignedInfo.SignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
        signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl;

        // Create a reference to be signed. 
        Reference reference = new Reference();
        reference.Uri = "#foo";
        reference.DigestMethod = "http://www.w3.org/2001/04/xmlenc#sha256";
        // Add an enveloped transformation to the reference. 
        reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());
        reference.AddTransform(new XmlDsigExcC14NTransform());
        signedXml.AddReference(reference);

        KeyInfo keyInfo = new KeyInfo();
        KeyInfoX509Data keyInfoData = new KeyInfoX509Data();
        keyInfoData.AddIssuerSerial(cert.IssuerName.Format(false), cert.SerialNumber);
        keyInfo.AddClause(keyInfoData);
        signedXml.KeyInfo = keyInfo;

        // Compute the signature. 
        signedXml.ComputeSignature();

        // Add prefix "ds:" to signature
        XmlElement signature = signedXml.GetXml();
        SetPrefix("ds", signature);

        // Load modified signature back
        signedXml.LoadXml(signature);

        // this is workaround for overcoming a bug in the library
        signedXml.SignedInfo.References.Clear();

        // Recompute the signature
        signedXml.ComputeSignature();
        string recomputedSignature = Convert.ToBase64String(signedXml.SignatureValue);

        // Replace value of the signature with recomputed one
        ReplaceSignature(signature, recomputedSignature);

        // Append the signature to the XML document. 
        xmlDoc.DocumentElement.InsertAfter(xmlDoc.ImportNode(signature, true), xmlDoc.DocumentElement.FirstChild);
    }

    private static void SetPrefix(string prefix, XmlNode node)
    {
        node.Prefix = prefix;
        foreach (XmlNode n in node.ChildNodes)
        {
            SetPrefix(prefix, n);
        }
    }

    private static void ReplaceSignature(XmlElement signature, string newValue)
    {
        if (signature == null) throw new ArgumentNullException(nameof(signature));
        if (signature.OwnerDocument == null) throw new ArgumentException("No owner document", nameof(signature));

        XmlNamespaceManager nsm = new XmlNamespaceManager(signature.OwnerDocument.NameTable);
        nsm.AddNamespace("ds", SignedXml.XmlDsigNamespaceUrl);

        XmlNode signatureValue = signature.SelectSingleNode("ds:SignatureValue", nsm);
        if (signatureValue == null)
            throw new Exception("Signature does not contain 'ds:SignatureValue'");

        signatureValue.InnerXml = newValue;
    }

Редактировать: Вы можете увидеть алгоритм, который этот пост вроде как упоминается в моем другом ответе.

Предполагая, что невозможно без написания собственного алгоритма канонизации и подписи документа, возможный обходной путь может заключаться в том, чтобы "вставить" префикс пространства имен в элементы подписи после подписания, а затем удалить его из них перед проверкой.

Например:

void SignXml(XmlDocument xmlDoc, RSA Key)
{
    SignedXml signedXml = new SignedXml(xmlDoc);
    signedXml.SigningKey = Key;

    Reference reference = new Reference("");
    reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());

    signedXml.AddReference(reference);

    signedXml.ComputeSignature();

    XmlElement xmlSignature = signedXml.GetXml();

    //Here we set the namespace prefix on the signature element and all child elements to "ds", invalidating the signature.
    AssignNameSpacePrefixToElementTree(xmlSignature, "ds");

    xmlDoc.DocumentElement.AppendChild(xmlDoc.ImportNode(xmlSignature, true));
}

bool VerifyXmlSignature(XmlDocument xmlDoc, RSA Key, string prefix)
{
    SignedXml signedXml = new SignedXml(xmlDoc);

    //Get the <ds:Signature /> element
    XmlElement xmlSignature = (XmlElement)xmlDoc.GetElementsByTagName(prefix + ":Signature")[0];

    //Undo what we did after signing
    AssignNameSpacePrefixToElementTree(xmlSignature, "");

    //Now it will pass verification.
    signedXml.LoadXml(xmlSignature);
    return signedXml.CheckSignature(Key);
}

void AssignNameSpacePrefixToElementTree(XmlElement element, string prefix)
{
    element.Prefix = prefix;

    foreach (var child in element.ChildNodes)
    {
        if (child is XmlElement)
            AssignNameSpacePrefixToElementTree(child as XmlElement, prefix);
    }
}

Поскольку именно вы подписываете документы, это должно быть достаточно просто, но классы в System.Security.Cryptography.Xml действительно не поддерживают это.

Если вам нужен только элемент Signature с префиксом, то при условии, что подпись не указана сама по себе (или если она является частью ссылочного элемента, тогда, если она удалена с преобразованием, как в " http://www.w3.org/2000/09/xmldsig") тогда все, что вам нужно сделать, это пересчитать SignatureValue на основе вашего измененного элемента SignedInfo. См. Ниже метод SignEnveloped для примера.

Однако ваша подпись не пройдет проверку, описанную в MSDN Как: проверить цифровые подписи документов XML, потому что вместо вычисления SignatureValue для проверки путем фактического чтения SignedInfo документа, класс SignedXml, по-видимому, создает новую без префиксные элементы. Приведенный ниже класс работает вокруг, казалось бы, ошибочных реализаций SignedXmls, но могут быть и проблемы с проверкой в ​​других средах, не ожидающих префиксных элементов.

public static class XmlSigning
{
    private static Type tSignedXml = typeof(SignedXml);
    private static ResourceManager SecurityResources = new ResourceManager("system.security", tSignedXml.Assembly);

    //these methods from the SignedXml class still work with prefixed Signature elements, but they are private
    private static ParameterExpression thisSignedXmlParam = Expression.Parameter(tSignedXml);
    private static Func<SignedXml, bool> CheckSignatureFormat
        = Expression.Lambda<Func<SignedXml, bool>>(
            Expression.Call(thisSignedXmlParam, tSignedXml.GetMethod("CheckSignatureFormat", BindingFlags.NonPublic | BindingFlags.Instance)),
            thisSignedXmlParam).Compile();
    private static Func<SignedXml, bool> CheckDigestedReferences
        = Expression.Lambda<Func<SignedXml, bool>>(
            Expression.Call(thisSignedXmlParam, tSignedXml.GetMethod("CheckDigestedReferences", BindingFlags.NonPublic | BindingFlags.Instance)),
            thisSignedXmlParam).Compile();

    public static void SignEnveloped(XmlDocument xmlDoc, RSACryptoServiceProvider key, string signatureNamespacePrefix)
    {
        SignedXml signedXml = new SignedXml(xmlDoc);
        signedXml.SigningKey = key;

        Reference reference = new Reference("");
        reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());

        signedXml.AddReference(reference);

        signedXml.ComputeSignature();

        XmlElement xmlSignature = signedXml.GetXml();

        if (!string.IsNullOrEmpty(signatureNamespacePrefix))
        {
            //Here we set the namespace prefix on the signature element and all child elements to "ds", invalidating the signature.
            AssignNameSpacePrefixToElementTree(xmlSignature, "ds");

            //So let's recompute the SignatureValue based on our new SignatureInfo...

            //For XPath
            XmlNamespaceManager namespaceManager = new XmlNamespaceManager(xmlDoc.NameTable);
            namespaceManager.AddNamespace("ds", "http://www.w3.org/2000/09/xmldsig#"); //this prefix is arbitrary and used only for XPath

            XmlElement xmlSignedInfo = xmlSignature.SelectSingleNode("ds:SignedInfo", namespaceManager) as XmlElement;

            //Canonicalize the SignedInfo element
            XmlDsigC14NTransform transform = new XmlDsigC14NTransform();
            XmlDocument signedInfoDoc = new XmlDocument();
            signedInfoDoc.LoadXml(xmlSignedInfo.OuterXml);
            transform.LoadInput(signedInfoDoc);

            //Compute the new SignatureValue
            string signatureValue = Convert.ToBase64String(key.SignData(transform.GetOutput() as MemoryStream, new SHA1CryptoServiceProvider()));
            //Set it in the xml
            XmlElement xmlSignatureValue = xmlSignature.SelectSingleNode("ds:SignatureValue", namespaceManager) as XmlElement;
            xmlSignatureValue.InnerText = signatureValue;
        }

        xmlDoc.DocumentElement.AppendChild(xmlDoc.ImportNode(xmlSignature, true));
    }

    public static bool CheckSignature(XmlDocument xmlDoc, RSACryptoServiceProvider key)
    {
        if (key == null)
            throw new ArgumentNullException("key");

        SignedXml signedXml = new SignedXml(xmlDoc);

        //For XPath
        XmlNamespaceManager namespaceManager = new XmlNamespaceManager(xmlDoc.NameTable);
        namespaceManager.AddNamespace("ds", "http://www.w3.org/2000/09/xmldsig#"); //this prefix is arbitrary and used only for XPath

        XmlElement xmlSignature = xmlDoc.SelectSingleNode("//ds:Signature", namespaceManager) as XmlElement;

        signedXml.LoadXml(xmlSignature);

        //These are the three methods called in SignedXml's CheckSignature method, but the built-in CheckSignedInfo will not validate prefixed Signature elements
        return CheckSignatureFormat(signedXml) && CheckDigestedReferences(signedXml) && CheckSignedInfo(signedXml, key);
    }

    private static bool CheckSignedInfo(SignedXml signedXml, AsymmetricAlgorithm key)
    {
        //Copied from reflected System.Security.Cryptography.Xml.SignedXml
        SignatureDescription signatureDescription = CryptoConfig.CreateFromName(signedXml.SignatureMethod) as SignatureDescription;
        if (signatureDescription == null)
            throw new CryptographicException(SecurityResources.GetString("Cryptography_Xml_SignatureDescriptionNotCreated"));

        Type type = Type.GetType(signatureDescription.KeyAlgorithm);
        Type type2 = key.GetType();
        if (type != type2 && !type.IsSubclassOf(type2) && !type2.IsSubclassOf(type))
            return false;

        HashAlgorithm hashAlgorithm = signatureDescription.CreateDigest();
        if (hashAlgorithm == null)
            throw new CryptographicException(SecurityResources.GetString("Cryptography_Xml_CreateHashAlgorithmFailed"));

        //Except this. The SignedXml class creates and cananicalizes a Signature element without any prefix, rather than using the element from the document provided
        byte[] c14NDigest = GetC14NDigest(signedXml, hashAlgorithm);

        AsymmetricSignatureDeformatter asymmetricSignatureDeformatter = signatureDescription.CreateDeformatter(key);
        return asymmetricSignatureDeformatter.VerifySignature(c14NDigest, signedXml.Signature.SignatureValue);
    }

    private static byte[] GetC14NDigest(SignedXml signedXml, HashAlgorithm hashAlgorithm)
    {
        Transform canonicalizeTransform = signedXml.SignedInfo.CanonicalizationMethodObject;
        XmlDocument xmlDoc = new XmlDocument();
        xmlDoc.LoadXml(signedXml.SignedInfo.GetXml().OuterXml);
        canonicalizeTransform.LoadInput(xmlDoc);
        return canonicalizeTransform.GetDigestedOutput(hashAlgorithm);
    }

    private static void AssignNameSpacePrefixToElementTree(XmlElement element, string prefix)
    {
        element.Prefix = prefix;

        foreach (var child in element.ChildNodes)
        {
            if (child is XmlElement)
                AssignNameSpacePrefixToElementTree(child as XmlElement, prefix);
        }
    }
}
Другие вопросы по тегам