Private/ConvertFrom-Jwk.ps1
function ConvertFrom-Jwk { [CmdletBinding(DefaultParameterSetName='JSON')] [OutputType('System.Security.Cryptography.AsymmetricAlgorithm')] [OutputType('Org.BouncyCastle.Crypto.AsymmetricCipherKeyPair')] param( [Parameter(ParameterSetName='JSON',Mandatory,Position=0,ValueFromPipeline)] [string]$JwkJson, [Parameter(ParameterSetName='Object',Mandatory,Position=0,ValueFromPipeline)] [pscustomobject]$Jwk, [switch]$AsBC ) # RFC 7515 - JSON Web Key (JWK) # https://tools.ietf.org/html/rfc7517 # Support enough of a subset of RFC 7515 to implement the ACME v2 # protocol (RFC 8555). # https://tools.ietf.org/html/rfc8555 # This basically includes RSA keys 2048-4096 bits and EC keys utilizing # P-256, P-384, or P-521 curves. Process { if ($PSCmdlet.ParameterSetName -eq 'JSON') { try { $Jwk = $JwkJson | ConvertFrom-Json -EA Stop } catch { throw } } if ([String]::IsNullOrWhiteSpace($Jwk.kty)) { throw "Invalid JWK. Missing 'kty' parameter." } if ($Jwk.kty -notin 'RSA','EC') { throw "Invalid JWK. Unsupported 'kty' element found." } # validate RSA parameters if ('RSA' -eq $Jwk.kty) { # error if any public params are missing 'n','e' | ForEach-Object { if ([String]::IsNullOrWhiteSpace($Jwk.$_)) { throw "Invalid RSA JWK. Missing '$_' parameter." } } $publicOnly = $true # for private params, we either want all or none $privCount = @('d','p','q','dp','dq','qi' | Where-Object { -not [String]::IsNullOrWhiteSpace($Jwk.$_) }).Count if ($privCount -gt 0 -and $privCount -lt 6) { throw "Invalid RSA JWK. Missing one or more private parameters." } elseif ($privCount -eq 6) { $publicOnly = $false } } # validate EC parameters if ('EC' -eq $Jwk.kty) { # error if any public params are missing 'crv','x','y' | ForEach-Object { if ([String]::IsNullOrWhiteSpace($Jwk.$_)) { throw "Invalid EC JWK. Missing '$_' parameter." } } $publicOnly = $true if (-not [String]::IsNullOrWhiteSpace($Jwk.d)) { $publicOnly = $false } } if ('RSA' -eq $Jwk.kty -and -not $AsBC) { # create a .NET AsymmetricAlgorithm (RSACryptoServiceProvider) $keyParams = [Security.Cryptography.RSAParameters]::new() # make sure we have the required public key parameters per # https://tools.ietf.org/html/rfc7518#section-6.3.1 $keyParams.Exponent = $Jwk.e | ConvertFrom-Base64Url -AsByteArray $keyParams.Modulus = $Jwk.n | ConvertFrom-Base64Url -AsByteArray # Add the private key parameters if they were included # Per https://tools.ietf.org/html/rfc7518#section-6.3.2, # 'd' is the only required private parameter. The rest SHOULD # be included and if any *are* included then they all MUST be included. # HOWEVER, Microsoft's RSA implementation either can't or won't create # a private key unless all (d,p,q,dp,dq,qi) are included. if (-not $publicOnly) { $keyParams.D = $Jwk.d | ConvertFrom-Base64Url -AsByteArray $keyParams.P = $Jwk.p | ConvertFrom-Base64Url -AsByteArray $keyParams.Q = $Jwk.q | ConvertFrom-Base64Url -AsByteArray $keyParams.DP = $Jwk.dp | ConvertFrom-Base64Url -AsByteArray $keyParams.DQ = $Jwk.dq | ConvertFrom-Base64Url -AsByteArray $keyParams.InverseQ = $Jwk.qi | ConvertFrom-Base64Url -AsByteArray } # create and return the key $key = [Security.Cryptography.RSACryptoServiceProvider]::new() $key.ImportParameters($keyParams) return $key } elseif ('EC' -eq $Jwk.kty -and -not $AsBC) { # create a .NET AsymmetricAlgorithm (ECDsa) # check for a valid curve $Curve = switch ($jwk.crv) { 'P-256' { [Security.Cryptography.ECCurve+NamedCurves]::nistP256; break } 'P-384' { [Security.Cryptography.ECCurve+NamedCurves]::nistP384; break } 'P-521' { [Security.Cryptography.ECCurve+NamedCurves]::nistP521; break } default { throw "Unsupported JWK curve (crv) found: $($Jwk.crv)." } } # make sure we have the required public key parameters per # https://tools.ietf.org/html/rfc7518#section-6.2.1 $Q = [Security.Cryptography.ECPoint]::new() $Q.X = $Jwk.x | ConvertFrom-Base64Url -AsByteArray $Q.Y = $Jwk.y | ConvertFrom-Base64Url -AsByteArray $keyParams = [Security.Cryptography.ECParameters]::new() $keyParams.Q = $Q $keyParams.Curve = $Curve if (-not $publicOnly) { # add the private key parameter $keyParams.D = $Jwk.d | ConvertFrom-Base64Url -AsByteArray } # create and return the key $key = [Security.Cryptography.ECDsa]::Create() $key.ImportParameters($keyParams) return $key } elseif ('RSA' -eq $Jwk.kty -and $AsBC) { # create a BouncyCastle AsymmetricCipherKeyPair (RSA) # create public N (Modulus) and E (Exponent) $nBytes = $Jwk.n | ConvertFrom-Base64Url -AsByteArray $eBytes = $Jwk.e | ConvertFrom-Base64Url -AsByteArray $n = [Org.BouncyCastle.Math.BigInteger]::new(1, $nBytes) $e = [Org.BouncyCastle.Math.BigInteger]::new(1, $eBytes) # build the public parameters $pubKeyParam = [Org.BouncyCastle.Crypto.Parameters.RsaKeyParameters]::new( $false, $n, $e ) if ($publicOnly) { # BouncyCastle won't let us return a full key pair without the private # portion, so just return the public parameters for now return $pubKeyParam } else { # create private pieces (D, P, Q, DP, DQ, QI) $dBytes = $Jwk.d | ConvertFrom-Base64Url -AsByteArray $pBytes = $Jwk.p | ConvertFrom-Base64Url -AsByteArray $qBytes = $Jwk.q | ConvertFrom-Base64Url -AsByteArray $dpBytes = $Jwk.dp | ConvertFrom-Base64Url -AsByteArray $dqBytes = $Jwk.dq | ConvertFrom-Base64Url -AsByteArray $qiBytes = $Jwk.qi | ConvertFrom-Base64Url -AsByteArray $d = [Org.BouncyCastle.Math.BigInteger]::new(1, $dBytes) $p = [Org.BouncyCastle.Math.BigInteger]::new(1, $pBytes) $q = [Org.BouncyCastle.Math.BigInteger]::new(1, $qBytes) $dp = [Org.BouncyCastle.Math.BigInteger]::new(1, $dpBytes) $dq = [Org.BouncyCastle.Math.BigInteger]::new(1, $dqBytes) $qi = [Org.BouncyCastle.Math.BigInteger]::new(1, $qiBytes) # build the private parameters $privKeyParam = [Org.BouncyCastle.Crypto.Parameters.RsaPrivateCrtKeyParameters]::new( $n, $e, $d, $p, $q, $dp, $dq, $qi ) # return the full keypair return [Org.BouncyCastle.Crypto.AsymmetricCipherKeyPair]::new( $pubKeyParam, $privKeyParam ) } } elseif ('EC' -eq $Jwk.kty -and $AsBC) { # create a BouncyCastle AsymmetricCipherKeyPair (EC) # find the curve and its oid (aka PublicKeyParamSet) $crv = [Org.BouncyCastle.Asn1.Nist.NistNamedCurves]::GetByName($Jwk.crv) $crvOid = [Org.BouncyCastle.Asn1.Nist.NistNamedCurves]::GetOid($Jwk.crv) # create the ECPoint, Q $xBytes = $Jwk.x | ConvertFrom-Base64Url -AsByteArray $yBytes = $Jwk.y | ConvertFrom-Base64Url -AsByteArray $x = [Org.BouncyCastle.Math.BigInteger]::new(1, $xBytes) $y = [Org.BouncyCastle.Math.BigInteger]::new(1, $yBytes) $q = $crv.Curve.CreatePoint($x, $y) # build the public parameters $pubKeyParam = [Org.BouncyCastle.Crypto.Parameters.ECPublicKeyParameters]::new( 'EC', $q, $crvOid ) if ($publicOnly) { # BouncyCastle won't let us return a full key pair without the private # portion, so just return the public parameters for now return $pubKeyParam } else { # create the BigInteger, D $dBytes = $Jwk.d | ConvertFrom-Base64Url -AsByteArray $d = [Org.BouncyCastle.Math.BigInteger]::new(1, $dBytes) # build the private parameters $privKeyParam = [Org.BouncyCastle.Crypto.Parameters.ECPrivateKeyParameters]::new( 'EC', $d, $crvOid ) # return the full keypair return [Org.BouncyCastle.Crypto.AsymmetricCipherKeyPair]::new( $pubKeyParam, $privKeyParam ) } } } } |