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
;
51 string inclusiveNamespacesPrefixList
;
54 private XmlNodeList xnl
;
55 private StringBuilder res
;
57 // namespaces rendering stack
58 private XmlCanonicalizerState state
;
59 private ArrayList visibleNamespaces
;
60 private int prevVisibleNamespacesStart
;
61 private int prevVisibleNamespacesEnd
;
62 private Hashtable propagatedNss
;
64 public XmlCanonicalizer (bool withComments
, bool excC14N
, Hashtable propagatedNamespaces
)
66 res
= new StringBuilder ();
67 comments
= withComments
;
69 propagatedNss
= propagatedNamespaces
;
74 state
= XmlCanonicalizerState
.BeforeDocElement
;
75 visibleNamespaces
= new ArrayList ();
76 prevVisibleNamespacesStart
= 0;
77 prevVisibleNamespacesEnd
= 0;
81 public Stream
Canonicalize (XmlDocument doc
)
84 throw new ArgumentNullException ("doc");
87 FillMissingPrefixes (doc
, new XmlNamespaceManager (doc
.NameTable
), new ArrayList ());
88 WriteDocumentNode (doc
);
90 UTF8Encoding utf8
= new UTF8Encoding ();
91 byte[] data
= utf8
.GetBytes (res
.ToString ());
92 return new MemoryStream (data
);
95 public Stream
Canonicalize (XmlNodeList nodes
)
98 if (nodes
== null || nodes
.Count
< 1)
99 return new MemoryStream ();
100 XmlNode n
= nodes
[0];
101 return Canonicalize (n
.NodeType
== XmlNodeType
.Document
? n
as XmlDocument
: n
.OwnerDocument
);
104 // See xml-enc-c14n specification
105 public string InclusiveNamespacesPrefixList
{
106 get { return inclusiveNamespacesPrefixList; }
107 set { inclusiveNamespacesPrefixList = value; }
110 XmlAttribute
CreateXmlns (XmlNode n
)
112 XmlAttribute a
= n
.Prefix
.Length
== 0 ?
113 n
.OwnerDocument
.CreateAttribute ("xmlns", "http://www.w3.org/2000/xmlns/") :
114 n
.OwnerDocument
.CreateAttribute ("xmlns", n
.Prefix
, "http://www.w3.org/2000/xmlns/");
115 a
.Value
= n
.NamespaceURI
;
119 // Note that this must be done *before* filtering nodes out
120 // by context node list.
121 private void FillMissingPrefixes (XmlNode n
, XmlNamespaceManager nsmgr
, ArrayList tmpList
)
123 if (n
.Prefix
.Length
== 0 && propagatedNss
!= null) {
124 foreach (DictionaryEntry de
in propagatedNss
)
125 if ((string) de
.Value
== n
.NamespaceURI
) {
126 n
.Prefix
= (string) de
.Key
;
131 if (n
.NodeType
== XmlNodeType
.Element
&& ((XmlElement
) n
).HasAttributes
) {
132 foreach (XmlAttribute a
in n
.Attributes
)
133 if (a
.NamespaceURI
== "http://www.w3.org/2000/xmlns/")
134 nsmgr
.AddNamespace (a
.Prefix
.Length
== 0 ? String
.Empty
: a
.LocalName
, a
.Value
);
138 if (n
.NamespaceURI
.Length
> 0 && nsmgr
.LookupPrefix (n
.NamespaceURI
) == null)
139 tmpList
.Add (CreateXmlns (n
));
141 if (n
.NodeType
== XmlNodeType
.Element
&& ((XmlElement
) n
).HasAttributes
) {
142 foreach (XmlAttribute a
in n
.Attributes
)
143 if (a
.NamespaceURI
.Length
> 0 && nsmgr
.LookupNamespace (a
.Prefix
) == null)
144 tmpList
.Add (CreateXmlns (a
));
147 foreach (XmlAttribute a
in tmpList
)
148 ((XmlElement
) n
).SetAttributeNode (a
);
151 if (n
.HasChildNodes
) {
152 for (XmlNode c
= n
.FirstChild
; c
!= null; c
= c
.NextSibling
)
153 if (c
.NodeType
== XmlNodeType
.Element
)
154 FillMissingPrefixes (c
, nsmgr
, tmpList
);
159 private void WriteNode (XmlNode node
)
161 // Console.WriteLine ("C14N Debug: node=" + node.Name);
163 bool visible
= IsNodeVisible (node
);
164 switch (node
.NodeType
) {
165 case XmlNodeType
.Document
:
166 case XmlNodeType
.DocumentFragment
:
167 WriteDocumentNode (node
);
169 case XmlNodeType
.Element
:
170 WriteElementNode (node
, visible
);
172 case XmlNodeType
.CDATA
:
173 case XmlNodeType
.SignificantWhitespace
:
174 case XmlNodeType
.Text
:
175 // CDATA sections are processed as text nodes
176 WriteTextNode (node
, visible
);
178 case XmlNodeType
.Whitespace
:
179 if (state
== XmlCanonicalizerState
.InsideDocElement
)
180 WriteTextNode (node
, visible
);
182 case XmlNodeType
.Comment
:
183 WriteCommentNode (node
, visible
);
185 case XmlNodeType
.ProcessingInstruction
:
186 WriteProcessingInstructionNode (node
, visible
);
188 case XmlNodeType
.EntityReference
:
189 for (int i
= 0; i
< node
.ChildNodes
.Count
; i
++)
190 WriteNode (node
.ChildNodes
[i
]);
192 case XmlNodeType
.Attribute
:
193 throw new XmlException ("Attribute node is impossible here", null);
194 case XmlNodeType
.EndElement
:
195 throw new XmlException ("EndElement node is impossible here", null);
196 case XmlNodeType
.EndEntity
:
197 throw new XmlException ("EndEntity node is impossible here", null);
198 case XmlNodeType
.DocumentType
:
199 case XmlNodeType
.Entity
:
200 case XmlNodeType
.Notation
:
201 case XmlNodeType
.XmlDeclaration
:
207 private void WriteDocumentNode (XmlNode node
)
209 state
= XmlCanonicalizerState
.BeforeDocElement
;
210 for (XmlNode child
= node
.FirstChild
; child
!= null; child
= child
.NextSibling
)
215 // If the element is not in the node-set, then the result is obtained
216 // by processing the namespace axis, then the attribute axis, then
217 // processing the child nodes of the element that are in the node-set
218 // (in document order). If the element is inthe node-set, then the result
219 // is an open angle bracket (<), the element QName, the result of
220 // processing the namespace axis, the result of processing the attribute
221 // axis, a close angle bracket (>), the result of processing the child
222 // nodes of the element that are in the node-set (in document order), an
223 // open angle bracket, a forward slash (/), the element QName, and a close
225 private void WriteElementNode (XmlNode node
, bool visible
)
227 // Console.WriteLine ("Debug: element node");
229 // remember current state
230 int savedPrevVisibleNamespacesStart
= prevVisibleNamespacesStart
;
231 int savedPrevVisibleNamespacesEnd
= prevVisibleNamespacesEnd
;
232 int savedVisibleNamespacesSize
= visibleNamespaces
.Count
;
233 XmlCanonicalizerState s
= state
;
234 if (visible
&& state
== XmlCanonicalizerState
.BeforeDocElement
)
235 state
= XmlCanonicalizerState
.InsideDocElement
;
240 res
.Append (node
.Name
);
243 // this is odd but you can select namespaces
244 // and attributes even if node itself is not visible
245 WriteNamespacesAxis (node
, visible
);
246 WriteAttributesAxis (node
);
252 for (XmlNode child
= node
.FirstChild
; child
!= null; child
= child
.NextSibling
)
258 res
.Append (node
.Name
);
263 if (visible
&& s
== XmlCanonicalizerState
.BeforeDocElement
)
264 state
= XmlCanonicalizerState
.AfterDocElement
;
265 prevVisibleNamespacesStart
= savedPrevVisibleNamespacesStart
;
266 prevVisibleNamespacesEnd
= savedPrevVisibleNamespacesEnd
;
267 if (visibleNamespaces
.Count
> savedVisibleNamespacesSize
) {
268 visibleNamespaces
.RemoveRange (savedVisibleNamespacesSize
,
269 visibleNamespaces
.Count
- savedVisibleNamespacesSize
);
274 // Consider a list L containing only namespace nodes in the
275 // axis and in the node-set in lexicographic order (ascending). To begin
276 // processing L, if the first node is not the default namespace node (a node
277 // with no namespace URI and no local name), then generate a space followed
278 // by xmlns="" if and only if the following conditions are met:
279 // - the element E that owns the axis is in the node-set
280 // - The nearest ancestor element of E in the node-set has a default
281 // namespace node in the node-set (default namespace nodes always
282 // have non-empty values in XPath)
283 // The latter condition eliminates unnecessary occurrences of xmlns="" in
284 // the canonical form since an element only receives an xmlns="" if its
285 // default namespace is empty and if it has an immediate parent in the
286 // canonical form that has a non-empty default namespace. To finish
287 // processing L, simply process every namespace node in L, except omit
288 // namespace node with local name xml, which defines the xml prefix,
289 // if its string value is http://www.w3.org/XML/1998/namespace.
290 private void WriteNamespacesAxis (XmlNode node
, bool visible
)
292 // Console.WriteLine ("Debug: namespaces");
294 XmlDocument doc
= node
.OwnerDocument
;
295 bool has_empty_namespace
= false;
296 ArrayList list
= new ArrayList ();
297 for (XmlNode cur
= node
; cur
!= null && cur
!= doc
; cur
= cur
.ParentNode
) {
298 foreach (XmlAttribute attribute
in cur
.Attributes
) {
299 if (!IsNamespaceNode (attribute
))
302 // get namespace prefix
303 string prefix
= string.Empty
;
304 if (attribute
.Prefix
== "xmlns")
305 prefix
= attribute
.LocalName
;
307 // check if it is "xml" namespace
308 if (prefix
== "xml" && attribute
.Value
== "http://www.w3.org/XML/1998/namespace")
311 // make sure that this is an active namespace
313 string ns
= node
.GetNamespaceOfPrefix (prefix
);
314 if (ns
!= attribute
.Value
)
317 // check that it is selected with XPath
318 if (!IsNodeVisible (attribute
))
321 // check that we have not rendered it yet
322 bool rendered
= IsNamespaceRendered (prefix
, attribute
.Value
);
324 // For exc-c14n, only visibly utilized
325 // namespaces are written.
326 if (exclusive
&& !IsVisiblyUtilized (node
as XmlElement
, attribute
))
329 // add to the visible namespaces stack
331 visibleNamespaces
.Add (attribute
);
334 list
.Add (attribute
);
336 if (prefix
== string.Empty
)
337 has_empty_namespace
= true;
341 // add empty namespace if needed
342 if (visible
&& !has_empty_namespace
&& !IsNamespaceRendered (string.Empty
, string.Empty
) && node
.NamespaceURI
== String
.Empty
)
343 res
.Append (" xmlns=\"\"");
345 list
.Sort (new XmlDsigC14NTransformNamespacesComparer ());
346 foreach (object obj
in list
) {
347 XmlNode attribute
= (obj
as XmlNode
);
348 if (attribute
!= null) {
350 res
.Append (attribute
.Name
);
352 res
.Append (attribute
.Value
);
357 // move the rendered namespaces stack
359 prevVisibleNamespacesStart
= prevVisibleNamespacesEnd
;
360 prevVisibleNamespacesEnd
= visibleNamespaces
.Count
;
365 // In lexicographic order (ascending), process each node that
366 // is in the element's attribute axis and in the node-set.
368 // The processing of an element node E MUST be modified slightly
369 // when an XPath node-set is given as input and the element's
370 // parent is omitted from the node-set.
371 private void WriteAttributesAxis (XmlNode node
)
373 // Console.WriteLine ("Debug: attributes");
375 ArrayList list
= new ArrayList ();
376 foreach (XmlNode attribute
in node
.Attributes
) {
377 if (!IsNamespaceNode (attribute
) && IsNodeVisible (attribute
))
378 list
.Add (attribute
);
381 // Add attributes from "xml" namespace for "inclusive" c14n only:
383 // The method for processing the attribute axis of an element E
384 // in the node-set is enhanced. All element nodes along E's
385 // ancestor axis are examined for nearest occurrences of
386 // attributes in the xml namespace, such as xml:lang and
387 // xml:space (whether or not they are in the node-set).
388 // From this list of attributes, remove any that are in E's
389 // attribute axis (whether or not they are in the node-set).
390 // Then, lexicographically merge this attribute list with the
391 // nodes of E's attribute axis that are in the node-set. The
392 // result of visiting the attribute axis is computed by
393 // processing the attribute nodes in this merged attribute list.
394 if (!exclusive
&& node
.ParentNode
!= null && node
.ParentNode
.ParentNode
!= null && !IsNodeVisible (node
.ParentNode
.ParentNode
)) {
395 // if we have whole document then the node.ParentNode.ParentNode
397 for (XmlNode cur
= node
.ParentNode
; cur
!= null; cur
= cur
.ParentNode
) {
398 if (cur
.Attributes
== null)
400 foreach (XmlNode attribute
in cur
.Attributes
) {
401 // we are looking for "xml:*" attributes
402 if (attribute
.Prefix
!= "xml")
405 // exclude ones that are in the node's attributes axis
406 if (node
.Attributes
.GetNamedItem (attribute
.LocalName
, attribute
.NamespaceURI
) != null)
409 // finally check that we don't have the same attribute in our list
411 foreach (object obj
in list
) {
412 XmlNode n
= (obj
as XmlNode
);
413 if (n
.Prefix
== "xml" && n
.LocalName
== attribute
.LocalName
) {
422 // now we can add this attribute to our list
423 list
.Add (attribute
);
428 // sort namespaces and write results
429 list
.Sort (new XmlDsigC14NTransformAttributesComparer ());
430 foreach (object obj
in list
) {
431 XmlNode attribute
= (obj
as XmlNode
);
432 if (attribute
!= null) {
434 res
.Append (attribute
.Name
);
436 res
.Append (NormalizeString (attribute
.Value
, XmlNodeType
.Attribute
));
443 // the string value, except all ampersands are replaced
444 // by &, all open angle brackets (<) are replaced by <, all closing
445 // angle brackets (>) are replaced by >, and all #xD characters are
446 // replaced by 
.
447 private void WriteTextNode (XmlNode node
, bool visible
)
449 // Console.WriteLine ("Debug: text node");
451 res
.Append (NormalizeString (node
.Value
, node
.NodeType
));
452 // res.Append (NormalizeString (node.Value, XmlNodeType.Text));
456 // Nothing if generating canonical XML without comments. For
457 // canonical XML with comments, generate the opening comment
458 // symbol (<!--), the string value of the node, and the
459 // closing comment symbol (-->). Also, a trailing #xA is rendered
460 // after the closing comment symbol for comment children of the
461 // root node with a lesser document order than the document
462 // element, and a leading #xA is rendered before the opening
463 // comment symbol of comment children of the root node with a
464 // greater document order than the document element. (Comment
465 // children of the root node represent comments outside of the
466 // top-level document element and outside of the document type
468 private void WriteCommentNode (XmlNode node
, bool visible
)
470 // Console.WriteLine ("Debug: comment node");
471 if (visible
&& comments
) {
472 if (state
== XmlCanonicalizerState
.AfterDocElement
)
473 res
.Append ("\x0A<!--");
477 res
.Append (NormalizeString (node
.Value
, XmlNodeType
.Comment
));
479 if (state
== XmlCanonicalizerState
.BeforeDocElement
)
480 res
.Append ("-->\x0A");
486 // Processing Instruction (PI) Nodes-
487 // The opening PI symbol (<?), the PI target name of the node,
488 // a leading space and the string value if it is not empty, and
489 // the closing PI symbol (?>). If the string value is empty,
490 // then the leading space is not added. Also, a trailing #xA is
491 // rendered after the closing PI symbol for PI children of the
492 // root node with a lesser document order than the document
493 // element, and a leading #xA is rendered before the opening PI
494 // symbol of PI children of the root node with a greater document
495 // order than the document element.
496 private void WriteProcessingInstructionNode (XmlNode node
, bool visible
)
498 // Console.WriteLine ("Debug: PI node");
501 if (state
== XmlCanonicalizerState
.AfterDocElement
)
502 res
.Append ("\x0A<?");
506 res
.Append (node
.Name
);
507 if (node
.Value
.Length
> 0) {
509 res
.Append (NormalizeString (node
.Value
, XmlNodeType
.ProcessingInstruction
));
512 if (state
== XmlCanonicalizerState
.BeforeDocElement
)
513 res
.Append ("?>\x0A");
519 // determines whether the node is in the node-set or not.
520 private bool IsNodeVisible (XmlNode node
)
522 // if node list is empty then we process whole document
526 // walk thru the list
527 foreach (XmlNode xn
in xnl
) {
528 if (node
.Equals (xn
))
535 // This method assumes that the namespace node is *not*
537 private bool IsVisiblyUtilized (XmlElement owner
, XmlAttribute ns
)
542 string prefix
= ns
.LocalName
== "xmlns" ? String
.Empty
: ns
.LocalName
;
543 if (owner
.Prefix
== prefix
&& owner
.NamespaceURI
== ns
.Value
)
545 if (!owner
.HasAttributes
)
547 foreach (XmlAttribute a
in owner
.Attributes
) {
548 if (a
.Prefix
== String
.Empty
)
550 if (a
.Prefix
!= prefix
|| a
.NamespaceURI
!= ns
.Value
)
552 if (IsNodeVisible (a
))
558 private bool IsNamespaceRendered (string prefix
, string uri
)
560 // if the default namespace xmlns="" is not re-defined yet
561 // then we do not want to print it out
562 bool IsEmptyNs
= prefix
== string.Empty
&& uri
== string.Empty
;
563 int start
= (IsEmptyNs
) ? 0 : prevVisibleNamespacesStart
;
564 for (int i
= visibleNamespaces
.Count
- 1; i
>= start
; i
--) {
565 XmlNode node
= (visibleNamespaces
[i
] as XmlNode
);
567 // get namespace prefix
568 string p
= string.Empty
;
569 if (node
.Prefix
== "xmlns")
572 return node
.Value
== uri
;
579 private bool IsNamespaceNode (XmlNode node
)
581 if (node
== null || node
.NodeType
!= XmlNodeType
.Attribute
)
583 return node
.NamespaceURI
== "http://www.w3.org/2000/xmlns/";
586 private bool IsTextNode (XmlNodeType type
)
589 case XmlNodeType
.Text
:
590 case XmlNodeType
.CDATA
:
591 case XmlNodeType
.SignificantWhitespace
:
592 case XmlNodeType
.Whitespace
:
598 private string NormalizeString (string input
, XmlNodeType type
)
600 StringBuilder sb
= new StringBuilder ();
601 for (int i
= 0; i
< input
.Length
; i
++) {
603 if (ch
== '<' && (type
== XmlNodeType
.Attribute
|| IsTextNode (type
)))
605 else if (ch
== '>' && IsTextNode (type
))
607 else if (ch
== '&' && (type
== XmlNodeType
.Attribute
|| IsTextNode (type
)))
609 else if (ch
== '\"' && type
== XmlNodeType
.Attribute
)
610 sb
.Append (""");
611 else if (ch
== '\x09' && type
== XmlNodeType
.Attribute
)
613 else if (ch
== '\x0A' && type
== XmlNodeType
.Attribute
)
615 else if (ch
== '\x0D')
621 return sb
.ToString ();
625 internal class XmlDsigC14NTransformAttributesComparer
: IComparer
627 public int Compare (object x
, object y
)
629 XmlNode n1
= (x
as XmlNode
);
630 XmlNode n2
= (y
as XmlNode
);
639 else if (n1
.Prefix
== n2
.Prefix
)
640 return string.Compare (n1
.LocalName
, n2
.LocalName
);
642 // Attributes in the default namespace are first
643 // because the default namespace is not applied to
644 // unqualified attributes
645 if (n1
.Prefix
== string.Empty
)
647 else if (n2
.Prefix
== string.Empty
)
650 int ret
= string.Compare (n1
.NamespaceURI
, n2
.NamespaceURI
);
652 ret
= string.Compare (n1
.LocalName
, n2
.LocalName
);
657 internal class XmlDsigC14NTransformNamespacesComparer
: IComparer
659 public int Compare (object x
, object y
)
661 XmlNode n1
= (x
as XmlNode
);
662 XmlNode n2
= (y
as XmlNode
);
671 else if (n1
.Prefix
== string.Empty
)
673 else if (n2
.Prefix
== string.Empty
)
676 return string.Compare (n1
.LocalName
, n2
.LocalName
);