├── .gitignore ├── LICENSE.txt ├── README.md ├── SelfSignedCertificate ├── SelfSignedCertificate.psd1 └── SelfSignedCertificate.psm1 └── Tests └── SelfSignedCertificate.Tests.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /.vscode/ 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Robert Holt and contributors. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SelfSignedCertificate 2 | === 3 | 4 | Table of Contents: 5 | 6 | - [Overview](#overview) 7 | - [Example Usage](#example-usage) 8 | - [Suggested Improvements](#suggested-improvements) 9 | - [License](#license) 10 | 11 | ### Disclaimer 12 | 13 | This module is not officially supported. 14 | It has been created as a convenience module 15 | for the generation of self-signed certificates 16 | to simplify the testing of HTTPS functionality. 17 | 18 | This module should not be used in any production scenarios; 19 | it is designed to create self-signed certificates for testing 20 | purposes only. 21 | 22 | Overview 23 | --- 24 | 25 | This module is designed to be a convenient, cross-platform way 26 | to generate self-signed certificates in both PowerShell Core and Windows PowerShell 5.1. 27 | 28 | Since .NET Core already embeds its own cross-platform cryptography/certificate API, 29 | this module is a native PowerShell script module, with no binary dependencies. 30 | 31 | Some goals for this module include: 32 | 33 | - Low or no dependency footprint 34 | - User-friendly certificate input: 35 | - No fiddling with distinguished name formats 36 | - No arcane `X509HighlySpecificCryptoObject` assigning and manipulation 37 | - No raw binary/ASN.1/DER manipulation 38 | - Relatively improved configurability: 39 | - Support multiple certificate formats 40 | - Support different certificate configurations, validity periods and extensions 41 | - Simple cross-platform functionality: 42 | - We should be able to generate a certificate that works 43 | on Windows, Linux and macOS 44 | - Default settings should "just work" on respective platforms 45 | - Favor simplicity when possible, but not as a hard requirement 46 | 47 | ### Alternative tools 48 | 49 | You may want to take a look at a few other alternatives for self-signed certificate generation, 50 | listed here: 51 | 52 | - Windows PowerShell's [`New-SelfSignedCertificate` cmdlet](https://docs.microsoft.com/en-us/powershell/module/pkiclient/new-selfsignedcertificate?view=win10-ps) 53 | from the PkiClient module. 54 | 55 | It can be used from PowerShell Core on Windows using the [WindowsCompatibility module](https://github.com/PowerShell/WindowsCompatibility) 56 | like this: 57 | 58 | ```powershell 59 | Install-Module WindowsCompatibility 60 | Import-WinModule PKI 61 | New-SelfSignedCertificate # args as needed 62 | ``` 63 | 64 | However, this module is only available on Windows — there is no Linux version. 65 | 66 | - The [`dotnet dotnet-dev-certs` global tool](https://www.nuget.org/packages/dotnet-dev-certs), 67 | designed for generating self-signed certificates for ASP.NET Core development. 68 | 69 | This can be installed from the dotnet CLI. 70 | 71 | - [`openssl`](https://www.openssl.org/), which does work cross-platform, 72 | but may not be favorable compared to a PowerShell-native option 73 | and uses a PEM rather than PFX format. 74 | 75 | Example Usage 76 | --- 77 | 78 | ### Basic Usage 79 | 80 | To create a simple certificate the following will work: 81 | 82 | ```powershell 83 | > New-SelfSignedCertificate 84 | Certificate written to C:\Users\roholt\Documents\Dev\sandbox\certificate.pfx 85 | 86 | Thumbprint Subject EnhancedKeyUsageList 87 | ---------- ------- -------------------- 88 | A51B016324B5D2F11340CDCC52004B8129C88D3B CN=localhost 89 | 90 | ``` 91 | 92 | This will create a new certificate called `certificate.pfx` in your CWD 93 | for `localhost`. 94 | The command itself returns an `X509Certificate2` object 95 | describing the certificate written to disk. 96 | You can inspect this object to find its properties. 97 | This certificate will have no key usages, no basic constraints, 98 | no enhanced key usages and a Subject Idenitifer Key extension. 99 | 100 | **Note**: To repeat this command, you will need the `-Force` parameter 101 | in order to overwrite the old certificate you generated before. 102 | 103 | ### More Advanced Usage 104 | 105 | The `New-SelfSignedCertificate` command allows the specification of 106 | full distinguished names as well as a few other options: 107 | 108 | ```powershell 109 | > $password = ConvertTo-SecureString -Force -AsPlainText 'your password' 110 | > $distinguishedName = @{ 111 | CommonName = 'example.org' 112 | Country = 'US' 113 | StateOrProvince = 'Nebraska' 114 | Locality = 'Omaha' 115 | Organization = 'Umbrella Corporation' 116 | OrganizationalUnit = 'Sales' 117 | EmailAddress = 'donotreply@umbrellacorp.com' 118 | } 119 | > $certificateParameters = $distinguishedName + @{ 120 | OutCertPath = 'C:\Users\you\Documents\cert.pfx' 121 | StartDate = [System.DateTimeOffset]::Now 122 | Duration = [timespan]::FromDays(365) 123 | Passphrase = $password 124 | CertificateFormat = 'Pfx' # Values from [System.Security.Cryptography.X509Certificates.X509ContentType] 125 | KeyLength = 4096 126 | ForCertificateAuthority = $true 127 | KeyUsage = 'DigitalSignature','KeyEncipherment' # Values from [System.Security.Cryptography.X509Certificates.X509KeyUsageFlags] 128 | EnhancedKeyUsage = 'ServerAuthentication','ClientAuthentication' 129 | } 130 | > New-SelfSignedCertificate @certificateParameters -Force 131 | WARNING: Parameter 'EmailAddress' is obsolete. The email name component is deprecated by the PKIX standard 132 | Certificate written to C:\Users\roholt\Documents\Dev\sandbox\here.pfx 133 | 134 | Thumbprint Subject EnhancedKeyUsageList 135 | ---------- ------- -------------------- 136 | 7445433CB2BB4948E12794A167C6725DC214AA84 CN=example.org, O... {Server Authentication, Client Authentication} 137 | ``` 138 | 139 | The certificate produced by the above command will have the following properties: 140 | 141 | - The issuer and subject distinguished name set to: 142 | 143 | ```text 144 | CN=example.org, OU=Sales, O=Umbrella Corporation, L=Omaha, S=Nebraska, C=US, E=donotreply@umbrellacorp.com 145 | ``` 146 | 147 | - Password protection (in this case with the password `'Your password'`). 148 | - A one-year validity period starting from the creation time (with the milliseconds truncated). 149 | - A 4096-bit RSA key. 150 | - A basic constraints extension with `CertificateAuthority` set to `true`. 151 | - The `Digital Signature` and `Key Encipherment` basic key usages indicated. 152 | - The `Server Authentication` and `Client Authentication` enhanced key usages indicated. 153 | 154 | The command also offers the `-AdditionalExtension` parameter, 155 | which takes an array of `System.Security.Cryptography.X509Certificates.X509Extension` 156 | to add to any generate certificate. 157 | 158 | Suggested Improvments 159 | --- 160 | 161 | ### Support for other certificate formats 162 | 163 | The module does not yet support PEM files, 164 | which are heavily used in the Linux world. 165 | While not a certificate format per-se, 166 | they are a common encoding of certificates 167 | and we should endeavour to support them in some way. 168 | 169 | Presently, the author is not aware of PEM support 170 | native to PowerShell Core or .NET Core. 171 | 172 | ### Ability to specify criticality on certificate extensions 173 | 174 | The certificate extensions generated by this module 175 | currently all set the `Critical` field to `false` to allow greater flexibility. 176 | 177 | However it might be desirable to configure 178 | any or all of these to be designated as `Critical`. 179 | Ideally this could be done without cluttering up the commands already 180 | large number of parameters. 181 | 182 | ### Better support for other enhanced key usages 183 | 184 | Currently on the `ServerAuthentication` and `ClientAuthentication` enhanced 185 | key usages are supported (in constraining way, for ease of use). 186 | 187 | Ideally more options for this could be made available. 188 | 189 | ### Better, more-modular support for common certificate extensions 190 | 191 | The module could provide a set of classes that generate `X509Extension`s 192 | describing commonly used certificate extensions. 193 | 194 | License 195 | --- 196 | 197 | This module is MIT licensed. See the [LICENSE.txt](./LICENSE.txt). 198 | -------------------------------------------------------------------------------- /SelfSignedCertificate/SelfSignedCertificate.psd1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Robert Holt. 2 | # Licensed under the MIT License. 3 | 4 | # Module manifest for module 'SelfSignedCertificate' 5 | # 6 | # Generated on: 9/18/2018 7 | 8 | @{ 9 | 10 | # Script module or binary module file associated with this manifest. 11 | RootModule = 'SelfSignedCertificate.psm1' 12 | 13 | # Version number of this module. 14 | ModuleVersion = '0.0.5' 15 | 16 | # Supported PSEditions 17 | CompatiblePSEditions = 'Core', 'Desktop' 18 | 19 | # ID used to uniquely identify this module 20 | GUID = '634218b8-3334-4f10-ba89-2b79b0fd9fc4' 21 | 22 | # Author of this module 23 | Author = 'Robert Holt' 24 | 25 | # Company or vendor of this module 26 | CompanyName = 'Microsoft Corporation' 27 | 28 | # Copyright statement for this module 29 | Copyright = '© Robert Holt' 30 | 31 | # Description of the functionality provided by this module 32 | Description = @' 33 | WARNING: This module is use-at-your-own-risk - it exists to test web cmdlets in PowerShell Core with. It is unsupported by Microsoft. 34 | 35 | This module provides functionality for creating, processing and manipulating self-signed certificates in PowerShell. 36 | 37 | It aims to be broadly useful and cross-platform, but is not intended for production use. 38 | 39 | If you experience any issues with or have feature requests for this module, please open an issue at https://github.com/rjmholt/SelfSignedCertificate. 40 | '@ 41 | 42 | # Minimum version of the PowerShell engine required by this module 43 | PowerShellVersion = '5.1' 44 | 45 | # Name of the PowerShell host required by this module 46 | # PowerShellHostName = '' 47 | 48 | # Minimum version of the PowerShell host required by this module 49 | # PowerShellHostVersion = '' 50 | 51 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 52 | # DotNetFrameworkVersion = '' 53 | 54 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 55 | # CLRVersion = '' 56 | 57 | # Processor architecture (None, X86, Amd64) required by this module 58 | # ProcessorArchitecture = '' 59 | 60 | # Modules that must be imported into the global environment prior to importing this module 61 | # RequiredModules = @() 62 | 63 | # Assemblies that must be loaded prior to importing this module 64 | # RequiredAssemblies = @() 65 | 66 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 67 | # ScriptsToProcess = @() 68 | 69 | # Type files (.ps1xml) to be loaded when importing this module 70 | # TypesToProcess = @() 71 | 72 | # Format files (.ps1xml) to be loaded when importing this module 73 | # FormatsToProcess = @() 74 | 75 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 76 | # NestedModules = @() 77 | 78 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 79 | FunctionsToExport = @( 80 | 'New-SelfSignedCertificate' 81 | 'Open-SelfSignedCertificateReadMe' 82 | ) 83 | 84 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 85 | CmdletsToExport = @() 86 | 87 | # Variables to export from this module 88 | VariablesToExport = '*' 89 | 90 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 91 | AliasesToExport = @() 92 | 93 | # DSC resources to export from this module 94 | # DscResourcesToExport = @() 95 | 96 | # List of all modules packaged with this module 97 | # ModuleList = @() 98 | 99 | # List of all files packaged with this module 100 | # FileList = @() 101 | 102 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 103 | PrivateData = @{ 104 | 105 | PSData = @{ 106 | 107 | # Tags applied to this module. These help with module discovery in online galleries. 108 | # Tags = @() 109 | 110 | # A URL to the license for this module. 111 | LicenseUri = 'https://github.com/rjmholt/SelfSignedCertificate/blob/master/LICENSE.txt' 112 | 113 | # A URL to the main website for this project. 114 | ProjectUri = 'https://github.com/rjmholt/SelfSignedCertificate' 115 | 116 | # A URL to an icon representing this module. 117 | # IconUri = '' 118 | 119 | # ReleaseNotes of this module 120 | # ReleaseNotes = '' 121 | 122 | } # End of PSData hashtable 123 | 124 | } # End of PrivateData hashtable 125 | 126 | # HelpInfo URI of this module 127 | # HelpInfoURI = '' 128 | 129 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 130 | # DefaultCommandPrefix = '' 131 | 132 | } 133 | -------------------------------------------------------------------------------- /SelfSignedCertificate/SelfSignedCertificate.psm1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Robert Holt. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | # Warn about support 5 | Write-Warning @' 6 | This module is use-at-your-own-risk. 7 | It exists to test PowerShell Core builds and is not supported by Microsoft. 8 | Please report any issues at https://github.com/rjmholt/SelfSignedCertificate. 9 | '@ 10 | 11 | # The default length of a certificate in days 12 | $script:DefaultCertDurationDays = 365 13 | # Default RSA key length 14 | $script:DefaultRsaKeyLength = 2048 15 | # Default format for certificates 16 | $script:DefaultCertificateFormat = [System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx 17 | # Default name for the certificate file without extension 18 | $script:DefaultCertificateFileName = 'certificate' 19 | # Default key usage for certificates 20 | $script:DefaultKeyUsage = [System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::None 21 | # Default certificate subject Common Name 22 | $script:DefaultCommonName = 'localhost' 23 | 24 | $script:IsUnix = $IsLinux -or $IsMacOS 25 | 26 | # List of certificate key usages supported 27 | enum EnhancedKeyUsage 28 | { 29 | ServerAuthentication 30 | ClientAuthentication 31 | } 32 | 33 | # Lookup table for usage OIDs 34 | $script:SupportedUsages = @{ 35 | [EnhancedKeyUsage]::ServerAuthentication = [System.Security.Cryptography.Oid]::new("1.3.6.1.5.5.7.3.1", "Server Authentication") 36 | [EnhancedKeyUsage]::ClientAuthentication = [System.Security.Cryptography.Oid]::new("1.3.6.1.5.5.7.3.2", "Client Authentication") 37 | } 38 | 39 | # Class to represent a certificate distinguished name 40 | # like "CN=com.contoso, C=US, S=Nebraska, L=Omaha, O=Contoso Ltd, OU=Sales, E=sales@contoso.com". 41 | # See https://docs.microsoft.com/en-us/windows/desktop/seccrypto/distinguished-name-fields. 42 | class CertificateDistinguishedName 43 | { 44 | # Name of a person or an object host name 45 | [ValidateNotNullOrEmpty()] 46 | [string]$CommonName 47 | 48 | # 2-character ISO country code 49 | [ValidateLength(2, 2)] 50 | [string]$Country 51 | 52 | # The state or province where the owner is physically located 53 | [string]$StateOrProvince 54 | 55 | # The city where the owner is located 56 | [string]$Locality 57 | 58 | # The name of the registering organization 59 | [string]$Organization 60 | 61 | # The division of the organization owning the certificate 62 | [string]$OrganizationalUnit 63 | 64 | # The email address of the certificate owner 65 | [Obsolete("The email field is deprecated by the PKIX standard")] 66 | [mailaddress]$EmailAddress 67 | 68 | # Format the distinguished name like 'CN="com.contoso"; C="US"; S="Nebraska"' 69 | [string] Format() 70 | { 71 | return $this.Format(';', <# UseQuotes #> $true) 72 | } 73 | 74 | # Format the distinguished name with the given separator and quote usage setting 75 | [string] Format([char]$Separator, [bool]$UseQuotes) 76 | { 77 | $sb = [System.Text.StringBuilder]::new() 78 | 79 | if ($UseQuotes) 80 | { 81 | $sb.Append("CN=`"$($this.CommonName)`"") 82 | } 83 | else 84 | { 85 | $sb.Append("CN=$($this.CommonName)") 86 | } 87 | 88 | $fields = @{ 89 | OU = $this.OrganizationalUnit 90 | O = $this.Organization 91 | L = $this.Locality 92 | S = $this.StateOrProvince 93 | C = $this.Country 94 | E = $this.EmailAddress.Address 95 | } 96 | 97 | foreach ($field in 'OU','O','L','S','C','E') 98 | { 99 | $val = $fields[$field] 100 | 101 | if (-not $val) 102 | { 103 | continue 104 | } 105 | 106 | $sb.Append($Separator) 107 | $sb.Append(" ") 108 | 109 | if ($UseQuotes) 110 | { 111 | $sb.Append("$field=`"$val`"") 112 | } 113 | else 114 | { 115 | $sb.Append("$field=$val") 116 | } 117 | } 118 | 119 | return $sb.ToString() 120 | } 121 | 122 | # Format the distinguished name like 'CN=com.contoso, C=US, S=Nebraska' 123 | [string] ToString() 124 | { 125 | return $this.Format(',', $false) 126 | } 127 | 128 | # OpenSSL expects a strange distinguished name format 129 | # like '/CN=com.contoso/C=US/S=Nebraska' 130 | [string] FormatForOpenSsl() 131 | { 132 | $sb = [System.Text.StringBuilder]::new() 133 | 134 | $sb.Append("/CN=$($this.CommonName)") 135 | 136 | $fields = @{ 137 | OU = $this.OrganizationalUnit 138 | O = $this.Organization 139 | L = $this.Locality 140 | S = $this.StateOrProvince 141 | C = $this.Country 142 | E = $this.EmailAddress.Address 143 | } 144 | 145 | foreach ($field in 'OU','O','L','S','C','E') 146 | { 147 | $val = $fields[$field] 148 | 149 | if (-not $val) 150 | { 151 | continue 152 | } 153 | 154 | $sb.Append("/$field=$val") 155 | } 156 | 157 | return $sb.ToString() 158 | } 159 | 160 | # Create a new X500DistinguishedName object from this certificate 161 | [X500DistinguishedName] AsX500DistinguishedName() 162 | { 163 | return [X500DistinguishedName]::new($this.Format()) 164 | } 165 | } 166 | 167 | # Represents the data in a self-signed certificate 168 | class SelfSignedCertificate 169 | { 170 | # The friendly name of the certificate 171 | [string]$FriendlyName = [string]::Empty 172 | 173 | # The length of the private key to use in bits 174 | [int]$KeyLength = $script:DefaultRsaKeyLength 175 | 176 | # The format of the certificate 177 | [System.Security.Cryptography.X509Certificates.X509ContentType]$Format = $script:DefaultCertificateFormat 178 | 179 | # The start time of the certificate's valid period 180 | [System.DateTimeOffset]$NotBefore = [System.DateTimeOffset]::Now 181 | 182 | # The end time of the certificate's valid period 183 | [System.DateTimeOffset]$NotAfter = [System.DateTimeOffset]::Now.AddDays($script:DefaultCertDurationDays) 184 | 185 | # The certificate's subject and issuer name (since it's self-signed) 186 | [CertificateDistinguishedName]$SubjectName 187 | 188 | # The key usages for the certificate -- what it will be used to do 189 | [System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]$KeyUsage = $script:DefaultKeyUsage 190 | 191 | # Extensions to be added to the certificate beyond those added automatically 192 | [System.Security.Cryptography.X509Certificates.X509Extension[]]$AdditionalExtensions 193 | 194 | # The enhanced key usages for the certificate -- what specific scenarios it will be used for 195 | [EnhancedKeyUsage[]]$EnhancedKeyUsage 196 | 197 | # Whether or not this certificate is for a certificate authority 198 | [bool]$ForCertificateAuthority 199 | 200 | # Instantiate an X509Certificate2 object from this object 201 | [System.Security.Cryptography.X509Certificates.X509Certificate2] AsX509Certificate2() 202 | { 203 | $extensions = [System.Collections.Generic.List[System.Security.Cryptography.X509Certificates.X509Extension]]::new() 204 | 205 | if ($this.AdditionalExtensions) 206 | { 207 | $extensions.AddRange($this.AdditionalExtensions) 208 | } 209 | 210 | if ($this.KeyUsage) 211 | { 212 | # Create Key Usage 213 | $keyUsages = [System.Security.Cryptography.X509Certificates.X509KeyUsageExtension]::new( 214 | $this.KeyUsage, 215 | <# critical #> $false) 216 | $extensions.Add($keyUsages) 217 | } 218 | 219 | # Create Enhanced Key Usage from configured usages 220 | if ($this.EnhancedKeyUsage) 221 | { 222 | $ekuOidCollection = [System.Security.Cryptography.OidCollection]::new() 223 | foreach ($usage in $this.EnhancedKeyUsage) 224 | { 225 | if ($script:SupportedUsages.Keys -contains $usage) 226 | { 227 | $ekuOidCollection.Add($script:SupportedUsages[$usage]) 228 | } 229 | } 230 | $enhancedKeyUsages = [System.Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension]::new( 231 | $ekuOidCollection, 232 | <# critical #> $false) 233 | $extensions.Add($enhancedKeyUsages) 234 | } 235 | 236 | # Create Basic Constraints 237 | $basicConstraints = [System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension]::new( 238 | <# certificateAuthority #> $this.ForCertificateAuthority, 239 | <# hasPathLengthConstraint #> $false, 240 | <# pathLengthConstraint #> 0, 241 | <# critical #> $false) 242 | $extensions.Add($basicConstraints) 243 | 244 | # Create Private Key 245 | $key = [System.Security.Cryptography.RSA]::Create($this.KeyLength) 246 | 247 | # Create the subject of the certificate 248 | $subject = $this.SubjectName.AsX500DistinguishedName() 249 | 250 | # Create Certificate Request 251 | $certRequest = [System.Security.Cryptography.X509Certificates.CertificateRequest]::new( 252 | $subject, 253 | $key, 254 | [System.Security.Cryptography.HashAlgorithmName]::SHA256, 255 | [System.Security.Cryptography.RSASignaturePadding]::Pkcs1) 256 | 257 | # Create the Subject Key Identifier extension 258 | $subjectKeyIdentifier = [System.Security.Cryptography.X509Certificates.X509SubjectKeyIdentifierExtension]::new( 259 | $certRequest.PublicKey, 260 | <# critical #> $false) 261 | $extensions.Add($subjectKeyIdentifier) 262 | 263 | # Create Authority Key Identifier if the certificate is for a CA 264 | if ($this.ForCertificateAuthority) 265 | { 266 | $authorityKeyIdentifier = New-AuthorityKeyIdentifier -SubjectKeyIdentifier $subjectKeyIdentifier 267 | $extensions.Add($authorityKeyIdentifier) 268 | } 269 | 270 | foreach ($extension in $extensions) 271 | { 272 | $certRequest.CertificateExtensions.Add($extension) 273 | } 274 | 275 | $cert = $certRequest.CreateSelfSigned($this.NotBefore, $this.NotAfter) 276 | 277 | # FriendlyName is not supported on UNIX platforms 278 | if (-not $script:IsUnix) 279 | { 280 | $cert.FriendlyName = $this.FriendlyName 281 | } 282 | 283 | return $cert 284 | } 285 | } 286 | 287 | <# 288 | .SYNOPSIS 289 | Creates a self-signed certificate for testing use. 290 | 291 | .DESCRIPTION 292 | Creates a self-signed certificate for testing usage in a 293 | given format and using a given backend and outputs it to a given filepath. 294 | 295 | .PARAMETER OutFilePath 296 | The filepath to output the generated certificate to. 297 | 298 | .PARAMETER CommonName 299 | The common name of the certificate subject, e.g. "com.contoso" or "Jennifer McCallum". 300 | 301 | .PARAMETER Country 302 | The country of the certificate subject as a two-character ISO code, e.g. "US" or "GB". 303 | 304 | .PARAMETER StateOrProvince 305 | The state or province of the physical location of the certificate subject, e.g. "California" or "New South Wales". 306 | 307 | .PARAMETER Locality 308 | The city or regional locality where the certificate subject is located, e.g. "Seattle". 309 | 310 | .PARAMETER Organization 311 | The organization to which the certificate subject belongs, e.g. "Contoso Ltd". 312 | 313 | .PARAMETER OrganizationalUnit 314 | The department or sub-organizational group the certificate subject belongs to, e.g. "Marketing". 315 | 316 | .PARAMETER EmailAddress 317 | --DEPRECATED-- The email address of the certificate owner. 318 | 319 | .PARAMETER FriendlyName 320 | A descriptive, human-readable name for the certificate. 321 | 322 | .PARAMETER CertificateFormat 323 | The file format the certificate will take. 324 | 325 | .PARAMETER KeyLength 326 | The length of the key in bits. 327 | 328 | .PARAMETER KeyUsage 329 | What general usages the certificate will be used for. 330 | 331 | .PARAMETER EnhancedKeyUsage 332 | The particular scenarios the certificate will be used for. 333 | 334 | .PARAMETER AdditionalExtension 335 | Additional certificate extensions desired to add to the certificate. 336 | 337 | .PARAMETER ForCertificateAuthority 338 | Specifies that the certificate is for a certification authority (CA). 339 | 340 | .PARAMETER Passphrase 341 | The encryption passphrase or password for the certificate to protect its contents. 342 | 343 | .PARAMETER Force 344 | Force overwriting if a certificate file already exists on the path to write to. 345 | 346 | .PARAMETER NotBefore 347 | The time when the certificate becomes valid. 348 | 349 | .PARAMETER NotAfter 350 | The time when the certificate ceases to be valid. 351 | 352 | .PARAMETER Duration 353 | The length of the validity period of the certificate. 354 | #> 355 | function New-SelfSignedCertificate 356 | { 357 | [CmdletBinding(DefaultParameterSetName = "NotAfter")] 358 | param( 359 | [Parameter()] 360 | [ValidateNotNullOrEmpty()] 361 | [string] 362 | $OutCertPath = $PWD, 363 | 364 | [Parameter()] 365 | [ValidateNotNullOrEmpty()] 366 | [Alias("CN")] 367 | [string] 368 | $CommonName = $script:DefaultCommonName, 369 | 370 | [Parameter()] 371 | [Alias("C")] 372 | [string] 373 | $Country, 374 | 375 | [Parameter()] 376 | [Alias("S")] 377 | [string] 378 | $StateOrProvince, 379 | 380 | [Parameter()] 381 | [Alias("L")] 382 | [string] 383 | $Locality, 384 | 385 | [Parameter()] 386 | [Alias("O")] 387 | [string] 388 | $Organization, 389 | 390 | [Parameter()] 391 | [Alias("OU")] 392 | [string] 393 | $OrganizationalUnit, 394 | 395 | [Parameter()] 396 | [Alias("E")] 397 | [Obsolete("The email name component is deprecated by the PKIX standard")] 398 | [mailaddress] 399 | $EmailAddress, 400 | 401 | [Parameter()] 402 | [ValidateNotNullOrEmpty()] 403 | [string] 404 | $FriendlyName, 405 | 406 | [Parameter()] 407 | [System.Security.Cryptography.X509Certificates.X509ContentType] 408 | $CertificateFormat = $script:DefaultCertificateFormat, 409 | 410 | [Parameter()] 411 | [ValidateSet(2048, 3072, 4096)] 412 | [int] 413 | $KeyLength = $script:DefaultRsaKeyLength, 414 | 415 | [Parameter()] 416 | [System.Security.Cryptography.X509Certificates.X509KeyUsageFlags[]] 417 | $KeyUsage = $script:DefaultKeyUsage, 418 | 419 | [Parameter()] 420 | [EnhancedKeyUsage[]] 421 | $EnhancedKeyUsage, 422 | 423 | [Parameter()] 424 | [System.Security.Cryptography.X509Certificates.X509Extension[]] 425 | $AdditionalExtension, 426 | 427 | [Parameter()] 428 | [Alias("CA")] 429 | [switch] 430 | $ForCertificateAuthority, 431 | 432 | [Parameter()] 433 | [Alias('Password')] 434 | [securestring] 435 | $Passphrase, 436 | 437 | [Parameter()] 438 | [switch] 439 | $Force, 440 | 441 | [Parameter()] 442 | [Alias("StartDate")] 443 | [System.DateTimeOffset] 444 | $NotBefore = [System.DateTimeOffset]::Now, 445 | 446 | [Parameter(ParameterSetName="NotAfter")] 447 | [System.DateTimeOffset] 448 | $NotAfter = [System.DateTimeOffset]::Now.AddDays($script:DefaultCertDurationDays), 449 | 450 | [Parameter(ParameterSetName="Duration")] 451 | [timespan] 452 | $Duration = [timespan]::FromDays($script:DefaultCertDurationDays) 453 | ) 454 | 455 | process 456 | { 457 | if ($PSCmdlet.ParameterSetName.Contains("Duration")) 458 | { 459 | $NotAfter = $NotBefore.Add($Duration) 460 | } 461 | 462 | # Normalize the given paths (make them absolute, with relative interpreted as relative to PWD) 463 | $OutCertPath = Get-AbsolutePathFromSupplied -Path $OutCertPath 464 | 465 | # Make sure the certificate's output path matches the given format 466 | $ext = Get-CertificateFormatExtension $CertificateFormat 467 | $OutCertPath = Get-ProperFilePath -Path $OutCertPath -DefaultFileName $script:DefaultCertificateFileName -RequiredExtension $ext 468 | 469 | # Ensure the directory where the certificate will go exists and that another certificate does not already exist 470 | $destinationDir = [System.IO.Path]::GetDirectoryName($OutCertPath) 471 | if (-not (Test-Path $destinationDir)) 472 | { 473 | throw [System.InvalidOperationException] "Destination directory '$destinationDir' for certificate does not exist or is not accessible" 474 | } 475 | elseif (Test-Path $OutCertPath) 476 | { 477 | if ($Force) 478 | { 479 | Remove-Item -Path $OutCertPath -Force 480 | } 481 | else 482 | { 483 | throw [System.IO.IOException] "File already exists at path $OutCertPath" 484 | } 485 | } 486 | 487 | # Roll the key usage flags into a single value (since they are flags) 488 | $keyUsageFlags = [System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::None 489 | foreach ($keyUsageFlag in $KeyUsage) 490 | { 491 | $keyUsageFlags = $keyUsageFlags -bor $keyUsageFlag 492 | } 493 | 494 | # Construct the subject name 495 | $subjectName = [CertificateDistinguishedName] (Get-FalsyRemovedHashtable -Hashtable @{ 496 | CommonName = $CommonName 497 | Country = $Country 498 | StateOrProvince = $StateOrProvince 499 | Locality = $Locality 500 | Organization = $Organization 501 | OrganizationalUnit = $OrganizationalUnit 502 | EmailAddress = $EmailAddress 503 | }) 504 | 505 | # Construct the certificate object 506 | $certificate = [SelfSignedCertificate] (Get-FalsyRemovedHashtable -Hashtable @{ 507 | SubjectName = $subjectName 508 | KeyLength = $KeyLength 509 | KeyUsage = $keyUsageFlags 510 | EnhancedKeyUsage = $EnhancedKeyUsage 511 | NotBefore = $NotBefore 512 | NotAfter = $NotAfter 513 | FriendlyName = $FriendlyName 514 | ForCertificateAuthority = $ForCertificateAuthority 515 | AdditionalExtensions = $AdditionalExtension 516 | }) 517 | 518 | # Turn the certificate object into an X509 certificate (2) object 519 | [System.Security.Cryptography.X509Certificates.X509Certificate2]$x509Certificate2 = $certificate.AsX509Certificate2() 520 | 521 | # Write the certificate to the file system 522 | if ($PSBoundParameters.ContainsKey('Passphrase')) 523 | { 524 | $bytes = $x509Certificate2.Export($CertificateFormat, $Passphrase) 525 | try 526 | { 527 | [System.IO.File]::WriteAllBytes($OutCertPath, $bytes) 528 | } 529 | finally 530 | { 531 | [array]::Clear($bytes, 0, $bytes.Length) 532 | } 533 | } 534 | else 535 | { 536 | $bytes = $x509Certificate2.Export($CertificateFormat) 537 | [System.IO.File]::WriteAllBytes($OutCertPath, $bytes) 538 | } 539 | 540 | Write-Host "Certificate written to $OutCertPath" 541 | 542 | # Return the certificate object for inspection 543 | return $x509Certificate2 544 | } 545 | } 546 | 547 | # Copy a hashtable with all the falsy entries removed 548 | # @{ x = 'x'; y = '' } -> @{ x = 'x' } 549 | function Get-FalsyRemovedHashtable 550 | { 551 | param([hashtable]$Hashtable) 552 | 553 | $outTable = @{} 554 | 555 | foreach ($key in $Hashtable.Keys) 556 | { 557 | if ($Hashtable[$key]) 558 | { 559 | $outTable[$key] = $Hashtable[$key] 560 | } 561 | } 562 | 563 | return $outTable 564 | } 565 | 566 | # Gets the appropriate extension for a given certificate format 567 | function Get-CertificateFormatExtension 568 | { 569 | param([System.Security.Cryptography.X509Certificates.X509ContentType]$CertificateFormat) 570 | 571 | switch ($CertificateFormat) 572 | { 573 | Cert 574 | { 575 | return 'cer' 576 | } 577 | 578 | Pfx 579 | { 580 | return 'pfx' 581 | } 582 | 583 | default 584 | { 585 | throw [System.NotSupportedException] "No extension known for format '$CertificateFormat'" 586 | } 587 | } 588 | } 589 | 590 | # Normalizes paths so that relative paths are interpreted 591 | # relative to PWD and returned as absolute 592 | function Get-AbsolutePathFromSupplied 593 | { 594 | param( 595 | [string]$Path 596 | ) 597 | 598 | if ([System.IO.Path]::IsPathRooted($Path)) 599 | { 600 | return $Path 601 | } 602 | 603 | return [System.IO.Path]::GetFullPath((Join-Path $PWD $Path)) 604 | } 605 | 606 | # Take a user-supplied path and fix it up to point to a file path that makes sense 607 | function Get-ProperFilePath 608 | { 609 | param( 610 | [string]$Path, 611 | [string]$DefaultFileName, 612 | [string]$RequiredExtension 613 | ) 614 | 615 | # If we're given a directory, point the path to a default filename in that directory 616 | if ($Path.EndsWith([System.IO.Path]::DirectorySeparatorChar) -or (Test-Path -Path $Path -PathType Container)) 617 | { 618 | return Join-Path $Path "$DefaultFileName.$RequiredExtension" 619 | } 620 | 621 | # We're pointing to a file, so just correct the extension if necessary 622 | if ([System.IO.Path]::GetExtension($Path) -ne $RequiredExtension) 623 | { 624 | return [System.IO.Path]::ChangeExtension($Path, $RequiredExtension) 625 | } 626 | 627 | return $Path 628 | } 629 | 630 | # Produce a new authority key identifier from the authority's subject key identifier 631 | function New-AuthorityKeyIdentifier 632 | { 633 | param( 634 | [System.Security.Cryptography.X509Certificates.X509SubjectKeyIdentifierExtension] 635 | $SubjectKeyIdentifier, 636 | 637 | [switch] 638 | $Critical 639 | ) 640 | 641 | # The canonical OID of an Authority Key Identifier 642 | $akiOid = "2.5.29.35" 643 | 644 | # AKI is not supported directly by .NET, we have to make our own 645 | # The ASN.1 rule we follow is: 646 | # AuthorityKeyId ::= SEQUENCE { keyIdentifier [0] IMPLICIT_OCTET_STRING } 647 | # Because nothing documents what that means in DER encoding: 648 | # - SEQUENCE: 0x30 tag, then length in bytes up to 0x79 649 | # - keyIdentifier: 650 | # - [0]: a context-specific tag (bit 8 = 1, bit 7 = 0) of value 0 (bits 6-1 = 0) 651 | # - IMPLICIT_OCTECT_STRING: no 0x04 octet string tag, first byte is length in bytes up to 0x79, then the string content 652 | # Example: 653 | # | SEQUENCE | [0] | IMPLICIT_OCTET_STRING | 0x01 0x02 0x03 0x04 654 | # | 0x30 0x06 | 0x80 | 0x04 | 0x01 0x02 0x03 0x04 655 | # sequence ^ length ^ octet string length 656 | # 657 | # For more information see: 658 | # - Microsoft's resources on this: https://docs.microsoft.com/en-us/windows/desktop/seccertenroll/about-certificate-request-encoding 659 | # - This helpful page: http://luca.ntop.org/Teaching/Appunti/asn1.html 660 | 661 | # Compose the key here 662 | # We could extract from the SKI's raw data, but the string is a safer bet 663 | $ski = $SubjectKeyIdentifier.SubjectKeyIdentifier 664 | $key = [System.Collections.Generic.List[byte]]::new() 665 | for ($i = 0; $i -lt $SubjectKeyIdentifier.SubjectKeyIdentifier.Length; $i += 2) 666 | { 667 | $x = $ski[$i] + $ski[$i+1] 668 | $b = [System.Convert]::ToByte($x, 16) 669 | [void]$key.Add($b) 670 | } 671 | 672 | # Ensure our assumptions about not having to encode too much are correct 673 | if ($key.Count + 2 -gt 0x79) 674 | { 675 | throw [System.InvalidOperationException] "Subject key identifier length is to high to encode: $($key.Count)" 676 | } 677 | 678 | [byte]$octetLength = $key.Count 679 | [byte]$sequenceLength = $octetLength+2 680 | 681 | [byte]$sequenceTag = 0x30 682 | [byte]$keyIdentifierTag = 0x80 683 | 684 | # Assemble the raw data 685 | [byte[]]$akiRawData = @($sequenceTag, $sequenceLength, $keyIdentifierTag, $octetLength) + $key 686 | 687 | # Construct the Authority Key Identifier extension 688 | return [System.Security.Cryptography.X509Certificates.X509Extension]::new( 689 | $akiOid, 690 | $akiRawData, 691 | $Critical) 692 | } 693 | 694 | <# 695 | .SYNOPSIS 696 | Displays the README for this module. 697 | 698 | .DESCRIPTION 699 | Retrieves the README for this module from the internet 700 | and displays in the format determined by parameters. 701 | 702 | .PARAMETER Online 703 | Opens the README page in the browser. 704 | 705 | .PARAMETER PassThru 706 | Passes through the README as a string object into the pipeline. 707 | 708 | .PARAMETER WriteToHost 709 | Writes the text of the README directly into the host. 710 | 711 | .PARAMETER NoVSCode 712 | Do not attempt to open the README in the current VSCode session. 713 | 714 | .PARAMETER NoMarkdown 715 | Do not attempt to render the README as markdown in the console. 716 | 717 | .PARAMETER NoEditor 718 | Do not attempt to create the README as a text file and open it in an editor. 719 | 720 | .PARAMETER NoMore 721 | Do not attempt to see the text file with more or less. 722 | 723 | .EXAMPLE 724 | # Displays the README in the console 725 | Open-SelfSignedCertificateReadMe 726 | 727 | .EXAMPLE 728 | # Opens the README page in the browser 729 | Open-SelfSignedCertificateReadMe -Online 730 | 731 | .EXAMPLE 732 | # Displays the README in the best way possible 733 | # without rendering markdown or using a text file 734 | Open-SelfSignedCertificateReadMe -NoMarkdown -NoTextFile 735 | 736 | .NOTES 737 | General notes 738 | #> 739 | 740 | function Open-SelfSignedCertificateReadMe 741 | { 742 | [CmdletBinding(DefaultParameterSetName='OptoutSwitches')] 743 | param( 744 | [Parameter(ParameterSetName='Online')] 745 | [switch] 746 | $Online, 747 | 748 | [Parameter(ParameterSetName='PassThru')] 749 | [switch] 750 | $PassThru, 751 | 752 | [Parameter(ParameterSetName='WriteHost')] 753 | [switch] 754 | $WriteToHost, 755 | 756 | [Parameter(ParameterSetName='OptoutSwitches')] 757 | [switch] 758 | $NoVSCode, 759 | 760 | [Parameter(ParameterSetName='OptoutSwitches')] 761 | [switch] 762 | $NoMarkdown, 763 | 764 | [Parameter(ParameterSetName='OptoutSwitches')] 765 | [switch] 766 | $NoEditor, 767 | 768 | [Parameter(ParameterSetName='OptoutSwitches')] 769 | [switch] 770 | $NoMore, 771 | 772 | [switch] 773 | $NoGui 774 | ) 775 | 776 | # Open in the browser if requested 777 | if ($Online -and -not $NoGui) 778 | { 779 | Start-Process 'https://github.com/rjmholt/SelfSignedCertificate#selfsignedcertificate' 780 | return 781 | } 782 | 783 | # Otherwise download the README 784 | $readmeContent = (Invoke-WebRequest -Uri 'https://raw.githubusercontent.com/rjmholt/SelfSignedCertificate/master/README.md').Content 785 | 786 | # If asked to pass it through as an object, comply 787 | if ($PassThru) 788 | { 789 | return $readmeContent 790 | } 791 | 792 | # Just write out if we are asked to 793 | if ($WriteToHost) 794 | { 795 | Write-Host $readmeContent 796 | return 797 | } 798 | 799 | # If we're already in VSCode, try that 800 | if (-not $NoVSCode -and -not $NoGui -and $env:VSCODE_CWD) 801 | { 802 | $tmpDir = [System.IO.Path]::GetTempPath() 803 | $filePath = [System.IO.Path]::Combine($tmpDir, 'SelfSignedCertificate-README.md') 804 | $readmeContent > $filePath 805 | 806 | if (Get-Command 'Open-EditorFile' -ErrorAction SilentlyContinue) 807 | { 808 | Open-EditorFile $filePath 809 | return 810 | } 811 | 812 | if (Get-Command 'code' -CommandType Application -ErrorAction SilentlyContinue) 813 | { 814 | code $filePath 815 | return 816 | } 817 | 818 | if (Get-Command 'code-insiders' -CommandType Application -ErrorAction SilentlyContinue) 819 | { 820 | code-insiders $filePath 821 | return 822 | } 823 | } 824 | 825 | # See if we can just render the markdown in the terminal 826 | if (-not $NoMarkdown -and (Get-Command 'ConvertFrom-Markdown' -CommandType Cmdlet -ErrorAction SilentlyContinue)) 827 | { 828 | ConvertFrom-Markdown -InputObject $readmeContent -AsVT100EncodedString | Show-Markdown 829 | return 830 | } 831 | 832 | # Try opening with less/more 833 | if (-not $NoMore) 834 | { 835 | # On *nix use less, since less is more 836 | if (Get-Command 'less' -CommandType Application -ErrorAction SilentlyContinue) 837 | { 838 | $readmeContent | less 839 | return 840 | } 841 | 842 | if (Get-Command 'more' -CommandType Application -ErrorAction SilentlyContinue) 843 | { 844 | $readmeContent | more 845 | return 846 | } 847 | } 848 | 849 | # Try opening in a text editor 850 | if (-not $NoEditor -and -not $NoGui) 851 | { 852 | $tmpDir = [System.IO.Path]::GetTempPath() 853 | $filePath = [System.IO.Path]::Combine($tmpDir, 'SelfSignedCertificate-README.txt') 854 | $readmeContent > $filePath 855 | 856 | # Check the *nix EDITOR env var 857 | if ($script:IsUnix -and $env:EDITOR) 858 | { 859 | & $env:EDITOR $filePath 860 | return 861 | } 862 | 863 | # Invoke-Item has sane defaults for txt files 864 | Invoke-Item $filePath 865 | return 866 | } 867 | 868 | # Finally admit defeat in trying to make things nice 869 | Write-Host $readmeContent 870 | } 871 | -------------------------------------------------------------------------------- /Tests/SelfSignedCertificate.Tests.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Robert Holt. All rights reserved. 2 | # Licensed under the MIT License. 3 | 4 | using module ..\SelfSignedCertificate 5 | 6 | function Get-MillisecondTruncatedTime 7 | { 8 | param([System.DateTimeOffset]$Time) 9 | 10 | return $Time.AddTicks(-$Time.Ticks % [timespan]::TicksPerSecond) 11 | } 12 | 13 | Describe "Generates a simple self signed certificate" { 14 | BeforeAll { 15 | Import-Module ([System.IO.Path]::Combine($PSScriptRoot, '..', 'SelfSignedCertificate')) 16 | 17 | $certSubject = @{ 18 | CommonName = 'donotuse.example.info' 19 | Country = 'US' 20 | StateOrProvince = 'Nebraska' 21 | Locality = 'Omaha' 22 | Organization = 'Umbrella Corporation' 23 | OrganizationalUnit = 'Marketing' 24 | EmailAddress = 'donotreply@umbrella.com' 25 | } 26 | 27 | $certParameters = @{ 28 | OutCertPath = Join-Path $TestDrive 'cert.pfx' 29 | FriendlyName = 'Test Certificate' 30 | KeyLength = 4096 31 | KeyUsage = [System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::DigitalSignature,[System.Security.Cryptography.X509Certificates.X509KeyUsageFlags]::DataEncipherment 32 | CertificateFormat = [System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx 33 | EnhancedKeyUsage = 'ServerAuthentication','ClientAuthentication' 34 | ForCertificateAuthority = $true 35 | Passphrase = ConvertTo-SecureString -Force -AsPlainText 'password' 36 | StartDate = [System.DateTimeOffset]::Now.Subtract([timespan]::FromDays(1)) 37 | Duration = [timespan]::FromDays(365) 38 | } + $certSubject 39 | 40 | New-SelfSignedCertificate @certParameters 41 | 42 | $distinguishedName = [CertificateDistinguishedName]$certSubject 43 | 44 | $loadedCert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($certParameters.OutCertPath, $certParameters.Passphrase) 45 | } 46 | 47 | It "Has the correct friendly name" { 48 | $loadedCert.FriendlyName | Should -BeExactly $certParameters.FriendlyName 49 | } 50 | 51 | It "Renders the distinguished name correctly" { 52 | $loadedCert.Issuer | Should -BeExactly $distinguishedName.ToString() 53 | } 54 | 55 | It "Has the issuer and subject as the same entity" { 56 | $loadedCert.Subject | Should -BeExactly $loadedCert.Issuer 57 | } 58 | 59 | It "Has the correct cryptographic parameters" { 60 | $sha256RsaOid = "1.2.840.113549.1.1.11" 61 | $loadedCert.SignatureAlgorithm.Value | Should -Be $sha256RsaOid 62 | $loadedCert.PublicKey.Key.KeySize | Should -Be $certParameters.KeyLength 63 | } 64 | 65 | It "Has the expected start date" { 66 | $expectedTime = Get-MillisecondTruncatedTime -Time $certParameters.StartDate 67 | $loadedCert.NotBefore | Should -Be $expectedTime 68 | } 69 | 70 | It "Has the expected expiry date" { 71 | $expiryDate = $certParameters.StartDate.Add($certParameters.Duration) 72 | $expectedTime = Get-MillisecondTruncatedTime -Time $expiryDate 73 | $loadedCert.NotAfter | Should -Be $expectedTime 74 | } 75 | 76 | It "Has a basic constraints extension" { 77 | $constraints = ($loadedCert.Extensions | Where-Object { $_ -is [System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension] })[0] 78 | 79 | $basicConstraintsOid = "2.5.29.19" 80 | 81 | $constraints.Critical | Should -BeFalse 82 | $constraints.HasPathLengthConstraint | Should -BeFalse 83 | $constraints.CertificateAuthority | Should -BeTrue 84 | $constraints.Oid.Value | Should -Be $basicConstraintsOid 85 | } 86 | 87 | It "Has a Subject Key Idenitifer" { 88 | $ski = ($loadedCert.Extensions | Where-Object { $_ -is [System.Security.Cryptography.X509Certificates.X509SubjectKeyIdentifierExtension] })[0] 89 | 90 | $skiOid = "2.5.29.14" 91 | 92 | $ski.Critical | Should -BeFalse 93 | $ski.Oid.Value | Should -Be $skiOid 94 | } 95 | 96 | It "Has an Authority Key Identifier" { 97 | $akiOid = "2.5.29.35" 98 | 99 | $aki = ($loadedCert.Extensions | Where-Object { $_.Oid.Value -eq $akiOid }) 100 | 101 | $aki.Critical | Should -BeFalse 102 | $aki.Oid.Value | Should -Be $akiOid 103 | } 104 | 105 | It "Subject Key Identifier and Authority Key Identifier agree" { 106 | $ski = ($loadedCert.Extensions | Where-Object { $_ -is [System.Security.Cryptography.X509Certificates.X509SubjectKeyIdentifierExtension] })[0] 107 | $aki = ($loadedCert.Extensions | Where-Object { $_.Oid.Value -eq "2.5.29.35" })[0] 108 | 109 | $authorityIdentifier = ($aki.RawData[4..23] | ForEach-Object { $_.ToString('X2') }) -join '' 110 | 111 | $authorityIdentifier | Should -Be $ski.SubjectKeyIdentifier 112 | } 113 | } 114 | --------------------------------------------------------------------------------