From 13f329eafed016880a623801a55736e99dd60a63 Mon Sep 17 00:00:00 2001 From: Ghufz <18732053+Ghufz@users.noreply.github.com> Date: Thu, 7 May 2020 20:16:40 +0530 Subject: [PATCH] [powershell-experimental] : http signature authentication implementation (#6176) * ValidatePattern having double quote(") throws exception on running Build.ps1 * fix tab with space * [powershell-experimental] : http signature auth * fix the tab issue Co-authored-by: Ghufran Zahidi --- .../PowerShellExperimentalClientCodegen.java | 6 +- .../api_client.mustache | 15 + .../http_signature_auth.mustache | 162 ++++++++ .../rsa_provider.mustache | 377 ++++++++++++++++++ 4 files changed, 559 insertions(+), 1 deletion(-) create mode 100644 modules/openapi-generator/src/main/resources/powershell-experimental/http_signature_auth.mustache create mode 100644 modules/openapi-generator/src/main/resources/powershell-experimental/rsa_provider.mustache diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PowerShellExperimentalClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PowerShellExperimentalClientCodegen.java index 5896ee99efea..3151617e438b 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PowerShellExperimentalClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PowerShellExperimentalClientCodegen.java @@ -615,6 +615,9 @@ public void processOpts() { supportingFiles.add(new SupportingFile("api_client.mustache", infrastructureFolder + "Private", apiNamePrefix + "ApiClient.ps1")); supportingFiles.add(new SupportingFile("Get-CommonParameters.mustache", infrastructureFolder + File.separator + "Private" + File.separator, "Get-CommonParameters.ps1")); supportingFiles.add(new SupportingFile("Out-DebugParameter.mustache", infrastructureFolder + File.separator + "Private" + File.separator, "Out-DebugParameter.ps1")); + supportingFiles.add(new SupportingFile("http_signature_auth.mustache", infrastructureFolder + "Private", apiNamePrefix + "HttpSignatureAuth.ps1")); + supportingFiles.add(new SupportingFile("rsa_provider.mustache", infrastructureFolder + "Private", apiNamePrefix + "RSAEncryptionProvider.cs")); + // en-US supportingFiles.add(new SupportingFile("about_Org.OpenAPITools.help.txt.mustache", infrastructureFolder + File.separator + "en-US" + File.separator + "about_" + packageName + ".help.txt")); @@ -626,7 +629,7 @@ public void processOpts() { @SuppressWarnings("static-method") @Override public String escapeText(String input) { - + if (input == null) { return input; } @@ -643,6 +646,7 @@ public String escapeText(String input) { .replaceAll("[\\t\\n\\r]", " ") .replace("\\", "\\\\") .replace("\"", "\"\"")); + } @Override diff --git a/modules/openapi-generator/src/main/resources/powershell-experimental/api_client.mustache b/modules/openapi-generator/src/main/resources/powershell-experimental/api_client.mustache index 2d785b840d1e..c6a456c481ec 100644 --- a/modules/openapi-generator/src/main/resources/powershell-experimental/api_client.mustache +++ b/modules/openapi-generator/src/main/resources/powershell-experimental/api_client.mustache @@ -89,6 +89,21 @@ function Invoke-{{{apiNamePrefix}}}ApiClient { $RequestBody = $Body } +# http signature authentication + if ($null -ne $Configuration['ApiKey'] -and $Configuration['ApiKey'].Count -gt 0) { + $httpSignHeaderArgument = @{ + Method = $Method + UriBuilder = $UriBuilder + Body = $Body + } + $signedHeader = Get-{{{apiNamePrefix}}}HttpSignedHeader @httpSignHeaderArgument + if($null -ne $signedHeader -and $signedHeader.Count -gt 0){ + foreach($item in $signedHeader.GetEnumerator()){ + $HeaderParameters[$item.Name] = $item.Value + } + } + } + if ($SkipCertificateCheck -eq $true) { $Response = Invoke-WebRequest -Uri $UriBuilder.Uri ` -Method $Method ` diff --git a/modules/openapi-generator/src/main/resources/powershell-experimental/http_signature_auth.mustache b/modules/openapi-generator/src/main/resources/powershell-experimental/http_signature_auth.mustache new file mode 100644 index 000000000000..aeb74be678dc --- /dev/null +++ b/modules/openapi-generator/src/main/resources/powershell-experimental/http_signature_auth.mustache @@ -0,0 +1,162 @@ +{{>partial_header}} +<# +.SYNOPSIS +Get the API key Id and API key file path. + +.DESCRIPTION +Get the API key Id and API key file path. If no api prefix is provided then it use default api key prefix 'Signature' +.OUTPUTS +PSCustomObject : This contains APIKeyId, APIKeyFilePath, APIKeyPrefix +#> +function Get-{{{apiNamePrefix}}}APIKeyInfo { + $ApiKeysList = $Script:Configuration['ApiKey'] + $ApiKeyPrefixList = $Script:Configuration['ApiKeyPrefix'] + $apiPrefix = "Signature" + + if ($null -eq $ApiKeysList -or $ApiKeysList.Count -eq 0) { + throw "Unable to reterieve the api key details" + } + + if ($null -eq $ApiKeyPrefixList -or $ApiKeyPrefixList.Count -eq 0) { + Write-Verbose "Unable to reterieve the api key prefix details,setting it to default ""Signature""" + } + + foreach ($item in $ApiKeysList.GetEnumerator()) { + if (![string]::IsNullOrEmpty($item.Name)) { + if (Test-Path -Path $item.Value) { + $apiKey = $item.Value + $apikeyId = $item.Name + break; + } + else { + throw "API key file path does not exist." + } + } + } + + if ($ApiKeyPrefixList.ContainsKey($apikeyId)) { + $apiPrefix = ApiKeyPrefixList[$apikeyId] + } + + if ($apikeyId -and $apiKey -and $apiPrefix) { + $result = New-Object -Type PSCustomObject -Property @{ + ApiKeyId = $apikeyId; + ApiKeyFilePath = $apiKey + ApiKeyPrefix = $apiPrefix + } + } + else { + return $null + } + return $result +} + +<# +.SYNOPSIS + Gets the headers for http signed auth. + +.DESCRIPTION + Gets the headers for the http signed auth. It use (targetpath), date, host and body digest to create authorization header. +.PARAMETER Method + Http method +.PARAMETER UriBuilder + UriBuilder for url and query parameter +.PARAMETER Body + Request body +.OUTPUTS + Hashtable +#> +function Get-{{{apiNamePrefix}}}HttpSignedHeader { + param( + [string]$Method, + [System.UriBuilder]$UriBuilder, + [string]$Body + ) + + #Hash table to store singed headers + $HttpSignedHeader = @{} + $TargetHost = $UriBuilder.Host + + #Check for Authentication type + $apiKeyInfo = Get-{{{apiNamePrefix}}}APIKeyInfo + if ($null -eq $apiKeyInfo) { + throw "Unable to reterieve the api key info " + } + + #get the body digest + $bodyHash = Get-{{{apiNamePrefix}}}StringHash -String $Body + $Digest = [String]::Format("SHA-256={0}", [Convert]::ToBase64String($bodyHash)) + + #get the date in UTC + $dateTime = Get-Date + $currentDate = $dateTime.ToUniversalTime().ToString("r") + + $requestTargetPath = [string]::Format("{0} {1}{2}",$Method.ToLower(),$UriBuilder.Path.ToLower(),$UriBuilder.Query) + $h_requestTarget = [string]::Format("(request-target): {0}",$requestTargetPath) + $h_cdate = [string]::Format("date: {0}",$currentDate) + $h_digest = [string]::Format("digest: {0}",$Digest) + $h_targetHost = [string]::Format("host: {0}",$TargetHost) + + $stringToSign = [String]::Format("{0}`n{1}`n{2}`n{3}", + $h_requestTarget,$h_cdate, + $h_targetHost,$h_digest) + + $hashedString = Get-{{{apiNamePrefix}}}StringHash -String $stringToSign + $signedHeader = Get-{{{apiNamePrefix}}}RSASHA256SignedString -APIKeyFilePath $apiKeyInfo.ApiKeyFilePath -DataToSign $hashedString + $authorizationHeader = [string]::Format("{0} keyId=""{1}"",algorithm=""rsa-sha256"",headers=""(request-target) date host digest"",signature=""{2}""", + $apiKeyInfo.ApiKeyPrefix, $apiKeyInfo.ApiKeyId, $signedHeader) + + $HttpSignedHeader["Date"] = $currentDate + $HttpSignedHeader["Host"] = $TargetHost + $HttpSignedHeader["Content-Type"] = "application/json" + $HttpSignedHeader["Digest"] = $Digest + $HttpSignedHeader["Authorization"] = $authorizationHeader + return $HttpSignedHeader +} + +<# +.SYNOPSIS + Gets the headers for http signed auth. + +.DESCRIPTION + Gets the headers for the http signed auth. It use (targetpath), date, host and body digest to create authorization header. +.PARAMETER APIKeyFilePath + Specify the API key file path +.PARAMETER DataToSign + Specify the data to sign +.OUTPUTS + String +#> +function Get-{{{apiNamePrefix}}}RSASHA256SignedString { + Param( + [string]$APIKeyFilePath, + [byte[]]$DataToSign + ) + try { + + $rsa_provider_path = Join-Path -Path $PSScriptRoot -ChildPath "{{{apiNamePrefix}}}RSAEncryptionProvider.cs" + $rsa_provider_sourceCode = Get-Content -Path $rsa_provider_path -Raw + Add-Type -TypeDefinition $rsa_provider_sourceCode + $signed_string = [RSAEncryption.RSAEncryptionProvider]::GetRSASignb64encode($APIKeyFilePath, $DataToSign) + if ($null -eq $signed_string) { + throw "Unable to sign the header using the API key" + } + return $signed_string + } + catch { + throw $_ + } +} +<# +.Synopsis + Gets the hash of string. +.Description + Gets the hash of string +.Outputs +String +#> +Function Get-{{{apiNamePrefix}}}StringHash([String] $String, $HashName = "SHA256") { + + $hashAlogrithm = [System.Security.Cryptography.HashAlgorithm]::Create($HashName) + $hashAlogrithm.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($String)) +} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/powershell-experimental/rsa_provider.mustache b/modules/openapi-generator/src/main/resources/powershell-experimental/rsa_provider.mustache new file mode 100644 index 000000000000..a23490b366e4 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/powershell-experimental/rsa_provider.mustache @@ -0,0 +1,377 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.Cryptography; +using System.Text; + +namespace RSAEncryption +{ + public class RSAEncryptionProvider + { + + const String pemprivheader = "-----BEGIN RSA PRIVATE KEY-----"; + const String pemprivfooter = "-----END RSA PRIVATE KEY-----"; + const String pempubheader = "-----BEGIN PUBLIC KEY-----"; + const String pempubfooter = "-----END PUBLIC KEY-----"; + const String pemp8header = "-----BEGIN PRIVATE KEY-----"; + const String pemp8footer = "-----END PRIVATE KEY-----"; + const String pemp8encheader = "-----BEGIN ENCRYPTED PRIVATE KEY-----"; + const String pemp8encfooter = "-----END ENCRYPTED PRIVATE KEY-----"; + public static RSACryptoServiceProvider DecodeRSAPrivateKey(byte[] privkey) + { + byte[] MODULUS, E, D, P, Q, DP, DQ, IQ; + + // --------- Set up stream to decode the asn.1 encoded RSA private key ------ + MemoryStream mem = new MemoryStream(privkey); + BinaryReader binr = new BinaryReader(mem); //wrap Memory Stream with BinaryReader for easy reading + byte bt = 0; + ushort twobytes = 0; + int elems = 0; + try + { + twobytes = binr.ReadUInt16(); + if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81) + binr.ReadByte(); //advance 1 byte + else if (twobytes == 0x8230) + binr.ReadInt16(); //advance 2 bytes + else + return null; + + twobytes = binr.ReadUInt16(); + if (twobytes != 0x0102) //version number + return null; + bt = binr.ReadByte(); + if (bt != 0x00) + return null; + + + //------ all private key components are Integer sequences ---- + elems = GetIntegerSize(binr); + MODULUS = binr.ReadBytes(elems); + + elems = GetIntegerSize(binr); + E = binr.ReadBytes(elems); + + elems = GetIntegerSize(binr); + D = binr.ReadBytes(elems); + + elems = GetIntegerSize(binr); + P = binr.ReadBytes(elems); + + elems = GetIntegerSize(binr); + Q = binr.ReadBytes(elems); + + elems = GetIntegerSize(binr); + DP = binr.ReadBytes(elems); + + elems = GetIntegerSize(binr); + DQ = binr.ReadBytes(elems); + + elems = GetIntegerSize(binr); + IQ = binr.ReadBytes(elems); + + /*Console.WriteLine("showing components .."); + if (true) + { + showBytes("\nModulus", MODULUS); + showBytes("\nExponent", E); + showBytes("\nD", D); + showBytes("\nP", P); + showBytes("\nQ", Q); + showBytes("\nDP", DP); + showBytes("\nDQ", DQ); + showBytes("\nIQ", IQ); + }*/ + + // ------- create RSACryptoServiceProvider instance and initialize with public key ----- + RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(); + RSAParameters RSAparams = new RSAParameters(); + RSAparams.Modulus = MODULUS; + RSAparams.Exponent = E; + RSAparams.D = D; + RSAparams.P = P; + RSAparams.Q = Q; + RSAparams.DP = DP; + RSAparams.DQ = DQ; + RSAparams.InverseQ = IQ; + RSA.ImportParameters(RSAparams); + return RSA; + } + catch (Exception) + { + return null; + } + finally { binr.Close(); } + } + + private static int GetIntegerSize(BinaryReader binr) + { + byte bt = 0; + byte lowbyte = 0x00; + byte highbyte = 0x00; + int count = 0; + bt = binr.ReadByte(); + if (bt != 0x02) //expect integer + return 0; + bt = binr.ReadByte(); + + if (bt == 0x81) + count = binr.ReadByte(); // data size in next byte + else + if (bt == 0x82) + { + highbyte = binr.ReadByte(); // data size in next 2 bytes + lowbyte = binr.ReadByte(); + byte[] modint = { lowbyte, highbyte, 0x00, 0x00 }; + count = BitConverter.ToInt32(modint, 0); + } + else + { + count = bt; // we already have the data size + } + while (binr.ReadByte() == 0x00) + { //remove high order zeros in data + count -= 1; + } + binr.BaseStream.Seek(-1, SeekOrigin.Current); + //last ReadByte wasn't a removed zero, so back up a byte + return count; + } + + static byte[] DecodeOpenSSLPrivateKey(String instr) + { + const String pemprivheader = "-----BEGIN RSA PRIVATE KEY-----"; + const String pemprivfooter = "-----END RSA PRIVATE KEY-----"; + String pemstr = instr.Trim(); + byte[] binkey; + if (!pemstr.StartsWith(pemprivheader) || !pemstr.EndsWith(pemprivfooter)) + return null; + + StringBuilder sb = new StringBuilder(pemstr); + sb.Replace(pemprivheader, ""); //remove headers/footers, if present + sb.Replace(pemprivfooter, ""); + + String pvkstr = sb.ToString().Trim(); //get string after removing leading/trailing whitespace + + try + { // if there are no PEM encryption info lines, this is an UNencrypted PEM private key + binkey = Convert.FromBase64String(pvkstr); + return binkey; + } + catch (System.FormatException) + { //if can't b64 decode, it must be an encrypted private key + //Console.WriteLine("Not an unencrypted OpenSSL PEM private key"); + } + + StringReader str = new StringReader(pvkstr); + + //-------- read PEM encryption info. lines and extract salt ----- + if (!str.ReadLine().StartsWith("Proc-Type: 4,ENCRYPTED")) + return null; + String saltline = str.ReadLine(); + if (!saltline.StartsWith("DEK-Info: DES-EDE3-CBC,")) + return null; + String saltstr = saltline.Substring(saltline.IndexOf(",") + 1).Trim(); + byte[] salt = new byte[saltstr.Length / 2]; + for (int i = 0; i < salt.Length; i++) + salt[i] = Convert.ToByte(saltstr.Substring(i * 2, 2), 16); + if (!(str.ReadLine() == "")) + return null; + + //------ remaining b64 data is encrypted RSA key ---- + String encryptedstr = str.ReadToEnd(); + + try + { //should have b64 encrypted RSA key now + binkey = Convert.FromBase64String(encryptedstr); + } + catch (System.FormatException) + { // bad b64 data. + return null; + } + + //------ Get the 3DES 24 byte key using PDK used by OpenSSL ---- + + SecureString despswd = GetSecPswd("Enter password to derive 3DES key==>"); + //Console.Write("\nEnter password to derive 3DES key: "); + //String pswd = Console.ReadLine(); + byte[] deskey = GetOpenSSL3deskey(salt, despswd, 1, 2); // count=1 (for OpenSSL implementation); 2 iterations to get at least 24 bytes + if (deskey == null) + return null; + //showBytes("3DES key", deskey) ; + + //------ Decrypt the encrypted 3des-encrypted RSA private key ------ + byte[] rsakey = DecryptKey(binkey, deskey, salt); //OpenSSL uses salt value in PEM header also as 3DES IV + if (rsakey != null) + return rsakey; //we have a decrypted RSA private key + else + { + Console.WriteLine("Failed to decrypt RSA private key; probably wrong password."); + return null; + } + } + + static byte[] GetOpenSSL3deskey(byte[] salt, SecureString secpswd, int count, int miter) + { + IntPtr unmanagedPswd = IntPtr.Zero; + int HASHLENGTH = 16; //MD5 bytes + byte[] keymaterial = new byte[HASHLENGTH * miter]; //to store contatenated Mi hashed results + + + byte[] psbytes = new byte[secpswd.Length]; + unmanagedPswd = Marshal.SecureStringToGlobalAllocAnsi(secpswd); + Marshal.Copy(unmanagedPswd, psbytes, 0, psbytes.Length); + Marshal.ZeroFreeGlobalAllocAnsi(unmanagedPswd); + + //UTF8Encoding utf8 = new UTF8Encoding(); + //byte[] psbytes = utf8.GetBytes(pswd); + + // --- contatenate salt and pswd bytes into fixed data array --- + byte[] data00 = new byte[psbytes.Length + salt.Length]; + Array.Copy(psbytes, data00, psbytes.Length); //copy the pswd bytes + Array.Copy(salt, 0, data00, psbytes.Length, salt.Length); //concatenate the salt bytes + + // ---- do multi-hashing and contatenate results D1, D2 ... into keymaterial bytes ---- + MD5 md5 = new MD5CryptoServiceProvider(); + byte[] result = null; + byte[] hashtarget = new byte[HASHLENGTH + data00.Length]; //fixed length initial hashtarget + + for (int j = 0; j < miter; j++) + { + // ---- Now hash consecutively for count times ------ + if (j == 0) + result = data00; //initialize + else + { + Array.Copy(result, hashtarget, result.Length); + Array.Copy(data00, 0, hashtarget, result.Length, data00.Length); + result = hashtarget; + //Console.WriteLine("Updated new initial hash target:") ; + //showBytes(result) ; + } + + for (int i = 0; i < count; i++) + result = md5.ComputeHash(result); + Array.Copy(result, 0, keymaterial, j * HASHLENGTH, result.Length); //contatenate to keymaterial + } + //showBytes("Final key material", keymaterial); + byte[] deskey = new byte[24]; + Array.Copy(keymaterial, deskey, deskey.Length); + + Array.Clear(psbytes, 0, psbytes.Length); + Array.Clear(data00, 0, data00.Length); + Array.Clear(result, 0, result.Length); + Array.Clear(hashtarget, 0, hashtarget.Length); + Array.Clear(keymaterial, 0, keymaterial.Length); + + return deskey; + } + + public static string GetRSASignb64encode(string private_key_path, byte[] digest) + { + RSACryptoServiceProvider cipher = new RSACryptoServiceProvider(); + cipher = GetRSAProviderFromPemFile(private_key_path); + RSAPKCS1SignatureFormatter RSAFormatter = new RSAPKCS1SignatureFormatter(cipher); + RSAFormatter.SetHashAlgorithm("SHA256"); + byte[] signedHash = RSAFormatter.CreateSignature(digest); + return Convert.ToBase64String(signedHash); + } + + public static RSACryptoServiceProvider GetRSAProviderFromPemFile(String pemfile) + { + bool isPrivateKeyFile = true; + if (!File.Exists(pemfile)) + { + throw new Exception("pemfile does not exist."); + } + string pemstr = File.ReadAllText(pemfile).Trim(); + if (pemstr.StartsWith(pempubheader) && pemstr.EndsWith(pempubfooter)) + isPrivateKeyFile = false; + + byte[] pemkey = null; + if (isPrivateKeyFile) + pemkey = DecodeOpenSSLPrivateKey(pemstr); + + + if (pemkey == null) + return null; + + if (isPrivateKeyFile) + { + return DecodeRSAPrivateKey(pemkey); + } + return null; + } + + static byte[] DecryptKey(byte[] cipherData, byte[] desKey, byte[] IV) + { + MemoryStream memst = new MemoryStream(); + TripleDES alg = TripleDES.Create(); + alg.Key = desKey; + alg.IV = IV; + try + { + CryptoStream cs = new CryptoStream(memst, alg.CreateDecryptor(), CryptoStreamMode.Write); + cs.Write(cipherData, 0, cipherData.Length); + cs.Close(); + } + catch (Exception exc) + { + Console.WriteLine(exc.Message); + return null; + } + byte[] decryptedData = memst.ToArray(); + return decryptedData; + } + + static SecureString GetSecPswd(String prompt) + { + SecureString password = new SecureString(); + + Console.ForegroundColor = ConsoleColor.Gray; + Console.ForegroundColor = ConsoleColor.Magenta; + + while (true) + { + ConsoleKeyInfo cki = Console.ReadKey(true); + if (cki.Key == ConsoleKey.Enter) + { + Console.ForegroundColor = ConsoleColor.Gray; + return password; + } + else if (cki.Key == ConsoleKey.Backspace) + { + // remove the last asterisk from the screen... + if (password.Length > 0) + { + Console.SetCursorPosition(Console.CursorLeft - 1, Console.CursorTop); + Console.SetCursorPosition(Console.CursorLeft - 1, Console.CursorTop); + password.RemoveAt(password.Length - 1); + } + } + else if (cki.Key == ConsoleKey.Escape) + { + Console.ForegroundColor = ConsoleColor.Gray; + return password; + } + else if (Char.IsLetterOrDigit(cki.KeyChar) || Char.IsSymbol(cki.KeyChar)) + { + if (password.Length < 20) + { + password.AppendChar(cki.KeyChar); + } + else + { + Console.Beep(); + } + } + else + { + Console.Beep(); + } + } + } + } +}