bakz
bakz

Reputation: 89

Azure storage SAS token generation - signature mismatch

I am struggling with this issue for 3 days now.. I would really appreciate the help.

I am trying to create SAS tokens for accessing Azure storage blobs dynamically, the token generates but I keep getting this error: Signature did not match. String to sign used was ...

Creating the SAS token from azure portal works fine, but I have tried generating it in the code using many different ways, Micrtosoft.Azure.Storage library:

private static string GetBlobSasUri(CloudBlobContainer container, string blobName)
{
    string sasBlobToken;

    // Get a reference to a blob within the container.
    // Note that the blob may not exist yet, but a SAS can still be created for it.
    CloudBlockBlob blob = container.GetBlockBlobReference(blobName);


    // Create a new access policy and define its constraints.
    // Note that the SharedAccessBlobPolicy class is used both to define the parameters of an ad hoc SAS, and
    // to construct a shared access policy that is saved to the container's shared access policies.
    SharedAccessBlobPolicy adHocSAS = new SharedAccessBlobPolicy()
    {
        // When the start time for the SAS is omitted, the start time is assumed to be the time when the storage service receives the request.
        // Omitting the start time for a SAS that is effective immediately helps to avoid clock skew.
        SharedAccessStartTime = DateTime.UtcNow.AddHours(-24),
        SharedAccessExpiryTime = DateTime.UtcNow.AddHours(24),
        Permissions = SharedAccessBlobPermissions.Read //| SharedAccessBlobPermissions.Write | SharedAccessBlobPermissions.Create
    };

    // Generate the shared access signature on the blob, setting the constraints directly on the signature.
    sasBlobToken = blob.GetSharedAccessSignature(adHocSAS);

    Console.WriteLine("SAS for blob (ad hoc): {0}", sasBlobToken);
    Console.WriteLine();

    // Return the URI string for the container, including the SAS token.
    return blob.Uri + sasBlobToken;
}

This has worked for a for few tries, the next day is stopped functioning.. I tested it on my local IIS and on azure as well, locally it never worked.

I have also tried creating the token myself, both generating and signing the token, but the same issue persists.. here is the code for the signing:

 public static string generateSAS(Uri resourceUri, string accountKey, string accountName, StorageFile f)
        {

            string
                sr = "b",
                sp = "r",
                st = DateTime.Now.AddHours(-50).ToString("yyyy-MM-ddTH:MM:ssZ"),
                se = DateTime.Now.AddHours(50).ToString("yyyy-MM-ddTH:MM:ssZ"),
                sv = "2016-05-31"; //"2019-07-07"

            string blobUri = $"https://{accountName}.blob.core.windows.net/{f.Model.Container}/{f.Model.FileName}";

            string stringToSign = string.Format("{0}\n{1}\n{2}\n{3}\n{4}\n{5}\n{6}\n{7}\n{8}\n{9}\n{10}\n{11}\n{12}", new object[]
            {
                "r",
                st,
                se,
                GetCanonicalizedResource(new Uri(blobUri), accountName),
                string.Empty,
                string.Empty,
                string.Empty,
                sv,
                sr,
                string.Empty,
                string.Empty,
                string.Empty,
                string.Empty
            });

            var sas = GetHash(stringToSign, accountKey);

            var token =
                $"?sv={sv}&sr=b&sig={HttpUtility.UrlEncode(sas)}&st={HttpUtility.UrlEncode(st)}&se={HttpUtility.UrlEncode(se)}&sp=r";


            return blobUri + token;
        }

        private static string GetHash(string stringToSign, string key)
        {
            byte[] keyValue = Convert.FromBase64String(key);

            using (HMACSHA256 hmac = new HMACSHA256(keyValue))
            {
                return Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)));
            }
        }



private static string GetCanonicalizedResource(Uri address, string storageAccountName)
        {
             // The absolute path is "/" because for we're getting a list of containers.
            StringBuilder sb = new StringBuilder("/").Append("blob").Append("/").Append(storageAccountName).Append(address.AbsolutePath);
            return sb.ToString();

        }

I have read the documentation and thought the issue might be a version issue, so I tried using the latest build and some versions prior to it, but to no avail.

I have tried not using and adding other parameters, nothing worked at all..

I also have been lead to believe that the issue might be due to clock, but I have adjusted my clock to every hour of the day so far, also it has been tested on an app service on Azure which is located in "UK South", in there the C# code worked for less than a single day.

I would appreciate you help very much! thank you

EDIT Using the RESTful version of the code where I self sign it manually, this is the "toSignString":

r
2020-02-17T14:02:56Z
2020-02-21T18:02:56Z
/blob/<accountName>/<contaioner1>/<subContainer2>/<subContainer3>/file.v



2016-05-31
b




and this is the full Uri:

https://<account>.blob.core.windows.net/<contaioner1>/<subContainer2>/<subContainer3>/file.v?
sv=2016-05-31&sr=b&sig=hI7MH0pT05cDpga2bMkanR%2b77CeocgttygDJbkRw6EM%3d&st=2020-02-17T14%3a02%3a56Z&se=2020-02-21T18%3a02%3a56Z&sp=r

Upvotes: 1

Views: 1342

Answers (1)

Gaurav Mantri
Gaurav Mantri

Reputation: 136136

This has worked for a single day, then it stopped working.. I tested it on my local IIS and on azure as well, locally it never worked.

This is expected behavior because you're setting the SAS Token expiry to be 24 hours from the current date/time (in UTC):

SharedAccessExpiryTime = DateTime.UtcNow.AddHours(24),

If you want your SAS URL to work for a longer duration, you will need to specify an expiry date much out in future.

Regarding the 2nd issue (manual creation of SAS Token), I believe there's an issue with your GetCanonicalizedResource method. Per the documentation, it should be in the following format:

/blob/<account-name>/<container-name>/<blob-name>

The canonicalizedresource portion of the string is a canonical path to the signed resource. It must include the service name (blob, table, queue or file) for version 2015-02-21 or later, the storage account name, and the resource name, and must be URL-decoded.

Based on this, please try the following code:

private static string GetCanonicalizedResource(Uri address, string storageAccountName)
{
    // The absolute path is "/" because for we're getting a list of containers.
    StringBuilder sb = new StringBuilder("/").Append("blob").Append("/").Append(storageAccountName).Append("/").Append(address.AbsolutePath);
    return sb.ToString();
}

UPDATE

Took me a while to figure out what's wrong :) but finally was able to do so. There are a number of issues I found:

  1. You're formatting dates incorrectly. It should be in yyyy-MM-ddTHH:MM:ssZ but you're formatting it in yyyy-MM-ddTH:MM:ssZ format. If you notice, it's missing an extra H.
  2. You're using 2016-05-31 signed version and that does not include the signed resource (b in your case) in stringToSign but you're including that. It should be included in your SAS URL though.
  3. Your string to sign is missing content-language (rscl) parameter.

According to the documentation, this is how you should construct your stringToSign:

StringToSign = signedpermissions + "\n" +  
               signedstart + "\n" +  
               signedexpiry + "\n" +  
               canonicalizedresource + "\n" +  
               signedidentifier + "\n" +  
               signedIP + "\n" +  
               signedProtocol + "\n" +  
               signedversion + "\n" +  
               rscc + "\n" +  
               rscd + "\n" +  
               rsce + "\n" +  
               rscl + "\n" +  
               rsct

Based on this, here should be your code:

    string stringToSign = string.Format("{0}\n{1}\n{2}\n{3}\n{4}\n{5}\n{6}\n{7}\n{8}\n{9}\n{10}\n{11}\n{12}", new object[]
    {
        "r",//permission
        st,//start
        se,//end
        GetCanonicalizedResource(new Uri(blobUri), accountName),//canonical resource
        string.Empty,//signed identifier
        string.Empty,//signed ip
        string.Empty,//signed protocol
        sv,//signed version
        string.Empty,//cache control
        string.Empty,//content disposition
        string.Empty,//content encoding
        string.Empty,//content language
        string.Empty//content type
    });

Give it a try and it should work.

Upvotes: 2

Related Questions