File: Base\MS\Internal\IO\Packaging\CustomSignedXml.cs
Project: wpf\src\WindowsBase.csproj (WindowsBase)
//-----------------------------------------------------------------------------
//
// <copyright file="CustomSignedXml.cs" company="Microsoft">
//    Copyright (C) Microsoft Corporation.  All rights reserved.
// </copyright>
//
// Description:
//  Wrapper class for existing SignedXml class that works around
//  DevDiv Schedule bug: 39530 (mdownen PM)
//
// History:
//  07/08/2005: BruceMac: Initial implementation.
//
//-----------------------------------------------------------------------------
 
using System;
using System.Xml;
using System.Windows;                          // for SR
using System.Security.Cryptography.Xml;
using MS.Internal.WindowsBase;
using Microsoft.Win32;                          // for Registry and RegistryKey classes
using System.Security;                          // for SecurityException
using System.Security.Permissions;              // for RegistryPermission
using System.Globalization;                     // for CultureInfo
 
namespace MS.Internal.IO.Packaging
{
    /// <summary>
    /// SignedXml wrapper that supports reference targeting of internal ID's
    /// </summary>
    /// <remarks>See: http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/ for details</remarks>
    internal class CustomSignedXml : SignedXml
    {
        /// <summary>
        /// Returns the XmlElement that matches the given id
        /// </summary>
        /// <param name="document"></param>
        /// <param name="idValue"></param>
        /// <returns>element if found, otherwise return null</returns>
        public override XmlElement GetIdElement(XmlDocument document, string idValue)
        {
            // Always let the base class have a first try at finding the element
            XmlElement elem = base.GetIdElement(document, idValue);
 
            // If not found then we will try to find it ourselves
            if (elem == null)
            {
                // Require the id to be an NCName (to avoid side-effects when it's
                // used in an XPath query).  The base class does this check, but
                // doesn't expose the answer.  Hence, we have to do it again.
                // [Code copied from SignedXml.DefaultGetIdElement, in
                // NDP/clr/src/ManagedLibraries/Security/System/Security/Cryptography/Xml/SignedXml.cs]
                if (RequireNCNameIdentifier())
                {
                    try
                    {
                        XmlConvert.VerifyNCName(idValue);
                    }
                    catch (XmlException)
                    {
                        // Identifiers are required to be an NCName
                        //   (xml:id version 1.0, part 4, paragraph 2, bullet 1)
                        //
                        // If it isn't an NCName, it isn't allowed to match.
                        return null;
                    }
                }
 
                elem = SelectNodeByIdFromObjects(m_signature, idValue);
            }
 
            return elem;
        }
 
        /// <summary>
        /// Locate and return the node identified by idValue
        /// </summary>
        /// <param name="signature"></param>
        /// <param name="idValue"></param>
        /// <returns>node if found - else null</returns>
        /// <remarks>Tries to match each object in the Object list.</remarks>
        private static XmlElement SelectNodeByIdFromObjects(Signature signature, string idValue)
        {
            XmlElement node = null;
 
            // enumerate the objects
            foreach (DataObject dataObject in signature.ObjectList)
            {
                // direct reference to Object id - supported for all reference typs
                if (String.CompareOrdinal(idValue, dataObject.Id) == 0)
                {
                    // anticipate duplicate ID's and throw if any found
                    if (node != null)
                        throw new XmlException(SR.Get(SRID.DuplicateObjectId));
 
                    node = dataObject.GetXml();
                }
            }
 
            // now search for XAdES specific references
            if (node == null)
            {
                // For XAdES we implement special case where the reference may
                // be to an internal tag with matching "Id" attribute.
                node = SelectSubObjectNodeForXAdES(signature, idValue);
            }
 
            return node;
        }
 
        /// <summary>
        /// Locate any signed Object tag that matches the XAdES "target type"
        /// </summary>
        /// <param name="signature"></param>
        /// <param name="idValue"></param>
        /// <returns>element if found; null if not found</returns>
        /// <remarks>Special purpose code to support Sub-Object signing required by XAdES signatures</remarks>
        private static XmlElement SelectSubObjectNodeForXAdES(Signature signature, string idValue)
        {
            XmlElement node = null;
 
            // enumerate the References to determine if any are of type XAdES
            foreach (Reference reference in signature.SignedInfo.References)
            {
                // if we get a match by Type?
                if (String.CompareOrdinal(reference.Type, _XAdESTargetType) == 0)
                {
                    // now try to match by Uri
                    // strip off any preceding # mark to facilitate matching
                    string uri;
                    if ((reference.Uri.Length > 0) && (reference.Uri[0] == '#'))
                        uri = reference.Uri.Substring(1);
                    else
                        continue;   // ignore non-local references
 
                    // if we have a XAdES type reference and the ID matches the requested one
                    // search all object tags for the XML with this ID
                    if (String.CompareOrdinal(uri, idValue) == 0)
                    {
                        node = SelectSubObjectNodeForXAdESInDataObjects(signature, idValue);
                        break;
                    }
                }
            }
 
            return node;
        }
 
        /// <summary>
        /// Locates and selects the target XmlElement from all available Object tags
        /// </summary>
        /// <param name="signature"></param>
        /// <param name="idValue"></param>
        /// <returns>element if found; null if not found</returns>
        /// <remarks>relies on XPath query to search the Xml in each Object tag</remarks>
        private static XmlElement SelectSubObjectNodeForXAdESInDataObjects(Signature signature, string idValue)
        {
            XmlElement node = null;
            bool foundMatch = false;    // true if id has matched, even with the wrong namespace
 
            // now find an object tag that includes an element that matches
            foreach (DataObject dataObject in signature.ObjectList)
            {
                // skip the package object
                if (String.CompareOrdinal(dataObject.Id, XTable.Get(XTable.ID.OpcAttrValue)) != 0)
                {
                    XmlElement element = dataObject.GetXml();
 
                    // NOTE: this is executing an XPath query
                    // idValue has already been tested as an NCName (unless overridden for compatibility), so there's no
                    // escaping that needs to be done here.
                    XmlNodeList nodeList = element.SelectNodes(".//*[@Id='" + idValue + "']");
 
                    if (nodeList.Count > 0)
                    {
                        if (!AllowAmbiguousReferenceTargets() &&
                            (nodeList.Count > 1 || foundMatch))
                        {
                            throw new XmlException(SR.Get(SRID.DuplicateObjectId));
                        }
 
                        foundMatch = true;
                        XmlNode local = nodeList[0] as XmlElement;
 
                        if (local != null)
                        {
                            XmlNode temp = local;
 
                            // climb the tree towards the root until we find our namespace
                            while ((temp != null) && (temp.NamespaceURI.Length == 0))
                                temp = temp.ParentNode;
 
                            // only match if the target is in the XAdES namespace
                            if ((temp != null) && (String.CompareOrdinal(temp.NamespaceURI, _XAdESNameSpace) == 0))
                            {
                                node = local as XmlElement;
                                // continue searching, to find duplicates from different objects
                            }
                        }
                    }
                }
            }
 
            return node;
        }
 
        #region Registry access
 
        // The code in this region is copied (with cosmetic changes) from
        // NDP/clr/src/ManagedLibraries/Security/System/Security/Cryptography/Xml/Utils.cs
 
        private const string _NetFxSecurityFullKeyName = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\Security";
        private const string _NetFxSecurityKey = @"SOFTWARE\Microsoft\.NETFramework\Security";
 
        [SecurityCritical]
        private static long GetNetFxSecurityRegistryValue(string regValueName, long defaultValue)
        {
            // Acquire permissions to read the one key we care about from the registry
            RegistryPermission permission = new RegistryPermission(
                    RegistryPermissionAccess.Read,
                    System.Security.AccessControl.AccessControlActions.View,
                    _NetFxSecurityFullKeyName);
 
            permission.Assert();
 
            try
            {
                using (RegistryKey securityRegKey = Registry.LocalMachine.OpenSubKey(_NetFxSecurityKey, false))
                {
                    if (securityRegKey != null)
                    {
                        object regValue = securityRegKey.GetValue(regValueName);
                        if (regValue != null)
                        {
                            RegistryValueKind valueKind = securityRegKey.GetValueKind(regValueName);
                            if (valueKind == RegistryValueKind.DWord || valueKind == RegistryValueKind.QWord)
                            {
                                return Convert.ToInt64(regValue, CultureInfo.InvariantCulture);
                            }
                        }
                    }
                }
            }
            catch (SecurityException)
            {
                // we could not open the key - that's fine, we can proceed with the default value
            }
            finally
            {
                RegistryPermission.RevertAssert();
            }
 
            return defaultValue;
        }
 
 
        private static bool s_readRequireNCNameIdentifier = false;
        private static bool s_requireNCNameIdentifier = true;
 
        [SecuritySafeCritical]
        private static bool RequireNCNameIdentifier()
        {
            if (s_readRequireNCNameIdentifier)
            {
                return s_requireNCNameIdentifier;
            }
 
            long numericValue = GetNetFxSecurityRegistryValue("SignedXmlRequireNCNameIdentifier", 1);
            bool requireNCName = numericValue != 0;
 
            s_requireNCNameIdentifier = requireNCName;
            System.Threading.Thread.MemoryBarrier();
            s_readRequireNCNameIdentifier = true;
 
            return s_requireNCNameIdentifier;
        }
 
 
        private static bool? s_allowAmbiguousReferenceTarget = null;
 
        [SecuritySafeCritical]
        private static bool AllowAmbiguousReferenceTargets()
        {
            // Allow machine administrators to specify that the legacy behavior of matching the first element
            // in an ambiguous reference situation should be persisted. The default behavior is to throw in that
            // situation, but a REG_DWORD or REG_QWORD value of 1 will revert.
            if (s_allowAmbiguousReferenceTarget.HasValue)
            {
                return s_allowAmbiguousReferenceTarget.Value;
            }
 
            long numericValue = GetNetFxSecurityRegistryValue("SignedXmlAllowAmbiguousReferenceTargets", 0);
            bool allowAmbiguousReferenceTarget = numericValue != 0;
 
            s_allowAmbiguousReferenceTarget = allowAmbiguousReferenceTarget;
            return s_allowAmbiguousReferenceTarget.Value;
        }
 
        #endregion Registry access
 
        private const string _XAdESNameSpace = @"http://uri.etsi.org/01903/v1.2.2#";
        private const string _XAdESTargetType = _XAdESNameSpace + @"SignedProperties";
    }
}