2

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.

rangi
  • 361
  • 2
  • 4
  • 21
  • It seems like google provides .Net wrappers for their APIs and powershell should be able to consume any .net code pretty well. Have you already explored this path? – veefu Aug 14 '18 at 04:19
  • There is one notable thing in your code. It seems to pass only the private key to the RSACryptoProvider, but [documentation](https://msdn.microsoft.com/en-us/library/system.security.cryptography.rsa.fromxmlstring(v=vs.110).aspx) says it expects either a public key or both a public and private key. – veefu Aug 14 '18 at 05:28
  • There's more that looks suspect. The documentation for [RSA.toXMLString](https://msdn.microsoft.com/en-us/library/system.security.cryptography.rsa.toxmlstring(v=vs.110).aspx) takes a boolean. It seems to be exporting an existing keypair and the boolean determines whether to include private key data. It does not take a string, though powershell may be reinterpreting the string as `$true` – veefu Aug 14 '18 at 05:32
  • No, I think RSACryptoServiceProvider is probably the right approach, it's just complicated to initialize from an outside source. I think you're right that signing the hash with the private key shouldn't require the public key, but... that's what is written in the docs. – veefu Aug 14 '18 at 06:13
  • @rangi Did you ever figure this out? I'm running into the same issue and I'm also using PowerShell. – LPG Oct 27 '18 at 00:15
  • Hey @LPG Not so far I'm afraid, haven't had a chance to get back to it, still want to though and any clues appreciated I think it must be possible if we can generate the correct key..! – rangi Oct 28 '18 at 04:04

2 Answers2

2

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)
Community
  • 1
  • 1
veefu
  • 2,820
  • 1
  • 19
  • 29
  • Edited the answer above with updated understanding of the problem and proposed a work-around with powershell sample code. – veefu Aug 17 '18 at 08:27
  • Wow @veefu thanks for all your tips.. amazing..! Quite a comprehensive list of possibilities for me to work through, might take me a day or three. Now I get why Google tells us to just use ready-made Apis, which is fine if you're using Java or Python in about 3 lines of code, but not so easy with REST/HTTP – rangi Aug 18 '18 at 23:37
  • Just haven't got time to do anymore work on this at the moment, so I'll put it on Upwork.com and see if anyone else has figured it out – rangi Dec 20 '18 at 03:15
1

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

}
bakiba
  • 11
  • 1