Public/New-PAAccount.ps1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
function New-PAAccount {
    [CmdletBinding(SupportsShouldProcess,DefaultParameterSetName='Generate')]
    [OutputType('PoshACME.PAAccount')]
    param(
        [Parameter(Position=0)]
        [string[]]$Contact,
        [Parameter(ParameterSetName='Generate',Position=1)]
        [ValidateScript({Test-ValidKeyLength $_ -ThrowOnFail})]
        [Alias('AccountKeyLength')]
        [string]$KeyLength='2048',
        [Parameter(ParameterSetName='ImportKey',Mandatory)]
        [string]$KeyFile,
        [switch]$AcceptTOS,
        [switch]$Force,
        [string]$ExtAcctKID,
        [string]$ExtAcctHMACKey,
        [ValidateSet('HS256','HS384','HS512')]
        [string]$ExtAcctAlgorithm = 'HS256',
        [switch]$UseAltPluginEncryption,
        [Parameter(ValueFromRemainingArguments=$true)]
        $ExtraParams
    )

    # make sure we have a server configured
    if (-not ($acmeServer = Get-PAServer)) {
        try { throw "No ACME server configured. Run Set-PAServer first." }
        catch { $PSCmdlet.ThrowTerminatingError($_) }
    }

    # make sure the external account binding parameters were specified if this ACME
    # server requires them.
    if ($acmeServer.meta -and $acmeServer.meta.externalAccountRequired -and
        (-not $ExtAcctKID -or -not $ExtAcctHMACKey))
    {
        try { throw "The current ACME server requires external account credentials to create a new ACME account. Please run New-PAAccount with the ExtAcctKID and ExtAcctHMACKey parameters." }
        catch { $PSCmdlet.ThrowTerminatingError($_) }
    }

    # try to decode the HMAC key if specified
    if ($ExtAcctHMACKey) {
        $keyBytes = ConvertFrom-Base64Url $ExtAcctHMACKey -AsByteArray
        $hmacKey = switch ($ExtAcctAlgorithm) {
            'HS256' { [Security.Cryptography.HMACSHA256]::new($keyBytes); break; }
            'HS384' { [Security.Cryptography.HMACSHA384]::new($keyBytes); break; }
            'HS512' { [Security.Cryptography.HMACSHA512]::new($keyBytes); break; }
        }
    }

    # make sure the Contact emails have a "mailto:" prefix
    # this may get more complex later if ACME servers support more than email based contacts
    if ($Contact.Count -gt 0) {
        0..($Contact.Count-1) | ForEach-Object {
            if ($Contact[$_] -notlike 'mailto:*') {
                $Contact[$_] = "mailto:$($Contact[$_])"
            }
        }
    } else {
        Write-Warning "No email contacts specified for this account. Certificate expiration warnings will not be sent unless you add at least one with Set-PAAccount."
    }

    if ('Generate' -eq $PSCmdlet.ParameterSetName) {

        # There's a chance we may be creating effectively a duplicate account. So check
        # for confirmation if there's already one with the same contacts and keylength.
        if (!$Force) {
            $accts = @(Get-PAAccount -List -Refresh -Contact $Contact -KeyLength $KeyLength -Status 'valid')
            if ($accts.Count -gt 0) {
                if (!$PSCmdlet.ShouldContinue("Do you wish to duplicate?",
                    "Existing account with matching contacts and key length.")) { return }
            }
        }

        Write-Debug "Creating new $KeyLength account with contact: $($Contact -join ', ')"

        # create the account key
        $acctKey = New-PAKey $KeyLength

    } else { # ImportKey parameter set

        try {
            $kLength = [string]::Empty
            $acctKey = New-PAKey -KeyFile $KeyFile -ParsedLength ([ref]$kLength)
            $KeyLength = $kLength
        }
        catch { $PSCmdlet.ThrowTerminatingError($_) }
    }

    # create the algorithm identifier as described by
    # https://tools.ietf.org/html/rfc7518#section-3.1
    # and what we know LetsEncrypt supports today which includes
    # RS256 for all RSA keys
    # ES256 for P-256 keys, ES384 for P-384 keys, ES512 for P-521 keys
    $alg = 'RS256'
    if     ($KeyLength -eq 'ec-256') { $alg = 'ES256' }
    elseif ($KeyLength -eq 'ec-384') { $alg = 'ES384' }
    elseif ($KeyLength -eq 'ec-521') { $alg = 'ES512' }

    # build the protected header for the request
    $header = @{
        alg   = $alg
        jwk   = ($acctKey | ConvertTo-Jwk -PublicOnly)
        nonce = $script:Dir.nonce
        url   = $script:Dir.newAccount
    }

    # init the payload
    $payload = @{}
    if ($Contact.Count -gt 0) {
        $payload.contact = $Contact
    }
    if ($AcceptTOS) {
        $payload.termsOfServiceAgreed = $true
    }

    # add external account binding if specified
    if ($ExtAcctKID -and $ExtAcctHMACKey) {

        $eabHeader = @{
            alg = $ExtAcctAlgorithm
            kid = $ExtAcctKID
            url = $script:Dir.newAccount
        }

        $eabPayload = $header.jwk | ConvertTo-Json -Depth 5 -Compress

        $payload.externalAccountBinding =
            New-Jws $hmacKey $eabHeader $eabPayload | ConvertFrom-Json
    }

    # convert it to json
    $payloadJson = $payload | ConvertTo-Json -Depth 5 -Compress

    # send the request
    try {
        $response = Invoke-ACME $header $payloadJson -Key $acctKey -EA Stop
    } catch { $PSCmdlet.ThrowTerminatingError($_) }

    # grab the Location header
    if ($response.Headers.ContainsKey('Location')) {
        $location = $response.Headers['Location'] | Select-Object -First 1
    } else {
        try { throw 'No Location header found in newAccount output' }
        catch { $PSCmdlet.ThrowTerminatingError($_) }
    }

    $respObj = $response.Content | ConvertFrom-Json

    # So historically, LE/Boulder returns the raw account ID value as a property in the JSON
    # output for new account requests. But the finalized RFC 8555 does not require this
    # and Boulder will be removing it. But it's still a useful value to have for referencing
    # accounts. So if it's not returned, we're going to try and parse it from the location
    # header. This may come back to haunt us if other ACME providers use different location
    # schemes in the future.
    if (-not $respObj.ID) {
        # https://acme-staging-v02.api.letsencrypt.org/acme/acct/xxxxxxxx
        # https://acme-v02.api.letsencrypt.org/acme/acct/xxxxxxxx
        $acctID = ([Uri]$location).Segments[-1]
    } else {
        $acctID = $respObj.ID.ToString()
    }

    # build the return value
    $acct = [pscustomobject]@{
        PSTypeName = 'PoshACME.PAAccount'
        id = $acctID
        status = $respObj.status
        contact = $respObj.contact
        location = $location
        key = ($acctKey | ConvertTo-Jwk)
        alg = $alg
        KeyLength = $KeyLength
        # The orders field is supposed to exist according to
        # https://tools.ietf.org/html/rfc8555#section-7.1.2
        # But it's not currently implemented in Boulder. Tracking issue is here:
        # https://github.com/letsencrypt/boulder/issues/3335
        orders = $respObj.orders
        sskey = $null
    }

    # add a new AES key if specified
    if ($UseAltPluginEncryption) {
        $acct.sskey = New-AesKey
    }

    # save it to memory and disk
    $acct.id | Out-File (Join-Path (Get-DirFolder) 'current-account.txt') -Force -EA Stop
    $script:Acct = $acct
    $script:AcctFolder = Join-Path (Get-DirFolder) $acct.id
    if (!(Test-Path $script:AcctFolder -PathType Container)) {
        New-Item -ItemType Directory -Path $script:AcctFolder -Force -EA Stop | Out-Null
    }
    $acct | ConvertTo-Json -Depth 5 | Out-File (Join-Path $script:AcctFolder 'acct.json') -Force -EA Stop

    return $acct




    <#
    .SYNOPSIS
        Create a new account on the current ACME server.
 
    .DESCRIPTION
        All certificate requests require a valid account on an ACME server. Adding an email contact is not required. But without one, certificate expiration notices will not be sent. The account KeyLength is personal preference and doesn't correspond to the KeyLength of the generated certificates.
 
    .PARAMETER Contact
        One or more email addresses to associate with this account. These addresses will be used by the ACME server to send certificate expiration notifications or other important account notices.
 
    .PARAMETER KeyLength
        The type and size of private key to use. For RSA keys, specify a number between 2048-4096 (divisible by 128). For ECC keys, specify either 'ec-256' or 'ec-384'. Defaults to '2048'.
 
    .PARAMETER KeyFile
        The path to an existing EC or RSA private key file. This will attempt to create the account using the specified key as the ACME account key. This can be used to recover/import an existing ACME account if one is already associated with the key.
 
    .PARAMETER AcceptTOS
        If not specified, the ACME server will throw an error with a link to the current Terms of Service. Using this switch indicates acceptance of those Terms of Service and is required for successful account creation.
 
    .PARAMETER Force
        If specified, confirmation prompts that may have been generated will be skipped.
 
    .PARAMETER ExtAcctKID
        The external account key identifier supplied by the CA. This is required for ACME CAs that require external account binding.
 
    .PARAMETER ExtAcctHMACKey
        The external account HMAC key supplied by the CA and encoded as Base64Url. This is required for ACME CAs that require external account binding.
 
    .PARAMETER ExtAcctAlgorithm
        The HMAC algorithm to use. Defaults to 'HS256'.
 
    .PARAMETER UseAltPluginEncryption
        If specified, the account will be configured to use a randomly generated AES key to encrypt sensitive plugin parameters on disk instead of using the OS's native encryption methods. This can be useful if the config is being shared across systems or platforms. You can revert to OS native encryption using -UseAltPluginEncryption:$false.
 
    .PARAMETER ExtraParams
        This parameter can be ignored and is only used to prevent errors when splatting with more parameters than this function supports.
 
    .EXAMPLE
        New-PAAccount -AcceptTOS
 
        Create a new account with no contact email and the default key length.
 
    .EXAMPLE
        New-PAAccount -Contact user1@example.com -AcceptTOS
 
        Create a new account with the specified email and the default key length.
 
    .EXAMPLE
        New-PAAccount -Contact user1@example.com -KeyLength 4096 -AcceptTOS
 
        Create a new account with the specified email and an RSA 4096 bit key.
 
    .EXAMPLE
        New-PAAccount -KeyLength 'ec-384' -AcceptTOS -Force
 
        Create a new account with no contact email and an ECC key using P-384 curve that ignores any confirmations.
 
    .Example
        New-PAAccount -KeyFile .\mykey.key -AcceptTOS
 
        Create a new account using a pre-generated private key file.
 
    .LINK
        Project: https://github.com/rmbolger/Posh-ACME
 
    .LINK
        Get-PAAccount
 
    .LINK
        Set-PAAccount
 
    #>

}