// // XmlCanonicalizer.cs - C14N implementation for XML Signature // http://www.w3.org/TR/xml-c14n // // Author: // Aleksey Sanin (aleksey@aleksey.com) // // (C) 2003 Aleksey Sanin (aleksey@aleksey.com) // // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // using System; using System.Collections; using System.IO; using System.Text; using System.Xml; namespace Mono.Xml { internal class XmlCanonicalizer { private enum XmlCanonicalizerState { BeforeDocElement, InsideDocElement, AfterDocElement } // c14n parameters private bool comments; private bool exclusive; string inclusiveNamespacesPrefixList; // input/output private XmlNodeList xnl; private StringBuilder res; // namespaces rendering stack private XmlCanonicalizerState state; private ArrayList visibleNamespaces; private int prevVisibleNamespacesStart; private int prevVisibleNamespacesEnd; private Hashtable propagatedNss; public XmlCanonicalizer (bool withComments, bool excC14N, Hashtable propagatedNamespaces) { res = new StringBuilder (); comments = withComments; exclusive = excC14N; propagatedNss = propagatedNamespaces; } void Initialize () { state = XmlCanonicalizerState.BeforeDocElement; visibleNamespaces = new ArrayList (); prevVisibleNamespacesStart = 0; prevVisibleNamespacesEnd = 0; res.Length = 0; } public Stream Canonicalize (XmlDocument doc) { if (doc == null) throw new ArgumentNullException ("doc"); Initialize (); FillMissingPrefixes (doc, new XmlNamespaceManager (doc.NameTable), new ArrayList ()); WriteDocumentNode (doc); UTF8Encoding utf8 = new UTF8Encoding (); byte[] data = utf8.GetBytes (res.ToString ()); return new MemoryStream (data); } public Stream Canonicalize (XmlNodeList nodes) { xnl = nodes; if (nodes == null || nodes.Count < 1) return new MemoryStream (); XmlNode n = nodes [0]; return Canonicalize (n.NodeType == XmlNodeType.Document ? n as XmlDocument : n.OwnerDocument); } // See xml-enc-c14n specification public string InclusiveNamespacesPrefixList { get { return inclusiveNamespacesPrefixList; } set { inclusiveNamespacesPrefixList = value; } } XmlAttribute CreateXmlns (XmlNode n) { XmlAttribute a = n.Prefix.Length == 0 ? n.OwnerDocument.CreateAttribute ("xmlns", "http://www.w3.org/2000/xmlns/") : n.OwnerDocument.CreateAttribute ("xmlns", n.Prefix, "http://www.w3.org/2000/xmlns/"); a.Value = n.NamespaceURI; return a; } // Note that this must be done *before* filtering nodes out // by context node list. private void FillMissingPrefixes (XmlNode n, XmlNamespaceManager nsmgr, ArrayList tmpList) { if (n.Prefix.Length == 0 && propagatedNss != null) { foreach (DictionaryEntry de in propagatedNss) if ((string) de.Value == n.NamespaceURI) { n.Prefix = (string) de.Key; break; } } if (n.NodeType == XmlNodeType.Element && ((XmlElement) n).HasAttributes) { foreach (XmlAttribute a in n.Attributes) if (a.NamespaceURI == "http://www.w3.org/2000/xmlns/") nsmgr.AddNamespace (a.Prefix.Length == 0 ? String.Empty : a.LocalName, a.Value); nsmgr.PushScope (); } if (n.NamespaceURI.Length > 0 && nsmgr.LookupPrefix (n.NamespaceURI) == null) tmpList.Add (CreateXmlns (n)); if (n.NodeType == XmlNodeType.Element && ((XmlElement) n).HasAttributes) { foreach (XmlAttribute a in n.Attributes) if (a.NamespaceURI.Length > 0 && nsmgr.LookupNamespace (a.Prefix) == null) tmpList.Add (CreateXmlns (a)); } foreach (XmlAttribute a in tmpList) ((XmlElement) n).SetAttributeNode (a); tmpList.Clear (); if (n.HasChildNodes) { for (XmlNode c = n.FirstChild; c != null; c = c.NextSibling) if (c.NodeType == XmlNodeType.Element) FillMissingPrefixes (c, nsmgr, tmpList); } nsmgr.PopScope (); } private void WriteNode (XmlNode node) { // Console.WriteLine ("C14N Debug: node=" + node.Name); bool visible = IsNodeVisible (node); switch (node.NodeType) { case XmlNodeType.Document: case XmlNodeType.DocumentFragment: WriteDocumentNode (node); break; case XmlNodeType.Element: WriteElementNode (node, visible); break; case XmlNodeType.CDATA: case XmlNodeType.SignificantWhitespace: case XmlNodeType.Text: // CDATA sections are processed as text nodes WriteTextNode (node, visible); break; case XmlNodeType.Whitespace: if (state == XmlCanonicalizerState.InsideDocElement) WriteTextNode (node, visible); break; case XmlNodeType.Comment: WriteCommentNode (node, visible); break; case XmlNodeType.ProcessingInstruction: WriteProcessingInstructionNode (node, visible); break; case XmlNodeType.EntityReference: for (int i = 0; i < node.ChildNodes.Count; i++) WriteNode (node.ChildNodes [i]); break; case XmlNodeType.Attribute: throw new XmlException ("Attribute node is impossible here", null); case XmlNodeType.EndElement: throw new XmlException ("EndElement node is impossible here", null); case XmlNodeType.EndEntity: throw new XmlException ("EndEntity node is impossible here", null); case XmlNodeType.DocumentType: case XmlNodeType.Entity: case XmlNodeType.Notation: case XmlNodeType.XmlDeclaration: // just do nothing break; } } private void WriteDocumentNode (XmlNode node) { state = XmlCanonicalizerState.BeforeDocElement; for (XmlNode child = node.FirstChild; child != null; child = child.NextSibling) WriteNode (child); } // Element Nodes // If the element is not in the node-set, then the result is obtained // by processing the namespace axis, then the attribute axis, then // processing the child nodes of the element that are in the node-set // (in document order). If the element is inthe node-set, then the result // is an open angle bracket (<), the element QName, the result of // processing the namespace axis, the result of processing the attribute // axis, a close angle bracket (>), the result of processing the child // nodes of the element that are in the node-set (in document order), an // open angle bracket, a forward slash (/), the element QName, and a close // angle bracket. private void WriteElementNode (XmlNode node, bool visible) { // Console.WriteLine ("Debug: element node"); // remember current state int savedPrevVisibleNamespacesStart = prevVisibleNamespacesStart; int savedPrevVisibleNamespacesEnd = prevVisibleNamespacesEnd; int savedVisibleNamespacesSize = visibleNamespaces.Count; XmlCanonicalizerState s = state; if (visible && state == XmlCanonicalizerState.BeforeDocElement) state = XmlCanonicalizerState.InsideDocElement; // write start tag if (visible) { res.Append ("<"); res.Append (node.Name); } // this is odd but you can select namespaces // and attributes even if node itself is not visible WriteNamespacesAxis (node, visible); WriteAttributesAxis (node); if (visible) res.Append (">"); // write children for (XmlNode child = node.FirstChild; child != null; child = child.NextSibling) WriteNode (child); // write end tag if (visible) { res.Append (""); } // restore state if (visible && s == XmlCanonicalizerState.BeforeDocElement) state = XmlCanonicalizerState.AfterDocElement; prevVisibleNamespacesStart = savedPrevVisibleNamespacesStart; prevVisibleNamespacesEnd = savedPrevVisibleNamespacesEnd; if (visibleNamespaces.Count > savedVisibleNamespacesSize) { visibleNamespaces.RemoveRange (savedVisibleNamespacesSize, visibleNamespaces.Count - savedVisibleNamespacesSize); } } // Namespace Axis // Consider a list L containing only namespace nodes in the // axis and in the node-set in lexicographic order (ascending). To begin // processing L, if the first node is not the default namespace node (a node // with no namespace URI and no local name), then generate a space followed // by xmlns="" if and only if the following conditions are met: // - the element E that owns the axis is in the node-set // - The nearest ancestor element of E in the node-set has a default // namespace node in the node-set (default namespace nodes always // have non-empty values in XPath) // The latter condition eliminates unnecessary occurrences of xmlns="" in // the canonical form since an element only receives an xmlns="" if its // default namespace is empty and if it has an immediate parent in the // canonical form that has a non-empty default namespace. To finish // processing L, simply process every namespace node in L, except omit // namespace node with local name xml, which defines the xml prefix, // if its string value is http://www.w3.org/XML/1998/namespace. private void WriteNamespacesAxis (XmlNode node, bool visible) { // Console.WriteLine ("Debug: namespaces"); XmlDocument doc = node.OwnerDocument; bool has_empty_namespace = false; ArrayList list = new ArrayList (); for (XmlNode cur = node; cur != null && cur != doc; cur = cur.ParentNode) { foreach (XmlAttribute attribute in cur.Attributes) { if (!IsNamespaceNode (attribute)) continue; // get namespace prefix string prefix = string.Empty; if (attribute.Prefix == "xmlns") prefix = attribute.LocalName; // check if it is "xml" namespace if (prefix == "xml" && attribute.Value == "http://www.w3.org/XML/1998/namespace") continue; // make sure that this is an active namespace // for our node string ns = node.GetNamespaceOfPrefix (prefix); if (ns != attribute.Value) continue; // check that it is selected with XPath if (!IsNodeVisible (attribute)) continue; // check that we have not rendered it yet bool rendered = IsNamespaceRendered (prefix, attribute.Value); // For exc-c14n, only visibly utilized // namespaces are written. if (exclusive && !IsVisiblyUtilized (node as XmlElement, attribute)) continue; // add to the visible namespaces stack if (visible) visibleNamespaces.Add (attribute); if (!rendered) list.Add (attribute); if (prefix == string.Empty) has_empty_namespace = true; } } // add empty namespace if needed if (visible && !has_empty_namespace && !IsNamespaceRendered (string.Empty, string.Empty) && node.NamespaceURI == String.Empty) res.Append (" xmlns=\"\""); list.Sort (new XmlDsigC14NTransformNamespacesComparer ()); foreach (object obj in list) { XmlNode attribute = (obj as XmlNode); if (attribute != null) { res.Append (" "); res.Append (attribute.Name); res.Append ("=\""); res.Append (attribute.Value); res.Append ("\""); } } // move the rendered namespaces stack if (visible) { prevVisibleNamespacesStart = prevVisibleNamespacesEnd; prevVisibleNamespacesEnd = visibleNamespaces.Count; } } // Attribute Axis // In lexicographic order (ascending), process each node that // is in the element's attribute axis and in the node-set. // // The processing of an element node E MUST be modified slightly // when an XPath node-set is given as input and the element's // parent is omitted from the node-set. private void WriteAttributesAxis (XmlNode node) { // Console.WriteLine ("Debug: attributes"); ArrayList list = new ArrayList (); foreach (XmlNode attribute in node.Attributes) { if (!IsNamespaceNode (attribute) && IsNodeVisible (attribute)) list.Add (attribute); } // Add attributes from "xml" namespace for "inclusive" c14n only: // // The method for processing the attribute axis of an element E // in the node-set is enhanced. All element nodes along E's // ancestor axis are examined for nearest occurrences of // attributes in the xml namespace, such as xml:lang and // xml:space (whether or not they are in the node-set). // From this list of attributes, remove any that are in E's // attribute axis (whether or not they are in the node-set). // Then, lexicographically merge this attribute list with the // nodes of E's attribute axis that are in the node-set. The // result of visiting the attribute axis is computed by // processing the attribute nodes in this merged attribute list. if (!exclusive && node.ParentNode != null && node.ParentNode.ParentNode != null && !IsNodeVisible (node.ParentNode.ParentNode)) { // if we have whole document then the node.ParentNode.ParentNode // is always visible for (XmlNode cur = node.ParentNode; cur != null; cur = cur.ParentNode) { if (cur.Attributes == null) continue; foreach (XmlNode attribute in cur.Attributes) { // we are looking for "xml:*" attributes if (attribute.Prefix != "xml") continue; // exclude ones that are in the node's attributes axis if (node.Attributes.GetNamedItem (attribute.LocalName, attribute.NamespaceURI) != null) continue; // finally check that we don't have the same attribute in our list bool found = false; foreach (object obj in list) { XmlNode n = (obj as XmlNode); if (n.Prefix == "xml" && n.LocalName == attribute.LocalName) { found = true; break; } } if (found) continue; // now we can add this attribute to our list list.Add (attribute); } } } // sort namespaces and write results list.Sort (new XmlDsigC14NTransformAttributesComparer ()); foreach (object obj in list) { XmlNode attribute = (obj as XmlNode); if (attribute != null) { res.Append (" "); res.Append (attribute.Name); res.Append ("=\""); res.Append (NormalizeString (attribute.Value, XmlNodeType.Attribute)); res.Append ("\""); } } } // Text Nodes // the string value, except all ampersands are replaced // by &, all open angle brackets (<) are replaced by <, all closing // angle brackets (>) are replaced by >, and all #xD characters are // replaced by . private void WriteTextNode (XmlNode node, bool visible) { // Console.WriteLine ("Debug: text node"); if (visible) res.Append (NormalizeString (node.Value, node.NodeType)); // res.Append (NormalizeString (node.Value, XmlNodeType.Text)); } // Comment Nodes // Nothing if generating canonical XML without comments. For // canonical XML with comments, generate the opening comment // symbol (). Also, a trailing #xA is rendered // after the closing comment symbol for comment children of the // root node with a lesser document order than the document // element, and a leading #xA is rendered before the opening // comment symbol of comment children of the root node with a // greater document order than the document element. (Comment // children of the root node represent comments outside of the // top-level document element and outside of the document type // declaration). private void WriteCommentNode (XmlNode node, bool visible) { // Console.WriteLine ("Debug: comment node"); if (visible && comments) { if (state == XmlCanonicalizerState.AfterDocElement) res.Append ("\x0A\x0A"); else res.Append ("-->"); } } // Processing Instruction (PI) Nodes- // The opening PI symbol (). If the string value is empty, // then the leading space is not added. Also, a trailing #xA is // rendered after the closing PI symbol for PI children of the // root node with a lesser document order than the document // element, and a leading #xA is rendered before the opening PI // symbol of PI children of the root node with a greater document // order than the document element. private void WriteProcessingInstructionNode (XmlNode node, bool visible) { // Console.WriteLine ("Debug: PI node"); if (visible) { if (state == XmlCanonicalizerState.AfterDocElement) res.Append ("\x0A 0) { res.Append (" "); res.Append (NormalizeString (node.Value, XmlNodeType.ProcessingInstruction)); } if (state == XmlCanonicalizerState.BeforeDocElement) res.Append ("?>\x0A"); else res.Append ("?>"); } } // determines whether the node is in the node-set or not. private bool IsNodeVisible (XmlNode node) { // if node list is empty then we process whole document if (xnl == null) return true; // walk thru the list foreach (XmlNode xn in xnl) { if (node.Equals (xn)) return true; } return false; } // This method assumes that the namespace node is *not* // rendered yet. private bool IsVisiblyUtilized (XmlElement owner, XmlAttribute ns) { if (owner == null) return false; string prefix = ns.LocalName == "xmlns" ? String.Empty : ns.LocalName; if (owner.Prefix == prefix && owner.NamespaceURI == ns.Value) return true; if (!owner.HasAttributes) return false; foreach (XmlAttribute a in owner.Attributes) { if (a.Prefix == String.Empty) continue; if (a.Prefix != prefix || a.NamespaceURI != ns.Value) continue; if (IsNodeVisible (a)) return true; } return false; } private bool IsNamespaceRendered (string prefix, string uri) { // if the default namespace xmlns="" is not re-defined yet // then we do not want to print it out bool IsEmptyNs = prefix == string.Empty && uri == string.Empty; int start = (IsEmptyNs) ? 0 : prevVisibleNamespacesStart; for (int i = visibleNamespaces.Count - 1; i >= start; i--) { XmlNode node = (visibleNamespaces[i] as XmlNode); if (node != null) { // get namespace prefix string p = string.Empty; if (node.Prefix == "xmlns") p = node.LocalName; if (p == prefix) return node.Value == uri; } } return IsEmptyNs; } private bool IsNamespaceNode (XmlNode node) { if (node == null || node.NodeType != XmlNodeType.Attribute) return false; return node.NamespaceURI == "http://www.w3.org/2000/xmlns/"; } private bool IsTextNode (XmlNodeType type) { switch (type) { case XmlNodeType.Text: case XmlNodeType.CDATA: case XmlNodeType.SignificantWhitespace: case XmlNodeType.Whitespace: return true; } return false; } private string NormalizeString (string input, XmlNodeType type) { StringBuilder sb = new StringBuilder (); for (int i = 0; i < input.Length; i++) { char ch = input[i]; if (ch == '<' && (type == XmlNodeType.Attribute || IsTextNode (type))) sb.Append ("<"); else if (ch == '>' && IsTextNode (type)) sb.Append (">"); else if (ch == '&' && (type == XmlNodeType.Attribute || IsTextNode (type))) sb.Append ("&"); else if (ch == '\"' && type == XmlNodeType.Attribute) sb.Append ("""); else if (ch == '\x09' && type == XmlNodeType.Attribute) sb.Append (" "); else if (ch == '\x0A' && type == XmlNodeType.Attribute) sb.Append (" "); else if (ch == '\x0D') sb.Append (" "); else sb.Append (ch); } return sb.ToString (); } } internal class XmlDsigC14NTransformAttributesComparer : IComparer { public int Compare (object x, object y) { XmlNode n1 = (x as XmlNode); XmlNode n2 = (y as XmlNode); // simple cases if (n1 == n2) return 0; else if (n1 == null) return -1; else if (n2 == null) return 1; else if (n1.Prefix == n2.Prefix) return string.CompareOrdinal (n1.LocalName, n2.LocalName); // Attributes in the default namespace are first // because the default namespace is not applied to // unqualified attributes if (n1.Prefix == string.Empty) return -1; else if (n2.Prefix == string.Empty) return 1; int ret = string.Compare (n1.NamespaceURI, n2.NamespaceURI); if (ret == 0) ret = string.Compare (n1.LocalName, n2.LocalName); return ret; } } internal class XmlDsigC14NTransformNamespacesComparer : IComparer { public int Compare (object x, object y) { XmlNode n1 = (x as XmlNode); XmlNode n2 = (y as XmlNode); // simple cases if (n1 == n2) return 0; else if (n1 == null) return -1; else if (n2 == null) return 1; else if (n1.Prefix == string.Empty) return -1; else if (n2.Prefix == string.Empty) return 1; return string.Compare (n1.LocalName, n2.LocalName); } } }