2 // CertMgr.cs: Certificate Manager clone tool (CLI version)
5 // Sebastien Pouliot <sebastien@ximian.com>
7 // Copyright (C) 2004-2005 Novell, Inc (http://www.novell.com)
11 using System.Collections;
12 using System.Globalization;
15 using System.Net.Sockets;
16 using System.Reflection;
17 using System.Security.Cryptography;
18 using SSCX = System.Security.Cryptography.X509Certificates;
21 using Mono.Security.Authenticode;
22 using Mono.Security.Cryptography;
23 using Mono.Security.X509;
24 using Mono.Security.Protocol.Tls;
26 [assembly: AssemblyTitle ("Mono Certificate Manager")]
27 [assembly: AssemblyDescription ("Manage X.509 certificates and CRL from stores.")]
29 namespace Mono.Tools {
31 class CertificateManager {
33 static private void Header ()
35 Console.WriteLine (new AssemblyInfo ().ToString ());
38 static private void Help ()
40 Console.WriteLine ("Usage: certmgr [action] [object-type] [options] store [filename]");
41 Console.WriteLine (" or: certmgr -list object-type [options] store");
42 Console.WriteLine (" or: certmgr -del object-type [options] store certhash");
43 Console.WriteLine (" or: certmgr -ssl [options] url");
44 Console.WriteLine (" or: certmgr -put object-type [options] store certfile");
45 Console.WriteLine (" or: certmgr -importKey [options] store pkcs12file");
47 Console.WriteLine ("actions");
48 Console.WriteLine ("\t-add\t\tAdd a certificate, CRL or CTL to specified store");
49 Console.WriteLine ("\t-del\t\tRemove a certificate, CRL or CTL to specified store");
50 Console.WriteLine ("\t-put\t\tCopy a certificate, CRL or CTL from a store to a file");
51 Console.WriteLine ("\t-list\t\tList certificates, CRL or CTL in the specified store.");
52 Console.WriteLine ("\t-ssl\t\tDownload and add certificates from an SSL session");
53 Console.WriteLine ("\t-importKey\tImport PKCS12 privateKey to keypair store.");
54 Console.WriteLine ("object types");
55 Console.WriteLine ("\t-c\t\tadd/del/put certificates");
56 Console.WriteLine ("\t-crl\t\tadd/del/put certificate revocation lists");
57 Console.WriteLine ("\t-ctl\t\tadd/del/put certificate trust lists [unsupported]");
58 Console.WriteLine ("other options");
59 Console.WriteLine ("\t-m\t\tuse the machine certificate store (default to user)");
60 Console.WriteLine ("\t-v\t\tverbose mode (display status for every steps)");
61 Console.WriteLine ("\t-p [password]\tPassword used to decrypt PKCS12");
62 Console.WriteLine ("\t-pem\t\tPut certificate in Base-64 encoded format (default DER encoded)");
63 Console.WriteLine ("\t-?\t\th[elp]\tDisplay this help message");
67 static string GetCommand (string arg)
69 if ((arg == null) || (arg.Length < 1))
74 return arg.Substring (1).ToUpper ();
78 int n = ((arg [1] == '-') ? 2 : 1);
79 return arg.Substring (n).ToUpper ();
95 static Action GetAction (string arg)
97 Action action = Action.None;
98 switch (GetCommand (arg)) {
104 action = Action.Delete;
111 action = Action.List;
118 action = Action.ImportKey;
131 static ObjectType GetObjectType (string arg)
133 ObjectType type = ObjectType.None;
134 switch (GetCommand (arg)) {
138 type = ObjectType.Certificate;
141 type = ObjectType.CRL;
144 type = ObjectType.CTL;
150 static X509Store GetStoreFromName (string storeName, bool machine)
152 X509Stores stores = ((machine) ? X509StoreManager.LocalMachine : X509StoreManager.CurrentUser);
153 X509Store store = null;
155 case X509Stores.Names.Personal:
156 return stores.Personal;
157 case X509Stores.Names.OtherPeople:
158 return stores.OtherPeople;
159 case X509Stores.Names.IntermediateCA:
160 return stores.IntermediateCA;
161 case "Root": // special case (same as trusted root)
162 case X509Stores.Names.TrustedRoot:
163 return stores.TrustedRoot;
164 case X509Stores.Names.Untrusted:
165 return stores.Untrusted;
170 static byte[] PEM (string type, byte[] data)
172 string pem = Encoding.ASCII.GetString (data);
173 string header = String.Format ("-----BEGIN {0}-----", type);
174 string footer = String.Format ("-----END {0}-----", type);
175 int start = pem.IndexOf (header) + header.Length;
176 int end = pem.IndexOf (footer, start);
177 string base64 = pem.Substring (start, (end - start));
178 return Convert.FromBase64String (base64);
181 static byte[] ToPEM (string type, byte[] data)
183 string header = String.Format ("-----BEGIN {0}-----", type);
184 string footer = String.Format ("-----END {0}-----", type);
186 string encodedString = Convert.ToBase64String (data);
188 StringBuilder sb = new StringBuilder ();
189 int remaining = encodedString.Length;
190 sb.AppendLine (header);
191 for (int i = 0; i <= encodedString.Length; i += 64) {
192 if (remaining >= 64) {
193 sb.AppendLine (encodedString.Substring (i, 64));
195 sb.AppendLine (encodedString.Substring (i, remaining));
199 sb.AppendLine (footer);
200 return Encoding.ASCII.GetBytes (sb.ToString ());
203 static X509CertificateCollection LoadCertificates (string filename, string password, bool verbose)
205 X509Certificate x509 = null;
206 X509CertificateCollection coll = new X509CertificateCollection ();
207 switch (Path.GetExtension (filename).ToUpper ()) {
210 SoftwarePublisherCertificate spc = SoftwarePublisherCertificate.CreateFromFile (filename);
211 coll.AddRange (spc.Certificates);
216 using (FileStream fs = File.OpenRead (filename)) {
217 byte[] data = new byte [fs.Length];
218 fs.Read (data, 0, data.Length);
219 if (data [0] != 0x30) {
220 // maybe it's ASCII PEM base64 encoded ?
221 data = PEM ("CERTIFICATE", data);
224 x509 = new X509Certificate (data);
231 PKCS12 p12 = password == null ? PKCS12.LoadFromFile (filename)
232 : PKCS12.LoadFromFile (filename, password);
233 X509CertificateCollection tmp = new X509CertificateCollection (p12.Certificates);
235 for (int i = 0; i != p12.Keys.Count; i++) {
236 X509Certificate cert = p12.Certificates[i];
237 RSACryptoServiceProvider pk = p12.Keys[i] as RSACryptoServiceProvider;
239 if (pk == null || pk.PublicOnly)
243 Console.WriteLine ("Found key for certificate: {0}", cert.SubjectName);
251 Console.WriteLine ("Unknown file extension: {0}",
252 Path.GetExtension (filename));
258 static ArrayList LoadCRLs (string filename)
261 ArrayList list = new ArrayList ();
262 switch (Path.GetExtension (filename).ToUpper ()) {
265 SoftwarePublisherCertificate spc = SoftwarePublisherCertificate.CreateFromFile (filename);
266 list.AddRange (spc.Crls);
270 using (FileStream fs = File.OpenRead (filename)) {
271 byte[] data = new byte [fs.Length];
272 fs.Read (data, 0, data.Length);
273 crl = new X509Crl (data);
278 Console.WriteLine ("Unknown file extension: {0}",
279 Path.GetExtension (filename));
285 static void Add (ObjectType type, X509Store store, string file, string password, bool verbose)
288 case ObjectType.Certificate:
289 X509CertificateCollection coll = LoadCertificates (file, password, verbose);
290 foreach (X509Certificate x509 in coll) {
293 Console.WriteLine ("{0} certificate(s) added to store {1}.",
294 coll.Count, store.Name);
297 ArrayList list = LoadCRLs (file);
298 foreach (X509Crl crl in list) {
301 Console.WriteLine ("{0} CRL(s) added to store {1}.",
302 list.Count, store.Name);
305 throw new NotSupportedException (type.ToString ());
309 static void Delete (ObjectType type, X509Store store, string hash, bool verbose)
312 case ObjectType.Certificate:
313 foreach (X509Certificate x509 in store.Certificates) {
314 if (hash == CryptoConvert.ToHex (x509.Hash)) {
316 Console.WriteLine ("Certificate removed from store.");
322 foreach (X509Crl crl in store.Crls) {
323 if (hash == CryptoConvert.ToHex (crl.Hash)) {
325 Console.WriteLine ("CRL removed from store.");
331 throw new NotSupportedException (type.ToString ());
335 static void Put (ObjectType type, X509Store store, string file, bool machine, bool pem, bool verbose)
337 if (String.IsNullOrEmpty (file)) {
338 Console.Error.WriteLine("error: no filename provided to put the certificate.");
344 case ObjectType.Certificate:
345 for(int i = 0; i < store.Certificates.Count; i++) {
346 Console.WriteLine ("==============Certificate # {0} ==========", i + 1);
347 DisplayCertificate (store.Certificates[i], machine, verbose);
350 Console.Write("Enter cert # from the above list to put-->");
351 if (!int.TryParse(Console.ReadLine(), out selection) || selection > store.Certificates.Count) {
352 Console.Error.WriteLine ("error: invalid selection.");
356 SSCX.X509Certificate2 cert = new SSCX.X509Certificate2 (store.Certificates[selection-1].RawData);
359 data = ToPEM ("CERTIFICATE", cert.Export (SSCX.X509ContentType.Cert));
361 data = cert.Export (SSCX.X509ContentType.Cert);
364 using (FileStream fs = File.Create (file)) {
365 fs.Write(data, 0, data.Length);
368 Console.WriteLine ("Certificate put to {0}.", file);
371 throw new NotSupportedException ("Put " + type + " not supported yet");
375 static void DisplayCertificate (X509Certificate x509, bool machine, bool verbose)
377 Console.WriteLine ("{0}X.509 v{1} Certificate", (x509.IsSelfSigned ? "Self-signed " : String.Empty), x509.Version);
378 Console.WriteLine (" Serial Number: {0}", CryptoConvert.ToHex (x509.SerialNumber));
379 Console.WriteLine (" Issuer Name: {0}", x509.IssuerName);
380 Console.WriteLine (" Subject Name: {0}", x509.SubjectName);
381 Console.WriteLine (" Valid From: {0}", x509.ValidFrom);
382 Console.WriteLine (" Valid Until: {0}", x509.ValidUntil);
383 Console.WriteLine (" Unique Hash: {0}", CryptoConvert.ToHex (x509.Hash));
385 Console.WriteLine (" Key Algorithm: {0}", x509.KeyAlgorithm);
386 Console.WriteLine (" Algorithm Parameters: {0}", (x509.KeyAlgorithmParameters == null) ? "None" :
387 CryptoConvert.ToHex (x509.KeyAlgorithmParameters));
388 Console.WriteLine (" Public Key: {0}", CryptoConvert.ToHex (x509.PublicKey));
389 Console.WriteLine (" Signature Algorithm: {0}", x509.SignatureAlgorithm);
390 Console.WriteLine (" Algorithm Parameters: {0}", (x509.SignatureAlgorithmParameters == null) ? "None" :
391 CryptoConvert.ToHex (x509.SignatureAlgorithmParameters));
392 Console.WriteLine (" Signature: {0}", CryptoConvert.ToHex (x509.Signature));
393 RSACryptoServiceProvider rsaCsp = x509.RSA as RSACryptoServiceProvider;
394 RSAManaged rsaManaged = x509.RSA as RSAManaged;
395 Console.WriteLine (" Private Key: {0}", ((rsaCsp != null && !rsaCsp.PublicOnly)
396 || (rsaManaged != null && !rsaManaged.PublicOnly)));
397 CspParameters cspParams = new CspParameters ();
398 cspParams.KeyContainerName = CryptoConvert.ToHex (x509.Hash);
399 cspParams.Flags = machine ? CspProviderFlags.UseMachineKeyStore : 0;
400 KeyPairPersistence kpp = new KeyPairPersistence (cspParams);
401 Console.WriteLine (" KeyPair Key: {0}", kpp.Load ());
403 Console.WriteLine ();
406 static void DisplayCrl (X509Crl crl, bool machine, bool verbose)
408 Console.WriteLine ("X.509 v{0} CRL", crl.Version);
409 Console.WriteLine (" Issuer Name: {0}", crl.IssuerName);
410 Console.WriteLine (" This Update: {0}", crl.ThisUpdate);
411 Console.WriteLine (" Next Update: {0} {1}", crl.NextUpdate, crl.IsCurrent ? String.Empty : "update overdue!");
412 Console.WriteLine (" Unique Hash: {0}", CryptoConvert.ToHex (crl.Hash));
414 Console.WriteLine (" Signature Algorithm: {0}", crl.SignatureAlgorithm);
415 Console.WriteLine (" Signature: {0}", CryptoConvert.ToHex (crl.Signature));
417 foreach (X509Crl.X509CrlEntry entry in crl.Entries) {
418 Console.WriteLine (" #{0}: Serial: {1} revoked on {2}",
419 ++n, CryptoConvert.ToHex (entry.SerialNumber), entry.RevocationDate);
424 static void List (ObjectType type, X509Store store, bool machine, string file, bool verbose)
427 case ObjectType.Certificate:
428 foreach (X509Certificate x509 in store.Certificates) {
429 DisplayCertificate (x509, machine, verbose);
433 foreach (X509Crl crl in store.Crls) {
434 DisplayCrl (crl, machine, verbose);
438 throw new NotSupportedException (type.ToString ());
442 static X509CertificateCollection GetCertificatesFromSslSession (string url)
444 Uri uri = new Uri (url);
445 IPHostEntry host = Dns.Resolve (uri.Host);
446 IPAddress ip = host.AddressList [0];
447 Socket socket = new Socket (ip.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
448 socket.Connect (new IPEndPoint (ip, uri.Port));
449 NetworkStream ns = new NetworkStream (socket, false);
450 SslClientStream ssl = new SslClientStream (ns, uri.Host, false, Mono.Security.Protocol.Tls.SecurityProtocolType.Default, null);
451 ssl.ServerCertValidationDelegate += new CertificateValidationCallback (CertificateValidation);
454 // we don't really want to write to the server (as we don't know
455 // the protocol it using) but we must send something to be sure the
456 // SSL handshake is done (so we receive the X.509 certificates).
457 StreamWriter sw = new StreamWriter (ssl);
458 sw.WriteLine (Environment.NewLine);
460 socket.Poll (30000, SelectMode.SelectRead);
466 // we need a little reflection magic to get this information
467 PropertyInfo pi = typeof (SslStreamBase).GetProperty ("ServerCertificates", BindingFlags.Instance | BindingFlags.NonPublic);
469 Console.WriteLine ("Sorry but you need a newer version of Mono.Security.dll to use this feature.");
472 return (X509CertificateCollection) pi.GetValue (ssl, null);
475 static bool CertificateValidation (SSCX.X509Certificate certificate, int[] certificateErrors)
477 // the main reason to download it is that it's not trusted
479 // OTOH we ask user confirmation before adding certificates into the stores
482 static void Ssl (string host, bool machine, bool verbose)
485 Console.WriteLine ("Importing certificates from '{0}' into the {1} stores.",
486 host, machine ? "machine" : "user");
490 X509CertificateCollection coll = GetCertificatesFromSslSession (host);
492 X509Store store = null;
493 // start by the end (root) so we can stop adding them anytime afterward
494 for (int i = coll.Count - 1; i >= 0; i--) {
495 X509Certificate x509 = coll [i];
496 bool selfsign = false;
499 selfsign = x509.IsSelfSigned;
502 // sadly it's hard to interpret old certificates with MD2
503 // without manually changing the machine.config file
509 store = GetStoreFromName (X509Stores.Names.TrustedRoot, machine);
511 // server certificate isn't (generally) an intermediate CA
512 store = GetStoreFromName (X509Stores.Names.OtherPeople, machine);
514 // all other certificates should be intermediate CA
515 store = GetStoreFromName (X509Stores.Names.IntermediateCA, machine);
518 Console.WriteLine ("{0}{1}X.509 Certificate v{2}",
520 selfsign ? "Self-signed " : String.Empty,
522 Console.WriteLine (" Issued from: {0}", x509.IssuerName);
523 Console.WriteLine (" Issued to: {0}", x509.SubjectName);
524 Console.WriteLine (" Valid from: {0}", x509.ValidFrom);
525 Console.WriteLine (" Valid until: {0}", x509.ValidUntil);
528 Console.WriteLine (" *** WARNING: Certificate isn't current ***");
529 if ((i > 0) && !selfsign) {
530 X509Certificate signer = coll [i-1];
533 if (signer.RSA != null) {
534 signed = x509.VerifySignature (signer.RSA);
535 } else if (signer.DSA != null) {
536 signed = x509.VerifySignature (signer.DSA);
538 Console.WriteLine (" *** WARNING: Couldn't not find who signed this certificate ***");
539 signed = true; // skip next warning
543 Console.WriteLine (" *** WARNING: Certificate signature is INVALID ***");
550 Console.WriteLine (" *** ERROR: Couldn't decode certificate properly ***");
551 Console.WriteLine (" *** try 'man certmgr' for additional help or report to bugzilla.novell.com ***");
555 if (store.Certificates.Contains (x509)) {
556 Console.WriteLine ("This certificate is already in the {0} store.", store.Name);
558 Console.Write ("Import this certificate into the {0} store ?", store.Name);
559 string answer = Console.ReadLine ().ToUpper ();
560 if ((answer == "YES") || (answer == "Y")) {
565 Console.WriteLine ("Certificate not imported into store {0}.",
574 Console.WriteLine ();
576 Console.WriteLine ("No certificate were added to the stores.");
578 Console.WriteLine ("{0} certificate{1} added to the stores.",
579 n, (n == 1) ? String.Empty : "s");
583 static void ImportKey (ObjectType type, bool machine, string file, string password, bool verbose)
586 case ObjectType.Certificate:
587 X509CertificateCollection coll = LoadCertificates (file, password, verbose);
590 foreach (X509Certificate x509 in coll) {
591 RSACryptoServiceProvider pk = x509.RSA as RSACryptoServiceProvider;
593 if (pk == null || pk.PublicOnly)
596 CspParameters csp = new CspParameters ();
597 csp.KeyContainerName = CryptoConvert.ToHex (x509.Hash);
598 csp.Flags = machine ? CspProviderFlags.UseMachineKeyStore : 0;
599 RSACryptoServiceProvider rsa = new RSACryptoServiceProvider (csp);
600 rsa.ImportParameters (pk.ExportParameters (true));
601 rsa.PersistKeyInCsp = true;
604 Console.WriteLine ("{0} keys(s) imported to KeyPair {1} persister.",
605 count, machine ? "LocalMachine" : "CurrentUser");
608 throw new NotSupportedException (type.ToString ());
613 static void Main (string[] args)
615 string password = null;
616 bool verbose = false;
618 bool machine = false;
621 if (args.Length < 2) {
626 Action action = GetAction (args [0]);
627 ObjectType type = ObjectType.None;
630 if (action != Action.Ssl) {
631 type = GetObjectType (args [n]);
632 if (type != ObjectType.None)
636 for (int i = n; i < args.Length; i++) {
637 switch (GetCommand (args[i])) {
647 password = args[++n];
657 X509Store store = null;
658 string storeName = null;
659 if (action != Action.Ssl) {
660 if ((action == Action.None) || (type == ObjectType.None)) {
664 if (type == ObjectType.CTL) {
665 Console.WriteLine ("CTL are not supported");
669 storeName = args [n++];
670 store = GetStoreFromName (storeName, machine);
672 Console.WriteLine ("Invalid Store: {0}", storeName);
673 Console.WriteLine ("Valid stores are: {0}, {1}, {2}, {3} and {4}",
674 X509Stores.Names.Personal,
675 X509Stores.Names.OtherPeople,
676 X509Stores.Names.IntermediateCA,
677 X509Stores.Names.TrustedRoot,
678 X509Stores.Names.Untrusted);
683 string file = (n < args.Length) ? args [n] : null;
689 Add (type, store, file, password, verbose);
692 Delete (type, store, file, verbose);
695 Put (type, store, file, machine, pem, verbose);
698 List (type, store, machine, file, verbose);
701 Ssl (file, machine, verbose);
703 case Action.ImportKey:
704 ImportKey (type, machine, file, password, verbose);
707 throw new NotSupportedException (action.ToString ());
710 catch (UnauthorizedAccessException uae) {
711 Console.WriteLine ("Access to the {0} '{1}' certificate store has been denied.",
712 (machine ? "machine" : "user"), storeName);
714 Console.WriteLine (uae);