diff --git a/samples/client/petstore/csharp-netcore/OpenAPIClient/src/Org.OpenAPITools/Client/HttpSigningConfiguration.cs b/samples/client/petstore/csharp-netcore/OpenAPIClient/src/Org.OpenAPITools/Client/HttpSigningConfiguration.cs new file mode 100644 index 00000000000..3bf59ad7f45 --- /dev/null +++ b/samples/client/petstore/csharp-netcore/OpenAPIClient/src/Org.OpenAPITools/Client/HttpSigningConfiguration.cs @@ -0,0 +1,706 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.Cryptography; +using System.Text; +using System.Web; + +namespace Org.OpenAPITools.Client +{ + /// + /// Class for HttpSigning auth related parameter and methods + /// + public class HttpSigningConfiguration + { + #region + /// + /// Initailize the HashAlgorithm and SigningAlgorithm to default value + /// + public HttpSigningConfiguration() + { + HashAlgorithm = HashAlgorithmName.SHA256; + SigningAlgorithm = "PKCS1-v15"; + } + #endregion + + #region Properties + /// + ///Gets the Api keyId + /// + public string KeyId { get; set; } + + /// + /// Gets the Key file path + /// + public string KeyFilePath { get; set; } + + /// + /// Gets the key pass phrase for password protected key + /// + public SecureString KeyPassPhrase { get; set; } + + /// + /// Gets the HTTP signing header + /// + public List HttpSigningHeader { get; set; } + + /// + /// Gets the hash algorithm sha256 or sha512 + /// + public HashAlgorithmName HashAlgorithm { get; set; } + + /// + /// Gets the signing algorithm + /// + public string SigningAlgorithm { get; set; } + + /// + /// Gets the Signature validaty period in seconds + /// + public int SignatureValidityPeriod { get; set; } + + #endregion + + #region enum + private enum PrivateKeyType + { + None = 0, + RSA = 1, + ECDSA = 2, + } + #endregion + + #region Methods + /// + /// Gets the Headers for HttpSigning + /// + /// + /// + /// + /// + internal Dictionary GetHttpSignedHeader(string basePath,string method, string path, RequestOptions requestOptions) + { + const string HEADER_REQUEST_TARGET = "(request-target)"; + //The time when the HTTP signature expires. The API server should reject HTTP requests + //that have expired. + const string HEADER_EXPIRES = "(expires)"; + //The 'Date' header. + const string HEADER_DATE = "Date"; + //The 'Host' header. + const string HEADER_HOST = "Host"; + //The time when the HTTP signature was generated. + const string HEADER_CREATED = "(created)"; + //When the 'Digest' header is included in the HTTP signature, the client automatically + //computes the digest of the HTTP request body, per RFC 3230. + const string HEADER_DIGEST = "Digest"; + //The 'Authorization' header is automatically generated by the client. It includes + //the list of signed headers and a base64-encoded signature. + const string HEADER_AUTHORIZATION = "Authorization"; + + //Hash table to store singed headers + var HttpSignedRequestHeader = new Dictionary(); + var HttpSignatureHeader = new Dictionary(); + + if (HttpSigningHeader.Count == 0) + { + HttpSigningHeader.Add("(created)"); + } + + if (requestOptions.PathParameters != null) + { + foreach (var pathParam in requestOptions.PathParameters) + { + var tempPath = path.Replace(pathParam.Key, "0"); + path = string.Format(tempPath, pathParam.Value); + } + } + + var httpValues = HttpUtility.ParseQueryString(String.Empty); + foreach (var parameter in requestOptions.QueryParameters) + { + if (parameter.Value.Count > 1) + { // array + foreach (var value in parameter.Value) + { + httpValues.Add(parameter.Key + "[]", value); + } + } + else + { + httpValues.Add(parameter.Key, parameter.Value[0]); + + } + } + var uriBuilder = new UriBuilder(string.Concat(basePath, path)); + uriBuilder.Query = httpValues.ToString(); + + var dateTime = DateTime.Now; + String Digest = String.Empty; + + //get the body + string requestBody = string.Empty; + if (requestOptions.Data != null) + { + var serializerSettings = new JsonSerializerSettings(); + serializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + requestBody = JsonConvert.SerializeObject(requestOptions.Data, serializerSettings); + } + + if (HashAlgorithm == HashAlgorithmName.SHA256) + { + var bodyDigest = GetStringHash(HashAlgorithm.ToString(), requestBody); + Digest = string.Format("SHA-256={0}", Convert.ToBase64String(bodyDigest)); + } + else if (HashAlgorithm == HashAlgorithmName.SHA512) + { + var bodyDigest = GetStringHash(HashAlgorithm.ToString(), requestBody); + Digest = string.Format("SHA-512={0}", Convert.ToBase64String(bodyDigest)); + } + else + { + throw new Exception(string.Format("{0} not supported", HashAlgorithm)); + } + + + foreach (var header in HttpSigningHeader) + { + if (header.Equals(HEADER_REQUEST_TARGET)) + { + var targetUrl = string.Format("{0} {1}{2}", method.ToLower(), uriBuilder.Path, uriBuilder.Query); + HttpSignatureHeader.Add(header.ToLower(), targetUrl); + } + else if (header.Equals(HEADER_EXPIRES)) + { + var expireDateTime = dateTime.AddSeconds(SignatureValidityPeriod); + HttpSignatureHeader.Add(header.ToLower(), GetUnixTime(expireDateTime).ToString()); + } + else if (header.Equals(HEADER_DATE)) + { + var utcDateTime = dateTime.ToString("r").ToString(); + HttpSignatureHeader.Add(header.ToLower(), utcDateTime); + HttpSignedRequestHeader.Add(HEADER_DATE, utcDateTime); + } + else if (header.Equals(HEADER_HOST)) + { + HttpSignatureHeader.Add(header.ToLower(), uriBuilder.Host); + HttpSignedRequestHeader.Add(HEADER_HOST, uriBuilder.Host); + } + else if (header.Equals(HEADER_CREATED)) + { + HttpSignatureHeader.Add(header.ToLower(), GetUnixTime(dateTime).ToString()); + } + else if (header.Equals(HEADER_DIGEST)) + { + HttpSignedRequestHeader.Add(HEADER_DIGEST, Digest); + HttpSignatureHeader.Add(header.ToLower(), Digest); + } + else + { + bool isHeaderFound = false; + foreach (var item in requestOptions.HeaderParameters) + { + if (string.Equals(item.Key, header, StringComparison.OrdinalIgnoreCase)) + { + HttpSignatureHeader.Add(header.ToLower(), item.Value.ToString()); + isHeaderFound = true; + break; + } + } + if (!isHeaderFound) + { + throw new Exception(string.Format("Cannot sign HTTP request.Request does not contain the {0} header.",header)); + } + } + + } + var headersKeysString = String.Join(" ", HttpSignatureHeader.Keys); + var headerValuesList = new List(); + + foreach (var keyVal in HttpSignatureHeader) + { + headerValuesList.Add(string.Format("{0}: {1}", keyVal.Key, keyVal.Value)); + + } + //Concatinate headers value separated by new line + var headerValuesString = string.Join("\n", headerValuesList); + var signatureStringHash = GetStringHash(HashAlgorithm.ToString(), headerValuesString); + string headerSignatureStr = null; + var keyType = GetKeyType(KeyFilePath); + + if (keyType == PrivateKeyType.RSA) + { + headerSignatureStr = GetRSASignature(signatureStringHash); + } + else if (keyType == PrivateKeyType.ECDSA) + { + headerSignatureStr = GetECDSASignature(signatureStringHash); + } + var cryptographicScheme = "hs2019"; + var authorizationHeaderValue = string.Format("Signature keyId=\"{0}\",algorithm=\"{1}\"", + KeyId, cryptographicScheme); + + + if (HttpSignatureHeader.ContainsKey(HEADER_CREATED)) + { + authorizationHeaderValue += string.Format(",created={0}", HttpSignatureHeader[HEADER_CREATED]); + } + + if (HttpSignatureHeader.ContainsKey(HEADER_EXPIRES)) + { + authorizationHeaderValue += string.Format(",expires={0}", HttpSignatureHeader[HEADER_EXPIRES]); + } + + authorizationHeaderValue += string.Format(",headers=\"{0}\",signature=\"{1}\"", + headersKeysString, headerSignatureStr); + + HttpSignedRequestHeader.Add(HEADER_AUTHORIZATION, authorizationHeaderValue); + + return HttpSignedRequestHeader; + } + + private byte[] GetStringHash(string hashName, string stringToBeHashed) + { + var hashAlgorithm = System.Security.Cryptography.HashAlgorithm.Create(hashName); + + var bytes = Encoding.UTF8.GetBytes(stringToBeHashed); + var stringHash = hashAlgorithm.ComputeHash(bytes); + return stringHash; + } + + private int GetUnixTime(DateTime date2) + { + DateTime date1 = new DateTime(1970, 01, 01); + TimeSpan timeSpan = date2 - date1; + return (int)timeSpan.TotalSeconds; + } + + private string GetRSASignature(byte[] stringToSign) + { + RSA rsa = GetRSAProviderFromPemFile(KeyFilePath, KeyPassPhrase); + if (SigningAlgorithm == "RSASSA-PSS") + { + var signedbytes = rsa.SignHash(stringToSign, HashAlgorithm, RSASignaturePadding.Pss); + return Convert.ToBase64String(signedbytes); + + } + else if (SigningAlgorithm == "PKCS1-v15") + { + var signedbytes = rsa.SignHash(stringToSign, HashAlgorithm, RSASignaturePadding.Pkcs1); + return Convert.ToBase64String(signedbytes); + } + return string.Empty; + } + + /// + /// Gets the ECDSA signature + /// + /// + /// + private string GetECDSASignature(byte[] dataToSign) + { + if (!File.Exists(KeyFilePath)) + { + throw new Exception("key file path does not exist."); + } + + var ecKeyHeader = "-----BEGIN EC PRIVATE KEY-----"; + var ecKeyFooter = "-----END EC PRIVATE KEY-----"; + var keyStr = File.ReadAllText(KeyFilePath); + var ecKeyBase64String = keyStr.Replace(ecKeyHeader, "").Replace(ecKeyFooter, "").Trim(); + var keyBytes = System.Convert.FromBase64String(ecKeyBase64String); + var ecdsa = ECDsa.Create(); + +#if (NETCOREAPP3_0 || NETCOREAPP3_1 || NET5_0) + var byteCount = 0; + if (configuration.KeyPassPhrase != null) + { + ecdsa.ImportEncryptedPkcs8PrivateKey(keyPassPhrase, keyBytes, out byteCount); + } + else + { + ecdsa.ImportPkcs8PrivateKey(keyBytes, out byteCount); + } + var signedBytes = ecdsa.SignHash(dataToSign); + var derBytes = ConvertToECDSAANS1Format(signedBytes); + var signedString = System.Convert.ToBase64String(derBytes); + + return signedString; +#else + throw new Exception("ECDSA signing is supported only on NETCOREAPP3_0 and above"); +#endif + + } + + private byte[] ConvertToECDSAANS1Format(byte[] signedBytes) + { + var derBytes = new List(); + byte derLength = 68; //default lenght for ECDSA code signinged bit 0x44 + byte rbytesLength = 32; //R length 0x20 + byte sbytesLength = 32; //S length 0x20 + var rBytes = new List(); + var sBytes = new List(); + for (int i = 0; i < 32; i++) + { + rBytes.Add(signedBytes[i]); + } + for (int i = 32; i < 64; i++) + { + sBytes.Add(signedBytes[i]); + } + + if (rBytes[0] > 0x7F) + { + derLength++; + rbytesLength++; + var tempBytes = new List(); + tempBytes.AddRange(rBytes); + rBytes.Clear(); + rBytes.Add(0x00); + rBytes.AddRange(tempBytes); + } + + if (sBytes[0] > 0x7F) + { + derLength++; + sbytesLength++; + var tempBytes = new List(); + tempBytes.AddRange(sBytes); + sBytes.Clear(); + sBytes.Add(0x00); + sBytes.AddRange(tempBytes); + + } + + derBytes.Add(48); //start of the sequence 0x30 + derBytes.Add(derLength); //total length r lenth, type and r bytes + + derBytes.Add(2); //tag for integer + derBytes.Add(rbytesLength); //length of r + derBytes.AddRange(rBytes); + + derBytes.Add(2); //tag for integer + derBytes.Add(sbytesLength); //length of s + derBytes.AddRange(sBytes); + return derBytes.ToArray(); + } + + private RSACryptoServiceProvider GetRSAProviderFromPemFile(String pemfile, SecureString keyPassPharse = null) + { + const String pempubheader = "-----BEGIN PUBLIC KEY-----"; + const String pempubfooter = "-----END PUBLIC KEY-----"; + bool isPrivateKeyFile = true; + byte[] pemkey = null; + + if (!File.Exists(pemfile)) + { + throw new Exception("private key file does not exist."); + } + string pemstr = File.ReadAllText(pemfile).Trim(); + + if (pemstr.StartsWith(pempubheader) && pemstr.EndsWith(pempubfooter)) + { + isPrivateKeyFile = false; + } + + if (isPrivateKeyFile) + { + pemkey = ConvertPrivateKeyToBytes(pemstr, keyPassPharse); + if (pemkey == null) + { + return null; + } + return DecodeRSAPrivateKey(pemkey); + } + return null; + } + + private byte[] ConvertPrivateKeyToBytes(String instr, SecureString keyPassPharse = null) + { + 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, ""); + sb.Replace(pemprivfooter, ""); + String pvkstr = sb.ToString().Trim(); + + 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) + { + 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) + { //data is not in base64 fromat + return null; + } + + byte[] deskey = GetEncryptedKey(salt, keyPassPharse, 1, 2); // count=1 (for OpenSSL implementation); 2 iterations to get at least 24 bytes + if (deskey == null) + return null; + + //------ 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 + return rsakey; + } + } + + private 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); + + // ------- 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 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; + } + + private byte[] GetEncryptedKey(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); + + // --- 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; + } + + for (int i = 0; i < count; i++) + result = md5.ComputeHash(result); + Array.Copy(result, 0, keymaterial, j * HASHLENGTH, result.Length); //contatenate to 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; + } + + private 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) + { + return null; + } + byte[] decryptedData = memst.ToArray(); + return decryptedData; + } + + /// + /// Detect the key type from the pem file. + /// + /// key file path in pem format + /// + private PrivateKeyType GetKeyType(string keyFilePath) + { + if (!File.Exists(keyFilePath)) + { + throw new Exception("Key file path does not exist."); + } + + var ecPrivateKeyHeader = "BEGIN EC PRIVATE KEY"; + var ecPrivateKeyFooter = "END EC PRIVATE KEY"; + var rsaPrivateKeyHeader = "BEGIN RSA PRIVATE KEY"; + var rsaPrivateFooter = "END RSA PRIVATE KEY"; + //var pkcs8Header = "BEGIN PRIVATE KEY"; + //var pkcs8Footer = "END PRIVATE KEY"; + var keyType = PrivateKeyType.None; + var key = File.ReadAllLines(keyFilePath); + + if (key[0].ToString().Contains(rsaPrivateKeyHeader) && + key[key.Length - 1].ToString().Contains(rsaPrivateFooter)) + { + keyType = PrivateKeyType.RSA; + } + else if (key[0].ToString().Contains(ecPrivateKeyHeader) && + key[key.Length - 1].ToString().Contains(ecPrivateKeyFooter)) + { + keyType = PrivateKeyType.ECDSA; + } + else if (key[0].ToString().Contains(ecPrivateKeyHeader) && + key[key.Length - 1].ToString().Contains(ecPrivateKeyFooter)) + { + + /*this type of key can hold many type different types of private key, but here due lack of pem header + Considering this as EC key + */ + //TODO :- update the key based on oid + keyType = PrivateKeyType.ECDSA; + } + else + { + throw new Exception("Either the key is invalid or key is not supported"); + + } + return keyType; + } + #endregion + } +} diff --git a/samples/client/petstore/csharp-netcore/OpenAPIClientCore/src/Org.OpenAPITools/Client/HttpSigningConfiguration.cs b/samples/client/petstore/csharp-netcore/OpenAPIClientCore/src/Org.OpenAPITools/Client/HttpSigningConfiguration.cs new file mode 100644 index 00000000000..3bf59ad7f45 --- /dev/null +++ b/samples/client/petstore/csharp-netcore/OpenAPIClientCore/src/Org.OpenAPITools/Client/HttpSigningConfiguration.cs @@ -0,0 +1,706 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.Cryptography; +using System.Text; +using System.Web; + +namespace Org.OpenAPITools.Client +{ + /// + /// Class for HttpSigning auth related parameter and methods + /// + public class HttpSigningConfiguration + { + #region + /// + /// Initailize the HashAlgorithm and SigningAlgorithm to default value + /// + public HttpSigningConfiguration() + { + HashAlgorithm = HashAlgorithmName.SHA256; + SigningAlgorithm = "PKCS1-v15"; + } + #endregion + + #region Properties + /// + ///Gets the Api keyId + /// + public string KeyId { get; set; } + + /// + /// Gets the Key file path + /// + public string KeyFilePath { get; set; } + + /// + /// Gets the key pass phrase for password protected key + /// + public SecureString KeyPassPhrase { get; set; } + + /// + /// Gets the HTTP signing header + /// + public List HttpSigningHeader { get; set; } + + /// + /// Gets the hash algorithm sha256 or sha512 + /// + public HashAlgorithmName HashAlgorithm { get; set; } + + /// + /// Gets the signing algorithm + /// + public string SigningAlgorithm { get; set; } + + /// + /// Gets the Signature validaty period in seconds + /// + public int SignatureValidityPeriod { get; set; } + + #endregion + + #region enum + private enum PrivateKeyType + { + None = 0, + RSA = 1, + ECDSA = 2, + } + #endregion + + #region Methods + /// + /// Gets the Headers for HttpSigning + /// + /// + /// + /// + /// + internal Dictionary GetHttpSignedHeader(string basePath,string method, string path, RequestOptions requestOptions) + { + const string HEADER_REQUEST_TARGET = "(request-target)"; + //The time when the HTTP signature expires. The API server should reject HTTP requests + //that have expired. + const string HEADER_EXPIRES = "(expires)"; + //The 'Date' header. + const string HEADER_DATE = "Date"; + //The 'Host' header. + const string HEADER_HOST = "Host"; + //The time when the HTTP signature was generated. + const string HEADER_CREATED = "(created)"; + //When the 'Digest' header is included in the HTTP signature, the client automatically + //computes the digest of the HTTP request body, per RFC 3230. + const string HEADER_DIGEST = "Digest"; + //The 'Authorization' header is automatically generated by the client. It includes + //the list of signed headers and a base64-encoded signature. + const string HEADER_AUTHORIZATION = "Authorization"; + + //Hash table to store singed headers + var HttpSignedRequestHeader = new Dictionary(); + var HttpSignatureHeader = new Dictionary(); + + if (HttpSigningHeader.Count == 0) + { + HttpSigningHeader.Add("(created)"); + } + + if (requestOptions.PathParameters != null) + { + foreach (var pathParam in requestOptions.PathParameters) + { + var tempPath = path.Replace(pathParam.Key, "0"); + path = string.Format(tempPath, pathParam.Value); + } + } + + var httpValues = HttpUtility.ParseQueryString(String.Empty); + foreach (var parameter in requestOptions.QueryParameters) + { + if (parameter.Value.Count > 1) + { // array + foreach (var value in parameter.Value) + { + httpValues.Add(parameter.Key + "[]", value); + } + } + else + { + httpValues.Add(parameter.Key, parameter.Value[0]); + + } + } + var uriBuilder = new UriBuilder(string.Concat(basePath, path)); + uriBuilder.Query = httpValues.ToString(); + + var dateTime = DateTime.Now; + String Digest = String.Empty; + + //get the body + string requestBody = string.Empty; + if (requestOptions.Data != null) + { + var serializerSettings = new JsonSerializerSettings(); + serializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + requestBody = JsonConvert.SerializeObject(requestOptions.Data, serializerSettings); + } + + if (HashAlgorithm == HashAlgorithmName.SHA256) + { + var bodyDigest = GetStringHash(HashAlgorithm.ToString(), requestBody); + Digest = string.Format("SHA-256={0}", Convert.ToBase64String(bodyDigest)); + } + else if (HashAlgorithm == HashAlgorithmName.SHA512) + { + var bodyDigest = GetStringHash(HashAlgorithm.ToString(), requestBody); + Digest = string.Format("SHA-512={0}", Convert.ToBase64String(bodyDigest)); + } + else + { + throw new Exception(string.Format("{0} not supported", HashAlgorithm)); + } + + + foreach (var header in HttpSigningHeader) + { + if (header.Equals(HEADER_REQUEST_TARGET)) + { + var targetUrl = string.Format("{0} {1}{2}", method.ToLower(), uriBuilder.Path, uriBuilder.Query); + HttpSignatureHeader.Add(header.ToLower(), targetUrl); + } + else if (header.Equals(HEADER_EXPIRES)) + { + var expireDateTime = dateTime.AddSeconds(SignatureValidityPeriod); + HttpSignatureHeader.Add(header.ToLower(), GetUnixTime(expireDateTime).ToString()); + } + else if (header.Equals(HEADER_DATE)) + { + var utcDateTime = dateTime.ToString("r").ToString(); + HttpSignatureHeader.Add(header.ToLower(), utcDateTime); + HttpSignedRequestHeader.Add(HEADER_DATE, utcDateTime); + } + else if (header.Equals(HEADER_HOST)) + { + HttpSignatureHeader.Add(header.ToLower(), uriBuilder.Host); + HttpSignedRequestHeader.Add(HEADER_HOST, uriBuilder.Host); + } + else if (header.Equals(HEADER_CREATED)) + { + HttpSignatureHeader.Add(header.ToLower(), GetUnixTime(dateTime).ToString()); + } + else if (header.Equals(HEADER_DIGEST)) + { + HttpSignedRequestHeader.Add(HEADER_DIGEST, Digest); + HttpSignatureHeader.Add(header.ToLower(), Digest); + } + else + { + bool isHeaderFound = false; + foreach (var item in requestOptions.HeaderParameters) + { + if (string.Equals(item.Key, header, StringComparison.OrdinalIgnoreCase)) + { + HttpSignatureHeader.Add(header.ToLower(), item.Value.ToString()); + isHeaderFound = true; + break; + } + } + if (!isHeaderFound) + { + throw new Exception(string.Format("Cannot sign HTTP request.Request does not contain the {0} header.",header)); + } + } + + } + var headersKeysString = String.Join(" ", HttpSignatureHeader.Keys); + var headerValuesList = new List(); + + foreach (var keyVal in HttpSignatureHeader) + { + headerValuesList.Add(string.Format("{0}: {1}", keyVal.Key, keyVal.Value)); + + } + //Concatinate headers value separated by new line + var headerValuesString = string.Join("\n", headerValuesList); + var signatureStringHash = GetStringHash(HashAlgorithm.ToString(), headerValuesString); + string headerSignatureStr = null; + var keyType = GetKeyType(KeyFilePath); + + if (keyType == PrivateKeyType.RSA) + { + headerSignatureStr = GetRSASignature(signatureStringHash); + } + else if (keyType == PrivateKeyType.ECDSA) + { + headerSignatureStr = GetECDSASignature(signatureStringHash); + } + var cryptographicScheme = "hs2019"; + var authorizationHeaderValue = string.Format("Signature keyId=\"{0}\",algorithm=\"{1}\"", + KeyId, cryptographicScheme); + + + if (HttpSignatureHeader.ContainsKey(HEADER_CREATED)) + { + authorizationHeaderValue += string.Format(",created={0}", HttpSignatureHeader[HEADER_CREATED]); + } + + if (HttpSignatureHeader.ContainsKey(HEADER_EXPIRES)) + { + authorizationHeaderValue += string.Format(",expires={0}", HttpSignatureHeader[HEADER_EXPIRES]); + } + + authorizationHeaderValue += string.Format(",headers=\"{0}\",signature=\"{1}\"", + headersKeysString, headerSignatureStr); + + HttpSignedRequestHeader.Add(HEADER_AUTHORIZATION, authorizationHeaderValue); + + return HttpSignedRequestHeader; + } + + private byte[] GetStringHash(string hashName, string stringToBeHashed) + { + var hashAlgorithm = System.Security.Cryptography.HashAlgorithm.Create(hashName); + + var bytes = Encoding.UTF8.GetBytes(stringToBeHashed); + var stringHash = hashAlgorithm.ComputeHash(bytes); + return stringHash; + } + + private int GetUnixTime(DateTime date2) + { + DateTime date1 = new DateTime(1970, 01, 01); + TimeSpan timeSpan = date2 - date1; + return (int)timeSpan.TotalSeconds; + } + + private string GetRSASignature(byte[] stringToSign) + { + RSA rsa = GetRSAProviderFromPemFile(KeyFilePath, KeyPassPhrase); + if (SigningAlgorithm == "RSASSA-PSS") + { + var signedbytes = rsa.SignHash(stringToSign, HashAlgorithm, RSASignaturePadding.Pss); + return Convert.ToBase64String(signedbytes); + + } + else if (SigningAlgorithm == "PKCS1-v15") + { + var signedbytes = rsa.SignHash(stringToSign, HashAlgorithm, RSASignaturePadding.Pkcs1); + return Convert.ToBase64String(signedbytes); + } + return string.Empty; + } + + /// + /// Gets the ECDSA signature + /// + /// + /// + private string GetECDSASignature(byte[] dataToSign) + { + if (!File.Exists(KeyFilePath)) + { + throw new Exception("key file path does not exist."); + } + + var ecKeyHeader = "-----BEGIN EC PRIVATE KEY-----"; + var ecKeyFooter = "-----END EC PRIVATE KEY-----"; + var keyStr = File.ReadAllText(KeyFilePath); + var ecKeyBase64String = keyStr.Replace(ecKeyHeader, "").Replace(ecKeyFooter, "").Trim(); + var keyBytes = System.Convert.FromBase64String(ecKeyBase64String); + var ecdsa = ECDsa.Create(); + +#if (NETCOREAPP3_0 || NETCOREAPP3_1 || NET5_0) + var byteCount = 0; + if (configuration.KeyPassPhrase != null) + { + ecdsa.ImportEncryptedPkcs8PrivateKey(keyPassPhrase, keyBytes, out byteCount); + } + else + { + ecdsa.ImportPkcs8PrivateKey(keyBytes, out byteCount); + } + var signedBytes = ecdsa.SignHash(dataToSign); + var derBytes = ConvertToECDSAANS1Format(signedBytes); + var signedString = System.Convert.ToBase64String(derBytes); + + return signedString; +#else + throw new Exception("ECDSA signing is supported only on NETCOREAPP3_0 and above"); +#endif + + } + + private byte[] ConvertToECDSAANS1Format(byte[] signedBytes) + { + var derBytes = new List(); + byte derLength = 68; //default lenght for ECDSA code signinged bit 0x44 + byte rbytesLength = 32; //R length 0x20 + byte sbytesLength = 32; //S length 0x20 + var rBytes = new List(); + var sBytes = new List(); + for (int i = 0; i < 32; i++) + { + rBytes.Add(signedBytes[i]); + } + for (int i = 32; i < 64; i++) + { + sBytes.Add(signedBytes[i]); + } + + if (rBytes[0] > 0x7F) + { + derLength++; + rbytesLength++; + var tempBytes = new List(); + tempBytes.AddRange(rBytes); + rBytes.Clear(); + rBytes.Add(0x00); + rBytes.AddRange(tempBytes); + } + + if (sBytes[0] > 0x7F) + { + derLength++; + sbytesLength++; + var tempBytes = new List(); + tempBytes.AddRange(sBytes); + sBytes.Clear(); + sBytes.Add(0x00); + sBytes.AddRange(tempBytes); + + } + + derBytes.Add(48); //start of the sequence 0x30 + derBytes.Add(derLength); //total length r lenth, type and r bytes + + derBytes.Add(2); //tag for integer + derBytes.Add(rbytesLength); //length of r + derBytes.AddRange(rBytes); + + derBytes.Add(2); //tag for integer + derBytes.Add(sbytesLength); //length of s + derBytes.AddRange(sBytes); + return derBytes.ToArray(); + } + + private RSACryptoServiceProvider GetRSAProviderFromPemFile(String pemfile, SecureString keyPassPharse = null) + { + const String pempubheader = "-----BEGIN PUBLIC KEY-----"; + const String pempubfooter = "-----END PUBLIC KEY-----"; + bool isPrivateKeyFile = true; + byte[] pemkey = null; + + if (!File.Exists(pemfile)) + { + throw new Exception("private key file does not exist."); + } + string pemstr = File.ReadAllText(pemfile).Trim(); + + if (pemstr.StartsWith(pempubheader) && pemstr.EndsWith(pempubfooter)) + { + isPrivateKeyFile = false; + } + + if (isPrivateKeyFile) + { + pemkey = ConvertPrivateKeyToBytes(pemstr, keyPassPharse); + if (pemkey == null) + { + return null; + } + return DecodeRSAPrivateKey(pemkey); + } + return null; + } + + private byte[] ConvertPrivateKeyToBytes(String instr, SecureString keyPassPharse = null) + { + 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, ""); + sb.Replace(pemprivfooter, ""); + String pvkstr = sb.ToString().Trim(); + + 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) + { + 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) + { //data is not in base64 fromat + return null; + } + + byte[] deskey = GetEncryptedKey(salt, keyPassPharse, 1, 2); // count=1 (for OpenSSL implementation); 2 iterations to get at least 24 bytes + if (deskey == null) + return null; + + //------ 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 + return rsakey; + } + } + + private 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); + + // ------- 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 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; + } + + private byte[] GetEncryptedKey(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); + + // --- 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; + } + + for (int i = 0; i < count; i++) + result = md5.ComputeHash(result); + Array.Copy(result, 0, keymaterial, j * HASHLENGTH, result.Length); //contatenate to 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; + } + + private 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) + { + return null; + } + byte[] decryptedData = memst.ToArray(); + return decryptedData; + } + + /// + /// Detect the key type from the pem file. + /// + /// key file path in pem format + /// + private PrivateKeyType GetKeyType(string keyFilePath) + { + if (!File.Exists(keyFilePath)) + { + throw new Exception("Key file path does not exist."); + } + + var ecPrivateKeyHeader = "BEGIN EC PRIVATE KEY"; + var ecPrivateKeyFooter = "END EC PRIVATE KEY"; + var rsaPrivateKeyHeader = "BEGIN RSA PRIVATE KEY"; + var rsaPrivateFooter = "END RSA PRIVATE KEY"; + //var pkcs8Header = "BEGIN PRIVATE KEY"; + //var pkcs8Footer = "END PRIVATE KEY"; + var keyType = PrivateKeyType.None; + var key = File.ReadAllLines(keyFilePath); + + if (key[0].ToString().Contains(rsaPrivateKeyHeader) && + key[key.Length - 1].ToString().Contains(rsaPrivateFooter)) + { + keyType = PrivateKeyType.RSA; + } + else if (key[0].ToString().Contains(ecPrivateKeyHeader) && + key[key.Length - 1].ToString().Contains(ecPrivateKeyFooter)) + { + keyType = PrivateKeyType.ECDSA; + } + else if (key[0].ToString().Contains(ecPrivateKeyHeader) && + key[key.Length - 1].ToString().Contains(ecPrivateKeyFooter)) + { + + /*this type of key can hold many type different types of private key, but here due lack of pem header + Considering this as EC key + */ + //TODO :- update the key based on oid + keyType = PrivateKeyType.ECDSA; + } + else + { + throw new Exception("Either the key is invalid or key is not supported"); + + } + return keyType; + } + #endregion + } +}