signed video upload in rust returns "Invalid Signature"

olanetsoft
olanetsoft Member Posts: 9

I tried uploading a video to a Rust backend using Signed Upload. However, creating a signed upload using the API according to this documentation keeps returning an error saying "Invalid Signature".

I suspected it might be a hashing problem and tried it in another language using the same logic as in my Rust code. The Node.js version works, but the Golang version gave the same error as Rust, which made me suspect that the signature validation algorithm only caters to Node.js.

This type of upload works for unsigned uploads; however, it does not instantly return the expected (video) URL. It changes to a batch process and gives a batch identifier to track the upload.

{"status":"pending","resource_type":"video","type":"upload","public_id":".tmpcQmSj6","batch_id":"35d1cafa1c81ece90293eb0a856886c580f1a74860f6d7326238fe116a9ed3789aa0ac7e3c87bfea4d24a2dad478b91a"}

The ideal situation is to use the signed upload due to security reasons. Here are a few questions i would love to ask:

  1. How do I make this work for Rust? Has anyone experienced the same issue?
  2. If I am to use unsigned upload(I doubt), is there an endpoint I can call and pass in batch ID to know when the video is ready and then retrieve the URL?

Tagged:

Best Answer

  • johnr
    johnr Member Posts: 14
    Answer ✓

    Hi @olanetsoft,

    I'm not familiar with Rust code. But I saw the server logs and it indicates you are not passing the correct signature.

    The error mentioned you need to sign the string - 'public_id=.tmplO2DEd&timestamp=1718981576'. Hence, in your code, you will have to encode the string and append the API secret at the end - 'public_id=.tmplO2DEd&timestamp=1718981576<API_SECRET>'.

    Can you please verify that the mac value passed is 'public_id=<public_id>&timestamp=<timestamp><API_SECRET>'?

    Best,

    John

Answers

  • Vdeub
    Vdeub Member, Cloudinary Staff Posts: 78

    Hi @olanetsoft,

    It seems you have a few accounts with us and I am struggling to locate exactly which one you are talking about.

    I see you have olanetsoft which has 2 upload errors in the last 7 days which returned the following error:

     Upload preset must be specified when using unsigned upload
    

    And I can see that the file .tmpcQmSj6 is uploaded to the environment dtgbzmpca . The upload is successful using an unsigned upload via the preset wgyszbgz.

    The response you are getting is because you are using async:true as part of the preset so if you want to be notified when the upload is completed, you will need to use a webhook to be notified as documented here.

    For those 2 environments, none of them have an invalid signature error as per the title of your requests so if you can share the code used and the full logs you are getting for this error, that would be helpful.

    Thanks,

    Loic

  • olanetsoft
    olanetsoft Member Posts: 9

    Hello @Vdeub thank you for your response.

    I have retried again, i got the same error:

    {"error":{"message":"Invalid Signature e70d4bad88dc44094ab5df94341d70562dd3b866. String to sign - 'public_id=.tmplO2DEd&timestamp=1718981576'."}}
    


    I want to implement a signed upload, not an unsigned one.

    Generating the signature:

    fn generate_signature(params: &str, api_secret: &str) -> String {
    let mut mac = Hmac::<Sha1>::new_from_slice(api_secret.as_bytes())
    .expect("HMAC can take key of any size");
    mac.update(params.as_bytes());
    hex::encode(mac.finalize().into_bytes())
    }

  • olanetsoft
    olanetsoft Member Posts: 9

    The full code:

    use crate::models::CloudinaryResponse;
    use actix_multipart::Multipart;
    use actix_web::Error;
    use dotenv::dotenv;
    use futures_util::StreamExt;
    use hmac::{Hmac, Mac};
    use reqwest::{
    multipart::{self, Part},
    Client,
    };
    use sha1::Sha1;
    // use std::convert::TryInto;
    use std::{env, io::Write, path::Path};
    use tempfile::NamedTempFile;
    use tokio::io::AsyncReadExt; const MAX_SIZE: usize = 10 * 1024 * 1024; // 10MB pub struct VideoService; impl VideoService {
    fn env_loader(key: &str) -> String {
    dotenv().ok();
    match env::var(key) {
    Ok(v) => v.to_string(),
    Err(_) => format!("Error loading env variable"),
    }
    } fn generate_signature(params: &str, api_secret: &str) -> String { let mut mac = Hmac::<Sha1>::new_from_slice(api_secret.as_bytes()) .expect("HMAC can take key of any size"); mac.update(params.as_bytes()); hex::encode(mac.finalize().into_bytes()) } pub async fn save_file(mut payload: Multipart) -> Result<NamedTempFile, Error> { let mut total_size = 0; // Create a temporary file let mut temp_file = NamedTempFile::new()?; // Iterate over multipart stream while let Some(field) = payload.next().await { let mut field = field?; let content_disposition = field.content_disposition(); let filename = content_disposition .get_filename() .ok_or_else(|| actix_web::error::ParseError::Incomplete)?; // Get the MIME type of the file let content_type = field.content_type(); // Ensure content_type is present and it is a video if let Some(content_type) = content_type { if content_type.type_() != mime::VIDEO { return Err(actix_web::error::ErrorBadRequest( "Only video files are allowed", )); } } else { return Err(actix_web::error::ErrorBadRequest("Missing content type")); } // Write the file content to the temporary file synchronously while let Some(chunk) = field.next().await { let data = chunk?; total_size += data.len(); if total_size > MAX_SIZE { return Err(actix_web::error::ErrorBadRequest( "File size limit exceeded", )); } temp_file.write_all(&data)?; } } Ok(temp_file) } pub async fn upload_to_cloudinary( temp_file: &NamedTempFile, ) -> Result<CloudinaryResponse, Box<dyn std::error::Error>> { let client = Client::new(); let cloud_name = VideoService::env_loader("CLOUD_NAME"); let api_secret = VideoService::env_loader("API_SECRET"); let upload_preset = VideoService::env_loader("UPLOAD_PRESET"); let api_key = VideoService::env_loader("API_KEY"); let timestamp = chrono::Utc::now().timestamp(); let public_id = temp_file .path() .file_name() .and_then(|name| name.to_str()) .unwrap_or("file") .to_string(); // Include only public_id and timestamp in the signature let params = format!("public_id={}&timestamp={}", public_id, timestamp); let signature = VideoService::generate_signature(&params, &api_secret); println!("{}", &params); let mut file = tokio::fs::File::open(temp_file.path()).await?; let mut buffer = Vec::new(); file.read_to_end(&mut buffer).await?; let part = Part::bytes(buffer).file_name(public_id.clone()); let form = multipart::Form::new() // .text("upload_preset", upload_preset.clone()) .text("public_id", public_id.clone()) .text("timestamp", timestamp.to_string()) .text("signature", signature) .text("api_key", api_key) .part("file", part); let res = client .post(format!( "https://api.cloudinary.com/v1_1/{}/video/upload", cloud_name )) .multipart(form) .send() .await?; let result = res.text().await?; println!("{}", &result); let cloudinary_response: CloudinaryResponse = serde_json::from_str(&result)?; Ok(cloudinary_response) } }

  • Vdeub
    Vdeub Member, Cloudinary Staff Posts: 78
    edited June 26

    Hi @olanetsoft

    It is most likely your rust code here. I took your params_to_sign and api_secret and I do get the right signature.

    Do you mind sharing the format of params and the last 4 digits of your api_secret ? I found also a code here that seems to generate the right value.

    Please let me know how it goes.

    Best,

    Loic