I'm trying to implement signed URLs for short lived access to static files.
The idea is:
generate an URL with an expiration timestamp (e.g. https://example.com/file.png?download=false&expires=1586852158)
sign it with HMACSHA256 and a shared secret and append the signature at the end of URL (e.g. https://example.com/file.png?download=false&expires=1586852158&signature=6635ea14baeeaaffe71333cf6c7fa1f0af9f6cd1a17abb4e75ca275dec5906d1
When i receive the request on the server, I take out the signature parameter and verify that the rest of the URL signed with HMACSHA256 and the same shared secret results in the same signature.
The implementation is as follows:
public static class URLSigner
{
private static string GetSignatureForUri(string uri, byte[] key)
{
var hmac = new HMACSHA256(key);
var signature = hmac.ComputeHash(Encoding.UTF8.GetBytes(uri));
var hexSignature = BitConverter.ToString(signature).Replace("-", string.Empty).ToLowerInvariant();
return hexSignature;
}
public static string SignUri(string uri, byte[] key)
{
var hexSignature = GetSignatureForUri(uri, key);
return QueryHelpers.AddQueryString(uri, new Dictionary<string, string> { { "signature", hexSignature }});
}
public static bool VerifyUri(string uri, byte[] key)
{
var signatureRegex = "[\\?&]signature=([a-z0-9]+)$";
var signatureMatch = Regex.Match(uri, signatureRegex);
if (!signatureMatch.Success || signatureMatch.Groups.Count != 2)
return false;
var parsedSignature = signatureMatch.Groups[1].Value;
var originalUri = Regex.Replace(uri, signatureRegex, "");
var hexSignature = GetSignatureForUri(originalUri, key);
return hexSignature == parsedSignature;
}
}
and it's used like so:
var signedUri = URLSigner.SignUri("https://example.com/file.png?download=false", secretKey);
var isVerified = URLSigner.VerifyUri(signedUri, secretKey);
Is this implementation of signed URLs reasonably secure?
Your implementation seems to be missing the verification of the expiration time, so any one key would currently work indefinitely.
Otherwise, I don't see anything wrong with this approach in general. You may want to add in a key beyond just the timestamp for identifying the user or request in some way though.
Here's a good article on how the general approach is used for one time passwords which is essentially what you are doing.
https://www.freecodecamp.org/news/how-time-based-one-time-passwords-work-and-why-you-should-use-them-in-your-app-fdd2b9ed43c3/
Yes, it is secure, as long as the key is treated properly. The hash should be able to ensure data integrity (data in URL are not modified by other people).
Perhaps, one little improvement is to dispose the HMACSHA256 object (maybe by using), but that may not be related to security.
I have one concern. You are saying you want to use HMACSHA256 and a private key, but in security terminology what you're passing to the HMAC is not a private key, it's a shared secret.
If you have to had a public, private key for your sign and verify authentication, I would suggest using the RSACryptoServiceProvider. With RSA you have two keys, public key and private key.
Your client creates a private key and keep it and give its public key to the server. So only client can sign and anyone with public key can verify it.
On another note, no matter what algorithm you ended up using, I would suggest to add the signature to a authorization header instead of query string. This is more common and you don't need to match a regex in each request.
Related
I have a signature created using metamask and the personal_sign RPC method.
Now I want to verify this signature in my C# backend.
In order to do so I have found the Nethereum library.
I have written the below code trying to verify the signature (for now I have used 'test' as the signed message).
public void VerifySignature(string signatureString, string originalMessage)
{
string msg = "\x19Ethereum Signed Message:\n" + originalMessage.Length + originalMessage;
byte[] msgHash = new Sha3Keccack().CalculateHash(Encoding.UTF8.GetBytes(msg));
EthECDSASignature signature = MessageSigner.ExtractEcdsaSignature(signatureString);
EthECKey key = EthECKey.RecoverFromSignature(signature, msgHash);
bool isValid = key.Verify(msgHash, signature);
}
Now the isValid comes back as true. However, if I use key.GetPublicAddress() this address is different than my own public address, so I assume I'm doing something wrong. Can anyone explain to me what, or correct if I'm wrong?
NOTE:
If instead of
EthECKey testKey = EthECKey.RecoverFromSignature(signature, msgHash);
I use
EthECKey testKey = EthECKey.RecoverFromSignature(signature, msgHash, new BigInteger(1));
(I'm using the main network to sign which is chain 1)
I get an error saying "recId should be positive", not sure if this is related but I thought it's worth mentioning.
UPDATE:
Managed to fix this by changing the msg string to use "\x19" + "Ethereum ..." instead of "\x19Ethereum ...", \x19E results in a different character, and results in a different message hash.
The Ethereum address and the public key are different. The Ethereum address is the last 20 bytes of the hash of the public key (see https://ethereum.org/en/developers/docs/accounts/ and https://github.com/Nethereum/Nethereum/blob/master/src/Nethereum.Signer/EthECKey.cs#L201).
I have a randomly generated 128 bit guid (cryptographically secure). How can I use this as a seed to generate a public and private key for Bitcoin, using C#? By seed, I mean that every time I use the same guid as input, it should result in the same public/private keys.
I have looked at NBitcoin, but don't understand how to pull it off.
You can directly create 32 random bytes to be your private key. Example below. But it is VERY important: these 32 bytes must be from cryptographically-secure pseudorandom generator. For example, if you use C# built-in Random class, anyone will be able to restore your private keys with regular computer. You need to be very careful if you plan to use this in real bitcoin network. I am not sure if Guid generation is cryptographically-secure.
static void Main(string[] args)
{
byte[] GetRawKey()
{
// private key=1 in this example
byte[] data = new byte[32];
data[^1] = 1;
return data;
}
var key = new Key(GetRawKey()); // private key
var pair = key.CreateKeyPair(); // key pair (pub+priv keys)
var addrP2pkh = key.GetAddress(ScriptPubKeyType.Legacy, Network.Main); // P2PKH address
var addrP2sh = key.GetAddress(ScriptPubKeyType.SegwitP2SH, Network.Main); // Segwit P2SH address
Console.WriteLine(addrP2pkh.ToString());
Console.WriteLine(addrP2sh.ToString());
}
I want to sign a message with a private key and verify it with a public key, but I can't get it to work..
Here is how I sign the data (edited, but still not working):
public static string SignData(string message, string privateKey) {
byte[] plainText = ASCIIEncoding.Unicode.GetBytes(message);
var rsaWrite = new RSACryptoServiceProvider();
rsaWrite.FromXmlString(privateKey);
byte[] signature = rsaWrite.SignData(plainText, new SHA1CryptoServiceProvider());
return Convert.ToBase64String(signature);
}
Here is how I test the data (edited, still not working):
public static bool VerifyData(string sign, string publicKey, string orig) {
byte[] signature = Convert.FromBase64String(sign);
byte[] original = ASCIIEncoding.Unicode.GetBytes(orig);
var rsaRead = new RSACryptoServiceProvider();
rsaRead.FromXmlString(publicKey);
if (rsaRead.VerifyData(original, new SHA1CryptoServiceProvider(), signature)) {
return true;
} else {
return false;
}
}
I store the keypair as an xml string inside my account class. This function is executed in the constructor of account.cs:
public void addKeys() {
RSACryptoServiceProvider provider = new RSACryptoServiceProvider(1024);
privateKey = provider.ToXmlString(true);
publicKey = provider.ToXmlString(false);
}
I test the overall thing with this:
string signedHash = Utility.SignData("test" ,account.privateKey);
if (Utility.VerifyData(signedHash, account.publicKey, "test")) {
Console.WriteLine("WORKING!");
} else {
Console.WriteLine("SIGNING NOT WORKING");
}
Why isn't the overall thing working? My guess is that it doesn't work because of some encoding stuff.
return ASCIIEncoding.Unicode.GetString(signature);
The signature is arbitrary binary data, it isn't necessarily legal Unicode/UCS-2. You need to use an arbitrary encoding (https://en.wikipedia.org/wiki/Binary-to-text_encoding#Encoding_standards) to encode all of the arbitrary data. The most popular transport for signatures is Base64, so you'd want
return Convert.ToBase64String(signature);
And, of course, use Convert.FromBase64String in the verify method.
If you're compiling with a target of .NET 4.6 or higher you can also make use of the newer sign/verify API:
rsaRead.VerifyData(original, new SHA1CryptoServiceProvider(), signature)
would be
rsaRead.VerifyData(original, signature, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1)
While it might not look simpler, it prevents the allocation and finalization of the SHA1CryptoServiceProvider that the other method did, and it sets up for The Future when you may want to switch from Pkcs1 signature padding to PSS signature padding. (But the real advantage is that method is on the RSA base class instead of the RSACryptoServiceProvider specific type).
I want apply the RSA encryption to my project, but encountered some troubles:
First, I have download the JavaScripts library from
http://www.ohdave.com/rsa/ ,and add reference to my project;
Second, I have define the RSA object and code to initialize that:
internal RSACryptoServiceProvider Rsa
{
get
{
if (HttpContext.Cache["Rsa"] != null)
{
RSACryptoServiceProvider encryptKeys = (RSACryptoServiceProvider)HttpContext.Cache["Rsa"];
return encryptKeys;
}
else
{
return new RSACryptoServiceProvider(1024);
}
}
set
{
HttpContext.Cache.Remove("Rsa");
HttpContext.Cache.Insert("Rsa", value);
}
}
public ActionResult SignUp()
{
this.Rsa = Security.GetRsa();
RSAParameters param= this.Rsa.ExportParameters(true);
//this will bind to view
TempData["exponent"] = Util.BytesToHexString(param.Exponent);
TempData["key"] = Util.BytesToHexString(param.Modulus);
UserInfo user = new UserInfo();
user.Birthday = DateTime.Now.Date;
return View(user);
}
private RSACryptoServiceProvider GetRsa()
{
RSACryptoServiceProvider Rsa = new RSACryptoServiceProvider(1024);
return Rsa;
}
3.then, on JavaScript side , I have code, it encrypt the password user input and the bind it control:
var hash = document.getElementById("Pwd").value;
var exponent = document.getElementById("exponent").innerHTML;
var rsa_n = document.getElementById("key").innerHTML;
setMaxDigits(131);
var key = new RSAKeyPair(exponent, "", rsa_n);
hash = encryptedString(key, "111");
document.getElementById("Pwd").value = hash;
document.getElementById("Pwd2").value = hash;
document.getElementById("error").innerHTML = "";
document.getElementById("submit").click();
4.when user click submit, my C# code get the encrypted pwd string and try to decrypt it but failed with exception: bad data:
[HttpPost]
public ActionResult SignUp(UserInfo user)
{
user.UserId = user.UserId.ToLower(); //ignore case
user.UserGUID = Guid.NewGuid();
user.CreatedDate = DateTime.Now;
user.IsEnabled = false;
user.Pwd = Convert.ToBase64String(Rsa.Decrypt(Util.HexStringToBytes(user.Pwd), false));//Exception:Rsa.Decrypt throw bad data exception
who do you know how to fix it? thank you in advance.
I had a very similar problem in that most of the JavaScript based RSA encryption solutions wasn't "compatible" with .NET's implementation.
Almost all the implementations I found online had one or both of the following items causing the incompatibility with .NET's implementation.
The byte order encoding in JavaScript is different to the byte order that .NET used. This is a biggie as for example a string is represented with a different order of bytes in JS than it is in .NET so you'll need to convert before encrypting and after decrypting. I believe it's enough to just reverse the byte order to match .NET, but don't quote me on that.
Padding was different: .NET uses OAEP padding by default for RSA so the JS implementation of RSA should support the same padding too. I believe OAEP padding is also called PKCS#1 v2.0 padding, but don't quote me on that either.
Aside: I found an amazing JS library, called JavaScript.NET (from jocys.com) that mirrors tons of the .NET BCL functionality, including the RSA implementation, such that I could even use similar classes, properties and methods. Have a look at this. I can confirm it works with .NET RSA implementation. Give it a go - here are some links for it:
Jocys JS.NET Code Project demo
Jocys JS.NET Download
Hth
I'm trying to asymetrically encrypt a message of arbitrary length with bouncycastle. (1.4+ with C#)
This is the code I have right now. It is supposed to (but doesn't) generate a CMS message where the data itself is encrypted with AES256 with a random key and the key is encrypted with the public key from keyPair.
keyPair is an RSA-Key (RsaKeyParameters)
public static byte[] Encrypt(byte[] input, AsymmetricCipherKeyPair keyPair)
{
CmsEnvelopedDataGenerator generator = new CmsEnvelopedDataGenerator();
// those two lines are certainly wrong.
// I have no idea what the subKeyID parameter does
byte[] subKeyId = new byte[] {};
generator.AddKeyTransRecipient(keyPair.Public, subKeyId);
CmsProcessableByteArray cmsByteArray = new CmsProcessableByteArray(input);
CmsEnvelopedData envelopeData =
generator.Generate(cmsByteArray, CmsEnvelopedDataGenerator.Aes256Cbc);
return envelopeData.GetEncoded();
}
What is the subKeyId parameter in the Encrypt method for and what value does it need to have?
aaronls is being a little unfair to the author of "Beginning cryptography with Java", who after all wrote all the unit tests himself in the first place...
As other commenters have pointed out, CMS works with certificates, you can't just pass a public key; it must be possible to refer to the key either by "SubjectKeyIdentifier" or by "IssuerAndSerialNumber". The two alternatives of AddKeyTransRecipient allow this. If these terms don't mean anything to you, you probably need to do some background reading on X.509.
Look at the function TryKekAlgorithm in the EnvelopedDataTest.cs file of the BouncyCastle source. Instead of doing AddKeyTransRecipient, they are doing AddKekRecipient.
public static byte[] Encrypt(byte[] input, AsymmetricCipherKeyPair keyPair)
{
CmsEnvelopedDataGenerator generator = new CmsEnvelopedDataGenerator();
DerObjectIdentifier algOid = //initialize
//Still trying to figure out kekId here.
byte[] kekId = new byte[] { 1, 2, 3, 4, 5 };
string keyAlgorithm = ParameterUtilities.GetCanonicalAlgorithmName("AES256");
generator.AddKekRecipient(keyAlgorithm, keyPair.Public, kekId);
CmsProcessableByteArray cmsByteArray = new CmsProcessableByteArray(input);
CmsEnvelopedData envelopeData =
generator.Generate(cmsByteArray, CmsEnvelopedDataGenerator.Aes256Cbc);
return envelopeData.GetEncoded();
}
Edit: I think the kekId is just a unique identifier used to reference the key. Just a way to "name" the key. So you can have a book of keys, and each one has an identifier. When you send an encrypted message, the unencrypted key identifier tells you which of the keys was used to encrypt the message.
Here is a pretty good explanation of key identifiers on page 140:
[http://books.google.com/books?id=Pgg-Es2j3UEC&pg=PA140&lpg=PA140&dq=understanding+key+identifiers+encrypt&source=bl&ots=nFg0BzM2ht&sig=Ux5sreXMKyuEEZu0uaxE7cXC1VI&hl=en&ei=JKKJStbHGJivtgffsNznDA&sa=X&oi=book_result&ct=result&resnum=6#v=onepage&q=&f=false][1]
And here is another book that is using BouncyCastleCrypto, but it looks like they did little more than rip off the unit test source code. They have explained it a little:
[http://books.google.com/books?id=WLLAD2FKH3IC&pg=PA343&lpg=PA343&dq=CmsEnvelopedDataGenerator+AddKekRecipient&source=bl&ots=O9HinJm3yB&sig=K5Z99DIVWW4-0abPIFR7x4lzBhU&hl=en&ei=g6aJSrjeDuHktgennNjnDA&sa=X&oi=book_result&ct=result&resnum=6#v=onepage&q=CmsEnvelopedDataGenerator%20AddKekRecipient&f=false][2]
To use AES, it is not enough to use a AsymmetricCipherKeyPair.
You should use a X509 certificate, where the public key is signed by an certificate authority (CA).
the subKeyId is an attribute of the certificate, the subject Key Identifier:
(X509Certificate) cert.getSubjectUniqueID()
To encrypt a message of artrary length, you should use AES only to exchange a symmetric Keypassword and use this key for symmetric encryption.