Emsg
Emsg

Reputation: 441

Google Identity Platform: Using OAuth 2.0 in Powershell using Firebase Admin SDK private key

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

Answers (3)

Emsg
Emsg

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

bakiba
bakiba

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

veefu
veefu

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.

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

Related Questions