2 // XmlCanonicalizer.cs - C14N implementation for XML Signature
3 // http://www.w3.org/TR/xml-c14n
6 // Aleksey Sanin (aleksey@aleksey.com)
8 // (C) 2003 Aleksey Sanin (aleksey@aleksey.com)
12 // Permission is hereby granted, free of charge, to any person obtaining
13 // a copy of this software and associated documentation files (the
14 // "Software"), to deal in the Software without restriction, including
15 // without limitation the rights to use, copy, modify, merge, publish,
16 // distribute, sublicense, and/or sell copies of the Software, and to
17 // permit persons to whom the Software is furnished to do so, subject to
18 // the following conditions:
20 // The above copyright notice and this permission notice shall be
21 // included in all copies or substantial portions of the Software.
23 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
25 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
27 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
28 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
29 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32 using System.Collections;
39 internal class XmlCanonicalizer {
41 private enum XmlCanonicalizerState
49 private bool comments;
50 private bool exclusive;
53 private XmlNodeList xnl;
54 private StringBuilder res;
56 // namespaces rendering stack
57 private XmlCanonicalizerState state;
58 private ArrayList visibleNamespaces;
59 private int prevVisibleNamespacesStart;
60 private int prevVisibleNamespacesEnd;
62 public XmlCanonicalizer (bool withComments, bool excC14N)
64 res = new StringBuilder ();
65 comments = withComments;
67 state = XmlCanonicalizerState.BeforeDocElement;
68 visibleNamespaces = new ArrayList ();
69 prevVisibleNamespacesStart = 0;
70 prevVisibleNamespacesEnd = 0;
73 public Stream Canonicalize (XmlDocument doc)
75 WriteDocumentNode (doc);
77 UTF8Encoding utf8 = new UTF8Encoding ();
78 byte[] data = utf8.GetBytes (res.ToString ());
79 return new MemoryStream (data);
82 public Stream Canonicalize (XmlNodeList nodes)
85 if (nodes == null || nodes.Count < 1)
86 return new MemoryStream ();
87 return Canonicalize (nodes[0].OwnerDocument);
90 private void WriteNode (XmlNode node)
92 // Console.WriteLine ("C14N Debug: node=" + node.Name);
94 bool visible = IsNodeVisible (node);
95 switch (node.NodeType) {
96 case XmlNodeType.Document:
97 case XmlNodeType.DocumentFragment:
98 WriteDocumentNode (node);
100 case XmlNodeType.Element:
101 WriteElementNode (node, visible);
103 case XmlNodeType.CDATA:
104 case XmlNodeType.SignificantWhitespace:
105 case XmlNodeType.Text:
106 // CDATA sections are processed as text nodes
107 WriteTextNode (node, visible);
109 case XmlNodeType.Whitespace:
110 if (state == XmlCanonicalizerState.InsideDocElement)
111 WriteTextNode (node, visible);
113 case XmlNodeType.Comment:
114 WriteCommentNode (node, visible);
116 case XmlNodeType.ProcessingInstruction:
117 WriteProcessingInstructionNode (node, visible);
119 case XmlNodeType.EntityReference:
120 for (int i = 0; i < node.ChildNodes.Count; i++)
121 WriteNode (node.ChildNodes [i]);
123 case XmlNodeType.Attribute:
124 throw new XmlException ("Attribute node is impossible here", null);
125 case XmlNodeType.EndElement:
126 throw new XmlException ("EndElement node is impossible here", null);
127 case XmlNodeType.EndEntity:
128 throw new XmlException ("EndEntity node is impossible here", null);
129 case XmlNodeType.DocumentType:
130 case XmlNodeType.Entity:
131 case XmlNodeType.Notation:
132 case XmlNodeType.XmlDeclaration:
138 private void WriteDocumentNode (XmlNode node)
140 state = XmlCanonicalizerState.BeforeDocElement;
141 for (XmlNode child = node.FirstChild; child != null; child = child.NextSibling)
146 // If the element is not in the node-set, then the result is obtained
147 // by processing the namespace axis, then the attribute axis, then
148 // processing the child nodes of the element that are in the node-set
149 // (in document order). If the element is inthe node-set, then the result
150 // is an open angle bracket (<), the element QName, the result of
151 // processing the namespace axis, the result of processing the attribute
152 // axis, a close angle bracket (>), the result of processing the child
153 // nodes of the element that are in the node-set (in document order), an
154 // open angle bracket, a forward slash (/), the element QName, and a close
156 private void WriteElementNode (XmlNode node, bool visible)
158 // Console.WriteLine ("Debug: element node");
160 // remember current state
161 int savedPrevVisibleNamespacesStart = prevVisibleNamespacesStart;
162 int savedPrevVisibleNamespacesEnd = prevVisibleNamespacesEnd;
163 int savedVisibleNamespacesSize = visibleNamespaces.Count;
164 XmlCanonicalizerState s = state;
165 if (visible && state == XmlCanonicalizerState.BeforeDocElement)
166 state = XmlCanonicalizerState.InsideDocElement;
171 res.Append (node.Name);
174 // this is odd but you can select namespaces
175 // and attributes even if node itself is not visible
176 WriteNamespacesAxis (node, visible);
177 WriteAttributesAxis (node);
183 for (XmlNode child = node.FirstChild; child != null; child = child.NextSibling)
189 res.Append (node.Name);
194 if (visible && s == XmlCanonicalizerState.BeforeDocElement)
195 state = XmlCanonicalizerState.AfterDocElement;
196 prevVisibleNamespacesStart = savedPrevVisibleNamespacesStart;
197 prevVisibleNamespacesEnd = savedPrevVisibleNamespacesEnd;
198 if (visibleNamespaces.Count > savedVisibleNamespacesSize) {
199 visibleNamespaces.RemoveRange (savedVisibleNamespacesSize,
200 visibleNamespaces.Count - savedVisibleNamespacesSize);
205 // Consider a list L containing only namespace nodes in the
206 // axis and in the node-set in lexicographic order (ascending). To begin
207 // processing L, if the first node is not the default namespace node (a node
208 // with no namespace URI and no local name), then generate a space followed
209 // by xmlns="" if and only if the following conditions are met:
210 // - the element E that owns the axis is in the node-set
211 // - The nearest ancestor element of E in the node-set has a default
212 // namespace node in the node-set (default namespace nodes always
213 // have non-empty values in XPath)
214 // The latter condition eliminates unnecessary occurrences of xmlns="" in
215 // the canonical form since an element only receives an xmlns="" if its
216 // default namespace is empty and if it has an immediate parent in the
217 // canonical form that has a non-empty default namespace. To finish
218 // processing L, simply process every namespace node in L, except omit
219 // namespace node with local name xml, which defines the xml prefix,
220 // if its string value is http://www.w3.org/XML/1998/namespace.
221 private void WriteNamespacesAxis (XmlNode node, bool visible)
223 // Console.WriteLine ("Debug: namespaces");
225 XmlDocument doc = node.OwnerDocument;
226 bool has_empty_namespace = false;
227 ArrayList list = new ArrayList ();
228 for (XmlNode cur = node; cur != null && cur != doc; cur = cur.ParentNode) {
229 foreach (XmlNode attribute in cur.Attributes) {
230 if (!IsNamespaceNode (attribute))
233 // get namespace prefix
234 string prefix = string.Empty;
235 if (attribute.Prefix == "xmlns")
236 prefix = attribute.LocalName;
238 // check if it is "xml" namespace
239 if (prefix == "xml" && attribute.Value == "http://www.w3.org/XML/1998/namespace")
242 // make sure that this is an active namespace
244 string ns = node.GetNamespaceOfPrefix (prefix);
245 if (ns != attribute.Value)
248 // check that it is selected with XPath
249 if (!IsNodeVisible (attribute))
252 // check that we have not rendered it yet
253 bool rendered = IsNamespaceRendered (prefix, attribute.Value);
255 // add to the visible namespaces stack
257 visibleNamespaces.Add (attribute);
260 list.Add (attribute);
262 if (prefix == string.Empty)
263 has_empty_namespace = true;
267 // add empty namespace if needed
268 if (visible && !has_empty_namespace && !IsNamespaceRendered (string.Empty, string.Empty))
269 res.Append (" xmlns=\"\"");
271 list.Sort (new XmlDsigC14NTransformNamespacesComparer ());
272 foreach (object obj in list) {
273 XmlNode attribute = (obj as XmlNode);
274 if (attribute != null) {
276 res.Append (attribute.Name);
278 res.Append (attribute.Value);
283 // move the rendered namespaces stack
285 prevVisibleNamespacesStart = prevVisibleNamespacesEnd;
286 prevVisibleNamespacesEnd = visibleNamespaces.Count;
291 // In lexicographic order (ascending), process each node that
292 // is in the element's attribute axis and in the node-set.
294 // The processing of an element node E MUST be modified slightly
295 // when an XPath node-set is given as input and the element's
296 // parent is omitted from the node-set.
297 private void WriteAttributesAxis (XmlNode node)
299 // Console.WriteLine ("Debug: attributes");
301 ArrayList list = new ArrayList ();
302 foreach (XmlNode attribute in node.Attributes) {
303 if (!IsNamespaceNode (attribute) && IsNodeVisible (attribute))
304 list.Add (attribute);
307 // Add attributes from "xml" namespace for "inclusive" c14n only:
309 // The method for processing the attribute axis of an element E
310 // in the node-set is enhanced. All element nodes along E's
311 // ancestor axis are examined for nearest occurrences of
312 // attributes in the xml namespace, such as xml:lang and
313 // xml:space (whether or not they are in the node-set).
314 // From this list of attributes, remove any that are in E's
315 // attribute axis (whether or not they are in the node-set).
316 // Then, lexicographically merge this attribute list with the
317 // nodes of E's attribute axis that are in the node-set. The
318 // result of visiting the attribute axis is computed by
319 // processing the attribute nodes in this merged attribute list.
320 if (!exclusive && node.ParentNode != null && node.ParentNode.ParentNode != null && !IsNodeVisible (node.ParentNode.ParentNode)) {
321 // if we have whole document then the node.ParentNode.ParentNode
323 for (XmlNode cur = node.ParentNode; cur != null; cur = cur.ParentNode) {
324 if (cur.Attributes == null)
326 foreach (XmlNode attribute in cur.Attributes) {
327 // we are looking for "xml:*" attributes
328 if (attribute.Prefix != "xml")
331 // exclude ones that are in the node's attributes axis
332 if (node.Attributes.GetNamedItem (attribute.LocalName, attribute.NamespaceURI) != null)
335 // finally check that we don't have the same attribute in our list
337 foreach (object obj in list) {
338 XmlNode n = (obj as XmlNode);
339 if (n.Prefix == "xml" && n.LocalName == attribute.LocalName) {
348 // now we can add this attribute to our list
349 list.Add (attribute);
354 // sort namespaces and write results
355 list.Sort (new XmlDsigC14NTransformAttributesComparer ());
356 foreach (object obj in list) {
357 XmlNode attribute = (obj as XmlNode);
358 if (attribute != null) {
360 res.Append (attribute.Name);
362 res.Append (NormalizeString (attribute.Value, XmlNodeType.Attribute));
369 // the string value, except all ampersands are replaced
370 // by &, all open angle brackets (<) are replaced by <, all closing
371 // angle brackets (>) are replaced by >, and all #xD characters are
372 // replaced by 
.
373 private void WriteTextNode (XmlNode node, bool visible)
375 // Console.WriteLine ("Debug: text node");
377 res.Append (NormalizeString (node.Value, node.NodeType));
378 // res.Append (NormalizeString (node.Value, XmlNodeType.Text));
382 // Nothing if generating canonical XML without comments. For
383 // canonical XML with comments, generate the opening comment
384 // symbol (<!--), the string value of the node, and the
385 // closing comment symbol (-->). Also, a trailing #xA is rendered
386 // after the closing comment symbol for comment children of the
387 // root node with a lesser document order than the document
388 // element, and a leading #xA is rendered before the opening
389 // comment symbol of comment children of the root node with a
390 // greater document order than the document element. (Comment
391 // children of the root node represent comments outside of the
392 // top-level document element and outside of the document type
394 private void WriteCommentNode (XmlNode node, bool visible)
396 // Console.WriteLine ("Debug: comment node");
397 if (visible && comments) {
398 if (state == XmlCanonicalizerState.AfterDocElement)
399 res.Append ("\x0A<!--");
403 res.Append (NormalizeString (node.Value, XmlNodeType.Comment));
405 if (state == XmlCanonicalizerState.BeforeDocElement)
406 res.Append ("-->\x0A");
412 // Processing Instruction (PI) Nodes-
413 // The opening PI symbol (<?), the PI target name of the node,
414 // a leading space and the string value if it is not empty, and
415 // the closing PI symbol (?>). If the string value is empty,
416 // then the leading space is not added. Also, a trailing #xA is
417 // rendered after the closing PI symbol for PI children of the
418 // root node with a lesser document order than the document
419 // element, and a leading #xA is rendered before the opening PI
420 // symbol of PI children of the root node with a greater document
421 // order than the document element.
422 private void WriteProcessingInstructionNode (XmlNode node, bool visible)
424 // Console.WriteLine ("Debug: PI node");
427 if (state == XmlCanonicalizerState.AfterDocElement)
428 res.Append ("\x0A<?");
432 res.Append (node.Name);
433 if (node.Value.Length > 0) {
435 res.Append (NormalizeString (node.Value, XmlNodeType.ProcessingInstruction));
438 if (state == XmlCanonicalizerState.BeforeDocElement)
439 res.Append ("?>\x0A");
445 private bool IsNodeVisible (XmlNode node)
447 // if node list is empty then we process whole document
451 // walk thru the list
452 foreach (XmlNode xn in xnl) {
453 if (node.Equals (xn))
460 private bool IsNamespaceRendered (string prefix, string uri)
462 // if the default namespace xmlns="" is not re-defined yet
463 // then we do not want to print it out
464 bool IsEmptyNs = prefix == string.Empty && uri == string.Empty;
465 int start = (IsEmptyNs) ? 0 : prevVisibleNamespacesStart;
466 for (int i = visibleNamespaces.Count - 1; i >= start; i--) {
467 XmlNode node = (visibleNamespaces[i] as XmlNode);
469 // get namespace prefix
470 string p = string.Empty;
471 if (node.Prefix == "xmlns")
474 return node.Value == uri;
481 private bool IsNamespaceNode (XmlNode node)
483 if (node == null || node.NodeType != XmlNodeType.Attribute)
485 return node.NamespaceURI == "http://www.w3.org/2000/xmlns/";
488 private bool IsTextNode (XmlNodeType type)
491 case XmlNodeType.Text:
492 case XmlNodeType.CDATA:
493 case XmlNodeType.SignificantWhitespace:
494 case XmlNodeType.Whitespace:
500 private string NormalizeString (string input, XmlNodeType type)
502 StringBuilder sb = new StringBuilder ();
503 for (int i = 0; i < input.Length; i++) {
505 if (ch == '<' && (type == XmlNodeType.Attribute || IsTextNode (type)))
507 else if (ch == '>' && IsTextNode (type))
509 else if (ch == '&' && (type == XmlNodeType.Attribute || IsTextNode (type)))
511 else if (ch == '\"' && type == XmlNodeType.Attribute)
512 sb.Append (""");
513 else if (ch == '\x09' && type == XmlNodeType.Attribute)
515 else if (ch == '\x0A' && type == XmlNodeType.Attribute)
517 else if (ch == '\x0D')
523 return sb.ToString ();
527 internal class XmlDsigC14NTransformAttributesComparer : IComparer
529 public int Compare (object x, object y)
531 XmlNode n1 = (x as XmlNode);
532 XmlNode n2 = (y as XmlNode);
541 else if (n1.Prefix == n2.Prefix)
542 return string.Compare (n1.LocalName, n2.LocalName);
544 // Attributes in the default namespace are first
545 // because the default namespace is not applied to
546 // unqualified attributes
547 if (n1.Prefix == string.Empty)
549 else if (n2.Prefix == string.Empty)
552 int ret = string.Compare (n1.NamespaceURI, n2.NamespaceURI);
554 ret = string.Compare (n1.LocalName, n2.LocalName);
559 internal class XmlDsigC14NTransformNamespacesComparer : IComparer
561 public int Compare (object x, object y)
563 XmlNode n1 = (x as XmlNode);
564 XmlNode n2 = (y as XmlNode);
573 else if (n1.Prefix == string.Empty)
575 else if (n2.Prefix == string.Empty)
578 return string.Compare (n1.LocalName, n2.LocalName);