tools/CACertificates/ca-certs.ps1 (282 lines of code) (raw):

# # Routines to help *experiment* (not necessarily productize) CA certificates. # # This will make PowerShell complain more on unsafe practices Set-StrictMode -Version 2.0 # # Globals # # Errors in system routines will stop script execution $errorActionPreference = "stop" $_rootCertCommonName = "Azure IoT CA TestOnly Root CA" $_rootCertSubject = "CN=$_rootCertCommonName" $_intermediateCertCommonName = "Azure IoT CA TestOnly Intermediate {0} CA" $_intermediateCertSubject = "CN=$_intermediateCertCommonName" $rootCACerFileName = "./RootCA.cer" $rootCAPemFileName = "./RootCA.pem" $intermediate1CAPemFileName = "./Intermediate1.pem" $intermediate2CAPemFileName = "./Intermediate2.pem" $intermediate3CAPemFileName = "./Intermediate3.pem" # Variables containing file paths for Edge certificates $edgePublicCertDir = "certs" $edgePrivateCertDir = "private" $edgeDeviceCertificate = Join-Path $edgePublicCertDir "new-edge-device.cert.pem" $edgeDevicePrivateKey = Join-Path $edgePrivateCertDir "new-edge-device.key.pem" $edgeDeviceFullCertChain = Join-Path $edgePublicCertDir "new-edge-device-full-chain.cert.pem" $edgeIotHubOwnerCA = Join-Path $edgePublicCertDir "azure-iot-test-only.root.ca.cert.pem" $_edgeDeviceCACNSuffix = ".ca" # Whether to use ECC or RSA is stored in a file. If it doesn't exist, we default to ECC. $algorithmUsedFile = "./algorithmUsed.txt" # The script puts certs into the global certificate store. If there is already a cert of the # same name present, we're not going to be able to tell the new apart from the old, so error out. function Test-CACertNotInstalledAlready() { Write-Host ("Testing if any test certificates have already been installed...") $certInstalled = $null try { $certInstalled = Get-CACertsCertBySubjectName $_rootCertSubject } catch { } if ($NULL -ne $certInstalled) { $nl = [Environment]::NewLine $cleanup_msg = "$nl$nl" $cleanup_msg += "To fix this, cleanup any certificates in the certificate store and try running this script again.$nl" $cleanup_msg += "Steps to cleanup, from Start menu, 'open manage computer certificates':$nl" $cleanup_msg += " - Navigate to Certificates -> Trusted Root Certification Authority -> Certificates. Remove certificates issued by 'Azure IoT CA TestOnly*'.$nl" $cleanup_msg += " - Navigate to Certificates -> Intermediate Certificate Authorities -> Certificates. Remove certificates issued by 'Azure IoT CA TestOnly*'.$nl" $cleanup_msg += " - Navigate to Certificates -> Local Computer -> Personal. Remove certificates issued by 'Azure IoT CA TestOnly*'.$nl" $cleanup_msg += "$nl$nl" Write-Error("Certificate {0} already installed in the certificate store. {1}" -f $_rootCertSubject, $cleanup_msg) throw ("Certificate {0} already installed." -f $_rootCertSubject) } Write-Host (" Ok.") } <# Verify that the prerequisites for this script are met #> function Test-CACertsPrerequisites() { Test-CACertNotInstalledAlready Write-Host ("Testing if openssl.exe is set in PATH...") if ((Get-Command "openssl.exe" -ErrorAction SilentlyContinue) -eq $NULL) { throw ("Openssl is unavailable. Please install openssl and set it in the PATH before proceeding.") } Write-Host (" Ok.") Write-Host ("Testing if environment variable OPENSSL_CONF is set...") if ($NULL -eq $ENV:OPENSSL_CONF) { throw ("Environment variable OPENSSL_CONF was not set, OpenSSL configuration not set on this system.") } Write-Host (" Ok.") Write-Host "Success" } function New-CACertsSelfsignedCertificate([string]$commonName, [object]$signingCert, [bool]$isASigner=$true) { # Build up argument list $selfSignedArgs =@{"-DnsName"=$commonName; "-CertStoreLocation"="cert:\LocalMachine\My"; "-NotAfter"=(get-date).AddDays(30); } if ($isASigner -eq $true) { $selfSignedArgs += @{"-KeyUsage"="CertSign"; } $selfSignedArgs += @{"-TextExtension"= @(("2.5.29.19={text}ca=TRUE&pathlength=12")) ; } } else { $selfSignedArgs += @{"-TextExtension"= @("2.5.29.37={text}1.3.6.1.5.5.7.3.2,1.3.6.1.5.5.7.3.1", "2.5.29.19={text}ca=FALSE&pathlength=0") } } if ($signingCert -ne $null) { $selfSignedArgs += @{"-Signer"=$signingCert } } if ((Get-CACertsCertUseRSA) -eq $false) { $selfSignedArgs += @{"-KeyAlgorithm"="ECDSA_nistP256"; "-CurveExport"="CurveName" } } # Now use splatting to process this Write-Warning ("Generating certificate CN={0} which is for prototyping, NOT PRODUCTION. It has a hard-coded password and will expire in 30 days." -f $commonName) write (New-SelfSignedCertificate @selfSignedArgs) } function New-CACertsIntermediateCert([string]$commonName, [Microsoft.CertificateServices.Commands.Certificate]$signingCert, [string]$pemFileName) { $certFileName = ($commonName + ".cer") $newCert = New-CACertsSelfsignedCertificate $commonName $signingCert Export-Certificate -Cert $newCert -FilePath $certFileName -Type CERT | Out-Null Import-Certificate -CertStoreLocation "cert:\LocalMachine\CA" -FilePath $certFileName | Out-Null # Store public PEM for later chaining openssl x509 -inform der -in $certFileName -out $pemFileName del $certFileName Write-Output $newCert } # Creates a new certificate chain. function New-CACertsCertChain([Parameter(Mandatory=$TRUE)][ValidateSet("rsa","ecc")][string]$algorithm) { Write-Host "Beginning to install certificate chain to your LocalMachine\My store" Test-CACertNotInstalledAlready # Store the algorithm we're using in a file so later stages always use the same one (without forcing user to keep passing it around) Set-Content $algorithmUsedFile $algorithm $rootCACert = New-CACertsSelfsignedCertificate $_rootCertCommonName $null Export-Certificate -Cert $rootCACert -FilePath $rootCACerFileName -Type CERT Import-Certificate -CertStoreLocation "cert:\LocalMachine\Root" -FilePath $rootCACerFileName openssl x509 -inform der -in $rootCACerFileName -out $rootCAPemFileName $intermediateCert1 = New-CACertsIntermediateCert ($_intermediateCertCommonName -f "1") $rootCACert $intermediate1CAPemFileName $intermediateCert2 = New-CACertsIntermediateCert ($_intermediateCertCommonName -f "2") $intermediateCert1 $intermediate2CAPemFileName $intermediateCert3 = New-CACertsIntermediateCert ($_intermediateCertCommonName -f "3") $intermediateCert2 $intermediate3CAPemFileName Write-Host "Success" } # Get-CACertsCertUseEdge retrieves the algorithm (RSA vs ECC) that was specified during New-CACertsCertChain function Get-CACertsCertUseRsa() { Write-Output ((Get-Content $algorithmUsedFile -ErrorAction SilentlyContinue) -eq "rsa") } function Get-CACertsCertBySubjectName([string]$subjectName) { $certificates = gci -Recurse Cert:\LocalMachine\ |? { $_.gettype().name -eq "X509Certificate2" } $cert = $certificates |? { $_.subject -eq $subjectName -and $_.PSParentPath -eq "Microsoft.PowerShell.Security\Certificate::LocalMachine\My" } if ($NULL -eq $cert) { throw ("Unable to find certificate with subjectName {0}" -f $subjectName) } Write-Output $cert } function New-CACertsVerificationCert([string]$requestedCommonName) { $verifyRequestedFileName = ".\verifyCert4.cer" $rootCACert = Get-CACertsCertBySubjectName $_rootCertSubject Write-Host "Using Signing Cert:::" Write-Host $rootCACert $verifyCert = New-CACertsSelfsignedCertificate $requestedCommonName $rootCACert $false Export-Certificate -cert $verifyCert -filePath $verifyRequestedFileName -Type Cert if (-not (Test-Path $verifyRequestedFileName)) { throw ("Error: CERT file {0} doesn't exist" -f $verifyRequestedFileName) } Write-Host ("Certificate with subject CN={0} has been output to {1}" -f $requestedCommonName, (Join-Path (get-location).path $verifyRequestedFileName)) } function New-CACertsDevice() { param([Parameter(Mandatory=$true)][string]$deviceName, [Parameter(Mandatory=$true)][System.Security.SecureString]$certPassword, [string]$signingCertSubject=$_rootCertSubject, [bool]$isEdgeDevice=$false ) $newDevicePfxFileName = ("./{0}.pfx" -f $deviceName) $newDevicePemAllFileName = ("./{0}-all.pem" -f $deviceName) $newDevicePemPrivateFileName = ("./{0}-private.pem" -f $deviceName) $newDevicePemPublicFileName = ("./{0}-public.pem" -f $deviceName) $signingCert = Get-CACertsCertBySubjectName $signingCertSubject ## "CN=Azure IoT CA TestOnly Intermediate 1 CA" # Certificates for edge devices need to be able to sign other certs. if ($isEdgeDevice -eq $true) { $isASigner = $true } else { $isASigner = $false } $newDeviceCertPfx = New-CACertsSelfSignedCertificate $deviceName $signingCert $isASigner # Export the PFX of the cert we've just created. The PFX is a format that contains both public and private keys but is NOT something # clients written to IOT Hub SDK's now how to process, so we'll need to do some massaging. Export-PFXCertificate -cert $newDeviceCertPfx -filePath $newDevicePfxFileName -password $certPassword if (-not (Test-Path $newDevicePfxFileName)) { throw ("Error: CERT file {0} doesn't exist" -f $newDevicePfxFileName) } # Begin the massaging. First, turn the PFX into a PEM file which contains public key, private key, and bunches of attributes. # We're closer to what IOTHub SDK's can handle but not there yet. openssl pkcs12 -in $newDevicePfxFileName -out $newDevicePemAllFileName -nodes # Now that we have a PEM, do some conversions on it to get formats we can process if ((Get-CACertsCertUseRSA) -eq $true) { openssl rsa -in $newDevicePemAllFileName -out $newDevicePemPrivateFileName } else { openssl ec -in $newDevicePemAllFileName -out $newDevicePemPrivateFileName } openssl x509 -in $newDevicePemAllFileName -out $newDevicePemPublicFileName Write-Host ("Certificate with subject CN={0} has been output to {1}" -f $deviceName, (Join-Path (get-location).path $newDevicePemPublicFileName)) } function New-CACertsEdgeDevice() { param([Parameter(Mandatory=$true)][string]$deviceName, [Parameter(Mandatory=$true)][System.Security.SecureString]$certPassword, [string]$signingCertSubject=($_intermediateCertSubject -f "1") ) # Note: Appending a '.ca' to the common name is useful in situations # where a user names their hostname as the edge device name. # By doing so we avoid TLS validation errors where we have a server or # client certificate where the hostname is used as the common name # which essentially results in "loop" for validation purposes. $deviceName += $_edgeDeviceCACNSuffix New-CACertsDevice $deviceName $certPassword $signingCertSubject $true } function Write-CACertsCertificatesToEnvironment([string]$deviceName, [string]$iothubName, [bool]$useIntermediate) { $newDevicePemPrivateFileName = ("./{0}-private.pem" -f $deviceName) $newDevicePemPublicFileName = ("./{0}-public.pem" -f $deviceName) $rootCAPem = Get-CACertsPemEncodingForEnvironmentVariable $rootCAPemFileName $devicePublicPem = Get-CACertsPemEncodingForEnvironmentVariable $newDevicePemPublicFileName $devicePrivatePem = Get-CACertsPemEncodingForEnvironmentVariable $newDevicePemPrivateFileName if ($useIntermediate -eq $true) { $intermediate1CAPem = Get-CACertsPemEncodingForEnvironmentVariable $intermediate1CAPemFileName } else { $intermediate1CAPem = $null } $env:IOTHUB_CA_X509_PUBLIC = $devicePublicPem + $intermediate1CAPem + $rootCAPem $env:IOTHUB_CA_X509_PRIVATE_KEY = $devicePrivatePem $env:IOTHUB_CA_CONNECTION_STRING_TO_DEVICE = "HostName={0};DeviceId={1};x509=true" -f $iothubName, $deviceName if ((Get-CACertsCertUseRSA) -eq $true) { $env:IOTHUB_CA_USE_ECC = "0" } else { $env:IOTHUB_CA_USE_ECC = "1" } Write-Host "Success" } # Outputs certificates for Edge device using naming conventions from tutorials function Write-CACertsCertificatesForEdgeDevice([string]$deviceName) { $deviceName += $_edgeDeviceCACNSuffix $originalDevicePublicPem = ("./{0}-public.pem" -f $deviceName) $originalDevicePrivatePem = ("./{0}-private.pem" -f $deviceName) if (-not (Test-Path $edgePublicCertDir)) { mkdir $edgePublicCertDir | Out-Null } if (-not (Test-Path $edgePrivateCertDir)) { mkdir $edgePrivateCertDir | Out-Null } Copy-Item $originalDevicePublicPem $edgeDeviceCertificate Copy-Item $originalDevicePrivatePem $edgeDevicePrivateKey Get-Content $originalDevicePublicPem, $intermediate1CAPemFileName, $rootCAPemFileName | Set-Content $edgeDeviceFullCertChain Copy-Item $rootCAPemFileName $edgeIotHubOwnerCA Write-Host "Success" } # This will read in a given .PEM file and output it in a format that we can # immediately set ENV variable in it with \r\n done right. function Get-CACertsPemEncodingForEnvironmentVariable([string]$fileName) { $outputString = $null $data = Get-Content $fileName foreach ($line in $data) { $outputString += ($line + "`r`n") } Write-Output $outputString } Write-Warning "This script is provided for prototyping only." Write-Warning "DO NOT USE CERTIFICATES FROM THIS SCRIPT FOR PRODUCTION!"