Skip to content

חתימת XML המוטמעת במסמך עם גבולות אימות מפורשים

חתימת XML המוטמעת במסמך עם גבולות אימות מפורשים

הקוד להמחשה ולא הורץ.

המקור האנגלי פורסם בתאריך 2022-08-25T15:19:00.003+03:00. המאמר המקורי באנגלית

סמל XML מהמאמר המקורי

חתימת XML המוטמעת במסמך היא חתימה מסוג enveloped, הנמצאת בתוך המסמך שעליו היא חותמת. הדוגמה הבאה חותמת על מסמך שלם אחד, ובאימות מקבלת רק את אותו פרופיל מוגבל. היא אינה מאמת כללי של SAML, SOAP או חתימות XML שרירותיות. החותם מקבל מפתח RSA שנבחר במפורש; הוא אינו פותח מאגר תעודות ואינו בוחר את ההתאמה הראשונה לטביעת אצבע. [1]

GetRSAPrivateKey היא הדרך הייעודית לאלגוריתם לבקש מפתח RSA פרטי של תעודה, והיא עשויה להחזיר null. היא מחזירה הפשטה מסוג RSA, ולא הבטחה לשימוש במימוש CSP או CNG מסוים בכל פלטפורמה. יש להימנע מהמאפיין המיושן PrivateKey; אין לטעון ש־GetRSAPrivateKey הופיעה לראשונה רק ב־.NET Framework 4.6.2. יש לשחרר ידיות מפתח שהתקבלו מתעודות, אך לא לשחרר מפתח שבבעלות הקורא מתוך פונקציית עזר שרק משתמשת בו זמנית. [4][5][6]

פרופיל מצומצם במכוון

קוד ההמחשה מיועד ל־.NET מודרני עם System.Security.Cryptography.Xml. הוא אוסר עיבוד DTD, משבית פתרון משאבי XML ומגביל את גודל הקלט. הוא משתמש במפורש ב־RSA-SHA256, בגיבובי SHA-256 ובקנוניזציה בלעדית. ההפניה היחידה בעלת URI ריק מכסה את המסמך, עם שני טרנספורמים בלבד: הסרת החתימה המוטמעת וקנוניזציה בלעדית. כל הקוד לא הורץ; לא בוצעה פעולה על מפתח או תעודה. [1][7]

using System;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.Xml;
using System.Xml;

public static class NarrowXmlSignature
{
    private const string PayloadNs = "urn:example:payload";
    private const long Limit = 1_000_000; // Example application limit.

    private static XmlDocument Parse(string xml)
    {
        if (xml == null || xml.Length > Limit)
            throw new ArgumentException("XML exceeds the input policy.");
        var settings = new XmlReaderSettings {
            DtdProcessing = DtdProcessing.Prohibit,
            XmlResolver = null,
            MaxCharactersInDocument = Limit
        };
        using var input = new StringReader(xml);
        using var reader = XmlReader.Create(input, settings);
        var doc = new XmlDocument { PreserveWhitespace = true, XmlResolver = null };
        doc.Load(reader);
        if (doc.DocumentElement == null ||
            doc.DocumentElement.LocalName != "payload" ||
            doc.DocumentElement.NamespaceURI != PayloadNs)
            throw new CryptographicException("Unexpected payload root.");
        return doc;
    }

    public static string Sign(string xml, RSA callerOwnedKey)
    {
        var doc = Parse(xml);
        if (doc.GetElementsByTagName("Signature", SignedXml.XmlDsigNamespaceUrl).Count != 0)
            throw new CryptographicException("Input must not already contain a signature.");
        var sx = new SignedXml(doc) { SigningKey = callerOwnedKey, Resolver = null };
        sx.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl;
        sx.SignedInfo.SignatureMethod = SignedXml.XmlDsigRSASHA256Url;
        var reference = new Reference("") { DigestMethod = SignedXml.XmlDsigSHA256Url };
        reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());
        reference.AddTransform(new XmlDsigExcC14NTransform());
        sx.AddReference(reference);
        sx.ComputeSignature();
        doc.DocumentElement!.AppendChild(doc.ImportNode(sx.GetXml(), true));
        return doc.OuterXml;
    }

    public static XmlElement VerifyAndGetPayload(string xml, RSA trustedPublicKey)
    {
        var doc = Parse(xml);
        var signatures = doc.GetElementsByTagName("Signature", SignedXml.XmlDsigNamespaceUrl);
        if (signatures.Count != 1 || signatures[0]!.ParentNode != doc.DocumentElement)
            throw new CryptographicException("Exactly one root-child signature is required.");
        var sx = new SignedXml(doc) { Resolver = null };
        sx.LoadXml((XmlElement)signatures[0]!);
        if (sx.SignedInfo.SignatureMethod != SignedXml.XmlDsigRSASHA256Url ||
            sx.SignedInfo.CanonicalizationMethod != SignedXml.XmlDsigExcC14NTransformUrl ||
            sx.SignedInfo.References.Count != 1 || sx.KeyInfo.Count != 0 ||
            sx.Signature.ObjectList.Count != 0)
            throw new CryptographicException("Signature profile mismatch.");
        var r = (Reference)sx.SignedInfo.References[0]!;
        if (r.Uri != "" || r.DigestMethod != SignedXml.XmlDsigSHA256Url ||
            r.TransformChain.Count != 2 ||
            r.TransformChain[0].Algorithm != SignedXml.XmlDsigEnvelopedSignatureTransformUrl ||
            r.TransformChain[1].Algorithm != SignedXml.XmlDsigExcC14NTransformUrl)
            throw new CryptographicException("Reference profile mismatch.");
        if (!sx.CheckSignature(trustedPublicKey))
            throw new CryptographicException("Signature verification failed.");
        return doc.DocumentElement!; // Consume this verified root; do not reparse another copy.
    }
}

הפרמטר trustedPublicKey חייב להגיע מזהות שאושרה בנפרד או מתעודה שאומתה, ולעולם לא מ־KeyInfo לא מהימן שבהודעה. המאמת בודק את החתימה ואת ההפניות שלה באמצעות CheckSignature, ודוחה חתימות נוספות, הפניות חיצוניות וטרנספורמים בלתי צפויים לפני הקריאה. בדיקה קריפטוגרפית מוצלחת אינה מספיקה אם היישום קורא בהמשך צומת אחר. יש להחיל על צומת השורש המוחזר את סכמת המטען הצפויה, את כללי ההרשאה ואת מדיניות מניעת השידור החוזר; בדיקת שם השורש בלבד אינה סכמה עסקית מלאה. [2][3]

מרחב השמות הקבוע הוא מזהה יישום להמחשה, ולא כתובת ברשת. אין לשנות רווחים או לבצע סריאליזציה מחדש דרך טרנספורמציה אחרת בין החתימה לאימות בלי לבדוק תאימות. סקירת ייצור חייבת לכלול מקרי עטיפה והחלפה, חתימות כפולות, הפניות אסורות, XML פגום ושינויים בנתוני היישום. לא נטען שבדיקות אלו בוצעו כאן. [2]

מקורות ראשוניים

  1. Microsoft SignedXml
  2. W3C XML Signature Best Practices
  3. Microsoft XML signature security considerations
  4. Microsoft GetRSAPrivateKey
  5. Microsoft X509Certificate2.PrivateKey
  6. Microsoft GetRSAPrivateKey applicability
  7. Official .NET SignedXml source

Post a Comment

Previous Post Next Post