Search code examples
azure-blob-storage

Unable to generate a valid Azure Storage SAS token in Cpp


I have tried several methods, but they all give the same error when creating a SAS token programatically.

version="1.0" encoding="utf-8"?><Error><Code>AuthenticationFailed</Code><Message>Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature. RequestId:32b24722-801e-0024-1e9c-3b3c1d000000 Time:2024-11-20T22:36:21.0299082Z</Message><AuthenticationErrorDetail>Signature fields not well formed.</AuthenticationErrorDetail></Error>

Of course, this all works when using a SAS token generated in the portal. I have a (working) storage account and container and the containers key (I assume this is base64 encoded and needs to be decoded prior to signing).

bool downloadFile(const std::string& blobName, const std::string& localFilePath) {
        if (!m_curl) {
            std::cerr << "Failed to initialize cURL" << std::endl;
            return false;
        }

        // Generate SAS token just before the file download
        //IotHubHelpers iotHubHelper;
        std::string resourceUri = m_storage_account + ".blob.core.windows.net/" + m_container_name + "/" + blobName;
        std::string account_key = "Only Half the eyJBuYU2S2G99MDFfb5K6aGRrZJRdAlonKchD+AStQfq0Ig==";  // Your container access key
        std::string start_time = "2024-11-19T19:21:22Z";
        std::string expiry_time = "2028-11-20T03:21:22Z";
        std::string permissions = "rwdlacupiyx";


         std::string sasToken = generate_blob_sas_token(
            //m_storage_account,
            //m_container_name,
            //blobName,
            account_key //,
            //permissions,
            //start_time,
            //expiry_time
        );

        if (sasToken.empty()) {
            std::cerr << "Error: Failed to generate SAS token." << std::endl;
            return false;
        }

        std::cout << "SAS Token: " << sasToken << std::endl;

        // Construct URL with SAS token
        std::string url = "https://" + resourceUri + "?" + sasToken;
        std::cout << "Downloading from URL: " << url << std::endl;

        // Open the file stream in binary mode
        std::ofstream outFile(localFilePath, std::ios::binary);
        if (!outFile) {
            std::cerr << "Failed to open file for writing: " << localFilePath << std::endl;
            return false;
        }

        // Set cURL options
        curl_easy_setopt(m_curl, CURLOPT_URL, url.c_str());
        curl_easy_setopt(m_curl, CURLOPT_WRITEFUNCTION, writeData);
        curl_easy_setopt(m_curl, CURLOPT_WRITEDATA, &outFile);
        curl_easy_setopt(m_curl, CURLOPT_SSL_VERIFYPEER, 1L);
        curl_easy_setopt(m_curl, CURLOPT_SSL_VERIFYHOST, 2L);

        CURLcode res = curl_easy_perform(m_curl);
        if (res != CURLE_OK) {
            std::cerr << "cURL error: " << curl_easy_strerror(res) << std::endl;
            outFile.close();
            return false;
        }

        outFile.close();
        return true;
    }

This is the Token generator, where I cant replicate a token from Azure Portal.

std::string generate_blob_sas_token(const std::string& key) {
        const std::string canonicalizedResource = "/blob/fotacontainer/fota/testfile.txt";

        std::vector<std::pair<std::string, std::string>> sas_token_properties = {
            {"sp", "r"},
            {"st", "2024-11-19T19:21:22Z"}, //get_utc_time(-120)},
            {"se", "2025-11-19T19:21:22Z"}, //get_utc_time(1440)},
            {"canonicalizedResource", canonicalizedResource},
            {"si", ""},
            {"sip", ""},
            {"spr", "https"},
            {"sv", "2023-01-03"},
            {"sr", "b"},
            {"sst", ""},
            {"ses", ""},
            {"rscc", ""},
            {"rscd", ""},
            {"rsce", ""},
            {"rscl", ""},
            {"rsct", ""}
        };

        std::vector<std::string> values;
        for (const auto& entry : sas_token_properties) {
            values.push_back(entry.second);
        }

        std::string string_to_sign = join_with_newline(values);
        std::cout << string_to_sign << std::endl;

        // The keys we get from the storage account are base64 encoded, so we need to decode them first
        std::string decoded_key = base64_decode(key);
        unsigned char* digest = HMAC(EVP_sha256(), decoded_key.data(), decoded_key.size(),
                                     reinterpret_cast<const unsigned char*>(string_to_sign.data()), string_to_sign.size(), nullptr, nullptr);

        std::string signature = base64_encode(digest, SHA256_DIGEST_LENGTH);

        std::vector<std::string> parameters;
        for (const auto& entry : sas_token_properties) {
            if (!entry.second.empty() && entry.first != "canonicalizedResource") {
                parameters.push_back(entry.first + "=" + url_encode(entry.second));
            }
        }
        parameters.push_back("sig=" + url_encode(signature));

        return join(parameters, "&");
}
    

Using the code above (needs cleanup, yes), I get the error message

<?xml version="1.0" encoding="utf-8"?><Error><Code>AuthenticationFailed</Code><Message>Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly including the signature. RequestId:32b24722-801e-0024-1e9c-3b3c1d000000 Time:2024-11-20T22:36:21.0299082Z</Message><AuthenticationErrorDetail>Signature fields not well formed.</AuthenticationErrorDetail></Error> I must be missing something basic in the algorithm to assemble and generate the SAS Token. I've read the docs, and looking online. still no solution I can find.


Solution

  • Unable to generate a valid Azure Storage SAS token in Cpp

    You can use the below code to generate Azure blob sas token using cpp with crypto library.

    Code:

    #include <iostream>
    #include <iomanip>
    #include <sstream>
    #include <ctime>
    #include <string>
    #include <cryptopp/cryptlib.h>
    #include <cryptopp/hex.h>
    #include <cryptopp/hmac.h>
    #include <cryptopp/sha.h>
    #include <cryptopp/base64.h>
    #include <cryptopp/filters.h>
    #include <cctype>
    #include <iomanip>
    
    using namespace CryptoPP;
    
    std::string GenerateSas(const std::string& storageAccountKey, const std::string& input) {
        std::string decodedKey;
        StringSource(storageAccountKey, true, new Base64Decoder(new StringSink(decodedKey)));
    
        // Create HMAC-SHA256 signature
        std::string digest;
        HMAC<SHA256> hmac((const byte*)decodedKey.data(), decodedKey.size());
        StringSource(input, true,
            new HashFilter(hmac, new StringSink(digest))); // Compute HMAC digest
    
        // Encode the digest to Base64
        std::string encodedDigest;
        StringSource(digest, true, new Base64Encoder(new StringSink(encodedDigest), false));
        encodedDigest.erase(std::remove(encodedDigest.begin(), encodedDigest.end(), '\n'), encodedDigest.end());
        return encodedDigest; 
    }
    
    std::string TimePointToString(const std::chrono::seconds& timePoint) {
        std::time_t t = timePoint.count();
        std::tm tm{};
        gmtime_s(&tm, &t);
        std::ostringstream oss;
        oss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%SZ");
        return oss.str();
    }
    
    // URL encode function
    std::string UrlEncode(const std::string& str) {
        std::ostringstream escaped;
        for (char c : str) {
            if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
                escaped << c;
            }
            else {
                escaped << '%' << std::setw(2) << std::setfill('0') << std::hex << (int)(unsigned char)c;
            }
        }
        return escaped.str();
    }
    
    // Function to generate SAS token
    void GenerateSasToken(const std::string& blobName, const std::string& accessKey, const std::string& containerName) {
        // Define SAS token parameters
        auto currentTime = std::chrono::system_clock::now();
        auto currentTimeSeconds = std::chrono::duration_cast<std::chrono::seconds>(currentTime.time_since_epoch());
        auto expirationTime = currentTimeSeconds + std::chrono::minutes(120);
    
        // Convert time to ISO 8601 format
        std::string signedStart = TimePointToString(currentTimeSeconds);
        std::string signedExpiry = TimePointToString(expirationTime);
    
        // Define other SAS parameters
        std::string signedPermissions = "rwl";
        std::string signedService = "b"; // 'b' for blob
        std::string signedProtocol = "https";
        std::string signedVersion = "2022-11-02";
        std::string accountName = "venkat326123";
    
        // Create canonicalized resource
        std::string canonicalizedResource = "/blob/" + accountName + "/" + containerName + "/" + blobName;
    
        // Construct the string to sign
        std::ostringstream stringToSign;
        stringToSign << signedPermissions << "\n"
            << signedStart << "\n"
            << signedExpiry << "\n"
            << canonicalizedResource << "\n"
            << "\n\n"
            << signedProtocol << "\n"
            << signedVersion << "\n"
            << signedService << "\n"
            << "\n\n\n\n\n\n";
    
        // Generate the signature
        std::string signature = GenerateSas(accessKey, stringToSign.str());
    
        // URL encode the signature
        std::string urlEncodedSignature = UrlEncode(signature);
    
        // Construct the SAS token
        std::ostringstream sasToken;
        sasToken << "sv=" << signedVersion
            << "&sr=" << signedService
            << "&sp=" << signedPermissions
            << "&st=" << signedStart
            << "&se=" << signedExpiry
            << "&spr=" << signedProtocol
            << "&sig=" << urlEncodedSignature;
        std::cout << "Blob URL: https://" << accountName << ".blob.core.windows.net/"
            << containerName << "/" << blobName << "?" << sasToken.str() << std::endl;
    }
    
    int main() {
        // Define blob details
        std::string blobName = "scenery.jpg";
        std::string containerName = "test";
        std::string accessKey = "<storage account key>";
    
        // Generate SAS token
        GenerateSasToken(blobName, accessKey, containerName);
    
        return 0;
    }
    

    Output:

    Blob URL: https://venkat326123.blob.core.windows.net/test/scenery.jpg?sv=2022-11-02&sr=b&sp=rwl&st=2024-11-21T09:20:49Z&se=2024-11-21T11:20:49Z&spr=https&sig=erc8cD0Jxxxxxxx1hxxxGR%2btK9muys%3d
    

    enter image description here

    I verified the blob URL in the browser.

    Browser: enter image description here