2004-05-13 Sebastien Pouliot <sebastien@ximian.com>
[mono.git] / mcs / class / System.Security / System.Security.Cryptography.Xml / SignedXml.cs
1 //
2 // SignedXml.cs - SignedXml implementation for XML Signature
3 //
4 // Author:
5 //      Sebastien Pouliot  <sebastien@ximian.com>
6 //      Atsushi Enomoto <atsushi@ximian.com>
7 //
8 // (C) 2002, 2003 Motus Technologies Inc. (http://www.motus.com)
9 // (C) 2004 Novell (http://www.novell.com)
10 //
11
12 using System.Collections;
13 using System.IO;
14 using System.Runtime.InteropServices;
15 using System.Security.Cryptography;
16 using System.Security.Policy;
17 using System.Net;
18 using System.Text;
19 using System.Xml;
20
21 namespace System.Security.Cryptography.Xml {
22
23         public class SignedXml {
24
25                 private Signature signature;
26                 private AsymmetricAlgorithm key;
27                 private string keyName;
28                 private XmlDocument envdoc;
29                 private IEnumerator pkEnumerator;
30                 private XmlElement signatureElement;
31                 private Hashtable hashes;
32                 // FIXME: enable it after CAS implementation
33 #if false //NET_1_1
34                 private XmlResolver xmlResolver = new XmlSecureResolver (new XmlUrlResolver (), new Evidence ());
35 #else
36                 private XmlResolver xmlResolver = new XmlUrlResolver ();
37 #endif
38                 private ArrayList manifests;
39
40                 public SignedXml () 
41                 {
42                         signature = new Signature ();
43                         signature.SignedInfo = new SignedInfo ();
44                         hashes = new Hashtable (2); // 98% SHA1 for now
45                 }
46
47                 public SignedXml (XmlDocument document) : this ()
48                 {
49                         if (document == null)
50                                 throw new ArgumentNullException ("document");
51                         envdoc = document;
52                 }
53
54                 public SignedXml (XmlElement elem) : this ()
55                 {
56                         if (elem == null)
57                                 throw new ArgumentNullException ("elem");
58                         envdoc = new XmlDocument ();
59                         envdoc.LoadXml (elem.OuterXml);
60                 }
61
62                 public const string XmlDsigCanonicalizationUrl = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315";
63                 public const string XmlDsigCanonicalizationWithCommentsUrl = XmlDsigCanonicalizationUrl + "#WithComments";
64                 public const string XmlDsigNamespaceUrl = "http://www.w3.org/2000/09/xmldsig#";
65                 public const string XmlDsigDSAUrl = XmlDsigNamespaceUrl + "dsa-sha1";
66                 public const string XmlDsigHMACSHA1Url = XmlDsigNamespaceUrl + "hmac-sha1";
67                 public const string XmlDsigMinimalCanonicalizationUrl = XmlDsigNamespaceUrl + "minimal";
68                 public const string XmlDsigRSASHA1Url = XmlDsigNamespaceUrl + "rsa-sha1";
69                 public const string XmlDsigSHA1Url = XmlDsigNamespaceUrl + "sha1";
70
71                 public KeyInfo KeyInfo {
72                         get { return signature.KeyInfo; }
73                         set { signature.KeyInfo = value; }
74                 }
75
76                 public Signature Signature {
77                         get { return signature; }
78                 }
79
80                 public string SignatureLength {
81                         get { return signature.SignedInfo.SignatureLength; }
82                 }
83
84                 public string SignatureMethod {
85                         get { return signature.SignedInfo.SignatureMethod; }
86                 }
87
88                 public byte[] SignatureValue {
89                         get { return signature.SignatureValue; }
90                 }
91
92                 public SignedInfo SignedInfo {
93                         get { return signature.SignedInfo; }
94                 }
95
96                 public AsymmetricAlgorithm SigningKey {
97                         get { return key; }
98                         set { key = value; }
99                 }
100
101                 // NOTE: CryptoAPI related ? documented as fx internal
102                 public string SigningKeyName {
103                         get { return keyName; }
104                         set { keyName = value; }
105                 }
106
107                 public void AddObject (DataObject dataObject) 
108                 {
109                         signature.AddObject (dataObject);
110                 }
111
112                 public void AddReference (Reference reference) 
113                 {
114                         signature.SignedInfo.AddReference (reference);
115                 }
116
117                 private Stream ApplyTransform (Transform t, XmlDocument input) 
118                 {
119                         // These transformer modify input document, which should
120                         // not affect to the input itself.
121                         if (t is XmlDsigXPathTransform ||
122                                 t is XmlDsigEnvelopedSignatureTransform)
123                                 input = (XmlDocument) input.Clone ();
124
125                         t.LoadInput (input);
126
127                         if (t is XmlDsigEnvelopedSignatureTransform)
128                                 // It returns XmlDocument for XmlDocument input.
129                                 return CanonicalizeOutput (t.GetOutput ());
130
131                         object obj = t.GetOutput ();
132                         if (obj is Stream)
133                                 return (Stream) obj;
134                         else if (obj is XmlDocument) {
135                                 MemoryStream ms = new MemoryStream ();
136                                 XmlTextWriter xtw = new XmlTextWriter (ms, Encoding.UTF8);
137                                 ((XmlDocument) obj).WriteTo (xtw);
138                                 return ms;
139                         }
140                         else if (obj == null) {
141                                 throw new NotImplementedException ("This should not occur. Transform is " + t + ".");
142                         }
143                         else {
144                                 // e.g. XmlDsigXPathTransform returns XmlNodeList
145                                 return CanonicalizeOutput (obj);
146                         }
147                 }
148
149                 private Stream CanonicalizeOutput (object obj)
150                 {
151                         Transform c14n = GetC14NMethod ();
152                         c14n.LoadInput (obj);
153                         return (Stream) c14n.GetOutput ();
154                 }
155
156                 private XmlDocument GetManifest (Reference r) 
157                 {
158                         XmlDocument doc = new XmlDocument ();
159                         doc.PreserveWhitespace = true;
160
161                         if (r.Uri [0] == '#') {
162                                 // local manifest
163                                 if (signatureElement != null) {
164                                         XmlElement xel = GetIdElement (signatureElement.OwnerDocument, r.Uri.Substring (1));
165                                         if (xel == null)
166                                                 throw new CryptographicException ("Manifest targeted by Reference was not found: " + r.Uri.Substring (1));
167                                         doc.LoadXml (xel.OuterXml);
168                                         FixupNamespaceNodes (xel, doc.DocumentElement);
169                                 }
170                         }
171                         else if (xmlResolver != null) {
172                                 // TODO: need testing
173                                 Stream s = (Stream) xmlResolver.GetEntity (new Uri (r.Uri), null, typeof (Stream));
174                                 doc.Load (s);
175                         }
176
177                         if (doc.FirstChild != null) {
178                                 // keep a copy of the manifests to check their references later
179                                 if (manifests == null)
180                                         manifests = new ArrayList ();
181                                 manifests.Add (doc);
182
183                                 return doc;
184                         }
185                         return null;
186                 }
187
188                 private void FixupNamespaceNodes (XmlElement src, XmlElement dst)
189                 {
190                         // add namespace nodes
191                         foreach (XmlAttribute attr in src.SelectNodes ("namespace::*")) {
192                                 if (attr.LocalName == "xml")
193                                         continue;
194                                 if (attr.OwnerElement == src)
195                                         continue;
196                                 dst.SetAttributeNode (dst.OwnerDocument.ImportNode (attr, true) as XmlAttribute);
197                         }
198                 }
199
200                 [MonoTODO ("Need testing")]
201                 private byte[] GetReferenceHash (Reference r) 
202                 {
203                         Stream s = null;
204                         XmlDocument doc = null;
205                         if (r.Uri == String.Empty) {
206                                 doc = envdoc;
207                         }
208                         else if (r.Type == XmlSignature.Uri.Manifest) {
209                                 doc = GetManifest (r);
210                         }
211                         else {
212                                 doc = new XmlDocument ();
213                                 doc.PreserveWhitespace = true;
214
215                                 if (r.Uri [0] == '#') {
216                                         foreach (DataObject obj in signature.ObjectList) {
217                                                 if ("#" + obj.Id == r.Uri) {
218                                                         XmlElement xel = obj.GetXml ();
219                                                         doc.LoadXml (xel.OuterXml);
220                                                         FixupNamespaceNodes (xel, doc.DocumentElement);
221                                                         break;
222                                                 }
223                                         }
224                                 }
225                                 else if (xmlResolver != null) {
226                                         // TODO: test but doc says that Resolver = null -> no access
227                                         try {
228                                                 // no way to know if valid without throwing an exception
229                                                 Uri uri = new Uri (r.Uri);
230                                                 s = (Stream) xmlResolver.GetEntity (new Uri (r.Uri), null, typeof (Stream));
231                                         }
232                                         catch {
233                                                 // may still be a local file (and maybe not xml)
234                                                 s = File.OpenRead (r.Uri);
235                                         }
236                                 }
237                         }
238
239                         if (r.TransformChain.Count > 0) {               
240                                 foreach (Transform t in r.TransformChain) {
241                                         if (s == null) {
242                                                 s = ApplyTransform (t, doc);
243                                         }
244                                         else {
245                                                 t.LoadInput (s);
246                                                 object o = t.GetOutput ();
247                                                 if (o is Stream)
248                                                         s = (Stream) o;
249                                                 else
250                                                         s = CanonicalizeOutput (o);
251                                         }
252                                 }
253                         }
254                         else if (s == null) {
255                                 // we must not C14N references from outside the document
256                                 // e.g. non-xml documents
257                                 if (r.Uri [0] != '#') {
258                                         s = new MemoryStream ();
259                                         doc.Save (s);
260                                 }
261                                 else {
262                                         // apply default C14N transformation
263                                         s = ApplyTransform (new XmlDsigC14NTransform (), doc);
264                                 }
265                         }
266
267                         HashAlgorithm digest = GetHash (r.DigestMethod);
268                         return digest.ComputeHash (s);
269                 }
270
271                 private void DigestReferences () 
272                 {
273                         // we must tell each reference which hash algorithm to use 
274                         // before asking for the SignedInfo XML !
275                         foreach (Reference r in signature.SignedInfo.References) {
276                                 // assume SHA-1 if nothing is specified
277                                 if (r.DigestMethod == null)
278                                         r.DigestMethod = XmlDsigSHA1Url;
279                                 r.DigestValue = GetReferenceHash (r);
280                         }
281                 }
282
283                 private Transform GetC14NMethod ()
284                 {
285                         Transform t = (Transform) CryptoConfig.CreateFromName (signature.SignedInfo.CanonicalizationMethod);
286                         if (t == null)
287                                 throw new CryptographicException ("Unknown Canonicalization Method {0}", signature.SignedInfo.CanonicalizationMethod);
288                         return t;
289                 }
290
291                 private Stream SignedInfoTransformed () 
292                 {
293                         Transform t = GetC14NMethod ();
294
295                         if (signatureElement == null) {
296                                 // when creating signatures
297                                 XmlDocument doc = new XmlDocument ();
298                                 doc.PreserveWhitespace = true;
299                                 doc.LoadXml (signature.SignedInfo.GetXml ().OuterXml);
300
301                                 t.LoadInput (doc);
302                         }
303                         else {
304                                 // when verifying signatures
305                                 // TODO - check signature.SignedInfo.Id
306                                 XmlElement el = signatureElement.GetElementsByTagName (XmlSignature.ElementNames.SignedInfo, XmlSignature.NamespaceURI) [0] as XmlElement;
307                                 StringWriter sw = new StringWriter ();
308                                 XmlTextWriter xtw = new XmlTextWriter (sw);
309                                 xtw.WriteStartElement (el.Prefix, el.LocalName, el.NamespaceURI);
310
311                                 // context namespace nodes (except for "xmlns:xml")
312                                 XmlNodeList nl = el.SelectNodes ("namespace::*");
313                                 foreach (XmlAttribute attr in nl) {
314                                         if (attr.ParentNode == el)
315                                                 continue;
316                                         if (attr.LocalName == "xml")
317                                                 continue;
318                                         attr.WriteTo (xtw);
319                                 }
320
321                                 foreach (XmlNode attr in el.Attributes)
322                                         attr.WriteTo (xtw);
323                                 foreach (XmlNode n in el.ChildNodes)
324                                         n.WriteTo (xtw);
325
326                                 xtw.WriteEndElement ();
327
328                                 byte [] si = Encoding.UTF8.GetBytes (sw.ToString ());
329
330                                 MemoryStream ms = new MemoryStream ();
331                                 ms.Write (si, 0, si.Length);
332                                 ms.Position = 0;
333
334                                 t.LoadInput (ms);
335                         }
336                         // C14N and C14NWithComments always return a Stream in GetOutput
337                         return (Stream) t.GetOutput ();
338                 }
339
340                 // reuse hash - most document will always use the same hash
341                 private HashAlgorithm GetHash (string algorithm) 
342                 {
343                         HashAlgorithm hash = (HashAlgorithm) hashes [algorithm];
344                         if (hash == null) {
345                                 hash = HashAlgorithm.Create (algorithm);
346                                 if (hash == null)
347                                         throw new CryptographicException ("Unknown hash algorithm: {0}", algorithm);
348                                 hashes.Add (algorithm, hash);
349                                 // now ready to be used
350                         }
351                         else {
352                                 // important before reusing an hash object
353                                 hash.Initialize ();
354                         }
355                         return hash;
356                 }
357
358                 public bool CheckSignature () 
359                 {
360                         return (CheckSignatureInternal (null) != null);
361                 }
362
363                 private bool CheckReferenceIntegrity (ArrayList referenceList) 
364                 {
365                         if (referenceList == null)
366                                 return false;
367
368                         // check digest (hash) for every reference
369                         foreach (Reference r in referenceList) {
370                                 // stop at first broken reference
371                                 byte[] hash = GetReferenceHash (r);
372                                 if (! Compare (r.DigestValue, hash))
373                                         return false;
374                         }
375                         return true;
376                 }
377
378                 public bool CheckSignature (AsymmetricAlgorithm key) 
379                 {
380                         if (key == null)
381                                 throw new ArgumentNullException ("key");
382                         return (CheckSignatureInternal (key) != null);
383                 }
384
385                 private AsymmetricAlgorithm CheckSignatureInternal (AsymmetricAlgorithm key)
386                 {
387                         pkEnumerator = null;
388
389                         if (key != null) {
390                                 // check with supplied key
391                                 if (!CheckSignatureWithKey (key))
392                                         return null;
393                         }
394                         else {
395                                 if (Signature.KeyInfo == null)
396                                         throw new CryptographicException ("At least one KeyInfo is required.");
397                                 // no supplied key, iterates all KeyInfo
398                                 while ((key = GetPublicKey ()) != null) {
399                                         if (CheckSignatureWithKey (key)) {
400                                                 break;
401                                         }
402                                 }
403                                 pkEnumerator = null;
404                                 if (key == null)
405                                         return null;
406                         }
407
408                         // some parts may need to be downloaded
409                         // so where doing it last
410                         if (! CheckReferenceIntegrity (signature.SignedInfo.References))
411                                 return null;
412
413                         if (manifests != null) {
414                                 // do not use foreach as a manifest could contain manifests...
415                                 for (int i=0; i < manifests.Count; i++) {
416                                         Manifest manifest = new Manifest ((manifests [i] as XmlDocument).DocumentElement);
417                                         if (! CheckReferenceIntegrity (manifest.References))
418                                                 return null;
419                                 }
420                         }
421                         return key;
422                 }
423
424                 // Is the signature (over SignedInfo) valid ?
425                 private bool CheckSignatureWithKey (AsymmetricAlgorithm key) 
426                 {
427                         if (key == null)
428                                 return false;
429
430                         SignatureDescription sd = (SignatureDescription) CryptoConfig.CreateFromName (signature.SignedInfo.SignatureMethod);
431                         if (sd == null)
432                                 return false;
433
434                         AsymmetricSignatureDeformatter verifier = (AsymmetricSignatureDeformatter) CryptoConfig.CreateFromName (sd.DeformatterAlgorithm);
435                         if (verifier == null)
436                                 return false;
437
438                         try {
439                                 verifier.SetKey (key);
440                                 verifier.SetHashAlgorithm (sd.DigestAlgorithm);
441
442                                 HashAlgorithm hash = GetHash (sd.DigestAlgorithm);
443                                 // get the hash of the C14N SignedInfo element
444                                 MemoryStream ms = (MemoryStream) SignedInfoTransformed ();
445                                 byte[] debug = ms.ToArray ();
446                                 byte[] digest = hash.ComputeHash (ms);
447                                 return verifier.VerifySignature (digest, signature.SignatureValue);
448                         }
449                         catch {
450                                 // e.g. SignatureMethod != AsymmetricAlgorithm type
451                                 return false;
452                         } 
453                 }
454
455                 private bool Compare (byte[] expected, byte[] actual) 
456                 {
457                         bool result = ((expected != null) && (actual != null));
458                         if (result) {
459                                 int l = expected.Length;
460                                 result = (l == actual.Length);
461                                 if (result) {
462                                         for (int i=0; i < l; i++) {
463                                                 if (expected[i] != actual[i])
464                                                         return false;
465                                         }
466                                 }
467                         }
468                         return result;
469                 }
470
471                 public bool CheckSignature (KeyedHashAlgorithm macAlg) 
472                 {
473                         if (macAlg == null)
474                                 throw new ArgumentNullException ("macAlg");
475
476                         pkEnumerator = null;
477
478                         // Is the signature (over SignedInfo) valid ?
479                         Stream s = SignedInfoTransformed ();
480                         if (s == null)
481                                 return false;
482
483                         byte[] actual = macAlg.ComputeHash (s);
484                         // HMAC signature may be partial
485                         if (signature.SignedInfo.SignatureLength != null) {
486                                 int length = actual.Length;
487                                 try {
488                                         // SignatureLength is in bits
489                                         length = (Int32.Parse (signature.SignedInfo.SignatureLength) >> 3);
490                                 }
491                                 catch {
492                                 }
493
494                                 if (length != actual.Length) {
495                                         byte[] trunked = new byte [length];
496                                         Buffer.BlockCopy (actual, 0, trunked, 0, length);
497                                         actual = trunked;
498                                 }
499                         }
500
501                         if (Compare (signature.SignatureValue, actual)) {
502                                 // some parts may need to be downloaded
503                                 // so where doing it last
504                                 return CheckReferenceIntegrity (signature.SignedInfo.References);
505                         }
506                         return false;
507                 }
508
509                 public bool CheckSignatureReturningKey (out AsymmetricAlgorithm signingKey) 
510                 {
511                         signingKey = CheckSignatureInternal (null);
512                         return (signingKey != null);
513                 }
514
515                 public void ComputeSignature () 
516                 {
517                         if (key != null) {
518                                 // required before hashing
519                                 signature.SignedInfo.SignatureMethod = key.SignatureAlgorithm;
520                                 DigestReferences ();
521
522                                 AsymmetricSignatureFormatter signer = null;
523                                 // in need for a CryptoConfig factory
524                                 if (key is DSA)
525                                         signer = new DSASignatureFormatter (key);
526                                 else if (key is RSA) 
527                                         signer = new RSAPKCS1SignatureFormatter (key);
528
529                                 if (signer != null) {
530                                         SignatureDescription sd = (SignatureDescription) CryptoConfig.CreateFromName (signature.SignedInfo.SignatureMethod);
531
532                                         HashAlgorithm hash = GetHash (sd.DigestAlgorithm);
533                                         // get the hash of the C14N SignedInfo element
534                                         byte[] digest = hash.ComputeHash (SignedInfoTransformed ());
535
536                                         signer.SetHashAlgorithm ("SHA1");
537                                         signature.SignatureValue = signer.CreateSignature (digest);
538                                 }
539                         }
540                 }
541
542                 public void ComputeSignature (KeyedHashAlgorithm macAlg) 
543                 {
544                         if (macAlg == null)
545                                 throw new ArgumentNullException ("macAlg");
546
547                         if (macAlg is HMACSHA1) {
548                                 DigestReferences ();
549
550                                 signature.SignedInfo.SignatureMethod = XmlDsigHMACSHA1Url;
551                                 signature.SignatureValue = macAlg.ComputeHash (SignedInfoTransformed ());
552                         }
553                         else 
554                                 throw new CryptographicException ("unsupported algorithm");
555                 }
556
557                 public virtual XmlElement GetIdElement (XmlDocument document, string idValue) 
558                 {
559                         // this works only if there's a DTD or XSD available to define the ID
560                         XmlElement xel = document.GetElementById (idValue);
561                         if (xel == null) {
562                                 // search an "undefined" ID
563                                 xel = (XmlElement) document.SelectSingleNode ("//*[@Id='" + idValue + "']");
564                         }
565                         return xel;
566                 }
567
568                 // According to book ".NET Framework Security" this method
569                 // iterates all possible keys then return null
570                 protected virtual AsymmetricAlgorithm GetPublicKey () 
571                 {
572                         if (signature.KeyInfo == null)
573                                 return null;
574
575                         if (pkEnumerator == null) {
576                                 pkEnumerator = signature.KeyInfo.GetEnumerator ();
577                         }
578
579                         if (pkEnumerator.MoveNext ()) {
580                                 AsymmetricAlgorithm key = null;
581                                 KeyInfoClause kic = (KeyInfoClause) pkEnumerator.Current;
582
583                                 if (kic is DSAKeyValue)
584                                         key = DSA.Create ();
585                                 else if (kic is RSAKeyValue) 
586                                         key = RSA.Create ();
587
588                                 if (key != null) {
589                                         key.FromXmlString (kic.GetXml ().InnerXml);
590                                         return key;
591                                 }
592                         }
593                         return null;
594                 }
595
596                 public XmlElement GetXml () 
597                 {
598                         return signature.GetXml ();
599                 }
600
601                 public void LoadXml (XmlElement value) 
602                 {
603                         if (value == null)
604                                 throw new ArgumentNullException ("value");
605
606                         signatureElement = value;
607                         signature.LoadXml (value);
608                 }
609
610 #if NET_1_1
611                 [ComVisible (false)]
612                 public XmlResolver Resolver {
613                         set { xmlResolver = value; }
614                 }
615 #endif
616         }
617 }