File: Base\MS\Internal\IO\Packaging\XmlSignatureProperties.cs
Project: wpf\src\WindowsBase.csproj (WindowsBase)
//-----------------------------------------------------------------------------
//
// <copyright file="XmlSignatureProperties.cs" company="Microsoft">
//    Copyright (C) Microsoft Corporation.  All rights reserved.
// </copyright>
//
// Description:
//  Helper for XmlDigitalSignatureProcessor.
//  Generates and consumes Metro-compliant SignatureProperties element within an
//  XmlDSig signature.
//
// History:
//  01/25/2004: BruceMac: First iteration
//  12/02/2005: BruceMac: Security Mitigations and support short form for UTC dates "Z"
//
//-----------------------------------------------------------------------------
 
using System;
using System.Collections;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Security.Cryptography;
using System.Security.Cryptography.Xml;
using System.Security.Cryptography.X509Certificates;
using System.Security.Permissions;
using System.Xml;
using System.IO;
using System.Windows;
using System.IO.Packaging;
using System.Diagnostics;
using MS.Internal.WindowsBase;
 
namespace MS.Internal.IO.Packaging
{
    /// <summary>
    /// Signature Handler implementation that follows the Feb 12, 2002 W3C DigSig Recommendation
    /// </summary>
    /// <remarks>See: http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/ for details</remarks>
    static internal class XmlSignatureProperties
    {
        //-----------------------------------------------------------------------------
        //
        // Internal Properties
        //
        //-----------------------------------------------------------------------------
        /// <summary>
        /// Signature Time Format - default to most descriptive form - in Xml syntax
        /// </summary>
        internal static String DefaultDateTimeFormat
        {
            get
            {
                return _dateTimePatternMap[0].Format;
            }
        }
 
        //-----------------------------------------------------------------------------
        //
        // Internal Methods
        //
        //-----------------------------------------------------------------------------
        /// <summary>
        /// Verify the given string is a legal Xml format
        /// </summary>
        /// <param name="candidateFormat">xml format to verify</param>
        /// <returns>true if legal</returns>
        internal static bool LegalFormat(String candidateFormat)
        {
            if (candidateFormat == null)
                throw new ArgumentNullException("candidateFormat");
 
            return (GetIndex(candidateFormat) != -1);
        }
 
        /// <summary>
        /// Obtain a W3C formatted SigningTime (equivalent to TimeStamp)
        /// </summary>
        /// <param name="xDoc">xml document we are building</param>
        /// <param name="dateTime">time to persist</param>
        /// <param name="signatureId">id of new signature</param>
        /// <param name="xmlDateTimeFormat">format to use - must be Xml date format legal syntax</param>
        /// <returns>given writer with SignatureProperties xml added to it</returns>
        /// <remarks>format matches that described in http://www.w3.org/TR/NOTE-datetime </remarks>
        /// <example>
        /// <Object Id="Package">
        ///     <SignatureProperties>
        ///         <SignatureProperty Target="#signatureId">
        ///             <SignatureTime>
        ///                 <Format>YYYY-MM-DDThh:mm:ssTZD</Format>
        ///                 <Value>1997-07-16T19:20:30.45+01:00</Value>
        ///             </SignatureTime>
        ///         </SignatureProperty>
        ///     </SignatureProperties>
        /// </Object>
        /// </example>
        internal static XmlElement AssembleSignatureProperties(
            XmlDocument xDoc, 
            DateTime dateTime, 
            String xmlDateTimeFormat,
            String signatureId)
        {
            Invariant.Assert(xDoc != null);
            Invariant.Assert(signatureId != null);
 
            // check for null format - use default if null
            if (xmlDateTimeFormat == null)
            {
                xmlDateTimeFormat = DefaultDateTimeFormat;
            }
 
            string[] dateTimeFormats = ConvertXmlFormatStringToDateTimeFormatString(xmlDateTimeFormat);
 
            // <SignatureProperties>
            XmlElement signatureProperties = xDoc.CreateElement(XTable.Get(XTable.ID.SignaturePropertiesTagName),
                SignedXml.XmlDsigNamespaceUrl);
 
            // <SignatureProperty Id="idSignatureTime" Target="#signatureId">
            XmlElement signatureProperty = xDoc.CreateElement(XTable.Get(XTable.ID.SignaturePropertyTagName), 
                SignedXml.XmlDsigNamespaceUrl);
            signatureProperties.AppendChild(signatureProperty);            
            XmlAttribute idAttr = xDoc.CreateAttribute(XTable.Get(XTable.ID.SignaturePropertyIdAttrName));
            idAttr.Value = XTable.Get(XTable.ID.SignaturePropertyIdAttrValue);
            signatureProperty.Attributes.Append(idAttr);
            XmlAttribute targetAttr = xDoc.CreateAttribute(XTable.Get(XTable.ID.TargetAttrName));
            targetAttr.Value = "#" + signatureId;
            signatureProperty.Attributes.Append(targetAttr);
 
            // <SignatureTime>
            XmlElement signatureTime = xDoc.CreateElement(XTable.Get(XTable.ID.SignatureTimeTagName),
                XTable.Get(XTable.ID.OpcSignatureNamespace));
            XmlElement signatureTimeFormat = xDoc.CreateElement(XTable.Get(XTable.ID.SignatureTimeFormatTagName),
                XTable.Get(XTable.ID.OpcSignatureNamespace));
            XmlElement signatureTimeValue = xDoc.CreateElement(XTable.Get(XTable.ID.SignatureTimeValueTagName),
                XTable.Get(XTable.ID.OpcSignatureNamespace));
 
            signatureTimeFormat.AppendChild(xDoc.CreateTextNode(xmlDateTimeFormat));
            signatureTimeValue.AppendChild(xDoc.CreateTextNode(DateTimeToXmlFormattedTime(dateTime, dateTimeFormats[0])));
 
            signatureTime.AppendChild(signatureTimeFormat);
            signatureTime.AppendChild(signatureTimeValue);
 
            signatureProperty.AppendChild(signatureTime);
 
            return signatureProperties;
        }
 
        /// <summary>
        /// Parse the xml and determine the signing time
        /// </summary>
        /// <param name="reader">NodeReader positioned at the SignatureProperties tag</param>
        /// <param name="signatureId">value of the Id attribute on the Signature tag</param>
        /// <param name="timeFormat">format found</param>
        /// <exception cref="XmlException">illegal format</exception>
        /// <returns>signing time</returns>
        internal static DateTime ParseSigningTime(XmlReader reader, string signatureId, out String timeFormat)
        {
            if (reader == null)
                throw new ArgumentNullException("reader");
 
            bool signatureTimePropertyFound = false;
            bool signatureTimeIdFound = false;
            string w3cSignatureNameSpace = SignedXml.XmlDsigNamespaceUrl;
 
            // <SignatureProperty> tag
            string signaturePropertyTag = XTable.Get(XTable.ID.SignaturePropertyTagName);
            string signaturePropertiesTag = XTable.Get(XTable.ID.SignaturePropertiesTagName);
 
            //initializing to a dummy value
            DateTime signingTime = DateTime.Now;
            timeFormat = null;
            
            while (reader.Read())
            {
                //Looking for <SignatureProperty> tag
                if (reader.MoveToContent() == XmlNodeType.Element
                    && (String.CompareOrdinal(reader.NamespaceURI, w3cSignatureNameSpace) == 0)
                    && (String.CompareOrdinal(reader.LocalName, signaturePropertyTag) == 0)
                    && reader.Depth == 2)
                {
                    //Verify Attributes
                    //Look for well-defined Id attribute and if it is present 
                    if (VerifyIdAttribute(reader))
                    {
                        //If we encounter more than one <SignatureProperty> tag with the expected
                        //id, then its an error.
                        if (signatureTimeIdFound)
                            throw new XmlException(SR.Get(SRID.PackageSignatureCorruption));
                        else
                            signatureTimeIdFound = true;
                        
                        //VerifyTargetAttribute will return false, if the Target attribute is missing
                        //or contains an incorrect value.
                        if(VerifyTargetAttribute(reader, signatureId))
                        {
                            signingTime = ParseSignatureTimeTag(reader, out timeFormat);
                            signatureTimePropertyFound = true;
                        }
                    }
                }
                else
                    //Expected <SignatureProperty> tag not found.
                    //Look for end tag corresponding to </SignatureProperty> or 
                    //if these are other custom defined properties, then anything with
                    //depth greater than 2 should be ignored as these can be nested elements.
                    if (((String.CompareOrdinal(signaturePropertyTag, reader.LocalName) == 0
                        && (reader.NodeType == XmlNodeType.EndElement)))
                        || reader.Depth > 2)
                        continue;
                    else
                        //If we find the end tag for </SignatureProperties> then we can stop parsing
                        if ((String.CompareOrdinal(signaturePropertiesTag, reader.LocalName) == 0
                        && (reader.NodeType == XmlNodeType.EndElement)))
                            break;
                        else
                            throw new XmlException(SR.Get(SRID.RequiredTagNotFound, signaturePropertyTag));
            }
 
            //We did find one or more <SignatureProperty> tags but there were none that
            //defined the id attribute and target attribute and <SignatureTime> element tag correctly.
            if(!signatureTimePropertyFound)
                throw new XmlException(SR.Get(SRID.PackageSignatureCorruption));
 
            return signingTime;
        }
 
       
        //-----------------------------------------------------------------------------
        //
        // Private Methods
        //
        //-----------------------------------------------------------------------------
 
        /// <summary>
        /// Parse the SignatureTime tag
        /// </summary>
        /// <param name="reader">NodeReader positioned at the SignatureProperty tag</param>
        /// <param name="timeFormat">format found</param>
        /// <exception cref="XmlException">illegal format</exception>
        /// <returns>signing time</returns>
        private static DateTime ParseSignatureTimeTag(XmlReader reader, out String timeFormat)
        {
            //There are no attributes on all the three tags that we parse in this method
            //<SignatureTime>, <Format>, <Value>
            int expectedAttributeCount = 0;
 
            string opcSignatureNameSpace = XTable.Get(XTable.ID.OpcSignatureNamespace);
            string signaturePropertyTag = XTable.Get(XTable.ID.SignaturePropertyTagName);
            string signatureTimeTag = XTable.Get(XTable.ID.SignatureTimeTagName);
            string timeValueTagName = XTable.Get(XTable.ID.SignatureTimeValueTagName);
            string timeFormatTagName = XTable.Get(XTable.ID.SignatureTimeFormatTagName);
 
            // <SignatureTime> must be one of <Format> or <Time>
            timeFormat = null;
            string timeValue = null;
 
            //Look for <SignatureTime> Tag
            if (reader.Read()
                && reader.MoveToContent() == XmlNodeType.Element
                && (String.CompareOrdinal(reader.NamespaceURI, opcSignatureNameSpace) == 0)
                && (String.CompareOrdinal(reader.LocalName, signatureTimeTag) == 0)
                && reader.Depth == 3
                && PackagingUtilities.GetNonXmlnsAttributeCount(reader) == expectedAttributeCount)
            {
                while (reader.Read())
                {
                    if (String.CompareOrdinal(reader.NamespaceURI, opcSignatureNameSpace) == 0
                        && reader.MoveToContent() == XmlNodeType.Element
                        && reader.Depth == 4)
                    {
                        // which tag do we have?
                        if ((String.CompareOrdinal(reader.LocalName, timeValueTagName) == 0)
                            && PackagingUtilities.GetNonXmlnsAttributeCount(reader) == expectedAttributeCount)
                        {
                            if (timeValue == null
                                && reader.Read()
                                && reader.MoveToContent() == XmlNodeType.Text
                                && reader.Depth == 5)
                            {
                                //After reading the content, the reader progresses to the next element.
                                //So after this method is called, the reader is positioned at the
                                //EndElement corresponding to Value tag - </Value>
                                // Note: ReadContentAsString will return String.Empty but never null
                                timeValue = reader.ReadContentAsString();
                                Debug.Assert(reader.NodeType == XmlNodeType.EndElement);
                            }
                            else
                                //This would happen if we found more than one Value tags or if there
                                //are other nested elements of if they are of a different XmlNodeType type
                                throw new XmlException(SR.Get(SRID.PackageSignatureCorruption));
 
                        }
                        else if ((String.CompareOrdinal(reader.LocalName, timeFormatTagName) == 0)
                                 && PackagingUtilities.GetNonXmlnsAttributeCount(reader) == expectedAttributeCount)
                        {
                            if (timeFormat == null
                                && reader.Read()
                                && reader.MoveToContent() == XmlNodeType.Text
                                && reader.Depth == 5)
                            {
                                //After reading the content, the reader progresses to the next element.
                                //So after this method is called, the reader is positioned at the
                                //EndElement corresponding to Format tag - </Format>
                                // Note: ReadContentAsString will return String.Empty but never null
                                timeFormat = reader.ReadContentAsString();
                                Debug.Assert(reader.NodeType == XmlNodeType.EndElement);
                            }
                            else
                                //This would happen if we found more than one Format tags or if there
                                //are other nested elements of if they are of a different XmlNodeType type
                                throw new XmlException(SR.Get(SRID.PackageSignatureCorruption));
                        }
                        else
                            //If we encounter any tag other than <Format> or <Time> nested within the <SignatureTime> tag
                            throw new XmlException(SR.Get(SRID.PackageSignatureCorruption));
                    }
                    else
                        //If we have encountered the end tag for the <SignatureTime> tag
                        //then we are done parsing the tag, and we can stop the parsing.
                        if (String.CompareOrdinal(signatureTimeTag, reader.LocalName) == 0
                        && (reader.NodeType == XmlNodeType.EndElement))
                        {
                            //We must find a  </SignatureProperty> tag at this point, 
                            //else it could be that there are more SignatureTime or  
                            //other tags nested here and that is an error.
                            if (reader.Read()
                                && reader.MoveToContent() == XmlNodeType.EndElement
                                && String.CompareOrdinal(signaturePropertyTag, reader.LocalName) == 0)
                                break;
                            else
                                throw new XmlException(SR.Get(SRID.PackageSignatureCorruption));
                        }
                        else
                            // if we do not find the nested elements as expected
                            throw new XmlException(SR.Get(SRID.PackageSignatureCorruption));
                }
            }
            else
                throw new XmlException(SR.Get(SRID.RequiredTagNotFound, signatureTimeTag));
 
 
            // generate an equivalent DateTime object
            if (timeValue != null && timeFormat != null)
                return XmlFormattedTimeToDateTime(timeValue, timeFormat);
            else
                throw new XmlException(SR.Get(SRID.PackageSignatureCorruption));
        }
        
        /// <summary>
        /// DateTime to XML Format
        /// </summary>
        /// <param name="dt">date time to convert</param>
        /// <param name="format">format to use - specified in DateTime syntax</param>
        /// <returns>opc-legal string suitable for embedding in XML digital signatures</returns>
        private static String DateTimeToXmlFormattedTime(DateTime dt, string format)
        {
            DateTimeFormatInfo formatter = new DateTimeFormatInfo();
            formatter.FullDateTimePattern = format;
            return dt.ToString(format, formatter);
        }
 
        /// <summary>
        /// XML Format time string to DateTime
        /// </summary>
        /// <param name="s">string to parse</param>
        /// <param name="format">format to use - specified in Xml Signature date syntax</param>
        /// <exception cref="XmlException">Format does not match the given string</exception>
        /// <returns>DateTime</returns>
        private static DateTime XmlFormattedTimeToDateTime(String s, String format)
        {
            // convert Xml syntax to equivalent DateTime syntax
            string[] legalFormats = ConvertXmlFormatStringToDateTimeFormatString(format);
 
            // the default formatter is culture-invariant (which is what we want)
            DateTimeFormatInfo formatter = new DateTimeFormatInfo();
            formatter.FullDateTimePattern = format;
            return DateTime.ParseExact(s, legalFormats, formatter, 
                DateTimeStyles.NoCurrentDateDefault 
                | DateTimeStyles.AllowLeadingWhite 
                | DateTimeStyles.AllowTrailingWhite);
        }
 
        /// <summary>
        /// Get index of the row that matches the given format
        /// </summary>
        /// <param name="format">format to lookup</param>
        /// <returns>-1 if not found</returns>
        private static int GetIndex(String format)
        {
            for (int i = 0; i < _dateTimePatternMap.GetLength(0); i++)
            {
                if (String.CompareOrdinal(_dateTimePatternMap[i].Format, format) == 0)
                {
                    return i;
                }
            }
            return -1;
        }
 
        /// <summary>
        /// Convert Xml format syntax to DateTime format syntax
        /// </summary>
        /// <param name="format"></param>
        /// <returns></returns>
        private static string[] ConvertXmlFormatStringToDateTimeFormatString(String format)
        {
            return _dateTimePatternMap[GetIndex(format)].Patterns; 
        }
                
        /// <summary>
        /// Verify if the SignatureProperty tag has a valid Id attribute
        /// </summary>
        /// <param name="reader">NodeReader positioned at the SignatureProperty tag</param>
        /// <returns>true, if Id attribute is present and has the correct value, else false</returns>
        private static bool VerifyIdAttribute(XmlReader reader)
        {
            string idAttrValue = reader.GetAttribute(XTable.Get(XTable.ID.SignaturePropertyIdAttrName));
 
            if(idAttrValue!=null 
                && (String.CompareOrdinal(idAttrValue,XTable.Get(XTable.ID.SignaturePropertyIdAttrValue)) == 0))
                return true;
            else
                return false;
        }
 
        /// <summary>
        /// Verify if the mandatory Target attribute exists on the SignatureProperty tag
        /// </summary>
        /// <param name="reader">NodeReader positioned at the SignatureProperty tag</param>
        /// <param name="signatureId">value of the Id attribute on the Signature tag</param>
        /// <returns>true, if Target attribute is present and has the correct value, else false</returns>
        private static bool VerifyTargetAttribute(XmlReader reader, string signatureId)
        {
            string idTargetValue = reader.GetAttribute(XTable.Get(XTable.ID.TargetAttrName));
 
            if (idTargetValue != null)
            {
                //whether there is an Id attribute on the <Signature> tag or no,
                //an empty Target attribute on <SignatureProperty> tag, is allowed.
                //Empty string means current document 
                if (String.CompareOrdinal(idTargetValue, String.Empty) == 0)
                    return true;
                else
                {
                    //If the Target attribute has a non-empty string then
                    //it must match the <Signature> tag Id attribute value
                    if (signatureId != null && String.CompareOrdinal(idTargetValue, "#" + signatureId) == 0)
                        return true;
                    else
                        return false;
                }
            }
            else
                return false;
        }
 
        //-----------------------------------------------------------------------------
        //
        // Private Fields
        //
        //-----------------------------------------------------------------------------
 
        // This is a mapping between time formats allowed by Opc spec (taken from 
        // http://www.w3.org/TR/NOTE-datetime) and the equivalent formatting string 
        // expected by the DateTimeFormatInfo class.
        private struct TimeFormatMapEntry
        {
            public TimeFormatMapEntry(string xmlFormatString, string[] dateTimePatterns)
            {
                _xmlFormatString = xmlFormatString;
                _dateTimePatterns = dateTimePatterns;
            }
 
            public string   Format { get { return _xmlFormatString; }}
            public string[] Patterns { get { return _dateTimePatterns; }}
 
            private string      _xmlFormatString;
            private string[]    _dateTimePatterns;
        };
 
        private static readonly TimeFormatMapEntry[] _dateTimePatternMap = 
        {
            // Opc Spec value                                   Equivalent DateTimePattern(s)
            new TimeFormatMapEntry("YYYY-MM-DDThh:mm:ss.sTZD",  new string[] {"yyyy-MM-ddTHH:mm:ss.fzzz",   "yyyy-MM-ddTHH:mm:ss.fZ"}),
            new TimeFormatMapEntry("YYYY-MM-DDThh:mm:ssTZD",    new string[] {"yyyy-MM-ddTHH:mm:sszzz",     "yyyy-MM-ddTHH:mm:ssZ"}),
            new TimeFormatMapEntry("YYYY-MM-DDThh:mmTZD",       new string[] {"yyyy-MM-ddTHH:mmzzz",        "yyyy-MM-ddTHH:mmZ"}),
            new TimeFormatMapEntry("YYYY-MM-DD",                new string[] {"yyyy-MM-dd"}),
            new TimeFormatMapEntry("YYYY-MM",                   new string[] {"yyyy-MM"}),
            new TimeFormatMapEntry("YYYY",                      new string[] {"yyyy"}),
        };
    }
  }