Skip to content

Commit

Permalink
Add ARI support for draft-03 (#551)
Browse files Browse the repository at this point in the history
  • Loading branch information
rmbolger authored May 30, 2024
1 parent ee9a0d6 commit 2543c43
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 17 deletions.
43 changes: 43 additions & 0 deletions Posh-ACME/Private/Update-PAOrder.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,49 @@ function Update-PAOrder {
Write-Debug "Order '$($Order.Name)' is expired. Skipping server refresh."
}

# Check for ARI renewal window updates if supported and there's an unexpired cert
# https://www.ietf.org/archive/id/draft-ietf-acme-ari-03.html#name-getting-renewal-information
$server = Get-PAServer
if (-not $SaveOnly -and -not $server.DisableARI -and ($ariBase = $server.renewalInfo) -and
$Order.CertExpires -and (Get-DateTimeOffsetNow) -lt [DateTimeOffset]::Parse($Order.CertExpires) )
{
Write-Verbose "Checking for updated renewal window via ARI"
$cert = $Order | Get-PACertificate
$queryParams = @{
Uri = '{0}/{1}' -f $ariBase,$cert.ARIId
UserAgent = $script:USER_AGENT
Headers = $script:COMMON_HEADERS
ErrorAction = 'Stop'
Verbose = $false
}
try {
Write-Debug "GET $($queryParams.Uri)"
$resp = Invoke-RestMethod @queryParams @script:UseBasic
Write-Debug "Response:`n$($resp|ConvertTo-Json)"
} catch {
Write-Warning "ARI request failed."
$PSCmdlet.WriteError($_)
}

if ($resp.suggestedWindow) {
$renewAfter = $resp.suggestedWindow.start
if ($renewAfter -ne $Order.RenewAfter) {
Write-Verbose "Updating renewal window to $renewAfter from ARI response"
$Order.RenewAfter = $renewAfter

# Warn if there's an explanation URL
if ($resp.explanationUrl) {
Write-Warning "The ACME Server has suggested an updated renewal window. Visit the following URL for more information:`n$($resp.explanationUrl)"
}
}

# Warn if the new window is in the past
if ((Get-DateTimeOffsetNow) -gt [DateTimeOffset]::Parse($renewAfter)) {
Write-Warning "The ACME Server has indicated this order's certificate should be renewed AS SOON AS POSSIBLE."
}
}
}

# Make sure the order folder exists
if (-not (Test-Path $Order.Folder -PathType Container)) {
New-Item -ItemType Directory -Path $Order.Folder -Force -EA Stop | Out-Null
Expand Down
28 changes: 21 additions & 7 deletions Posh-ACME/Public/Get-PACertificate.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ function Get-PACertificate {
$secPfxPass = [Security.SecureString]::new()
}

# derive the ARI ID value
# https://letsencrypt.org/2024/04/25/guide-to-integrating-ari-into-existing-acme-clients#step-3-constructing-the-ari-certid
# https://www.ietf.org/archive/id/draft-ietf-acme-ari-03.html#name-the-renewalinfo-resource
$akiExt = $cert.GetExtensionValue([Org.BouncyCastle.Asn1.X509.X509Extensions]::AuthorityKeyIdentifier)
$akiBytes = [Org.BouncyCastle.Asn1.X509.AuthorityKeyIdentifier]::GetInstance($akiExt.GetOctets()).GetKeyIdentifier()
$serialBytes = $cert.SerialNumber.ToByteArray()
$ariID = '{0}.{1}' -f (ConvertTo-Base64Url $akiBytes),(ConvertTo-Base64Url $serialBytes)

# send the output object to the pipeline
[pscustomobject]@{
PSTypeName = 'PoshACME.PACertificate'
Expand All @@ -82,16 +90,22 @@ function Get-PACertificate {
# stored in the cert itself
Thumbprint = [BitConverter]::ToString($sha1.ComputeHash($cert.GetEncoded())).Replace('-','')

# add the ARI ID value
ARIId = $ariID

# add the serial
Serial = $cert.SerialNumber.ToString()

# add the full list of SANs
AllSANs = @($altNames)
AllSANs = [string[]]@($altNames)

# add the associated file paths whether they exist or not
CertFile = Join-Path $order.Folder 'cert.cer'
KeyFile = Join-Path $order.Folder 'cert.key'
ChainFile = Join-Path $order.Folder 'chain.cer'
FullChainFile = Join-Path $order.Folder 'fullchain.cer'
PfxFile = Join-Path $order.Folder 'cert.pfx'
PfxFullChain = Join-Path $order.Folder 'fullchain.pfx'
CertFile = (Join-Path $order.Folder 'cert.cer').ToString()
KeyFile = (Join-Path $order.Folder 'cert.key').ToString()
ChainFile = (Join-Path $order.Folder 'chain.cer').ToString()
FullChainFile = (Join-Path $order.Folder 'fullchain.cer').ToString()
PfxFile = (Join-Path $order.Folder 'cert.pfx').ToString()
PfxFullChain = (Join-Path $order.Folder 'fullchain.pfx').ToString()

PfxPass = $secPfxPass
}
Expand Down
13 changes: 8 additions & 5 deletions Posh-ACME/Public/New-PACertificate.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,10 @@ function New-PACertificate {
# grab the set of parameter keys to make comparisons easier later
$psbKeys = $PSBoundParameters.Keys

# Make sure we have a server set. But don't override the current
# Make sure we have a refreshed server. But don't override the current
# one unless explicitly specified.
if (-not (Get-PAServer) -or 'DirectoryUrl' -in $psbKeys) {
if ('DirectoryUrl' -in $psbKeys -or -not (Get-PAServer -Refresh)) {
Set-PAServer -DirectoryUrl $DirectoryUrl
} else {
# refresh the directory info (which should also get a fresh nonce)
Set-PAServer
}
Write-Verbose "Using ACME Server $($script:Dir.location)"

Expand Down Expand Up @@ -193,6 +190,12 @@ function New-PACertificate {
}
}

# Add the replaced cert ID if it exists
# New-PAOrder will ignore it if the server doesn't support ARI
if ($oldOrder -and ($cert = ($oldOrder | Get-PACertificate))) {
$orderParams.ReplacesCert = $cert.ARIId
}

# add common explicit order parameters backed up by old order params
@( 'Plugin'
'LifetimeDays'
Expand Down
9 changes: 8 additions & 1 deletion Posh-ACME/Public/New-PAOrder.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ function New-PAOrder {
[int]$DnsSleep=120,
[int]$ValidationTimeout=60,
[string]$PreferredChain,
[switch]$Force
[switch]$Force,
[string]$ReplacesCert
)

try {
Expand Down Expand Up @@ -197,6 +198,12 @@ function New-PAOrder {
$payload.notAfter = $notAfter
}

# Add the ARI replaces field if supported and specified
# https://www.ietf.org/archive/id/draft-ietf-acme-ari-03.html#name-extensions-to-the-order-obj
if ($ReplacesCert -and -not (Get-PAServer).DisableARI -and (Get-PAServer).renewalInfo) {
$payload.replaces = $ReplacesCert
}

$payloadJson = $payload | ConvertTo-Json -Depth 5 -Compress

# send the request
Expand Down
14 changes: 14 additions & 0 deletions Posh-ACME/Public/Set-PAServer.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ function Set-PAServer {
[Parameter(ValueFromPipelineByPropertyName)]
[switch]$DisableTelemetry,
[switch]$UseAltAccountRefresh,
[switch]$DisableARI,
[switch]$NoRefresh,
[switch]$NoSwitch
)
Expand Down Expand Up @@ -72,6 +73,7 @@ function Set-PAServer {
DisableTelemetry = $DisableTelemetry.IsPresent
SkipCertificateCheck = $SkipCertificateCheck.IsPresent
UseAltAccountRefresh = $UseAltAccountRefresh.IsPresent
DisableARI = $DisableARI.IsPresent
newAccount = $null
newOrder = $null
newNonce = $null
Expand Down Expand Up @@ -161,6 +163,12 @@ function Set-PAServer {
$newDir.revokeCert = $dirObj.revokeCert
$newDir.meta = $dirObj.meta

# check for the renewalInfo field
if ($dirObj.renewalInfo) {
$newDir | Add-Member 'renewalInfo' $dirObj.renewalInfo -Force
}


# update the nonce value
if ($response.Headers.ContainsKey($script:HEADER_NONCE)) {
$newDir.nonce = $response.Headers[$script:HEADER_NONCE] | Select-Object -First 1
Expand Down Expand Up @@ -188,6 +196,12 @@ function Set-PAServer {
Write-Debug "Setting UseAltAccountRefresh value to $($UseAltAccountRefresh.IsPresent)"
$newDir | Add-Member 'UseAltAccountRefresh' $UseAltAccountRefresh.IsPresent -Force
}
if ($PSBoundParameters.ContainsKey('DisableARI') -and
$newDir.DisableARI -ne $DisableARI.IsPresent)
{
Write-Debug "Setting DisableARI value to $($DisableARI.IsPresent)"
$newDir | Add-Member 'DisableARI' $DisableARI.IsPresent -Force
}

# save the object to disk except for the dynamic properties
Write-Debug "Saving PAServer to disk"
Expand Down
5 changes: 5 additions & 0 deletions Posh-ACME/Public/Submit-Renewal.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ function Submit-Renewal {
catch { $PSCmdlet.ThrowTerminatingError($_) }
}

# trigger an ARI check if supported
if (-not (Get-PAServer).DisableARI -and (Get-PAServer).renewalInfo) {
Update-PAOrder -Order $order
}

# skip if the renewal window hasn't been reached and no -Force
if (-not $Force -and $null -ne $order.RenewAfter -and (Get-DateTimeOffsetNow) -lt ([DateTimeOffset]::Parse($order.RenewAfter))) {
Write-Warning "Order '$($order.Name)' is not recommended for renewal yet. Use -Force to override."
Expand Down
71 changes: 71 additions & 0 deletions Posh-ACME/en-US/Posh-ACME-help.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3903,6 +3903,18 @@ New-PACertificate 'example.com' -Plugin FakeDNS -PluginArgs $pArgs -DnsAlias 'ac
</dev:type>
<dev:defaultValue>False</dev:defaultValue>
</command:parameter>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="False" position="named" aliases="none">
<maml:name>ReplacesCert</maml:name>
<maml:description>
<maml:para>The ARI certificate ID from the ARIId parameter on a PACertificate object returned by Get-PACertificate. This is optional and only used if the server supports the ACME Renewal Information Extension (ARI).</maml:para>
</maml:description>
<command:parameterValue required="true" variableLength="false">String</command:parameterValue>
<dev:type>
<maml:name>String</maml:name>
<maml:uri />
</dev:type>
<dev:defaultValue>None</dev:defaultValue>
</command:parameter>
</command:syntaxItem>
<command:syntaxItem>
<maml:name>New-PAOrder</maml:name>
Expand Down Expand Up @@ -4162,6 +4174,18 @@ New-PACertificate 'example.com' -Plugin FakeDNS -PluginArgs $pArgs -DnsAlias 'ac
</dev:type>
<dev:defaultValue>False</dev:defaultValue>
</command:parameter>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="False" position="named" aliases="none">
<maml:name>ReplacesCert</maml:name>
<maml:description>
<maml:para>The ARI certificate ID from the ARIId parameter on a PACertificate object returned by Get-PACertificate. This is optional and only used if the server supports the ACME Renewal Information Extension (ARI).</maml:para>
</maml:description>
<command:parameterValue required="true" variableLength="false">String</command:parameterValue>
<dev:type>
<maml:name>String</maml:name>
<maml:uri />
</dev:type>
<dev:defaultValue>None</dev:defaultValue>
</command:parameter>
</command:syntaxItem>
<command:syntaxItem>
<maml:name>New-PAOrder</maml:name>
Expand Down Expand Up @@ -4317,6 +4341,18 @@ New-PACertificate 'example.com' -Plugin FakeDNS -PluginArgs $pArgs -DnsAlias 'ac
</dev:type>
<dev:defaultValue>None</dev:defaultValue>
</command:parameter>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="False" position="named" aliases="none">
<maml:name>ReplacesCert</maml:name>
<maml:description>
<maml:para>The ARI certificate ID from the ARIId parameter on a PACertificate object returned by Get-PACertificate. This is optional and only used if the server supports the ACME Renewal Information Extension (ARI).</maml:para>
</maml:description>
<command:parameterValue required="true" variableLength="false">String</command:parameterValue>
<dev:type>
<maml:name>String</maml:name>
<maml:uri />
</dev:type>
<dev:defaultValue>None</dev:defaultValue>
</command:parameter>
</command:syntaxItem>
</command:syntax>
<command:parameters>
Expand Down Expand Up @@ -4608,6 +4644,18 @@ New-PACertificate 'example.com' -Plugin FakeDNS -PluginArgs $pArgs -DnsAlias 'ac
</dev:type>
<dev:defaultValue>False</dev:defaultValue>
</command:parameter>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="False" position="named" aliases="none">
<maml:name>ReplacesCert</maml:name>
<maml:description>
<maml:para>The ARI certificate ID from the ARIId parameter on a PACertificate object returned by Get-PACertificate. This is optional and only used if the server supports the ACME Renewal Information Extension (ARI).</maml:para>
</maml:description>
<command:parameterValue required="true" variableLength="false">String</command:parameterValue>
<dev:type>
<maml:name>String</maml:name>
<maml:uri />
</dev:type>
<dev:defaultValue>None</dev:defaultValue>
</command:parameter>
</command:parameters>
<command:inputTypes />
<command:returnValues>
Expand Down Expand Up @@ -7543,6 +7591,17 @@ Set-PAOrder example.com -Plugin FakeDNS -PluginArgs $pArgs</dev:code>
</dev:type>
<dev:defaultValue>False</dev:defaultValue>
</command:parameter>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="False" position="named" aliases="none">
<maml:name>DisableARI</maml:name>
<maml:description>
<maml:para>Explicitly disables ARI (ACME Renewal Information) for this server even if it claims to support the feature. While the ARI RFC is still in draft status, this should only be necessary if ACME servers move to a newer draft version that breaks compatibility with the version currently supported (draft-03).</maml:para>
</maml:description>
<dev:type>
<maml:name>SwitchParameter</maml:name>
<maml:uri />
</dev:type>
<dev:defaultValue>False</dev:defaultValue>
</command:parameter>
</command:syntaxItem>
</command:syntax>
<command:parameters>
Expand Down Expand Up @@ -7642,6 +7701,18 @@ Set-PAOrder example.com -Plugin FakeDNS -PluginArgs $pArgs</dev:code>
</dev:type>
<dev:defaultValue>False</dev:defaultValue>
</command:parameter>
<command:parameter required="false" variableLength="true" globbing="false" pipelineInput="False" position="named" aliases="none">
<maml:name>DisableARI</maml:name>
<maml:description>
<maml:para>Explicitly disables ARI (ACME Renewal Information) for this server even if it claims to support the feature. While the ARI RFC is still in draft status, this should only be necessary if ACME servers move to a newer draft version that breaks compatibility with the version currently supported (draft-03).</maml:para>
</maml:description>
<command:parameterValue required="false" variableLength="false">SwitchParameter</command:parameterValue>
<dev:type>
<maml:name>SwitchParameter</maml:name>
<maml:uri />
</dev:type>
<dev:defaultValue>False</dev:defaultValue>
</command:parameter>
</command:parameters>
<command:inputTypes />
<command:returnValues />
Expand Down
22 changes: 19 additions & 3 deletions docs/Functions/New-PAOrder.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ New-PAOrder [-Domain] <String[]> [[-KeyLength] <String>] [-Name <String>] [-Plug
[-PluginArgs <Hashtable>] [-LifetimeDays <Int32>] [-DnsAlias <String[]>] [-OCSPMustStaple] [-AlwaysNewKey]
[-Subject <String>] [-FriendlyName <String>] [-PfxPass <String>] [-PfxPassSecure <SecureString>]
[-UseModernPfxEncryption] [-Install] [-UseSerialValidation] [-DnsSleep <Int32>] [-ValidationTimeout <Int32>]
[-PreferredChain <String>] [-Force] [-WhatIf] [-Confirm] [<CommonParameters>]
[-PreferredChain <String>] [-Force] [-ReplacesCert <String>] [-WhatIf] [-Confirm] [<CommonParameters>]
```

### ImportKey
Expand All @@ -28,14 +28,15 @@ New-PAOrder [-Domain] <String[]> -KeyFile <String> [-Name <String>] [-Plugin <St
[-PluginArgs <Hashtable>] [-LifetimeDays <Int32>] [-DnsAlias <String[]>] [-OCSPMustStaple] [-AlwaysNewKey]
[-Subject <String>] [-FriendlyName <String>] [-PfxPass <String>] [-PfxPassSecure <SecureString>]
[-UseModernPfxEncryption] [-Install] [-UseSerialValidation] [-DnsSleep <Int32>] [-ValidationTimeout <Int32>]
[-PreferredChain <String>] [-Force] [-WhatIf] [-Confirm] [<CommonParameters>]
[-PreferredChain <String>] [-Force] [-ReplacesCert <String>] [-WhatIf] [-Confirm] [<CommonParameters>]
```

### FromCSR
```powershell
New-PAOrder [-CSRPath] <String> [-Name <String>] [-Plugin <String[]>] [-PluginArgs <Hashtable>]
[-LifetimeDays <Int32>] [-DnsAlias <String[]>] [-UseSerialValidation] [-DnsSleep <Int32>]
[-ValidationTimeout <Int32>] [-PreferredChain <String>] [-Force] [-WhatIf] [-Confirm] [<CommonParameters>]
[-ValidationTimeout <Int32>] [-PreferredChain <String>] [-Force] [-ReplacesCert <String>] [-WhatIf] [-Confirm]
[<CommonParameters>]
```

## Description
Expand Down Expand Up @@ -482,6 +483,21 @@ Accept pipeline input: False
Accept wildcard characters: False
```

### -ReplacesCert
The ARI certificate ID from the ARIId parameter on a PACertificate object returned by Get-PACertificate. This is optional and only used if the server supports the ACME Renewal Information Extension (ARI).

```yaml
Type: String
Parameter Sets: (All)
Aliases:
Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```

### CommonParameters
This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216).

Expand Down
17 changes: 16 additions & 1 deletion docs/Functions/Set-PAServer.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Set the current ACME server and/or its configuration.

```powershell
Set-PAServer [[-DirectoryUrl] <String>] [-Name <String>] [-NewName <String>] [-SkipCertificateCheck]
[-DisableTelemetry] [-UseAltAccountRefresh] [-NoRefresh] [-NoSwitch] [<CommonParameters>]
[-DisableTelemetry] [-UseAltAccountRefresh] [-DisableARI] [-NoRefresh] [-NoSwitch] [<CommonParameters>]
```

## Description
Expand Down Expand Up @@ -181,6 +181,21 @@ Accept pipeline input: False
Accept wildcard characters: False
```
### -DisableARI
Explicitly disables ARI (ACME Renewal Information) for this server even if it claims to support the feature. While the ARI RFC is still in draft status, this should only be necessary if ACME servers move to a newer draft version that breaks compatibility with the version currently supported (draft-03).
```yaml
Type: SwitchParameter
Parameter Sets: (All)
Aliases:

Required: False
Position: Named
Default value: None
Accept pipeline input: False
Accept wildcard characters: False
```
### CommonParameters
This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216).
Expand Down

0 comments on commit 2543c43

Please sign in to comment.