Last active
January 23, 2019 20:59
-
-
Save haf/5629584 to your computer and use it in GitHub Desktop.
Finding the private key of a Windows certificate from PowerShell/C#.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace PKI | |
{ | |
class Results : IEquatable<Results> | |
{ | |
internal static readonly Results NotFound = new Results | |
{ | |
Directory = "", | |
KeyName = "" | |
}; | |
public Results() | |
{ | |
Directory = ""; | |
KeyName = ""; | |
} | |
public string Directory { get; set; } | |
public string KeyName { get; set; } | |
public override string ToString() | |
{ | |
return string.Format(@"Directory: {0} | |
KeyName: {1}", Directory, KeyName); | |
} | |
public bool Equals(Results other) | |
{ | |
if (ReferenceEquals(null, other)) return false; | |
if (ReferenceEquals(this, other)) return true; | |
return string.Equals(Directory, other.Directory) && string.Equals(KeyName, other.KeyName); | |
} | |
public override bool Equals(object obj) | |
{ | |
if (ReferenceEquals(null, obj)) return false; | |
if (ReferenceEquals(this, obj)) return true; | |
if (obj.GetType() != GetType()) return false; | |
return Equals((Results) obj); | |
} | |
public override int GetHashCode() | |
{ | |
unchecked | |
{ | |
return (Directory.GetHashCode()*397) ^ KeyName.GetHashCode(); | |
} | |
} | |
public static bool operator ==(Results left, Results right) | |
{ | |
return Equals(left, right); | |
} | |
public static bool operator !=(Results left, Results right) | |
{ | |
return !Equals(left, right); | |
} | |
} | |
public static class FindPrivateKey | |
{ | |
static void PrintHelp() | |
{ | |
Console.WriteLine("FindPrivateKey helps user to find the location of the Private Key file of a X.509 Certificate."); | |
Console.WriteLine( | |
"Usage: FindPrivateKey <storeName> <storeLocation> [{ {-n <subjectName>} | {-t <thumbprint>} } [-f | -d | -a]]"); | |
Console.WriteLine(" <subjectName> subject name of the certificate"); | |
Console.WriteLine(" <thumbprint> thumbprint of the certificate (use certmgr.exe to get it)"); | |
Console.WriteLine(" -f output file name only"); | |
Console.WriteLine(" -d output directory only"); | |
Console.WriteLine(" -a output absolute file name"); | |
Console.WriteLine("e.g. FindPrivateKey My CurrentUser -n \"CN=John Doe\""); | |
Console.WriteLine( | |
"e.g. FindPrivateKey My LocalMachine -t \"03 33 98 63 d0 47 e7 48 71 33 62 64 76 5c 4c 9d 42 1d 6b 52\" -c"); | |
} | |
static void Entry(string[] args) | |
{ | |
Options parser = new OptParser(args); | |
if (parser.ValidInput) | |
{ | |
PrintHelp(); | |
return; | |
} | |
try | |
{ | |
var r = FindResults(parser); | |
Console.WriteLine(r.ToString()); | |
} | |
catch (Exception ex) | |
{ | |
Console.Error.WriteLine("FindPrivateKey failed for the following reason:"); | |
Console.Error.WriteLine(ex.Message); | |
Console.Error.WriteLine("\nUse /? option for help"); | |
} | |
} | |
public static string FindKeyByName(string storeName, string storeLocation, string commonName) | |
{ | |
return Find(storeName, storeLocation, commonName, "-n"); | |
} | |
public static string FindByThumbprint(string storeName, string storeLocation, string thumbPrint) | |
{ | |
return Find(storeName, storeLocation, thumbPrint, "-f"); | |
} | |
static string Find(string storeName, string storeLocation, string commonName, string nameFlag) | |
{ | |
var results = FindResults(new OptParser(new[] | |
{ | |
storeName, | |
storeLocation, | |
nameFlag, | |
commonName | |
})); | |
return results.Equals(Results.NotFound) ? "Not Found" : Path.Combine(results.Directory, results.KeyName); | |
} | |
static Results FindResults(Options parser) | |
{ | |
var storeName = (StoreName) Enum.Parse(typeof (StoreName), parser.StoreName, true); | |
var storeLocation = (StoreLocation) Enum.Parse(typeof (StoreLocation), parser.StoreLocation, true); | |
var cert = parser.FindByPredicate | |
? LoadCertificate(storeName, storeLocation, parser.ComputeKey(), parser.FindType) | |
: SelectCertificate(storeName, storeLocation); | |
if (cert == null) return Results.NotFound; | |
var privateKeyFile = GetKeyFileName(cert); | |
return new Results | |
{ | |
KeyName = privateKeyFile, | |
Directory = GetKeyFileDirectory(privateKeyFile) | |
}; | |
} | |
static X509Certificate2 SelectCertificate(StoreName storeName, StoreLocation storeLocation) | |
{ | |
X509Certificate2 result; | |
var store = new X509Store(storeName, storeLocation); | |
store.Open(OpenFlags.ReadOnly); | |
try | |
{ | |
X509Certificate2Collection matches; | |
matches = X509Certificate2UI.SelectFromCollection(store.Certificates, "Select certificate", | |
"Select the certificate to find the location of the associated private key file:", | |
X509SelectionFlag.SingleSelection); | |
if (matches.Count != 1) | |
result = null; | |
else | |
result = matches[0]; | |
} | |
finally | |
{ | |
store.Close(); | |
} | |
return result; | |
} | |
static X509Certificate2 LoadCertificate(StoreName storeName, StoreLocation storeLocation, string key, | |
X509FindType findType) | |
{ | |
X509Certificate2 result; | |
var store = new X509Store(storeName, storeLocation); | |
store.Open(OpenFlags.ReadOnly); | |
try | |
{ | |
var matches = store.Certificates.Find(findType, key, false); | |
if (matches.Count > 1) | |
throw new InvalidOperationException(String.Format("More than one certificate with key '{0}' found in the store.", | |
key)); | |
if (matches.Count == 0) | |
throw new InvalidOperationException(String.Format("No certificates with key '{0}' found in the store.", key)); | |
result = matches[0]; | |
} | |
finally | |
{ | |
store.Close(); | |
} | |
return result; | |
} | |
static string GetKeyFileName(X509Certificate2 cert) | |
{ | |
var hProvider = IntPtr.Zero; // CSP handle | |
var freeProvider = false; // Do we need to free the CSP ? | |
uint acquireFlags = 0; | |
var _keyNumber = 0; | |
string keyFileName = null; | |
byte[] keyFileBytes = null; | |
// | |
// Determine whether there is private key information available for this certificate in the key store | |
// | |
if (CryptAcquireCertificatePrivateKey(cert.Handle, | |
acquireFlags, | |
IntPtr.Zero, | |
ref hProvider, | |
ref _keyNumber, | |
ref freeProvider)) | |
{ | |
var pBytes = IntPtr.Zero; // Native Memory for the CRYPT_KEY_PROV_INFO structure | |
var cbBytes = 0; // Native Memory size | |
try | |
{ | |
if (CryptGetProvParam(hProvider, CryptGetProvParamType.PP_UNIQUE_CONTAINER, IntPtr.Zero, ref cbBytes, 0)) | |
{ | |
pBytes = Marshal.AllocHGlobal(cbBytes); | |
if (CryptGetProvParam(hProvider, CryptGetProvParamType.PP_UNIQUE_CONTAINER, pBytes, ref cbBytes, 0)) | |
{ | |
keyFileBytes = new byte[cbBytes]; | |
Marshal.Copy(pBytes, keyFileBytes, 0, cbBytes); | |
// Copy eveything except tailing null byte | |
keyFileName = Encoding.ASCII.GetString(keyFileBytes, 0, keyFileBytes.Length - 1); | |
} | |
} | |
} | |
finally | |
{ | |
if (freeProvider) | |
CryptReleaseContext(hProvider, 0); | |
// | |
// Free our native memory | |
// | |
if (pBytes != IntPtr.Zero) | |
Marshal.FreeHGlobal(pBytes); | |
} | |
} | |
if (keyFileName == null) | |
throw new InvalidOperationException("Unable to obtain private key file name"); | |
return keyFileName; | |
} | |
static string GetKeyFileDirectory(string keyFileName) | |
{ | |
// Look up All User profile from environment variable | |
var allUserProfile = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData); | |
// set up searching directory | |
var machineKeyDir = allUserProfile + "\\Microsoft\\Crypto\\RSA\\MachineKeys"; | |
// Seach the key file | |
var fs = Directory.GetFiles(machineKeyDir, keyFileName); | |
// If found | |
if (fs.Length > 0) | |
return machineKeyDir; | |
// Next try current user profile | |
var currentUserProfile = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); | |
// seach all sub directory | |
var userKeyDir = currentUserProfile + "\\Microsoft\\Crypto\\RSA\\"; | |
fs = Directory.GetDirectories(userKeyDir); | |
if (fs.Length > 0) | |
{ | |
// for each sub directory | |
foreach (var keyDir in fs) | |
{ | |
fs = Directory.GetFiles(keyDir, keyFileName); | |
if (fs.Length == 0) | |
continue; | |
else | |
// found | |
return keyDir; | |
} | |
} | |
throw new InvalidOperationException("Unable to locate private key file directory"); | |
} | |
[DllImport("crypt32", CharSet = CharSet.Unicode, SetLastError = true)] | |
internal static extern bool CryptAcquireCertificatePrivateKey(IntPtr pCert, uint dwFlags, IntPtr pvReserved, | |
ref IntPtr phCryptProv, ref int pdwKeySpec, | |
ref bool pfCallerFreeProv); | |
[DllImport("advapi32", CharSet = CharSet.Unicode, SetLastError = true)] | |
internal static extern bool CryptGetProvParam(IntPtr hCryptProv, CryptGetProvParamType dwParam, IntPtr pvData, | |
ref int pcbData, uint dwFlags); | |
[DllImport("advapi32", SetLastError = true)] | |
internal static extern bool CryptReleaseContext(IntPtr hProv, uint dwFlags); | |
} | |
interface Options | |
{ | |
bool ValidInput { get; } | |
bool FindByPredicate { get; } | |
X509FindType FindType { get; } | |
string StoreName { get; } | |
string StoreLocation { get; } | |
string ComputeKey(); | |
} | |
class OptParser : Options | |
{ | |
readonly string[] _args; | |
public OptParser(string[] args) | |
{ | |
_args = args; | |
} | |
public bool ValidInput | |
{ | |
get | |
{ | |
return _args.Length < 2 || _args.Length == 3 || _args.Length > 5 | |
|| (_args.Length > 2 && _args[2] != "-n" && _args[2] != "-t") | |
|| (_args.Length == 5 && _args[4] != "-f" && _args[4] != "-d" && _args[4] != "-a"); | |
} | |
} | |
public bool FindByPredicate | |
{ | |
get { return _args.Length > 2; } | |
} | |
public X509FindType FindType | |
{ | |
get | |
{ | |
return _args.Length > 2 && _args[2] == "-n" | |
? X509FindType.FindBySubjectDistinguishedName | |
: X509FindType.FindByThumbprint; | |
} | |
} | |
public string StoreName | |
{ | |
get { return _args[0]; } | |
} | |
public string StoreLocation | |
{ | |
get { return _args[1]; } | |
} | |
public string ComputeKey() | |
{ | |
// insert a comma followed by a space for store.Certificates.Find(findType, key, false) | |
// to successful find the certificate | |
if (_args.Length < 4) | |
return string.Empty; | |
var keys = _args[3].Split(','); | |
var res = string.Empty; | |
for (var i = 0; i < keys.Length; i++) | |
{ | |
res += keys[i]; | |
if (i != keys.Length - 1) | |
res += ", "; | |
} | |
return res; | |
} | |
} | |
enum CryptGetProvParamType | |
{ | |
PP_ENUMALGS = 1, | |
PP_ENUMCONTAINERS = 2, | |
PP_IMPTYPE = 3, | |
PP_NAME = 4, | |
PP_VERSION = 5, | |
PP_CONTAINER = 6, | |
PP_CHANGE_PASSWORD = 7, | |
PP_KEYSET_SEC_DESCR = 8, // get/set security descriptor of keyset | |
PP_CERTCHAIN = 9, // for retrieving certificates from tokens | |
PP_KEY_TYPE_SUBTYPE = 10, | |
PP_PROVTYPE = 16, | |
PP_KEYSTORAGE = 17, | |
PP_APPLI_CERT = 18, | |
PP_SYM_KEYSIZE = 19, | |
PP_SESSION_KEYSIZE = 20, | |
PP_UI_PROMPT = 21, | |
PP_ENUMALGS_EX = 22, | |
PP_ENUMMANDROOTS = 25, | |
PP_ENUMELECTROOTS = 26, | |
PP_KEYSET_TYPE = 27, | |
PP_ADMIN_PIN = 31, | |
PP_KEYEXCHANGE_PIN = 32, | |
PP_SIGNATURE_PIN = 33, | |
PP_SIG_KEYSIZE_INC = 34, | |
PP_KEYX_KEYSIZE_INC = 35, | |
PP_UNIQUE_CONTAINER = 36, | |
PP_SGC_INFO = 37, | |
PP_USE_HARDWARE_RNG = 38, | |
PP_KEYSPEC = 39, | |
PP_ENUMEX_SIGNING_PROT = 40, | |
PP_CRYPT_COUNT_KEY_USE = 41, | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
ls Cert:\LocalMachine\my | |
# find your thumbprint from the output | |
# or wherever your System.Security.dll is (Reference Assemblies?) | |
$References = ([System.Reflection.AssemblyName]::GetAssemblyName('C:\inetpub\System.Security.dll').FullName) | |
$FindPrivateKeySource = @" | |
THE ABOVE FILE HERE | |
"@ | |
Add-Type -ReferencedAssemblies $References -TypeDefinition $FindPrivateKeySource -Language CSharp | |
$keyPath = [PKI.PrivKey]::FindByThumbprint("My", "LocalMachine", "239848791283742983THUMBPRINT278872" | |
icacls $keyPath /grant "NetworkService:(R)" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment