param ( [Parameter(Mandatory=$true)][string]$method, [Parameter(Mandatory=$true)][string]$ocihost, [Parameter(Mandatory=$true)][string]$target, [bool]$echo_debug = $false, # TODO: Update these defaults or override them on the command line. [string]$tenancyId = 'ocid1.tenancy.oc1..aaaaaaaaba3pv6wkcr4jqae5f15p2b2m2yt2j6rx32uzr4h25vqstifsfdsq', [string]$authUserId= 'ocid1.user.oc1..aaaaaaaalj3z3isgtuqd5uqft424he7r3cuqfr3e5gpidgnmqsxwd5qevkha', [string]$keyFingerprint = '29:f3:01:46:07:b8:dc:8c:16:c3:2b:b3:8d:dc:26:c5', [string]$privateKeyPath = $PSScriptRoot + '/oci_api_key.pem', [Parameter(Mandatory=$false)][string]$body, [Parameter(Mandatory=$false)][string]$bouncycastlelib ) ############################################################################## # This is a powershell example of how to create request signatures for an # Oracle Cloud Infra REST API call. It was modeled after the bash example. # # Note that it utilizes the Bouncy Castle library dll for crypto functionality. # It is assumed to be in the same directory as this script, # but can be changed via commandline argument. # See https://www.bouncycastle.org for more details. # # Usage: # oci-rest.ps1 -host -method -body [file-to-send-as-body] -target -bouncycastlelib [BouncyCastle.Crypto.dll] # # Examples: # ./oci-rest.ps1 -method get -ocihost iaas.us-ashburn-1.oraclecloud.com -target "/20160918/instances?compartmentId=ocid1.compartment.oc1..aaaaaaaam3we6vgnherjq5q2idnccdflvjsnog7mlr6rtdb25gilchfeyjxa" # ./oci-rest.ps1 -method post -ocihost iaas.us-ashburn-1.oraclecloud.com -target "/20160918/vcns" -body ./request.json # ############################################################################## ############################################################################## # Creates a message digest for the request body. ############################################################################## function Digest($body_file_path) { $sha256digest = New-Object org.bouncycastle.crypto.digests.SHA256Digest $content = Get-Item $body_file_path $bytes = $null if ($PSVersionTable.PSVersion.Major -ge 6) { [byte[]]$bytes = Get-Content $body_file_path -AsByteStream } else { [byte[]]$bytes = Get-Content $body_file_path -Encoding byte } $sha256digest.BlockUpdate($bytes, 0, $bytes.Length) $result_size = $sha256digest.GetDigestSize() $result_bytes = New-Object Byte[] $result_size $sha256digest.DoFinal($result_bytes, 0) | Out-Null $content_sha256 = [Convert]::ToBase64String($result_bytes) return $content_sha256 } ############################################################################## # Creates the signature to be put in the Authorization request header. ############################################################################## function Sign($signing_string, $privateKeyPath) { $sha256digest = New-Object org.bouncycastle.crypto.digests.SHA256Digest $signer = New-Object Org.BouncyCastle.Crypto.Signers.RSADigestSigner $sha256digest $privateKeyFile = [System.IO.File]::OpenText($privateKeyPath) $pemReader = New-Object Org.BouncyCastle.OpenSsl.PemReader $privateKeyFile $keyPair = [Org.BouncyCastle.Crypto.AsymmetricCipherKeyPair]$pemReader.ReadObject() #$keyParameter = [Org.BouncyCastle.Security.DotNetUtilities]::ToRSAParameters($keyPair.Private) $keyParameter = $keyPair.Private $signer.Init($true, $keyParameter) $encoding = [System.Text.Encoding]::UTF8 [byte[]]$bytes = $encoding.GetBytes($signing_string) $signer.BlockUpdate($bytes, 0, $bytes.Length) $signature = $signer.GenerateSignature() $signedString = [Convert]::ToBase64String($signature) return $signedString } ############################################################################## # Makes the Oracle Cloud API REST call. ############################################################################## function RestCall($method, $ocihost, $target, $privateKeyPath, $keyId, $body_file_path='', $echo_debug=$false) { $alg = 'rsa-sha256' $sigVersion = '1' $now = Get-Date $now = $now.ToUniversalTime() $now_string = $now.ToString("ddd, dd MMM yyyy HH:mm:ss") + " GMT" $content_type = '' $content_length = 0 $content_sha256 = '' $request_method = $method.ToLower() If ($request_method -eq "get") { $method = "Get" } ElseIf ($request_method -eq "delete") { $method = "Delete" } ElseIf ($request_method -eq "head") { $method = "Head" } ElseIf ($request_method -eq "post") { if ($body_file_path.Length -eq 0) { echo "body parameter must be specified and point to valid json body file." Exit 1 } $method = "Post" $content_type = 'application/json' $content_length = (Get-Item $body_file_path).length $content_sha256 = Digest $body_file_path if ($echo_debug) { output_debug "digest=$content_sha256" } } ElseIf ($request_method -eq "put") { if ($body_file_path.Length -eq 0) { echo "body parameter must be specified and point to valid json body file." Exit 1 } $method = "Put" $content_type = 'application/json' $content_length = (Get-Item $body_file_path).length $content_sha256 = Digest $body_file_path if ($echo_debug) { output_debug "digest=$content_sha256" } } Else { echo "invalid method" Exit 1 } $escaped_target = rawurlencode $target $request_target = $request_method + " " + $escaped_target if ($echo_debug) { output_debug "escaped target=$escaped_target" } $headers = @{} $header_list = "(request-target) date host" $headers["date"] = $now_string $headers["host"] = $ocihost #$nl = [Environment]::NewLine # This doesn't work in windows environments $nl = "`n" $signing_string = "(request-target): " + $request_target + $nl + "date: " + $now_string + $nl + "host: " + $ocihost if (($request_method -eq "put") -or ($request_method -eq "post")) { $headers["x-content-sha256"] = $content_sha256 $headers["content-type"] = $content_type $headers["content-length"] = $content_length $header_list = $header_list + " x-content-sha256 content-type content-length" $signing_string = $signing_string + $nl + "x-content-sha256: " + $content_sha256 + $nl + "content-type: " + $content_type + $nl + "content-length: " + $content_length } if ($echo_debug) { output_debug "signing string=$signing_string" } $sig = Sign $signing_string $privateKeyPath if ($echo_debug) { output_debug "signature=$sig" } $authorization = 'Signature version="' + $sigVersion + '",keyId="' + $keyId + '",algorithm="' + $alg + '",headers="' + $header_list + '",signature="' + $sig + '"' $headers["Authorization"] = $authorization $url = "https://" + $ocihost + $escaped_target if ($echo_debug) { output_debug "authorization=$authorization" output_debug "url=$url" output_debug "headers=$headers" $headers.getenumerator() | Out-String } # Without this setting, Invoke-RestMethod was failing on windows with a connection error. [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 if ($body_file_path.Length -gt 0) { PostPutRequest $method $url $headers $body_file_path } else { Invoke-RestMethod -Method $method -Uri $url -Headers $headers | ConvertTo-Json } } ############################################################################## # Makes a Post or Put call. # We do this b/c Invoke-RestMethod doesn't seem to give the granular control # needed, but HttpWebRequest works well. ############################################################################## Function PostPutRequest($method, $url, $headers, $body_file_path) { $junk = [System.Reflection.Assembly]::LoadWithPartialName("System.Web") $request = [System.Net.HttpWebRequest]::Create($url) $request.Method = $method.ToUpper() $request.Accept = "application/json"; $request.ProtocolVersion = "1.0" $request.ContentType = $headers["content-type"] $request.ContentLength = $headers["content-length"] $request.Date = $headers["date"] $request.Host = $headers["host"] $request.Headers["x-content-sha256"] = $headers["x-content-sha256"] $request.Headers["authorization"] = $headers["authorization"] #$request.Headers["(request-target)"] = $headers["(request-target)"] # Create the input stream to the REST API $requestInputStream = $request.GetRequestStream() # Create a stream writer to write the json $writer = New-Object System.IO.StreamWriter($requestInputStream) $writer.AutoFlush = $true # Write the json Try { $bytes = $null if ($PSVersionTable.PSVersion.Major -ge 6) { [byte[]]$bytes = Get-Content $body_file_path -AsByteStream } else { [byte[]]$bytes = Get-Content $body_file_path -Encoding byte } $writer.Write($bytes, 0, $bytes.Length) } Catch [System.IO.IOException] { Throw "Cannot write to stream. Exception $($_.Exception)" } Catch [System.Exception] { Throw "Some other weird error caught...$($_.Exception)" } Finally { $writer.Close() } Get-WebResponseOutput $request } ############################################################################## # Gets the response output for a request. ############################################################################## Function Get-WebResponseOutput($request) { $junk = [System.Reflection.Assembly]::LoadWithPartialName("System.Web") $response = $null Try { $response = $request.GetResponse() } Catch [System.Net.WebException] { echo "Exception from server: " $_.Exception.Message $ex = $_.Exception.Response.StatusCode if ($response -ne $null) { $response.Close() } Throw "Exception from server: $ex" } Catch [System.Exception] { if ($response -ne $null) { $response.Close() } echo "Some other random error: " $_.Exception.Message Throw "Some other random error..$($_.Exception)" } $readStream = $response.GetResponseStream() $reader = New-Object System.IO.StreamReader($readStream) Try { $output = $reader.readtoend() } Catch { echo "Exception reading stream from server. Exception: " $_.Exception.Message Throw "Exception reading stream from server. Exception: $($_.Exception)" } Finally { if ($response -ne $null) { $response.Close() } $reader.Close() } return $output } ############################################################################## # url encode all special characters except "/", "?", "=", and "&" # This will url encode all special characters in the request target except "/", "?", "=", and "&", # since those characters are used in the request target to indicate path and query string structure. # If you need to encode any of "/", "?", "=", or "&", such as, when used # as part of a path value or query string key or value, # you will need to do that yourself in the request target you pass in. ############################################################################## function rawurlencode($target) { Add-Type -AssemblyName System.Web $chars_to_skip = "-_.~abcdefghijklmnopqrstuvwxzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/?=&"; $encoded = "" for ($i=0; $i -lt $target.Length; $i++) { $ch = $target[$i] $o = $ch if ($chars_to_skip.IndexOf($ch) -lt 0) { $o = [System.Web.HttpUtility]::UrlEncode($ch) } $encoded = $encoded + $o } return $encoded } ############################################################################## # Trivial function to output debug messages to console. ############################################################################## function output_debug($msg) { echo "[debug] $msg" #Write-Verbose $msg } ############################################################################## # Main entry point logic. ############################################################################## if ($bouncycastlelib.Length -eq 0) { $bouncycastlelib = $PSScriptRoot + "/BouncyCastle.Crypto.dll" } if ($echo_debug) { [Reflection.Assembly]::LoadFile($bouncycastlelib) output_debug "Bouncy Castle loaded and ready" } else { [Reflection.Assembly]::LoadFile($bouncycastlelib) | Out-Null } $keyId = $tenancyId + "/" + $authUserId + "/" + $keyFingerprint if ($echo_debug) { output_debug "keyId=$keyId" } RestCall $method $ocihost $target $privateKeyPath $keyId $body $echo_debug