Reputation: 441
Attempting to implement Firebase Admin SDK service account access using Powershell HTTP/REST and following this tutorial (there's no handy API in Powershell);
Using OAuth 2.0 for Server to Server Applications
Forming the JWT header and JWT claim set are straightforward enough and I can reproduce the examples in the tutorial, however this is where it gets tricky;
Sign the UTF-8 representation of the input using SHA256withRSA (also known as RSASSA-PKCS1-V1_5-SIGN with the SHA-256 hash function) with the private key obtained from the Google API Console. The output will be a byte array. The signature must then be Base64url encoded
RESOLVED 28 JUNE 2023, JOB DONE (5 years later)
There are two Powershell libraries available on JSON Web Token Libraries that work just great creating the required signatures for Google server to server services.
Upvotes: 2
Views: 778
Reputation: 441
RESOLVED 28 JUNE 2023, JOB DONE (5 years later)
There are two excellent Powershell libraries available on JSON Web Token Libraries that work just great creating the required signatures for Google server to server services. Simply include a library in your Powershell script then generate your JWT as below;
$headerTable = [ordered]@{
'alg' = 'RS256'
'typ' = 'JWT'
}
$headerjson = $headerTable | ConvertTo-Json -Compress;
# Setup of Payload claims
$now = (Get-Date).ToUniversalTime()
$createDate = [Math]::Floor([decimal](Get-Date($now) -UFormat "%s"))
$expiryDate = [Math]::Floor([decimal](Get-Date($now.AddHours(1)) -UFormat "%s"))
$payloadTable = [ordered]@{
'iss' = $jsonfile.client_email
'scope' = $Scope
'aud' = $jsonfile.token_uri
'exp' = $expiryDate
'iat' = $createDate
}
$payloadjson = $payloadTable | ConvertTo-Json -Compress;
$headerBase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($headerjson)).Split('=')[0].Replace('+', '-').Replace('/', '_')
$payloadBase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($payloadjson)).Split('=')[0].Replace('+', '-').Replace('/', '_')
$ToBeSigned = $headerBase64 + "." + $payloadBase64;
$signature = Get-SignatureRS "RS256" $rsaPrivateKey $ToBeSigned;
$jwt = ($ToBeSigned + "." + $signature);
$requestUri = $jsonfile.token_uri;
$method = 'POST';
$grant_type = [System.Web.HttpUtility]::UrlEncode("urn:ietf:params:oauth:grant-type:jwt-bearer");
$body = ("grant_type=" + $grant_type + "&assertion=" + $jwt);
try
{
$encodedbody = [System.Text.Encoding]::UTF8.GetBytes($body)
$JWTRequest = [System.Net.WebRequest]::Create($requestUri)
$JWTRequest.Method = "Post"
$JWTRequest.ContentType = "application/x-www-form-urlencoded"
$JWTRequest.ContentLength = $encodedbody.length
$requestStream = $JWTRequest.GetRequestStream()
$requestStream.Write($encodedbody, 0, $encodedbody.length)
$requestStream.Close()
[System.Net.WebResponse] $JWTresponse = $JWTRequest.GetResponse();
if($JWTresponse -ne $null)
{
$JWTstatus = ($JWTresponse.StatusCode.value__).ToString().Trim();
$JWTstatusDescription = ($JWTresponse.StatusDescription).ToString().Trim();
$JWTrstream = $JWTresponse.GetResponseStream();
[System.IO.StreamReader] $JWTsread = New-Object System.IO.StreamReader -argumentList $JWTrstream;
$JWTstatusJsonContent = $JWTsread.ReadToEnd();
}
}
etc...
This has served us well for the last 12 months
Upvotes: 0
Reputation: 11
After several hours of searching for a way how to use GCP service account json file with PowerShell, came across this post that explains difference between OpenSSL pem format and Microsoft CryptoAPI that ImportCspBlob understands.
https://www.sysadmins.lv/blog-en/how-to-convert-pem-file-to-a-cryptoapi-compatible-format.aspx
With some slight modification here is code that I use in in my PowerShell script:
function Get-ASNLength ($RawData, $offset) {
$return = "" | Select FullLength, Padding, LengthBytes, PayLoadLength
if ($RawData[$offset + 1] -lt 128) {
$return.lengthbytes = 1
$return.Padding = 0
$return.PayLoadLength = $RawData[$offset + 1]
$return.FullLength = $return.Padding + $return.lengthbytes + $return.PayLoadLength + 1
} else {
$return.lengthbytes = $RawData[$offset + 1] - 128
$return.Padding = 1
$lengthstring = -join ($RawData[($offset + 2)..($offset + 1 + $return.lengthbytes)] | %{"{0:x2}" -f $_})
$return.PayLoadLength = Invoke-Expression 0x$($lengthstring)
$return.FullLength = $return.Padding + $return.lengthbytes + $return.PayLoadLength + 1
}
$return
}
function Get-NormalizedArray ($array) {
$padding = $array.Length % 8
if ($padding) {
$array = $array[$padding..($array.Length - 1)]
}
[array]::Reverse($array)
[Byte[]]$array
}
function Convert-OpenSSLPrivateKey ($key) {
if ($key -match "(?msx).*-{5}BEGIN\sPRIVATE\sKEY-{5}(.+)-{5}END\sPRIVATE\sKEY-{5}") {
Write-Debug "Processing Private Key module."
$Bytes = [Convert]::FromBase64String($matches[1])
if ($Bytes[0] -eq 48) {Write-Debug "Starting asn.1 decoding."}
else {Write-Warning "The data is invalid."; return}
$offset = 0
# main sequence
Write-Debug "Process outer Sequence tag."
$return = Get-ASNLength $Bytes $offset
Write-Debug "outer Sequence length is $($return.PayloadLength) bytes."
$offset += $return.FullLength - $return.PayloadLength
Write-Debug "New offset is: $offset"
# zero integer
Write-Debug "Process zero byte"
$return = Get-ASNLength $Bytes $offset
Write-Debug "outer zero byte length is $($return.PayloadLength) bytes."
$offset += $return.FullLength
Write-Debug "New offset is: $offset"
# algorithm identifier
Write-Debug "Proess algorithm identifier"
$return = Get-ASNLength $Bytes $offset
Write-Debug "Algorithm identifier length is $($return.PayloadLength) bytes."
$offset += $return.FullLength
Write-Debug "New offset is: $offset"
# octet string
$return = Get-ASNLength $Bytes $offset
Write-Debug "Private key octet string length is $($return.PayloadLength) bytes."
$offset += $return.FullLength - $return.PayLoadLength
Write-Debug "New offset is: $offset"
} elseif ($key -match "(?msx).*-{5}BEGIN\sRSA\sPRIVATE\sKEY-{5}(.+)-{5}END\sRSA\sPRIVATE\sKEY-{5}") {
Write-Debug "Processing RSA KEY module."
$Bytes = [Convert]::FromBase64String($matches[1])
if ($Bytes[0] -eq 48) {Write-Debug "Starting asn.1 decoding"}
else {Write-Warning "The data is invalid"; return}
$offset = 0
Write-Debug "New offset is: $offset"
} else {Write-Warning "The data is invalid"; return}
# private key sequence
Write-Debug "Process private key sequence."
$return = Get-ASNLength $Bytes $offset
Write-Debug "Private key length (including inner ASN.1 tags) is $($return.PayloadLength) bytes."
$offset += $return.FullLength - $return.PayLoadLength
Write-Debug "New offset is: $offset"
# zero integer
Write-Debug "Process zero byte"
$return = Get-ASNLength $Bytes $offset
Write-Debug "Zero byte length is $($return.PayloadLength) bytes."
$offset += $return.FullLength
Write-Debug "New offset is: $offset"
# modulus
Write-Debug "Processing private key modulus."
$return = Get-ASNLength $Bytes $offset
Write-Debug "Private key modulus length is $($return.PayloadLength) bytes."
$modulus = $Bytes[($offset + $return.FullLength - $return.PayLoadLength)..($offset + $return.FullLength - 1)]
$modulus = Get-NormalizedArray $modulus
$offset += $return.FullLength
Write-Debug "New offset is: $offset"
# public exponent
Write-Debug "Process private key public exponent."
$return = Get-ASNLength $Bytes $offset
Write-Debug "Private key public exponent length is $($return.PayloadLength) bytes."
Write-Debug "Private key public exponent padding is $(4 - $return.PayLoadLength) byte(s)."
$padding = New-Object byte[] -ArgumentList (4 - $return.PayLoadLength)
[Byte[]]$PublicExponent = $padding + $Bytes[($offset + $return.FullLength - $return.PayLoadLength)..($offset + $return.FullLength - 1)]
$offset += $return.FullLength
Write-Debug "New offset is: $offset"
# private exponent
Write-Debug "Process private key private exponent."
$return = Get-ASNLength $Bytes $offset
Write-Debug "Private key private exponent length is $($return.PayloadLength) bytes."
$PrivateExponent = $Bytes[($offset + $return.FullLength - $return.PayLoadLength)..($offset + $return.FullLength - 1)]
$PrivateExponent = Get-NormalizedArray $PrivateExponent
$offset += $return.FullLength
Write-Debug "New offset is: $offset"
# prime1
Write-Debug "Process Prime1."
$return = Get-ASNLength $Bytes $offset
Write-Debug "Prime1 length is $($return.PayloadLength) bytes."
$Prime1 = $Bytes[($offset + $return.FullLength - $return.PayLoadLength)..($offset + $return.FullLength - 1)]
$Prime1 = Get-NormalizedArray $Prime1
$offset += $return.FullLength
Write-Debug "New offset is: $offset"
# prime2
Write-Debug "Process Prime2."
$return = Get-ASNLength $Bytes $offset
Write-Debug "Prime2 length is $($return.PayloadLength) bytes."
$Prime2 = $Bytes[($offset + $return.FullLength - $return.PayLoadLength)..($offset + $return.FullLength - 1)]
$Prime2 = Get-NormalizedArray $Prime2
$offset += $return.FullLength
Write-Debug "New offset is: $offset"
# exponent1
Write-Debug "Process Exponent1."
$return = Get-ASNLength $Bytes $offset
Write-Debug "Exponent1 length is $($return.PayloadLength) bytes."
$Exponent1 = $Bytes[($offset + $return.FullLength - $return.PayLoadLength)..($offset + $return.FullLength - 1)]
$Exponent1 = Get-NormalizedArray $Exponent1
$offset += $return.FullLength
Write-Debug "New offset is: $offset"
# exponent2
Write-Debug "Process Exponent2."
$return = Get-ASNLength $Bytes $offset
Write-Debug "Exponent2 length is $($return.PayloadLength) bytes."
$Exponent2 = $Bytes[($offset + $return.FullLength - $return.PayLoadLength)..($offset + $return.FullLength - 1)]
$Exponent2 = Get-NormalizedArray $Exponent2
$offset += $return.FullLength
Write-Debug "New offset is: $offset"
# coefficient
Write-Debug "Process Coefficient."
$return = Get-ASNLength $Bytes $offset
Write-Debug "Coeicient length is $($return.PayloadLength) bytes."
$Coefficient = $Bytes[($offset + $return.FullLength - $return.PayLoadLength)..($offset + $return.FullLength - 1)]
$Coefficient = Get-NormalizedArray $Coefficient
# creating Private Key BLOB structure
Write-Debug "Calculating key length."
$bitLen = "{0:X4}" -f $($modulus.Length * 8)
Write-Debug "Key length is $($modulus.Length * 8) bits."
[byte[]]$bitLen1 = iex 0x$([int]$bitLen.Substring(0,2))
[byte[]]$bitLen2 = iex 0x$([int]$bitLen.Substring(2,2))
[Byte[]]$PrivateKey = 0x07,0x02,0x00,0x00,0x00,0x24,0x00,0x00,0x52,0x53,0x41,0x32,0x00
[Byte[]]$PrivateKey = $PrivateKey + $bitLen1 + $bitLen2 + $PublicExponent + ,0x00 + `
$modulus + $Prime1 + $Prime2 + $Exponent1 + $Exponent2 + $Coefficient + $PrivateExponent
return $PrivateKey
}
Upvotes: 1
Reputation: 2890
I suspect the problem is with the initialization of the RSACryptoServiceProvider with the proper public and private keys, which you've been provided as a JSON object. The .toXML()
method you're calling probably isn't working.
There's some discussion of how to set your own public/private keys in this question that may be a path to take.
You might try generating a new keypair and .toXML($true)
on the result to see how the XML is formatted, then massage your JSON-based key data into that format.
edit
After researching, the challenge you face is converting the PKCS#8-encoded key provided by Google into a form consumable by RSACryptoServiceProvider. .NET doesn't currently have APIs for reading this key, but there is interest in rectifying this deficiency.
There is an issue on the .net core for providing ASN.1 decoding facilities, which would be a prerequisite.
Others have written their own code to handle this specific problem.
There are informative blog posts out there detailing the issue.
There is copious documentation on DER encoding and the PKCS#8 format that your private key is in.
One workaround that seems to function is to generate P12 keys for your service account. If creating a new service account isn't an option, there are ways to convert the private key file in the .json file to P12
I created a new service account with p12 keys and the powershell code for signing is even simpler than before:
$certificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($p12file,'thepasswordforthekeystore')
$dataToSign = "This is some data"
$certificate.privateKey.SignData(
[system.text.encoding]::utf8.getbytes($dataToSign),
[System.Security.Cryptography.HashAlgorithmName]::SHA256,
[System.Security.Cryptography.RSASignaturePadding]::Pkcs1)
Upvotes: 2