2 // SignedXml.cs - SignedXml implementation for XML Signature
5 // Sebastien Pouliot <sebastien@ximian.com>
6 // Atsushi Enomoto <atsushi@ximian.com>
7 // Tim Coleman <tim@timcoleman.com>
9 // (C) 2002, 2003 Motus Technologies Inc. (http://www.motus.com)
10 // Copyright (C) Tim Coleman, 2004
11 // Copyright (C) 2004-2005 Novell, Inc (http://www.novell.com)
13 // Permission is hereby granted, free of charge, to any person obtaining
14 // a copy of this software and associated documentation files (the
15 // "Software"), to deal in the Software without restriction, including
16 // without limitation the rights to use, copy, modify, merge, publish,
17 // distribute, sublicense, and/or sell copies of the Software, and to
18 // permit persons to whom the Software is furnished to do so, subject to
19 // the following conditions:
21 // The above copyright notice and this permission notice shall be
22 // included in all copies or substantial portions of the Software.
24 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
25 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
26 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
27 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
28 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
29 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
30 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
33 using System.Collections;
35 using System.Runtime.InteropServices;
36 using System.Security.Cryptography;
37 using System.Security.Policy;
41 using System.Security.Cryptography.X509Certificates;
43 namespace System.Security.Cryptography.Xml {
45 public class SignedXml {
47 public const string XmlDsigCanonicalizationUrl = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315";
48 public const string XmlDsigCanonicalizationWithCommentsUrl = XmlDsigCanonicalizationUrl + "#WithComments";
49 public const string XmlDsigDSAUrl = XmlDsigNamespaceUrl + "dsa-sha1";
50 public const string XmlDsigHMACSHA1Url = XmlDsigNamespaceUrl + "hmac-sha1";
51 public const string XmlDsigMinimalCanonicalizationUrl = XmlDsigNamespaceUrl + "minimal";
52 public const string XmlDsigNamespaceUrl = "http://www.w3.org/2000/09/xmldsig#";
53 public const string XmlDsigRSASHA1Url = XmlDsigNamespaceUrl + "rsa-sha1";
54 public const string XmlDsigSHA1Url = XmlDsigNamespaceUrl + "sha1";
56 public const string XmlDecryptionTransformUrl = "http://www.w3.org/2002/07/decrypt#XML";
57 public const string XmlDsigBase64TransformUrl = XmlDsigNamespaceUrl + "base64";
58 public const string XmlDsigC14NTransformUrl = XmlDsigCanonicalizationUrl;
59 public const string XmlDsigC14NWithCommentsTransformUrl = XmlDsigCanonicalizationWithCommentsUrl;
60 public const string XmlDsigEnvelopedSignatureTransformUrl = XmlDsigNamespaceUrl + "enveloped-signature";
61 public const string XmlDsigExcC14NTransformUrl = "http://www.w3.org/2001/10/xml-exc-c14n#";
62 public const string XmlDsigExcC14NWithCommentsTransformUrl = XmlDsigExcC14NTransformUrl + "WithComments";
63 public const string XmlDsigXPathTransformUrl = "http://www.w3.org/TR/1999/REC-xpath-19991116";
64 public const string XmlDsigXsltTransformUrl = "http://www.w3.org/TR/1999/REC-xslt-19991116";
65 public const string XmlLicenseTransformUrl = "urn:mpeg:mpeg21:2003:01-REL-R-NS:licenseTransform";
67 private EncryptedXml encryptedXml;
69 protected Signature m_signature;
70 private AsymmetricAlgorithm key;
71 protected string m_strSigningKeyName;
72 private XmlDocument envdoc;
73 private IEnumerator pkEnumerator;
74 private XmlElement signatureElement;
75 private Hashtable hashes;
76 // FIXME: enable it after CAS implementation
77 private XmlResolver xmlResolver = new XmlUrlResolver ();
78 private ArrayList manifests;
79 private IEnumerator _x509Enumerator;
81 private static readonly char [] whitespaceChars = new char [] {' ', '\r', '\n', '\t'};
85 m_signature = new Signature ();
86 m_signature.SignedInfo = new SignedInfo ();
87 hashes = new Hashtable (2); // 98% SHA1 for now
90 public SignedXml (XmlDocument document) : this ()
93 throw new ArgumentNullException ("document");
97 public SignedXml (XmlElement elem) : this ()
100 throw new ArgumentNullException ("elem");
101 envdoc = new XmlDocument ();
102 envdoc.LoadXml (elem.OuterXml);
106 public EncryptedXml EncryptedXml {
107 get { return encryptedXml; }
108 set { encryptedXml = value; }
111 public KeyInfo KeyInfo {
113 if (m_signature.KeyInfo == null)
114 m_signature.KeyInfo = new KeyInfo ();
115 return m_signature.KeyInfo;
117 set { m_signature.KeyInfo = value; }
120 public Signature Signature {
121 get { return m_signature; }
124 public string SignatureLength {
125 get { return m_signature.SignedInfo.SignatureLength; }
128 public string SignatureMethod {
129 get { return m_signature.SignedInfo.SignatureMethod; }
132 public byte[] SignatureValue {
133 get { return m_signature.SignatureValue; }
136 public SignedInfo SignedInfo {
137 get { return m_signature.SignedInfo; }
140 public AsymmetricAlgorithm SigningKey {
145 // NOTE: CryptoAPI related ? documented as fx internal
146 public string SigningKeyName {
147 get { return m_strSigningKeyName; }
148 set { m_strSigningKeyName = value; }
151 public void AddObject (DataObject dataObject)
153 m_signature.AddObject (dataObject);
156 public void AddReference (Reference reference)
158 if (reference == null)
159 throw new ArgumentNullException ("reference");
160 m_signature.SignedInfo.AddReference (reference);
163 private Stream ApplyTransform (Transform t, XmlDocument input)
165 // These transformer modify input document, which should
166 // not affect to the input itself.
167 if (t is XmlDsigXPathTransform
168 || t is XmlDsigEnvelopedSignatureTransform
169 || t is XmlDecryptionTransform
171 input = (XmlDocument) input.Clone ();
175 if (t is XmlDsigEnvelopedSignatureTransform)
176 // It returns XmlDocument for XmlDocument input.
177 return CanonicalizeOutput (t.GetOutput ());
179 object obj = t.GetOutput ();
182 else if (obj is XmlDocument) {
183 MemoryStream ms = new MemoryStream ();
184 XmlTextWriter xtw = new XmlTextWriter (ms, Encoding.UTF8);
185 ((XmlDocument) obj).WriteTo (xtw);
189 // Rewind to the start of the stream
193 else if (obj == null) {
194 throw new NotImplementedException ("This should not occur. Transform is " + t + ".");
197 // e.g. XmlDsigXPathTransform returns XmlNodeList
198 return CanonicalizeOutput (obj);
202 private Stream CanonicalizeOutput (object obj)
204 Transform c14n = GetC14NMethod ();
205 c14n.LoadInput (obj);
206 return (Stream) c14n.GetOutput ();
209 private XmlDocument GetManifest (Reference r)
211 XmlDocument doc = new XmlDocument ();
212 doc.PreserveWhitespace = true;
214 if (r.Uri [0] == '#') {
216 if (signatureElement != null) {
217 XmlElement xel = GetIdElement (signatureElement.OwnerDocument, r.Uri.Substring (1));
219 throw new CryptographicException ("Manifest targeted by Reference was not found: " + r.Uri.Substring (1));
220 doc.AppendChild (doc.ImportNode (xel, true));
221 FixupNamespaceNodes (xel, doc.DocumentElement, false);
224 else if (xmlResolver != null) {
225 // TODO: need testing
226 Stream s = (Stream) xmlResolver.GetEntity (new Uri (r.Uri), null, typeof (Stream));
230 if (doc.FirstChild != null) {
231 // keep a copy of the manifests to check their references later
232 if (manifests == null)
233 manifests = new ArrayList ();
241 private void FixupNamespaceNodes (XmlElement src, XmlElement dst, bool ignoreDefault)
243 // add namespace nodes
244 foreach (XmlAttribute attr in src.SelectNodes ("namespace::*")) {
245 if (attr.LocalName == "xml")
247 if (ignoreDefault && attr.LocalName == "xmlns")
249 dst.SetAttributeNode (dst.OwnerDocument.ImportNode (attr, true) as XmlAttribute);
253 private byte[] GetReferenceHash (Reference r, bool check_hmac)
256 XmlDocument doc = null;
257 if (r.Uri == String.Empty) {
260 else if (r.Type == XmlSignature.Uri.Manifest) {
261 doc = GetManifest (r);
264 doc = new XmlDocument ();
265 doc.PreserveWhitespace = true;
266 string objectName = null;
268 if (r.Uri.StartsWith ("#xpointer")) {
269 string uri = string.Join ("", r.Uri.Substring (9).Split (whitespaceChars));
270 if (uri.Length < 2 || uri [0] != '(' || uri [uri.Length - 1] != ')')
271 // FIXME: how to handle invalid xpointer?
274 uri = uri.Substring (1, uri.Length - 2);
277 else if (uri.Length > 6 && uri.StartsWith ("id(") && uri [uri.Length - 1] == ')')
278 // id('foo'), id("foo")
279 objectName = uri.Substring (4, uri.Length - 6);
281 else if (r.Uri [0] == '#') {
282 objectName = r.Uri.Substring (1);
284 else if (xmlResolver != null) {
285 // TODO: test but doc says that Resolver = null -> no access
287 // no way to know if valid without throwing an exception
288 Uri uri = new Uri (r.Uri);
289 s = (Stream) xmlResolver.GetEntity (uri, null, typeof (Stream));
292 // may still be a local file (and maybe not xml)
293 s = File.OpenRead (r.Uri);
296 if (objectName != null) {
297 XmlElement found = null;
298 foreach (DataObject obj in m_signature.ObjectList) {
299 if (obj.Id == objectName) {
300 found = obj.GetXml ();
301 found.SetAttribute ("xmlns", SignedXml.XmlDsigNamespaceUrl);
302 doc.AppendChild (doc.ImportNode (found, true));
303 // FIXME: there should be theoretical justification of copying namespace declaration nodes this way.
304 foreach (XmlNode n in found.ChildNodes)
305 // Do not copy default namespace as it must be xmldsig namespace for "Object" element.
306 if (n.NodeType == XmlNodeType.Element)
307 FixupNamespaceNodes (n as XmlElement, doc.DocumentElement, true);
311 if (found == null && envdoc != null) {
312 found = GetIdElement (envdoc, objectName);
314 doc.AppendChild (doc.ImportNode (found, true));
315 FixupNamespaceNodes (found, doc.DocumentElement, false);
319 throw new CryptographicException (String.Format ("Malformed reference object: {0}", objectName));
323 if (r.TransformChain.Count > 0) {
324 foreach (Transform t in r.TransformChain) {
326 s = ApplyTransform (t, doc);
330 object o = t.GetOutput ();
334 s = CanonicalizeOutput (o);
338 else if (s == null) {
339 // we must not C14N references from outside the document
340 // e.g. non-xml documents
341 if (r.Uri [0] != '#') {
342 s = new MemoryStream ();
346 // apply default C14N transformation
347 s = ApplyTransform (new XmlDsigC14NTransform (), doc);
350 HashAlgorithm digest = GetHash (r.DigestMethod, check_hmac);
351 return (digest == null) ? null : digest.ComputeHash (s);
354 private void DigestReferences ()
356 // we must tell each reference which hash algorithm to use
357 // before asking for the SignedInfo XML !
358 foreach (Reference r in m_signature.SignedInfo.References) {
359 // assume SHA-1 if nothing is specified
360 if (r.DigestMethod == null)
361 r.DigestMethod = XmlDsigSHA1Url;
362 r.DigestValue = GetReferenceHash (r, false);
366 private Transform GetC14NMethod ()
368 Transform t = (Transform) CryptoConfig.CreateFromName (m_signature.SignedInfo.CanonicalizationMethod);
370 throw new CryptographicException ("Unknown Canonicalization Method {0}", m_signature.SignedInfo.CanonicalizationMethod);
374 private Stream SignedInfoTransformed ()
376 Transform t = GetC14NMethod ();
378 if (signatureElement == null) {
379 // when creating signatures
380 XmlDocument doc = new XmlDocument ();
381 doc.PreserveWhitespace = true;
382 doc.LoadXml (m_signature.SignedInfo.GetXml ().OuterXml);
384 foreach (XmlAttribute attr in envdoc.DocumentElement.SelectNodes ("namespace::*")) {
385 if (attr.LocalName == "xml")
387 if (attr.Prefix == doc.DocumentElement.Prefix)
389 doc.DocumentElement.SetAttributeNode (doc.ImportNode (attr, true) as XmlAttribute);
394 // when verifying signatures
395 // TODO - check m_signature.SignedInfo.Id
396 XmlElement el = signatureElement.GetElementsByTagName (XmlSignature.ElementNames.SignedInfo, XmlSignature.NamespaceURI) [0] as XmlElement;
397 StringWriter sw = new StringWriter ();
398 XmlTextWriter xtw = new XmlTextWriter (sw);
399 xtw.WriteStartElement (el.Prefix, el.LocalName, el.NamespaceURI);
401 // context namespace nodes (except for "xmlns:xml")
402 XmlNodeList nl = el.SelectNodes ("namespace::*");
403 foreach (XmlAttribute attr in nl) {
404 if (attr.ParentNode == el)
406 if (attr.LocalName == "xml")
408 if (attr.Prefix == el.Prefix)
412 foreach (XmlNode attr in el.Attributes)
414 foreach (XmlNode n in el.ChildNodes)
417 xtw.WriteEndElement ();
418 byte [] si = Encoding.UTF8.GetBytes (sw.ToString ());
420 MemoryStream ms = new MemoryStream ();
421 ms.Write (si, 0, si.Length);
426 // C14N and C14NWithComments always return a Stream in GetOutput
427 return (Stream) t.GetOutput ();
430 // reuse hash - most document will always use the same hash
431 private HashAlgorithm GetHash (string algorithm, bool check_hmac)
433 HashAlgorithm hash = (HashAlgorithm) hashes [algorithm];
435 hash = HashAlgorithm.Create (algorithm);
437 throw new CryptographicException ("Unknown hash algorithm: {0}", algorithm);
438 hashes.Add (algorithm, hash);
439 // now ready to be used
442 // important before reusing an hash object
445 // we can sign using any hash algorith, including HMAC, but we can only verify hash (MS compatibility)
446 if (check_hmac && (hash is KeyedHashAlgorithm))
451 public bool CheckSignature ()
453 return (CheckSignatureInternal (null) != null);
456 private bool CheckReferenceIntegrity (ArrayList referenceList)
458 if (referenceList == null)
461 // check digest (hash) for every reference
462 foreach (Reference r in referenceList) {
463 // stop at first broken reference
464 byte[] hash = GetReferenceHash (r, true);
465 if (! Compare (r.DigestValue, hash))
471 public bool CheckSignature (AsymmetricAlgorithm key)
474 throw new ArgumentNullException ("key");
475 return (CheckSignatureInternal (key) != null);
478 private AsymmetricAlgorithm CheckSignatureInternal (AsymmetricAlgorithm key)
483 // check with supplied key
484 if (!CheckSignatureWithKey (key))
487 if (Signature.KeyInfo == null)
489 // no supplied key, iterates all KeyInfo
490 while ((key = GetPublicKey ()) != null) {
491 if (CheckSignatureWithKey (key)) {
500 // some parts may need to be downloaded
501 // so where doing it last
502 if (!CheckReferenceIntegrity (m_signature.SignedInfo.References))
505 if (manifests != null) {
506 // do not use foreach as a manifest could contain manifests...
507 for (int i=0; i < manifests.Count; i++) {
508 Manifest manifest = new Manifest ((manifests [i] as XmlDocument).DocumentElement);
509 if (! CheckReferenceIntegrity (manifest.References))
516 // Is the signature (over SignedInfo) valid ?
517 private bool CheckSignatureWithKey (AsymmetricAlgorithm key)
522 SignatureDescription sd = (SignatureDescription) CryptoConfig.CreateFromName (m_signature.SignedInfo.SignatureMethod);
526 AsymmetricSignatureDeformatter verifier = (AsymmetricSignatureDeformatter) CryptoConfig.CreateFromName (sd.DeformatterAlgorithm);
527 if (verifier == null)
531 verifier.SetKey (key);
532 verifier.SetHashAlgorithm (sd.DigestAlgorithm);
534 HashAlgorithm hash = GetHash (sd.DigestAlgorithm, true);
535 // get the hash of the C14N SignedInfo element
536 MemoryStream ms = (MemoryStream) SignedInfoTransformed ();
538 byte[] digest = hash.ComputeHash (ms);
539 return verifier.VerifySignature (digest, m_signature.SignatureValue);
542 // e.g. SignatureMethod != AsymmetricAlgorithm type
547 private bool Compare (byte[] expected, byte[] actual)
549 bool result = ((expected != null) && (actual != null));
551 int l = expected.Length;
552 result = (l == actual.Length);
554 for (int i=0; i < l; i++) {
555 if (expected[i] != actual[i])
563 public bool CheckSignature (KeyedHashAlgorithm macAlg)
566 throw new ArgumentNullException ("macAlg");
570 // Is the signature (over SignedInfo) valid ?
571 Stream s = SignedInfoTransformed ();
575 byte[] actual = macAlg.ComputeHash (s);
576 // HMAC signature may be partial and specified by <HMACOutputLength>
577 if (m_signature.SignedInfo.SignatureLength != null) {
578 int length = Int32.Parse (m_signature.SignedInfo.SignatureLength);
579 // we only support signatures with a multiple of 8 bits
580 // and the value must match the signature length
581 if ((length & 7) != 0)
582 throw new CryptographicException ("Signature length must be a multiple of 8 bits.");
584 // SignatureLength is in bits (and we works on bytes, only in multiple of 8 bits)
585 // and both values must match for a signature to be valid
587 if (length != m_signature.SignatureValue.Length)
588 throw new CryptographicException ("Invalid signature length.");
590 // is the length "big" enough to make the signature meaningful ?
591 // we use a minimum of 80 bits (10 bytes) or half the HMAC normal output length
592 // e.g. HMACMD5 output 128 bits but our minimum is 80 bits (not 64 bits)
593 int minimum = Math.Max (10, actual.Length / 2);
594 if (length < minimum)
595 throw new CryptographicException ("HMAC signature is too small");
597 if (length < actual.Length) {
598 byte[] trunked = new byte [length];
599 Buffer.BlockCopy (actual, 0, trunked, 0, length);
604 if (Compare (m_signature.SignatureValue, actual)) {
605 // some parts may need to be downloaded
606 // so where doing it last
607 return CheckReferenceIntegrity (m_signature.SignedInfo.References);
614 public bool CheckSignature (X509Certificate2 certificate, bool verifySignatureOnly)
616 throw new NotImplementedException ();
619 public bool CheckSignatureReturningKey (out AsymmetricAlgorithm signingKey)
621 signingKey = CheckSignatureInternal (null);
622 return (signingKey != null);
625 public void ComputeSignature ()
628 if (m_signature.SignedInfo.SignatureMethod == null)
629 // required before hashing
630 m_signature.SignedInfo.SignatureMethod = key.SignatureAlgorithm;
631 else if (m_signature.SignedInfo.SignatureMethod != key.SignatureAlgorithm)
632 throw new CryptographicException ("Specified SignatureAlgorithm is not supported by the signing key.");
635 AsymmetricSignatureFormatter signer = null;
636 // in need for a CryptoConfig factory
638 signer = new DSASignatureFormatter (key);
640 signer = new RSAPKCS1SignatureFormatter (key);
642 if (signer != null) {
643 SignatureDescription sd = (SignatureDescription) CryptoConfig.CreateFromName (m_signature.SignedInfo.SignatureMethod);
645 HashAlgorithm hash = GetHash (sd.DigestAlgorithm, false);
646 // get the hash of the C14N SignedInfo element
647 byte[] digest = hash.ComputeHash (SignedInfoTransformed ());
649 signer.SetHashAlgorithm ("SHA1");
650 m_signature.SignatureValue = signer.CreateSignature (digest);
654 throw new CryptographicException ("signing key is not specified");
657 public void ComputeSignature (KeyedHashAlgorithm macAlg)
660 throw new ArgumentNullException ("macAlg");
662 string method = null;
664 if (macAlg is HMACSHA1) {
665 method = XmlDsigHMACSHA1Url;
666 } else if (macAlg is HMACSHA256) {
667 method = "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256";
668 } else if (macAlg is HMACSHA384) {
669 method = "http://www.w3.org/2001/04/xmldsig-more#hmac-sha384";
670 } else if (macAlg is HMACSHA512) {
671 method = "http://www.w3.org/2001/04/xmldsig-more#hmac-sha512";
672 } else if (macAlg is HMACRIPEMD160) {
673 method = "http://www.w3.org/2001/04/xmldsig-more#hmac-ripemd160";
677 throw new CryptographicException ("unsupported algorithm");
680 m_signature.SignedInfo.SignatureMethod = method;
681 m_signature.SignatureValue = macAlg.ComputeHash (SignedInfoTransformed ());
684 public virtual XmlElement GetIdElement (XmlDocument document, string idValue)
686 if ((document == null) || (idValue == null))
689 // this works only if there's a DTD or XSD available to define the ID
690 XmlElement xel = document.GetElementById (idValue);
692 // search an "undefined" ID
693 xel = (XmlElement) document.SelectSingleNode ("//*[@Id='" + idValue + "']");
698 // According to book ".NET Framework Security" this method
699 // iterates all possible keys then return null
700 protected virtual AsymmetricAlgorithm GetPublicKey ()
702 if (m_signature.KeyInfo == null)
705 if (pkEnumerator == null) {
706 pkEnumerator = m_signature.KeyInfo.GetEnumerator ();
710 if (_x509Enumerator != null) {
711 if (_x509Enumerator.MoveNext ()) {
712 X509Certificate cert = (X509Certificate) _x509Enumerator.Current;
713 return new X509Certificate2 (cert.GetRawCertData ()).PublicKey.Key;
715 _x509Enumerator = null;
719 while (pkEnumerator.MoveNext ()) {
720 AsymmetricAlgorithm key = null;
721 KeyInfoClause kic = (KeyInfoClause) pkEnumerator.Current;
723 if (kic is DSAKeyValue)
725 else if (kic is RSAKeyValue)
729 key.FromXmlString (kic.GetXml ().InnerXml);
734 if (kic is KeyInfoX509Data) {
735 _x509Enumerator = ((KeyInfoX509Data) kic).Certificates.GetEnumerator ();
736 if (_x509Enumerator.MoveNext ()) {
737 X509Certificate cert = (X509Certificate) _x509Enumerator.Current;
738 return new X509Certificate2 (cert.GetRawCertData ()).PublicKey.Key;
746 public XmlElement GetXml ()
748 return m_signature.GetXml (envdoc);
751 public void LoadXml (XmlElement value)
754 throw new ArgumentNullException ("value");
756 signatureElement = value;
757 m_signature.LoadXml (value);
758 // Need to give the EncryptedXml object to the
759 // XmlDecryptionTransform to give it a fighting
760 // chance at decrypting the document.
761 foreach (Reference r in m_signature.SignedInfo.References) {
762 foreach (Transform t in r.TransformChain) {
763 if (t is XmlDecryptionTransform)
764 ((XmlDecryptionTransform) t).EncryptedXml = EncryptedXml;
770 public XmlResolver Resolver {
771 set { xmlResolver = value; }