Private/New-Jws.ps1
function New-Jws { [CmdletBinding()] [OutputType('System.String')] param( [Parameter(Mandatory)] [Security.Cryptography.AsymmetricAlgorithm]$Key, [Parameter(Mandatory)] [hashtable]$Header, [Parameter(Mandatory)] [string]$PayloadJson ) # 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/draft-ietf-acme-acme-10 # 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" } # 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 # being used. # ES256 = P-256 and SHA256 hash # ES384 = P-384 and SHA384 hash # ES521 = P-521 and SHA512 hash (note 521 vs 512, very confusing) $size = $Key.KeySize $hashAlgo = $Key.HashAlgorithm.Algorithm if (($Header.alg -notin 'ES256','ES384','ES512') -or ($Header.alg -eq 'ES256' -and ($size -ne 256 -or $hashAlgo -ne 'SHA256')) -or ($Header.alg -eq 'ES384' -and ($size -ne 384 -or $hashAlgo -ne 'SHA384')) -or ($Header.alg -eq 'ES512' -and ($size -ne 521 -or $hashAlgo -ne 'SHA512'))) { throw "Supplied EC Key (P-$size/$hashAlgo) does not match 'alg' ($($Header.alg)) in supplied Header." } # create the signature $SignedBytes = $Key.SignData($MessageBytes) } # now put everything together into the final JWS format $jws = [ordered]@{} $jws.payload = $PayloadB64 $jws.protected = $HeaderB64 $jws.signature = ConvertTo-Base64Url $SignedBytes # and return it return ($jws | ConvertTo-Json -Compress) } |