2005-01-20 Sebastien Pouliot <sebastien@ximian.com>
[mono.git] / mcs / tools / security / certmgr.cs
1 //
2 // CertMgr.cs: Certificate Manager clone tool (CLI version)
3 //
4 // Author:
5 //      Sebastien Pouliot  <sebastien@ximian.com>
6 //
7 // Copyright (C) 2004-2005 Novell, Inc (http://www.novell.com)
8 //
9
10 using System;
11 using System.Collections;
12 using System.IO;
13 using System.Net;
14 using System.Net.Sockets;
15 using System.Reflection;
16 using System.Security.Cryptography;
17 using SSCX = System.Security.Cryptography.X509Certificates;
18 using System.Text;
19
20 using Mono.Security.Authenticode;
21 using Mono.Security.Cryptography;
22 using Mono.Security.X509;
23 using Mono.Security.Protocol.Tls;
24
25 [assembly: AssemblyTitle ("Mono Certificate Manager")]
26 [assembly: AssemblyDescription ("Manage X.509 certificates and CRL from stores.")]
27
28 namespace Mono.Tools {
29
30         class CertificateManager {
31
32                 static private void Header () 
33                 {
34                         Console.WriteLine (new AssemblyInfo ().ToString ());
35                 }
36
37                 static private void Help () 
38                 {
39                         Console.WriteLine ("Usage: certmgr [action] [object type] [options] store [filename]");
40                         Console.WriteLine ("   or: certmgr -list [object type] [options] store");
41                         Console.WriteLine ("   or: certmgr -del [object type] [options] store certhash");
42                         Console.WriteLine ("   or: certmgr -ssl [options] url");
43                         Console.WriteLine ();
44                         Console.WriteLine ("actions");
45                         Console.WriteLine ("\t-add\tAdd a certificate, CRL or CTL to specified store");
46                         Console.WriteLine ("\t-del\tRemove a certificate, CRL or CTL to specified store");
47                         Console.WriteLine ("\t-put\tCopy a certificate, CRL or CTL from a store to a file");
48                         Console.WriteLine ("\t-list\tList certificates, CRL ot CTL in the specified store.");
49                         Console.WriteLine ("\t-ssl\tDownload and add certificates from an SSL session");
50                         Console.WriteLine ("object types");
51                         Console.WriteLine ("\t-c\tadd/del/put certificates");
52                         Console.WriteLine ("\t-crl\tadd/del/put certificate revocation lists");
53                         Console.WriteLine ("\t-ctl\tadd/del/put certificate trust lists [unsupported]");
54                         Console.WriteLine ("other options");
55                         Console.WriteLine ("\t-m\tuse the machine certificate store (default to user)");
56                         Console.WriteLine ("\t-v\tverbose mode (display status for every steps)");
57                         Console.WriteLine ("\t-?\th[elp]\tDisplay this help message");
58                         Console.WriteLine ();
59                 }
60
61                 static string GetCommand (string arg) 
62                 {
63                         if ((arg == null) || (arg.Length < 1))
64                                 return null;
65
66                         switch (arg [0]) {
67                                 case '/':
68                                         return arg.Substring (1).ToUpper ();
69                                 case '-':
70                                         if (arg.Length < 2)
71                                                 return null;
72                                         int n = ((arg [1] == '-') ? 2 : 1);
73                                         return arg.Substring (n).ToUpper ();
74                                 default:
75                                         return arg;
76                         }
77                 }
78
79                 enum Action {
80                         None,
81                         Add,
82                         Delete,
83                         Put,
84                         List,
85                         Ssl
86                 }
87
88                 static Action GetAction (string arg) 
89                 {
90                         Action action = Action.None;
91                         switch (GetCommand (arg)) {
92                                 case "ADD":
93                                         action = Action.Add;
94                                         break;
95                                 case "DEL":
96                                 case "DELETE":
97                                         action = Action.Delete;
98                                         break;
99                                 case "PUT":
100                                         action = Action.Put;
101                                         break;
102                                 case "LST":
103                                 case "LIST":
104                                         action = Action.List;
105                                         break;
106                                 case "SSL":
107                                 case "TLS":
108                                         action = Action.Ssl;
109                                         break;
110                         }
111                         return action;
112                 }
113
114                 enum ObjectType {
115                         None,
116                         Certificate,
117                         CRL,
118                         CTL
119                 }
120
121                 static ObjectType GetObjectType (string arg) 
122                 {
123                         ObjectType type = ObjectType.None;
124                         switch (GetCommand (arg)) {
125                                 case "C":
126                                 case "CERT":
127                                 case "CERTIFICATE":
128                                         type = ObjectType.Certificate;
129                                         break;
130                                 case "CRL":
131                                         type = ObjectType.CRL;
132                                         break;
133                                 case "CTL":
134                                         type = ObjectType.CTL;
135                                         break;
136                         }
137                         return type;
138                 }
139
140                 static X509Store GetStoreFromName (string storeName, bool machine) 
141                 {
142                         X509Stores stores = ((machine) ? X509StoreManager.LocalMachine : X509StoreManager.CurrentUser);
143                         X509Store store = null;
144                         switch (storeName) {
145                                 case X509Stores.Names.Personal:
146                                         return stores.Personal;
147                                 case X509Stores.Names.OtherPeople:
148                                         return stores.OtherPeople;
149                                 case X509Stores.Names.IntermediateCA:
150                                         return stores.IntermediateCA;
151                                 case X509Stores.Names.TrustedRoot:
152                                         return stores.TrustedRoot;
153                                 case X509Stores.Names.Untrusted:
154                                         return stores.Untrusted;
155                         }
156                         return store;
157                 }
158
159                 static byte[] PEM (string type, byte[] data) 
160                 {
161                         string pem = Encoding.ASCII.GetString (data);
162                         string header = String.Format ("-----BEGIN {0}-----", type);
163                         string footer = String.Format ("-----END {0}-----", type);
164                         int start = pem.IndexOf (header) + header.Length;
165                         int end = pem.IndexOf (footer, start);
166                         string base64 = pem.Substring (start, (end - start));
167                         return Convert.FromBase64String (base64);
168                 }
169
170                 static X509CertificateCollection LoadCertificates (string filename) 
171                 {
172                         X509Certificate x509 = null;
173                         X509CertificateCollection coll = new X509CertificateCollection ();
174                         switch (Path.GetExtension (filename).ToUpper ()) {
175                                 case ".P7B":
176                                 case ".SPC":
177                                         SoftwarePublisherCertificate spc = SoftwarePublisherCertificate.CreateFromFile (filename);
178                                         coll.AddRange (spc.Certificates);
179                                         spc = null;
180                                         break;
181                                 case ".CER":
182                                 case ".CRT":
183                                         using (FileStream fs = File.OpenRead (filename)) {
184                                                 byte[] data = new byte [fs.Length];
185                                                 fs.Read (data, 0, data.Length);
186                                                 if (data [0] != 0x30) {
187                                                         // maybe it's ASCII PEM base64 encoded ?
188                                                         data = PEM ("CERTIFICATE", data);
189                                                 }
190                                                 if (data != null)
191                                                         x509 = new X509Certificate (data);
192                                         }
193                                         if (x509 != null)
194                                                 coll.Add (x509);
195                                         break;
196                                 case ".P12":
197                                 case ".PFX":
198                                         // TODO - support PKCS12 with passwords
199                                         PKCS12 p12 = PKCS12.LoadFromFile (filename);
200                                         coll.AddRange (p12.Certificates);
201                                         p12 = null;
202                                         break;
203                                 default:
204                                         Console.WriteLine ("Unknown file extension: {0}", 
205                                                 Path.GetExtension (filename));
206                                         break;
207                         }
208                         return coll;
209                 }
210
211                 static ArrayList LoadCRLs (string filename) 
212                 {
213                         X509Crl crl = null;
214                         ArrayList list = new ArrayList ();
215                         switch (Path.GetExtension (filename).ToUpper ()) {
216                                 case ".P7B":
217                                 case ".SPC":
218                                         SoftwarePublisherCertificate spc = SoftwarePublisherCertificate.CreateFromFile (filename);
219                                         list.AddRange (spc.Crls);
220                                         spc = null;
221                                         break;
222                                 case ".CRL":
223                                         using (FileStream fs = File.OpenRead (filename)) {
224                                                 byte[] data = new byte [fs.Length];
225                                                 fs.Read (data, 0, data.Length);
226                                                 crl = new X509Crl (data);
227                                         }
228                                         list.Add (crl);
229                                         break;
230                                 default:
231                                         Console.WriteLine ("Unknown file extension: {0}", 
232                                                 Path.GetExtension (filename));
233                                         break;
234                         }
235                         return list;
236                 }
237
238                 static void Add (ObjectType type, X509Store store, string file, bool verbose) 
239                 {
240                         switch (type) {
241                                 case ObjectType.Certificate:
242                                         X509CertificateCollection coll = LoadCertificates (file);
243                                         foreach (X509Certificate x509 in coll) {
244                                                 store.Import (x509);
245                                         }
246                                         Console.WriteLine ("{0} certificate(s) added to store {1}.", 
247                                                 coll.Count, store.Name);
248                                         break;
249                                 case ObjectType.CRL:
250                                         // TODO ArrayList list = LoadCRLs (file);
251                                         throw new NotImplementedException ("Adding CRL not yet supported");
252                                 default:
253                                         throw new NotSupportedException (type.ToString ());
254                         }
255                 }
256
257                 static void Delete (ObjectType type, X509Store store, string hash, bool verbose) 
258                 {
259                         switch (type) {
260                                 case ObjectType.Certificate:
261                                         foreach (X509Certificate x509 in store.Certificates) {
262                                                 if (hash == CryptoConvert.ToHex (x509.Hash)) {
263                                                         store.Remove (x509);
264                                                         Console.WriteLine ("Certificate removed from store.");
265                                                         return;
266                                                 }
267                                         }
268                                         break;
269                                 case ObjectType.CRL:
270                                         throw new NotImplementedException ("Delete not yet supported");
271                                 default:
272                                         throw new NotSupportedException (type.ToString ());
273                         }
274                 }
275
276                 static void Put (ObjectType type, X509Store store, string file, bool verbose) 
277                 {
278                         throw new NotImplementedException ("Put not yet supported");
279 /*                      switch (type) {
280                                 case ObjectType.Certificate:
281                                         break;
282                                 case ObjectType.CRL:
283                                         // TODO
284                                         break;
285                                 default:
286                                         throw new NotSupportedException (type.ToString ());
287                         }*/
288                 }
289
290                 static void DisplayCertificate (X509Certificate x509, bool verbose)
291                 {
292                         Console.WriteLine ("{0}X.509 v{1} Certificate", (x509.IsSelfSigned ? "Self-signed " : String.Empty), x509.Version);
293                         Console.WriteLine ("  Serial Number: {0}", CryptoConvert.ToHex (x509.SerialNumber));
294                         Console.WriteLine ("  Issuer Name:   {0}", x509.IssuerName);
295                         Console.WriteLine ("  Subject Name:  {0}", x509.SubjectName);
296                         Console.WriteLine ("  Valid From:    {0}", x509.ValidFrom);
297                         Console.WriteLine ("  Valid Until:   {0}", x509.ValidUntil);
298                         Console.WriteLine ("  Unique Hash:   {0}", CryptoConvert.ToHex (x509.Hash));
299                         if (verbose) {
300                                 Console.WriteLine ("  Key Algorithm:        {0}", x509.KeyAlgorithm);
301                                 Console.WriteLine ("  Algorithm Parameters: {0}", (x509.KeyAlgorithmParameters == null) ? "None" :
302                                         CryptoConvert.ToHex (x509.KeyAlgorithmParameters));
303                                 Console.WriteLine ("  Public Key:           {0}", CryptoConvert.ToHex (x509.PublicKey));
304                                 Console.WriteLine ("  Signature Algorithm:  {0}", x509.SignatureAlgorithm);
305                                 Console.WriteLine ("  Algorithm Parameters: {0}", (x509.SignatureAlgorithmParameters == null) ? "None" :
306                                         CryptoConvert.ToHex (x509.SignatureAlgorithmParameters));
307                                 Console.WriteLine ("  Signature:            {0}", CryptoConvert.ToHex (x509.Signature));
308                         }
309                         Console.WriteLine ();
310                 }
311
312                 static void List (ObjectType type, X509Store store, string file, bool verbose) 
313                 {
314                         switch (type) {
315                                 case ObjectType.Certificate:
316                                         foreach (X509Certificate x509 in store.Certificates) {
317                                                 DisplayCertificate (x509, verbose);
318                                         }
319                                         break;
320                                 case ObjectType.CRL:
321                                         // TODO
322                                         break;
323                                 default:
324                                         throw new NotSupportedException (type.ToString ());
325                         }
326                 }
327
328                 static X509CertificateCollection GetCertificatesFromSslSession (string url) 
329                 {
330                         Uri uri = new Uri (url);
331                         IPHostEntry host = Dns.Resolve (uri.Host);
332                         IPAddress ip = host.AddressList [0];
333                         Socket socket = new Socket (ip.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
334                         socket.Connect (new IPEndPoint (ip, uri.Port));
335                         NetworkStream ns = new NetworkStream (socket, false);
336                         SslClientStream ssl = new SslClientStream (ns, uri.Host, false, Mono.Security.Protocol.Tls.SecurityProtocolType.Default, null);
337                         ssl.ServerCertValidationDelegate += new CertificateValidationCallback (CertificateValidation);
338
339                         try {
340                                 // we don't really want to write to the server (as we don't know
341                                 // the protocol it using) but we must send something to be sure the
342                                 // SSL handshake is done (so we receive the X.509 certificates).
343                                 StreamWriter sw = new StreamWriter (ssl);
344                                 sw.WriteLine (Environment.NewLine);
345                                 sw.Flush ();
346                                 socket.Poll (30000, SelectMode.SelectRead);
347                         }
348                         finally {
349                                 socket.Close ();
350                         }
351
352                         // we need a little reflection magic to get this information
353                         PropertyInfo pi = typeof (SslClientStream).GetProperty ("ServerCertificates", BindingFlags.Instance | BindingFlags.NonPublic);
354                         if (pi == null) {
355                                 Console.WriteLine ("Sorry but you need a newer version of Mono.Security.dll to use this feature.");
356                                 return null;
357                         }
358                         return (X509CertificateCollection) pi.GetValue (ssl, null);
359                 }
360
361                 static bool CertificateValidation (SSCX.X509Certificate certificate, int[] certificateErrors)
362                 {
363                         // the main reason to download it is that it's not trusted
364                         return true;
365                         // OTOH we ask user confirmation before adding certificates into the stores
366                 }
367
368                 static void Ssl (string host, bool machine, bool verbose) 
369                 {
370                         if (verbose) {
371                                 Console.WriteLine ("Importing certificates from '{0}' into the {1} stores.",
372                                         host, machine ? "machine" : "user");
373                         }
374                         int n=0;
375
376                         X509CertificateCollection coll = GetCertificatesFromSslSession (host);
377                         if (coll != null) {
378                                 X509Store store = null;
379                                 // start by the end (root) so we can stop adding them anytime afterward
380                                 for (int i = coll.Count - 1; i >= 0; i--) {
381                                         X509Certificate x509 = coll [i];
382                                         bool selfsign = false;
383                                         bool failed = false;
384                                         try {
385                                                 selfsign = x509.IsSelfSigned;
386                                         }
387                                         catch {
388                                                 // sadly it's hard to interpret old certificates with MD2
389                                                 // without manually changing the machine.config file
390                                                 failed = true;
391                                         }
392
393                                         if (selfsign) {
394                                                 // this is a root
395                                                 store = GetStoreFromName (X509Stores.Names.TrustedRoot, machine);
396                                         } else if (i == 0) {
397                                                 // server certificate isn't (generally) an intermediate CA
398                                                 store = GetStoreFromName (X509Stores.Names.OtherPeople, machine);
399                                         } else {
400                                                 // all other certificates should be intermediate CA
401                                                 store = GetStoreFromName (X509Stores.Names.IntermediateCA, machine);
402                                         }
403
404                                         Console.WriteLine ("{0}{1} X.509 Certificate v{2}",     
405                                                 Environment.NewLine,
406                                                 selfsign ? "Self-signed " : String.Empty,
407                                                 x509.Version);
408                                         Console.WriteLine ("   Issued from: {0}", x509.IssuerName);
409                                         Console.WriteLine ("   Issued to:   {0}", x509.SubjectName);
410                                         Console.WriteLine ("   Valid from:  {0}", x509.ValidFrom);
411                                         Console.WriteLine ("   Valid until: {0}", x509.ValidUntil);
412
413                                         if (!x509.IsCurrent)
414                                                 Console.WriteLine ("   *** WARNING: Certificate isn't current ***");
415                                         if ((i > 0) && !selfsign) {
416                                                 X509Certificate signer = coll [i-1];
417                                                 bool signed = false;
418                                                 try {
419                                                         if (signer.RSA != null) {
420                                                                 signed = x509.VerifySignature (signer.RSA);
421                                                         } else if (signer.DSA != null) {
422                                                                 signed = x509.VerifySignature (signer.RSA);
423                                                         } else {
424                                                                 Console.WriteLine ("   *** WARNING: Couldn't not find who signed this certificate ***");
425                                                                 signed = true; // skip next warning
426                                                         }
427
428                                                         if (!signed)
429                                                                 Console.WriteLine ("   *** WARNING: Certificate signature is INVALID ***");
430                                                 }
431                                                 catch {
432                                                         failed = true;
433                                                 }
434                                         }
435                                         if (failed) {
436                                                 Console.WriteLine ("   *** ERROR: Couldn't decode certificate properly ***");
437                                                 Console.WriteLine ("   *** try 'man certmgr' for additional help or report to bugzilla.ximian.com ***");
438                                                 break;
439                                         }
440
441                                         if (store.Certificates.Contains (x509)) {
442                                                 Console.WriteLine ("This certificate is already in the {0} store.", store.Name);
443                                         } else {
444                                                 Console.Write ("Import this certificate into the {0} store ?", store.Name);
445                                                 string answer = Console.ReadLine ().ToUpper ();
446                                                 if ((answer == "YES") || (answer == "Y")) {
447                                                         store.Import (x509);
448                                                         n++;
449                                                 } else {
450                                                         if (verbose) {
451                                                                 Console.WriteLine ("Certificate not imported into store {0}.", 
452                                                                         store.Name);
453                                                         }
454                                                         break;
455                                                 }
456                                         }
457                                 }
458                         }
459
460                         Console.WriteLine ();
461                         if (n == 0) {
462                                 Console.WriteLine ("No certificate were added to the stores.");
463                         } else {
464                                 Console.WriteLine ("{0} certificate{1} added to the stores.", 
465                                         n, (n == 1) ? String.Empty : "s");
466                         }
467                 }
468
469                 [STAThread]
470                 static void Main (string[] args)
471                 {
472                         Header ();
473                         if (args.Length < 2) {
474                                 Help ();
475                                 return;
476                         }
477
478                         Action action = GetAction (args [0]);
479                         ObjectType type = ObjectType.None;
480
481                         int n = 1;
482                         if (action != Action.Ssl)
483                                 type = GetObjectType (args [n++]);
484                         
485                         bool verbose = (GetCommand (args [n]) == "V");
486                         if (verbose)
487                                 n++;
488                         bool machine = (GetCommand (args [n]) == "M");
489                         if (machine)
490                                 n++;
491
492                         X509Store store = null;
493                         string storeName = null;
494                         if (action != Action.Ssl) {
495                                 if ((action == Action.None) || (type == ObjectType.None)) {
496                                         Help ();
497                                         return;
498                                 }
499                                 if (type == ObjectType.CTL) {
500                                         Console.WriteLine ("CTL are not supported");
501                                         return;
502                                 }
503
504                                 storeName = args [n++];
505                                 store = GetStoreFromName (storeName, machine);
506                                 if (store == null) {
507                                         Console.WriteLine ("Invalid Store: {0}", storeName);
508                                         Console.WriteLine ("Valid stores are: {0}, {1}, {2}, {3} and {4}",
509                                                 X509Stores.Names.Personal,
510                                                 X509Stores.Names.OtherPeople, 
511                                                 X509Stores.Names.IntermediateCA, 
512                                                 X509Stores.Names.TrustedRoot, 
513                                                 X509Stores.Names.Untrusted);
514                                         return;
515                                 }
516                         }
517
518                         string file = (n < args.Length) ? args [n] : null;
519
520                         // now action!
521                         try {
522                                 switch (action) {
523                                 case Action.Add:
524                                         Add (type, store, file, verbose);
525                                         break;
526                                 case Action.Delete:
527                                         Delete (type, store, file, verbose);
528                                         break;
529                                 case Action.Put:
530                                         Put (type, store, file, verbose);
531                                         break;
532                                 case Action.List:
533                                         List (type, store, file, verbose);
534                                         break;
535                                 case Action.Ssl:
536                                         Ssl (file, machine, verbose);
537                                         break;
538                                 default:
539                                         throw new NotSupportedException (action.ToString ());
540                                 }
541                         }
542                         catch (UnauthorizedAccessException uae) {
543                                 Console.WriteLine ("Access to the {0} '{1}' certificate store has been denied.", 
544                                         (machine ? "machine" : "user"), storeName);
545                                 if (verbose) {
546                                         Console.WriteLine (uae);
547                                 }
548                         }
549                 }
550         }
551 }