Get-LetsEncrypt.ps1

#requires -Modules AcmeSharp, Azure, AzureRM.Websites

<#PSScriptInfo
 
.VERSION 1.5
.GUID ef19a6c1-9075-429a-8193-ea54530b2ff7
.AUTHOR Poy Chang
.COMPANYNAME
.COPYRIGHT
.TAGS LetsEncrypt SSL Azure Automation
.LICENSEURI
.PROJECTURI
.ICONURI
.REQUIREDSCRIPTS
 
#>


<#
 
.DESCRIPTION
 此指令碼能夠自動取得/更新 Let's Encrypt 的 SSL 憑證並套用至 Azure Web App 網站中。可用 Azure Automation 自動化執行。
 
#>
 

param(
    ## The domain to generate a certificate for
    [Parameter(Mandatory, HelpMessage="請輸入您的網域名稱,例如 example.com")]
    [String] $Domain,

    ## The email used for Let's Encrypt registration
    [Parameter(Mandatory, HelpMessage="請輸入您要用來接收來自 Let's Encrypt 通知訊息的電子郵件")]
    [String] $RegistrationEmail,

    ## The resource group that contains the web app
    [Parameter(Mandatory, HelpMessage="請輸入您的 Azure 資源群組")]
    [String] $ResourceGroup,

    ## The web app name / ID
    [Parameter(Mandatory, HelpMessage="請輸入您的 Web App 資源名稱")]
    [String] $WebApp,

    ## True if hosting application is Unix-based, so we won't send the confirmation files to /key/index.html, but to /key directly
    [Parameter(HelpMessage="若您使用 Unix 環境,請開啟此設定")]
    [Switch] $UseUnixFileVerification
)

Set-StrictMode -Version Latest

function GetSafeFilename
{
    param(
        $BasePath = ".",
        $Text,
        $Extension = ".txt"
    )

    ## Remove invalid filesystem characters
    $invalidChars = [IO.Path]::GetInvalidFileNameChars()
    $invalidCharsRegex = "[" + (-join ($invalidChars | % { [Regex]::Escape($_) })) + "]"
    $baseFilename = $Text -replace $invalidCharsRegex,'_'

    ## Avoid reserved device names
    $reservedDeviceNames = -split "CON PRN AUX NUL COM1 COM2 COM3 COM4 COM5 COM6 COM7 COM8 COM9 LPT1 LPT2 LPT3 LPT4 LPT5 LPT6 LPT7 LPT8 LPT9"
    if($baseFilename -in $reservedDeviceNames)
    {
        $baseFilename = "_" + $baseFilename
    }

    ## Avoid path length issues
    $baseFilename = $baseFilename.Substring(0, [Math]::Min(50, $baseFilename.Length))

    ## Avoid existing files
    $counter = 1
    $fileName = $baseFilename + $Extension
    while(Test-Path (Join-Path $BasePath $fileName))
    {
        $filename = $baseFilename + "_${counter}${Extension}"
        $counter++
    }

    # Emit the result
    $fileName.Trim()
}

## Helper function to use Azure Kudu to publish a file to a site
function PublishWebsiteFile
{
    param(
        [Parameter(Mandatory)]
        $ResourceGroup,

        [Parameter(Mandatory)]
        $WebApp,

        [Parameter(Mandatory)]
        $PublishSettingsFile,

        [Parameter(Mandatory)]
        $RemotePath,

        [Parameter(Mandatory)]
        $FileContent
    )

    $RemotePath = $RemotePath.Trim("/\")

    ## Extract the publish settings credential from the publish settings file
    $publishSettings = [xml] (Get-Content $PublishSettingsFile -Raw)
    $ftpPublishSettings = $publishSettings.publishData.publishProfile | ? publishMethod -eq MSDeploy

    $username = $ftpPublishSettings.userName
    $password = $ftpPublishSettings.userPWD
    $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $username,$password)))

    ## Invoke the Kudu API
    $apiBaseUrl = "https://$WebApp.scm.azurewebsites.net/api"
    Invoke-RestMethod -Uri "$apiBaseUrl/vfs/site/wwwroot/$RemotePath" -Headers @{Authorization=("Basic {0}" -f $base64AuthInfo); 'If-Match' = '*'} -Method PUT -Body $FileContent
}

## Store temporary files / certificates in ./<domain>
$outputDirectory = GetSafeFilename -Text $Domain -Extension ""
if(-not (Test-Path $outputDirectory))
{
    $null = New-Item -Type Directory $outputDirectory
}

Write-Progress "Creating Let's Encrypt registration"

if(-not (Get-AcmeVault))
{
    $null = Initialize-ACMEVault -BaseURI https://acme-v01.api.letsencrypt.org/
}

## Create a new identifier for this request
$identifier = -join (([int][char]'a'..[int][char]'z') | Get-Random -Count 10 | % { [char] $_ })
$null = New-ACMERegistration -Contacts mailto:$RegistrationEmail -AcceptTos
$null = New-ACMEIdentifier -Dns $domain -Alias $identifier

## Get the challenge that Let's Encrypt will ask us to use and prove we own the domain
Write-Progress "Receiving challenge"
$completedChallenge = Complete-ACMEChallenge -Ref $identifier -Challenge http-01 -Handler manual -Regenerate
$challengeAnswer = ($completedChallenge.Challenges | Where-Object { $_.HandlerName -eq "manual" }).Challenge
$key = $challengeAnswer.FilePath

$target = "$key/index.html"
if($UseUnixFileVerification) { $target = $key }

## Upload the challenge to the website
Write-Progress "Uploading key and challenge to $domain/$target"

## Gets a Automation account connection to use
$Conn = Get-AutomationConnection -Name "AzureRunAsConnection"
## Adds an authenticated account to use for Azure Resource Manager cmdlet requests
Add-AzureRmAccount -ServicePrincipal -Tenant $Conn.TenantID -ApplicationId $Conn.ApplicationID -CertificateThumbprint $Conn.CertificateThumbprint

$tempFile = New-TemporaryFile
try
{
    $null = Get-AzureRmWebAppPublishingProfile -ResourceGroupName $ResourceGroup -Name $WebApp -OutputFile $tempFile
    PublishWebsiteFile -ResourceGroup $ResourceGroup -WebApp $WebApp -PublishSettingsFile $tempFile -RemotePath $target -FileContent $challengeAnswer.FileContent
}
finally
{
    Remove-Item $tempFile
}

## Tell Let's Encrypt that we've submitted the challenge, wait for its response
$counter = 0
Write-Progress "Waiting for challenge verification" -PercentComplete ($counter++)
$challenge = Submit-ACMEChallenge -Ref $identifier -ChallengeType http-01
while ($challenge.Status -eq "pending")
{
    Start-Sleep -m 500

    Write-Progress "Waiting for challenge verification" -PercentComplete ($counter++)
    $challenge = Update-ACMEIdentifier -Ref $identifier
}

## Create a SSL certificate out of the completed challenge, and bind it to our website.
if($challenge.Status -eq "valid")
{
    $rawPassword =  -join (([int][char]'a'..[int][char]'z') | Get-Random -Count 20 | % { [char] $_ })
    $certIdentifier = -join (([int][char]'a'..[int][char]'z') | Get-Random -Count 10 | % { [char] $_ })

    New-ACMECertificate -Identifier $identifier -Alias $certIdentifier -Generate
    $certificateInfo = Submit-ACMECertificate -Ref $certIdentifier

    Write-Progress "Waiting for IssuerSerialNumber to be issued"
    while(-not ((Test-Path variable:\certificate) -or $certificateInfo.IssuerSerialNumber))
    {
        Start-Sleep -m 500
        $certificateInfo = Update-ACMECertificate -Ref $certIdentifier
    }

    $outputFile = Join-Path $pwd cert1-all.pfx
    $null = Get-ACMECertificate -Ref $certIdentifier -ExportPkcs12 $outputFile -CertificatePassword $rawPassword
    Get-Item $outputFile

    New-AzureRmWebAppSSLBinding -ResourceGroupName $ResourceGroup -WebAppName $WebApp -CertificateFilePath $outputFile -CertificatePassword $rawPassword -Name $Domain
}
else
{
    Write-Error (("Certificate generation failed. Status is '{0}', can't continue as it is not 'valid'. " +
        "Let's Encrypt could not retrieve the expected content from '$domain/$target'") -f $challenge.Status)
    $challenge
}