bin/DSCHelperCerts.ps1

Function Test-DSCCert {
    Param(
        [Parameter(Mandatory = $true)][string]$commonName,
        [Parameter(Mandatory = $false)][System.String]$PFXPath,
        [Parameter(Mandatory = $false)][System.String]$CERPath,
        [Parameter(Mandatory = $false)][System.String]$ThumbprintPath);
    Write-Verbose "Looking for S2S Certificate in Computer Personal Store";
    Set-Location Cert:\LocalMachine\My;
    $myCert = Get-ChildItem | Where-Object { $_.Subject -ieq $commonName; }
    $result = ($null -ne $myCert);
    if ($result) {
        Write-Verbose "Looking for $($commonName) in Computer Trusted Root Store";
        Set-Location Cert:\LocalMachine\Root;
        $myCertRoot = Get-ChildItem | Where-Object { $_.Subject -ieq $commonName; }
        $result = ($null -ne $myCertRoot);
        if ($result) {
            if (![string]::IsNullOrEmpty($PFXPath) -and ![System.IO.File]::Exists($PFXPath)) {
                $result = $false;
            }
            elseif (![string]::IsNullOrEmpty($PFXPath) -and ![System.IO.File]::Exists($PFXPath)) {
                $result = $false;
            }
            elseif (![string]::IsNullOrEmpty($ThumbprintPath) -and ![System.IO.File]::Exists($ThumbprintPath)) {
                $result = $false;
            }
        }
    }
    return $result;
}

Function Remove-DSCCert {
    Param(
        [Parameter(Mandatory = $true)][string]$commonName,
        [Parameter(Mandatory = $false)][System.String]$PFXPath,
        [Parameter(Mandatory = $false)][System.String]$CERPath);
    Set-Location Cert:\LocalMachine\My;
    Get-ChildItem | Where-Object { $_.Subject -ieq $commonName; } | Remove-Item;
    Set-Location Cert:\LocalMachine\Root;
    Get-ChildItem | Where-Object { $_.Subject -ieq $commonName; } | Remove-Item;
    if (![string]::IsNullOrEmpty($PFXPath) -and ![string]::IsNullOrEmpty($CERPath)) {
        $pwdFile = $PFXPath -ireplace ".pfx", "-pwd.txt";
        if ([System.IO.File]::Exists($pwdFile)) { Remove-Item $pwdFile -Force; }
        if ([System.IO.File]::Exists($PFXPath)) { Remove-Item $PFXPath -Force; }
        if ([System.IO.File]::Exists($CERPath)) { Remove-Item $CERPath -Force; }
    }
    elseif (![string]::IsNullOrEmpty($PFXPath)) {
        $pwdFile = $PFXPath -ireplace ".pfx", "-pwd.txt";
        if ([System.IO.File]::Exists($pwdFile)) { Remove-Item $pwdFile -Force; }
        if ([System.IO.File]::Exists($PFXPath)) { Remove-Item $PFXPath -Force; }
    }
    elseif (![string]::IsNullOrEmpty($CERPath)) {
        if ([System.IO.File]::Exists($CERPath)) { Remove-Item $CERPath -Force; }
    }
}

Function New-DSCCert {
    Param(
        [Parameter(Mandatory = $true)][string]$commonName, 
        [Parameter(Mandatory = $true)][string]$friendlyName,
        [Parameter(Mandatory = $true)][switch]$isDSCCert);
    Set-Location Cert:\LocalMachine\My;
    $myCert = Get-ChildItem | Where-Object { $_.Subject -ieq $commonName; }
    if ($null -eq $myCert) {
        Write-Verbose "Did not find $($commonName) in the computer personal store";
        Write-Verbose "Creating self signed certificate $($commonName)";
        $signedCertParams = @{
            'Subject' = $commonName;
            'FriendlyName' = $friendlyName;
            'StoreLocation' = 'LocalMachine';
            'StoreName' = 'My';
            'Exportable' = $true;
        }
        if ($isDSCCert) {
            $signedCertParams.'EnhancedKeyUsage' = 'Document Encryption';
            $signedCertParams.'KeyUsage' = 'KeyEncipherment', 'DataEncipherment';
            $signedCertParams.'SignatureAlgorithm' = 'SHA256';
        } 
        _New-SelfSignedCertificateEx @signedCertParams;
    }
    else {
        Write-Verbose "Found $($commonName) in the computer personal store";
    }
    Write-Verbose "Looking for $($commonName) in Computer Trusted Root Store";
    Set-Location Cert:\LocalMachine\Root;
    $myCertRoot = Get-ChildItem | Where-Object { $_.Subject -ieq $commonName; }
    if ($null -eq $myCertRoot) {
        Write-Verbose "Did not find $($certCommonName) in the computer trusted root store";
        Write-Verbose "Copying $($commonName) cert from personal store to trusted root store";
        $srcStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList 'My', 'LocalMachine';
        $srcStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly);
        $srcCert = $srcStore.Certificates | Where-Object { $_.Subject -ieq $commonName };
        $destStore = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList 'root', 'LocalMachine';
        $destStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite);
        $destStore.Add($srcCert);
        $srcStore.Close();
        $destStore.Close();
    }
    else {
        Write-Verbose "Found $($commonName) in the computer trusted root store";
    }
}

Function Export-DSCCertPrivateKey {
    Param(
        [Parameter(Mandatory = $true)][string]$commonName, 
        [Parameter(Mandatory = $false)][string]$password,
        [Parameter(Mandatory = $true)][string]$filename);
    Set-Location Cert:\LocalMachine\My;
    if ([string]::IsNullOrEmpty($password)) {
        $pwd = _NewSWRandomPassword;
    } else {
        $pwd = $password;
    }
    $myCert = Get-ChildItem | Where-Object { $_.Subject -ieq $commonName; }
    $pwdFile = $filename -ireplace ".pfx", "-pwd.txt";
    $pwd | Out-File -FilePath $pwdFile;
    $secpwd = ConvertTo-SecureString -String $pwd -Force -AsPlainText;
    $myCert | Export-PfxCertificate -FilePath $filename -Password $secpwd | Out-Null;
}

Function Export-DSCCertPublicKey {
    Param(
        [Parameter(Mandatory = $true)][string]$commonName, 
        [Parameter(Mandatory = $true)][string]$filename,
        [Parameter(Mandatory = $false)][string]$thumbprintPath);
    Set-Location Cert:\LocalMachine\My;
    $myCert = Get-ChildItem | Where-Object { $_.Subject -ieq $commonName; }
    $myCert | Export-Certificate -FilePath $filename | Out-Null;
    if (![string]::IsNullOrEmpty($thumbprintPath)) {
        $myCert.Thumbprint | Out-File $thumbprintPath -Force;
    }
}

Function _NewSWRandomPassword { 
    Begin {
        Function Get-Seed {
            # Generate a seed for randomization
            $RandomBytes = New-Object -TypeName 'System.Byte[]' 4;
            $Random = New-Object -TypeName 'System.Security.Cryptography.RNGCryptoServiceProvider';
            $Random.GetBytes($RandomBytes);
            [BitConverter]::ToUInt32($RandomBytes, 0);
        }
    }
    Process {
        [String[]]$InputStrings = @('abcdefghijkmnpqrstuvwxyz', 'ABCEFGHJKLMNPQRSTUVWXYZ', '23456789');
        For ($iteration = 1; $iteration -le 1; $iteration++) {
            $Password = @{ };
            # Create char arrays containing groups of possible chars
            [char[][]]$CharGroups = $InputStrings;

            # Create char array containing all chars
            $AllChars = $CharGroups | ForEach-Object { [Char[]]$_ };

            # Randomize one char from each group
            Foreach ($Group in $CharGroups) {
                if ($Password.Count -lt 16) {
                    $Index = Get-Seed;
                    While ($Password.ContainsKey($Index)) {
                        $Index = Get-Seed;       
                    }
                    $Password.Add($Index, $Group[((Get-Seed) % $Group.Count)]);
                }
            }

            # Fill out with chars from $AllChars
            for ($i = $Password.Count; $i -lt 16; $i++) {
                $Index = Get-Seed;
                While ($Password.ContainsKey($Index)) {
                    $Index = Get-Seed;
                }
                $Password.Add($Index, $AllChars[((Get-Seed) % $AllChars.Count)]);
            }
            Write-Output -InputObject $( -join ($Password.GetEnumerator() | Sort-Object -Property Name | Select-Object -ExpandProperty Value));
        }
    }
}

# Create certificate in machine store.
Function _New-SelfSignedCertificateEx {
    [CmdletBinding(DefaultParameterSetName = 'Store')]Param(
        [Parameter(Mandatory, Position = 0)][string]$Subject,
        [Parameter(Position = 1)][DateTime]$NotBefore = [DateTime]::Now.AddDays(-1),
        [Parameter(Position = 2)][DateTime]$NotAfter = $NotBefore.AddYears(10), 
        [string]$SerialNumber,
        [Alias('CSP')][string]$ProviderName = 'Microsoft Enhanced Cryptographic Provider v1.0',
        [string]$AlgorithmName = 'RSA',
        [int]$KeyLength = 2048,
        [ValidateSet('Exchange', 'Signature')][string]$KeySpec = 'Exchange',
        [Alias('EKU')][Security.Cryptography.Oid[]]$EnhancedKeyUsage,
        [Alias('KU')][Security.Cryptography.X509Certificates.X509KeyUsageFlags]$KeyUsage,
        [Alias('SAN')][String[]]$SubjectAlternativeName,
        [bool]$IsCA,
        [int]$PathLength = -1,
        [Security.Cryptography.X509Certificates.X509ExtensionCollection]$CustomExtension,
        [ValidateSet('MD5', 'SHA1', 'SHA256', 'SHA384', 'SHA512')]
        [string]$SignatureAlgorithm = 'SHA1',
        [string]$FriendlyName,
        [Parameter(ParameterSetName = 'Store')][Security.Cryptography.X509Certificates.StoreLocation]$StoreLocation = 'CurrentUser',
        [Parameter(ParameterSetName = 'Store')][Security.Cryptography.X509Certificates.StoreName]$StoreName = 'My',
        [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [Alias('OutFile', 'OutPath', 'Out')][IO.FileInfo]$Path, [Parameter(Mandatory = $true, ParameterSetName = 'File')]
        [Security.SecureString]$Password,
        [Switch]$AllowSMIME,
        [Switch]$Exportable,
        [Parameter()][ValidateNotNullOrEmpty()][switch]$PassThru);
    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop;
    
    # Ensure we are running on a supported platform.
    if ([Environment]::OSVersion.Version.Major -lt 6) {
        throw (New-Object NotSupportedException -ArgumentList 'Windows XP and Windows Server 2003 are not supported!');
    }
    
    #region Constants
    
    #region Contexts
    New-Variable -Name UserContext -Value 0x1 -Option Constant;
    New-Variable -Name MachineContext -Value 0x2 -Option Constant;
    #endregion Contexts
    
    #region Encoding
    New-Variable -Name Base64Header -Value 0x0 -Option Constant;
    New-Variable -Name Base64 -Value 0x1 -Option Constant;
    New-Variable -Name Binary -Value 0x3 -Option Constant;
    New-Variable -Name Base64RequestHeader -Value 0x4 -Option Constant;
    #endregion Encoding
    
    #region SANs
    New-Variable -Name OtherName -Value 0x1 -Option Constant;
    New-Variable -Name RFC822Name -Value 0x2 -Option Constant;
    New-Variable -Name DNSName -Value 0x3 -Option Constant;
    New-Variable -Name DirectoryName -Value 0x5 -Option Constant;
    New-Variable -Name URL -Value 0x7 -Option Constant;
    New-Variable -Name IPAddress -Value 0x8 -Option Constant;
    New-Variable -Name RegisteredID -Value 0x9 -Option Constant;
    New-Variable -Name Guid -Value 0xa -Option Constant;
    New-Variable -Name UPN -Value 0xb -Option Constant;
    #endregion SANs
    
    #region Installation options
    New-Variable -Name AllowNone -Value 0x0 -Option Constant;
    New-Variable -Name AllowNoOutstandingRequest -Value 0x1 -Option Constant;
    New-Variable -Name AllowUntrustedCertificate -Value 0x2 -Option Constant;
    New-Variable -Name AllowUntrustedRoot -Value 0x4 -Option Constant;
    #endregion Installation options
    
    #region PFX export options
    New-Variable -Name PFXExportEEOnly -Value 0x0 -Option Constant;
    New-Variable -Name PFXExportChainNoRoot -Value 0x1 -Option Constant;
    New-Variable -Name PFXExportChainWithRoot -Value 0x2 -Option Constant;
    #endregion PFX export options
    
    #endregion Constants
    
    #region Subject processing
    # http://msdn.microsoft.com/en-us/library/aa377051(VS.85).aspx
    $subjectDN = New-Object -ComObject X509Enrollment.CX500DistinguishedName;
    $subjectDN.Encode($Subject, 0x0);
    #endregion Subject processing
    
    #region Extensions
    
    # Array of extensions to add to the certificate.
    $extensionsToAdd = @();
    
    #region Enhanced Key Usages processing
    if ($EnhancedKeyUsage) {
        $oIDs = New-Object -ComObject X509Enrollment.CObjectIDs;
        $EnhancedKeyUsage | ForEach-Object {
            $oID = New-Object -ComObject X509Enrollment.CObjectID;
            $oID.InitializeFromValue($_.Value);
            # http://msdn.microsoft.com/en-us/library/aa376785(VS.85).aspx
            $oIDs.Add($oID);
        }
        
        # http://msdn.microsoft.com/en-us/library/aa378132(VS.85).aspx
        $eku = New-Object -ComObject X509Enrollment.CX509ExtensionEnhancedKeyUsage;
        $eku.InitializeEncode($oIDs);
        $extensionsToAdd += 'EKU';
    }
    #endregion Enhanced Key Usages processing
    
    #region Key Usages processing
    if ($KeyUsage -ne $null) {
        $ku = New-Object -ComObject X509Enrollment.CX509ExtensionKeyUsage;
        $ku.InitializeEncode([int]$KeyUsage);
        $ku.Critical = $true;
        $extensionsToAdd += 'KU';
    }
    #endregion Key Usages processing
    
    #region Basic Constraints processing
    if ($PSBoundParameters.Keys.Contains('IsCA')) {
        # http://msdn.microsoft.com/en-us/library/aa378108(v=vs.85).aspx
        $basicConstraints = New-Object -ComObject X509Enrollment.CX509ExtensionBasicConstraints;
        if (!$IsCA) { $PathLength = -1; }
        $basicConstraints.InitializeEncode($IsCA, $PathLength);
        $basicConstraints.Critical = $IsCA;
        $extensionsToAdd += 'BasicConstraints';
    }
    #endregion Basic Constraints processing
    
    #region SAN processing
    if ($SubjectAlternativeName) {
        $san = New-Object -ComObject X509Enrollment.CX509ExtensionAlternativeNames;
        $names = New-Object -ComObject X509Enrollment.CAlternativeNames;
        foreach ($altName in $SubjectAlternativeName) {
            $name = New-Object -ComObject X509Enrollment.CAlternativeName;
            if ($altName.Contains('@')) {
                $name.InitializeFromString($RFC822Name, $altName);
            } else {
                try {
                    $bytes = [Net.IPAddress]::Parse($altName).GetAddressBytes();
                    $name.InitializeFromRawData($IPAddress, $Base64, [Convert]::ToBase64String($bytes));
                } catch {
                    try {
                        $bytes = [Guid]::Parse($altName).ToByteArray();
                        $name.InitializeFromRawData($Guid, $Base64, [Convert]::ToBase64String($bytes));
                    } catch {
                        try {
                            $bytes = ([Security.Cryptography.X509Certificates.X500DistinguishedName]$altName).RawData;
                            $name.InitializeFromRawData($DirectoryName, $Base64, [Convert]::ToBase64String($bytes));
                        } catch {
                            $name.InitializeFromString($DNSName, $altName);
                        }
                    }
                }
            }
            $names.Add($name);
        }
        $san.InitializeEncode($names);
        $extensionsToAdd += 'SAN';
    }
    #endregion SAN processing
    
    #region Custom Extensions
    if ($CustomExtension) {
        $count = 0;
        foreach ($ext in $CustomExtension) {
            # http://msdn.microsoft.com/en-us/library/aa378077(v=vs.85).aspx
            $extension = New-Object -ComObject X509Enrollment.CX509Extension;
            $extensionOID = New-Object -ComObject X509Enrollment.CObjectId;
            $extensionOID.InitializeFromValue($ext.Oid.Value);
            $extensionValue = [Convert]::ToBase64String($ext.RawData);
            $extension.Initialize($extensionOID, $Base64, $extensionValue);
            $extension.Critical = $ext.Critical;
            New-Variable -Name ('ext' + $count) -Value $extension;
            $extensionsToAdd += ('ext' + $count);
            $count++;
        }
    }
    #endregion Custom Extensions
    
    #endregion Extensions
    
    #region Private Key
    # http://msdn.microsoft.com/en-us/library/aa378921(VS.85).aspx
    $privateKey = New-Object -ComObject X509Enrollment.CX509PrivateKey;
    $privateKey.ProviderName = $ProviderName;
    $algorithmID = New-Object -ComObject X509Enrollment.CObjectId;
    $algorithmID.InitializeFromValue(([Security.Cryptography.Oid]$AlgorithmName).Value);
    $privateKey.Algorithm = $algorithmID;
    
    # http://msdn.microsoft.com/en-us/library/aa379409(VS.85).aspx
    $privateKey.KeySpec = switch ($KeySpec) { 'Exchange' { 1 }; 'Signature' { 2 } }
    $privateKey.Length = $KeyLength;
    
    # Key will be stored in current user certificate store.
    switch ($PSCmdlet.ParameterSetName) {
        'Store' {
            $privateKey.MachineContext = if ($StoreLocation -eq 'LocalMachine') { $true }
            else { $false }
        }
        'File' {
            $privateKey.MachineContext = $false;
        }
    }
    
    $privateKey.ExportPolicy = if ($Exportable) { 1 }
    else { 0 }
    $privateKey.Create();
    #endregion Private Key
    
    #region Build certificate request template
    
    # http://msdn.microsoft.com/en-us/library/aa377124(VS.85).aspx
    $cert = New-Object -ComObject X509Enrollment.CX509CertificateRequestCertificate;
    
    # Initialize private key in the proper store.
    if ($privateKey.MachineContext) {
        $cert.InitializeFromPrivateKey($MachineContext, $privateKey, '');
    } else {
        $cert.InitializeFromPrivateKey($UserContext, $privateKey, '');
    }
    
    $cert.Subject = $subjectDN;
    $cert.Issuer = $cert.Subject;
    $cert.NotBefore = $NotBefore;
    $cert.NotAfter = $NotAfter;
    
    #region Add extensions to the certificate
    foreach ($item in $extensionsToAdd) {
        $cert.X509Extensions.Add((Get-Variable -Name $item -ValueOnly));
    }
    #endregion Add extensions to the certificate
    
    if (![string]::IsNullOrEmpty($SerialNumber)) {
        if ($SerialNumber -match '[^0-9a-fA-F]') {
            throw 'Invalid serial number specified.';
        }
        
        if ($SerialNumber.Length % 2) {
            $SerialNumber = '0' + $SerialNumber;
        }
        
        $bytes = $SerialNumber -split '(.{2})' | Where-Object { $_ } | ForEach-Object { [Convert]::ToByte($_, 16) }
        $byteString = [Convert]::ToBase64String($bytes);
        $cert.SerialNumber.InvokeSet($byteString, 1);
    }
    
    if ($AllowSMIME) {
        $cert.SmimeCapabilities = $true;
    }
    
    $signatureOID = New-Object -ComObject X509Enrollment.CObjectId;
    $signatureOID.InitializeFromValue(([Security.Cryptography.Oid]$SignatureAlgorithm).Value);
    $cert.SignatureInformation.HashAlgorithm = $signatureOID;
    #endregion Build certificate request template
    
    # Encode the certificate.
    $cert.Encode();
    
    #region Create certificate request and install certificate in the proper store
    # Interface: http://msdn.microsoft.com/en-us/library/aa377809(VS.85).aspx
    $request = New-Object -ComObject X509Enrollment.CX509enrollment;
    $request.InitializeFromRequest($cert);
    $request.CertificateFriendlyName = $FriendlyName;
    $endCert = $request.CreateRequest($Base64);
    $request.InstallResponse($AllowUntrustedCertificate, $endCert, $Base64, '');
    #endregion Create certificate request and install certificate in the proper store
    
    #region Export to PFX if specified
    if ($PSCmdlet.ParameterSetName.Equals('File')) {
        $PFXString = $request.CreatePFX(
            [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password)),
            $PFXExportEEOnly,
            $Base64
        )
        Set-Content -Path $Path -Value ([Convert]::FromBase64String($PFXString)) -Encoding Byte;
    }
    #endregion Export to PFX if specified
    
    if ($PassThru.IsPresent) {
        @(Get-ChildItem -Path "Cert:\$StoreLocation\$StoreName").where( { $_.Subject -match $Subject })
    }
    
    $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Continue;
}