Private/New-Jws.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
function New-Jws {
    [CmdletBinding()]
    [OutputType('System.String')]
    param(
        [Parameter(Mandatory)]
        [Security.Cryptography.AsymmetricAlgorithm]$Key,
        [Parameter(Mandatory)]
        [hashtable]$Header,
        [Parameter(Mandatory)]
        [AllowEmptyString()]
        [string]$PayloadJson,
        [switch]$Compact,
        [switch]$NoHeaderValidation
    )

    # RFC 7515 - JSON Web Signature (JWS)
    # https://tools.ietf.org/html/rfc7515
    # https://tools.ietf.org/html/rfc7518#section-3.1

    # This is not a general JWS implementation. It will specifically
    # cater to making JWS messages for the ACME v2 protocol.
    # https://tools.ietf.org/html/rfc8555

    # validate the key type
    if ($Key -is [Security.Cryptography.RSA]) {
        $IsRSA = $true

        # validate the key size
        # LE supports 2048-4096
        # Windows claims to support 8-bit increments (mod 128)
        if ($Key.KeySize -lt 2048 -or $Key.KeySize -gt 4096 -or ($Key.KeySize % 128) -ne 0) {
            throw "Unsupported RSA key size. Must be 2048-4096 in 8 bit increments."
        }

        # make sure we have a private key to sign with
        if ($Key.PublicOnly) {
            throw "Supplied Key has no private key portion."
        }

    } elseif ($Key -is [Security.Cryptography.ECDsa]) {
        $IsRSA = $false

        # validate the curve size which is exposed via KeySize
        if ($Key.KeySize -ne 256 -and $Key.KeySize -ne 384) {
            throw "Unsupported EC curve. Must be P-256 or P-384"
        }

        # make sure we have a private key to sign with
        # since there's no PublicOnly property, we have to fake it by trying to export
        # the private parameters and catching the error
        try { $Key.ExportParameters($true) | Out-Null }
        catch { throw "Supplied Key has no private key portion." }

    } else {
        throw "Unsupported Key type. Must be RSA or ECDsa"
    }

    if (-not $NoHeaderValidation) {
        # validate the header
        if ('alg' -notin $Header.Keys -or $Header.alg -notin 'RS256','ES256','ES384') {
            throw "Missing or invalid 'alg' in supplied Header"
        }
        if (!('jwk' -in $Header.Keys -xor 'kid' -in $Header.Keys)) {
            if ('jwk' -in $Header.Keys) {
                throw "Conflicting key entries. Both 'jwk' and 'kid' found in supplied Header"
            } else {
                throw "Missing key entries. Neither 'jwk' or 'kid' found in supplied Header"
            }
        }
        if ('jwk' -in $Header.Keys -and [string]::IsNullOrWhiteSpace($Header.jwk)) {
            throw "Empty 'jwk' in supplied Header."
        }
        if ('kid' -in $Header.Keys -and [string]::IsNullOrWhiteSpace($Header.kid)) {
            throw "Empty 'kid' in supplied Header."
        }
        if ('nonce' -notin $Header.Keys -or [string]::IsNullOrWhiteSpace($Header.nonce)) {
            throw "Missing or empty 'nonce' in supplied Header."
        }
        if ('url' -notin $Header.Keys -or [string]::IsNullOrWhiteSpace($Header.url)) {
            throw "Missing or empty 'url' in supplied Header."
        }
    }

    # build the "<protected>.<payload>" string we're going to be signing
    Write-Debug "Header: $($Header | ConvertTo-Json)"
    $HeaderB64 = ConvertTo-Base64Url ($Header | ConvertTo-Json -Compress)
    Write-Debug "Payload: $PayloadJson"
    $PayloadB64 = ConvertTo-Base64Url $PayloadJson
    $Message = "$HeaderB64.$PayloadB64"
    $MessageBytes = [Text.Encoding]::ASCII.GetBytes($Message)

    if ($IsRSA) {
        # Make sure header 'alg' matches key type. All RSA keys are currently
        # restricted to RS256 regardless of key size.
        if ($Header.alg -ne 'RS256') {
            throw "Supplied RSA Key does not match 'alg' ($($Header.alg)) in supplied Header."
        }

        # create the signature
        $HashAlgo = [Security.Cryptography.HashAlgorithmName]::SHA256
        $PaddingType = [Security.Cryptography.RSASignaturePadding]::Pkcs1
        $SignedBytes = $Key.SignData($MessageBytes, $HashAlgo, $PaddingType)
    } else {
        # Make sure header 'alg' matches key type. EC keys depend on the curve
        # ES256 = P-256 and SHA256 hash
        # ES384 = P-384 and SHA384 hash
        # ES521 = P-521 and SHA512 hash (note 521 vs 512, very confusing)
        if (($Header.alg -notin 'ES256','ES384','ES512') -or
            ($Header.alg -eq 'ES256' -and $Key.KeySize -ne 256) -or
            ($Header.alg -eq 'ES384' -and $Key.KeySize -ne 384) -or
            ($Header.alg -eq 'ES512' -and $Key.KeySize -ne 521)) {
            throw "Supplied EC Key (P-$($Key.KeySize)) does not match 'alg' ($($Header.alg)) in supplied header or alg is not supported."
        }

        $HashAlgo = switch ($Key.KeySize) {
            256 { [Security.Cryptography.HashAlgorithmName]::SHA256; break }
            384 { [Security.Cryptography.HashAlgorithmName]::SHA384; break }
            521 { [Security.Cryptography.HashAlgorithmName]::SHA512; break }
        }

        # create the signature
        $SignedBytes = $Key.SignData($MessageBytes, $HashAlgo)
    }

    # now put everything together into the final JWS format
    if ($Compact) {
        # JWS Compact Serialization
        # https://tools.ietf.org/html/rfc7515#section-3.1

        return "$HeaderB64.$PayloadB64.$(ConvertTo-Base64Url $SignedBytes)"

    } else {
        # JWS JSON Serialization
        # https://tools.ietf.org/html/rfc7515#section-3.2

        $jws = [ordered]@{}
        $jws.payload = $PayloadB64
        $jws.protected = $HeaderB64
        $jws.signature = ConvertTo-Base64Url $SignedBytes

        # and return it
        return ($jws | ConvertTo-Json -Compress)
    }

}