diff --git a/eng/common/TestResources/README.md b/eng/common/TestResources/README.md index 757c7c337ac..b63307e2453 100644 --- a/eng/common/TestResources/README.md +++ b/eng/common/TestResources/README.md @@ -89,6 +89,7 @@ Below is an example of how `$templateFileParameters` can be used to pass data fr **Snippet from `test-resources-pre.ps1`** ```powershell +Import-Module -Name ./eng/common/scripts/X509Certificate2 $cert = New-X509Certificate2 -SubjectName 'E=opensource@microsoft.com, CN=Azure SDK, OU=Azure SDK, O=Microsoft, L=Frisco, S=TX, C=US' -ValidDays 3652 # Create new entries in $templateFileParameters $templateFileParameters['ConfidentialLedgerPrincipalPEM'] = Format-X509Certificate2 -Certificate $cert diff --git a/eng/common/scripts/X509Certificate2/README.md b/eng/common/scripts/X509Certificate2/README.md new file mode 100644 index 00000000000..9941994c1c9 --- /dev/null +++ b/eng/common/scripts/X509Certificate2/README.md @@ -0,0 +1,24 @@ +Powershell module for generating self-signed x509 certificates + +Usage: + +```powershell +Import-Module -Name ./eng/common/scripts/X509Certificate2 # assumes $PWD is repo root + +$cert1 = New-X509Certificate2 -SubjectName 'E=opensource@microsoft.com, CN=Azure SDK, OU=Azure SDK, O=Microsoft, L=Redmond, S=WA, C=US' -ValidDays 3652 + +$CaPublicKeyBase64 = $cert1 | Format-X509Certificate2 -Type CertificateBase64 +$CaPrivateKeyPem = $cert1 | Format-X509Certificate2 -Type Pkcs1 +$CaKeyPairPkcs12Base64 = $cert1 | Format-X509Certificate2 -Type Pkcs12Base64 +``` + +With V3 extensions + +```powershell +Import-Module -Name eng/scripts/X509Certificate2.psm1 # assumes $PWD is repo root + +$cert2 = New-X509Certificate2 -SubjectName 'CN=Azure SDK' -SubjectAlternativeNames (New-X509Certificate2SubjectAlternativeNames -EmailAddress azuresdk@microsoft.com) -KeyUsageFlags KeyEncipherment, NonRepudiation, DigitalSignature -CA -TLS -ValidDays 3652 + +$PemCertificateWithV3Extensions = ($cert2 | Format-X509Certificate2 -Type Certificate) + "`n" + ($cert2 | Format-X509Certificate2 -Type Pkcs8) +$CertificateWithV3ExtensionsBase64 = $cert2 | Format-X509Certificate2 -Type CertificateBase64 + ``` diff --git a/eng/common/scripts/X509Certificate2/X509Certificate2.psm1 b/eng/common/scripts/X509Certificate2/X509Certificate2.psm1 new file mode 100644 index 00000000000..f37ba1d5840 --- /dev/null +++ b/eng/common/scripts/X509Certificate2/X509Certificate2.psm1 @@ -0,0 +1,339 @@ +#Requires -Version 6.0 + +using namespace System.Security.Cryptography +using namespace System.Security.Cryptography.X509Certificates +using namespace System.Text + +<# +.Synopsis +Generate an X509 v3 certificate. + +.Description +Generates an [X509Certificate2] from either a subject name, or individual X500 distinguished names. + +.Parameter SubjectName +The entire X500 subject name. + +.Parameter Country +The country e.g., "US". + +.Parameter State +The state or province e.g., "WA". + +.Parameter City +The city or locality e.g., "Redmond". + +.Parameter Organization +The organization e.g., "Microsoft". + +.Parameter Department +The department e.g., "Azure SDK". + +.Parameter CommonName +A common name e.g., "www.microsoft.com". + +.Parameter SubjectAlternativeNames +Additional subject names from New-X509Certificate2SubjectAlternativeNames. + +.Parameter KeySize +Size of the RSA key. + +.Parameter KeyUsageFlags +Additional key usage flags. + +.Parameter CA +Create self-signed certificate authority. + +.Parameter TLS +Create self-signed certificate suitable for TLS. + +.Parameter NotBefore +The start date when the certificate is valid. The default is the current time. + +.Parameter ValidDays +How many days from NotBefore until the certificate expires. + +.Example +New-X509Certificate2 -SubjectName 'E=opensource@microsoft.com, CN=Azure SDK, OU=Azure SDK, O=Microsoft, L=Redmond, S=WA, C=US' -ValidDays 3652 + +Create a self-signed certificate valid for 10 years from now. + +.Example +New-X509Certificate2 -SubjectName 'CN=Azure SDK' -SubjectAlternativeNames (New-X509Certificate2SubjectAlternativeNames -EmailAddress azuresdk@microsoft.com) -KeyUsageFlags KeyEncipherment, NonRepudiation, DigitalSignature -CA -TLS -ValidDays 3652 + +Create a self-signed certificate valid for 10 years from now with an alternative name, additional key usages including TLS connections, and that can sign other certificate requests. +#> +function New-X509Certificate2 { + [CmdletBinding(DefaultParameterSetName='SubjectName')] + [OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2])] + param ( + [Parameter(ParameterSetName='SubjectName', Mandatory=$true, Position=0)] + [string] $SubjectName, + + [Parameter(ParameterSetName='Builder', HelpMessage='Country Name (2 letter code)')] + [Alias('C')] + [string] $Country, + + [Parameter(ParameterSetName='Builder', HelpMessage='State or Province Name (full name)')] + [Alias('S', 'Province')] + [string] $State, + + [Parameter(ParameterSetName='Builder', HelpMessage='Locality Name (eg, city)')] + [Alias('L', 'Locality')] + [string] $City, + + [Parameter(ParameterSetName='Builder', HelpMessage='Organization Name (eg, company)')] + [Alias('O')] + [string] $Organization, + + [Parameter(ParameterSetName='Builder', HelpMessage='Organizational Unit Name (eg, section)')] + [Alias('OU')] + [string] $Department, + + [Parameter(ParameterSetName='Builder', HelpMessage='Common Name (e.g. server FQDN or YOUR name)')] + [Alias('CN')] + [string] $CommonName, + + [Parameter()] + [ValidateNotNull()] + [SubjectAlternativeNameBuilder] $SubjectAlternativeNames, + + [Parameter()] + [ValidateSet(1024, 2048, 4096)] + [int] $KeySize = 2048, + + [Parameter()] + [X509KeyUsageFlags] $KeyUsageFlags, + + [Parameter()] + [switch] $CA, + + [Parameter()] + [switch] $TLS, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [DateTimeOffset] $NotBefore = [DateTimeOffset]::Now, + + [Parameter()] + [ValidateRange(1, [int]::MaxValue)] + [int] $ValidDays = 365 + ) + + if ($PSCmdlet.ParameterSetName -eq 'Builder') { + $sb = [StringBuilder]::new() + if ($Country) { $null = $sb.Append("C=$Country,") } + if ($State) { $null = $sb.Append("S=$State, ") } + if ($City) { $null = $sb.Append("L=$City, ") } + if ($Organization) { $null = $sb.Append("O=$Organization, ") } + if ($Department) { $null = $sb.Append("OU=$Department, ") } + if ($CommonName) { $null = $sb.Append("CN=$CommonName, ") } + + $SubjectName = [X500DistinguishedName]::new($sb.ToString()).Format($false) + } + + $rsa = [RSA]::Create($KeySize) + try { + $req = [CertificateRequest]::new( + [string] $SubjectName, + $rsa, + [HashAlgorithmName]::SHA256, + [RSASignaturePadding]::Pkcs1 + ) + + $req.CertificateExtensions.Add( + [X509BasicConstraintsExtension]::new( + $CA, + $false, + 0, + $true + ) + ) + + if ($SubjectAlternativeNames) { + $req.CertificateExtensions.Add( + $SubjectAlternativeNames.Build($false) + ) + } + + if ($KeyUsageFlags) { + $req.CertificateExtensions.Add( + [X509KeyUsageExtension]::new( + $KeyUsageFlags, + $true + ) + ) + } + + if ($TLS) { + $oids = [OidCollection]::new() + $null = $oids.Add([Oid]::new('1.3.6.1.5.5.7.3.1')) + + $req.CertificateExtensions.Add( + [X509EnhancedKeyUsageExtension]::new( + $oids, + $false + ) + ) + } + + $req.CreateSelfSigned($NotBefore, $NotBefore.AddDays($ValidDays)) + } + finally { + $rsa.Dispose() + } +} + +<# +.Synopsis +Create subject alternative names for New-X509Certificate2. + +.Description +Subject alternative names include DNS names, email addresses, and IP addresses for which a certificate may also authenticate connections. + +.Parameter DnsName +One or more DNS names e.g., "www.microsoft.com". + +.Parameter EmailAddress +One or more email addresses e.g., "opensource@microsoft.com". + +.Parameter IpAddress +One or more IP addresses. + +.Parameter Uri +Additional URIs not covered by other options. + +.Parameter UserPrincipalName +Additional user names not covered by other options. +#> +function New-X509Certificate2SubjectAlternativeNames { + [CmdletBinding()] + [OutputType([System.Security.Cryptography.X509Certificates.SubjectAlternativeNameBuilder])] + param ( + [Parameter()] + [ValidateNotNullOrEmpty()] + [string[]] $DnsName, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string[]] $EmailAddress, + + [Parameter()] + [ValidateScript({[System.Net.IPAddress]::TryParse($_, [ref] $null)})] + [string[]] $IpAddress, + + [Parameter()] + [ValidateScript({[System.Uri]::TryParse($_, [ref] $null)})] + [string[]] $Uri, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string[]] $UserPrincipalName + ) + + $subjectAlternativeNames = [SubjectAlternativeNameBuilder]::new() + + foreach ($value in $DnsName) { + $subjectAlternativeNames.AddDnsName($value) + } + + foreach ($value in $EmailAddress) { + $subjectAlternativeNames.AddEmailAddress($value) + } + + foreach ($value in $IpAddress) { + $subjectAlternativeNames.AddIpAddress($value) + } + + foreach ($value in $Uri) { + $subjectAlternativeNames.AddUri($value) + } + + foreach ($value in $UserPrincipalName) { + $subjectAlternativeNames.AddUserPrincipalName($value) + } + + $subjectAlternativeNames +} + +<# +.Synopsis +Exports a certificate to a file. + +.Description +Exports an X509Certificate2 to a file in one of the given formats. + +.Parameter Path +The path to the file to save. + +.Parameter Type +The type of encoding for the file to save. + +.Parameter Certificate +The certificate to save. +#> +function Export-X509Certificate2 { + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true, Position=0)] + [string] $Path, + + [Parameter(Position=1)] + [ValidateSet('Certificate', 'CertificateBase64', 'Pfx', 'Pkcs1', 'Pkcs12', 'Pkcs12Base64', 'Pkcs8', 'PrivateKey')] + [string] $Type = 'Pfx', + + [Parameter(Mandatory=$true, ValueFromPipeline=$true)] + [X509Certificate2] $Certificate + ) + + if ($Type -in 'Pfx', 'Pkcs12') { + $Certificate.Export([X509ContentType]::Pfx) | Set-Content $Path -AsByteStream + } else { + Format-X509Certificate2 -Type $Type -Certificate $Certificate | Set-Content $Path -Encoding ASCII + } +} + +<# +.Synopsis +Formats a certificate. + +.Description +Formats a certificate and prints it to the output buffer e.g., console. Useful for piping to clip.exe in Windows and pasting into code (additional formatting may be required). + +.Parameter Type +The type of encoding for the output. + +.Parameter Certificate +The certificate to format. +#> +function Format-X509Certificate2 { + [CmdletBinding()] + param ( + [Parameter(Position=0)] + [ValidateSet('Certificate', 'CertificateBase64', 'Pkcs1', 'Pkcs12Base64', 'Pkcs8', 'PrivateKey')] + [string] $Type = 'Certificate', + + [Parameter(Mandatory=$true, ValueFromPipeline=$true)] + [X509Certificate2] $Certificate + ) + + function ConvertTo-Pem($tag, $data) { + @" +-----BEGIN $tag----- +$([Convert]::ToBase64String($data, 'InsertLineBreaks')) +-----END $tag----- +"@ + } + + if ($Type -eq 'Certificate') { + ConvertTo-Pem 'CERTIFICATE' $Certificate.RawData + } elseif ($Type -eq 'CertificateBase64') { + [Convert]::ToBase64String($Certificate.RawData, 'InsertLineBreaks') + } elseif ($Type -eq 'Pkcs1') { + ConvertTo-Pem 'RSA PRIVATE KEY' $Certificate.PrivateKey.ExportRSAPrivateKey() + } elseif ($Type -eq 'Pkcs12Base64') { + [Convert]::ToBase64String($Certificate.Export([X509ContentType]::Pfx), 'InsertLineBreaks') + } elseif ($Type -in 'Pkcs8', 'PrivateKey') { + ConvertTo-Pem 'PRIVATE KEY' $Certificate.PrivateKey.ExportPkcs8PrivateKey() + } +}