Forum Programmation.web Signature S3 et comportement du navigateur Web

Posté par  . Licence CC By‑SA.
Étiquettes :
2
4
juin
2023

Bonjour,

Je suis bloqué depuis quelques hier sur quelque chose qui ne me semblait pas compliqué au démarrage : faire du S3 depuis le navigateur (sans se payer une bibliothèque obscure et/ou lourde). Le contexte : avoir la possibilité d'envoyer et récupérer du contenu S3 depuis une WebExtension (je devrais en parler bientôt sur LinuxFr).

En soi le plus compliqué est de générer l'entête Authorization. Je reste sur la v2 de l'authentification AWS, à destination d'un serveur local MinIO.

Voici l'équivalent Bash et Javascript que j'utilise :

# bash
signature=`echo -en "PUT\n\n${type}\n${date}\n${chemin}" | openssl sha1 -hmac ${s3Secret} -binary | base64`
// javascript
function signer(message, secretKey) {
  const encoder = new TextEncoder();
  const data = encoder.encode(message);
  const key = encoder.encode(secretKey);
  return crypto.subtle.importKey('raw', key, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'])
    .then(
      (key) => crypto.subtle.sign('HMAC', key, data)
    )
    .then(
      (signature) => { 
        return btoa(
          String.fromCharCode.apply(
            null, 
            new Uint8Array(signature)
          )
        );
      }
    );
}

Les deux me fournissent une signature valide. Par acquis de conscience, j'ai aussi testé en remettant la clé générée par Bash directement dans la requête de Firefox (avec la bonne date évidemment).

Rien à faire : le serveur n'en veut pas.

Firefox envoie la requête suivante :

PUT /tout/test-text.json HTTP/1.1
Host: localhost:9000
User-Agent: curl/7.81.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
authorization: AWS F1vfymAX4V4yp25DbX33:GbddtmO6/A2PGse8ySAwEPNn3vk=
content-type: text/plain
x-amz-date: Sun, 04 Jun 2023 13:13:28 +0200
Content-Length: 11
Origin: moz-extension://183b3036-20b3-4bc3-82e2-46f80d95c2ca
Connection: keep-alive
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Cache-Control: max-age=0

Le message de retour est sans appel : la requête par le navigateur échoue sur une 403…

<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>SignatureDoesNotMatch</Code><Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message><Key>test-text.json</Key><BucketName>tout</BucketName><Resource>/tout/test-text.json</Resource><RequestId>17657120F9D3A9D3</RequestId><HostId>dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8</HostId></Error>

Chose étrange, le navigateur me permet l'ajout de x-amz-date mais refuse semble-t-il (silencieusement) l'envoi de l'entête Date :

var headers = new Headers();
headers.set('User-Agent', 'curl/7.81.0');
headers.set('Host', 'localhost:9000');
headers.set('Date', date); // ça ne passe pas ?
headers.set('X-Amz-Date', date); // ça passe
headers.set( "Content-Type", contentType );
headers.set( "Expect", "100-continue" );
headers.set('Authorization', `AWS ${s3Key}:${signature}`);
var contexteRequete = { 
  method: 'PUT',
  headers: headers,
  mode: 'cors', // j'ai essayé aussi plusieurs valeurs suivant la doc du MDN, sans succès
  cache: 'no-cache', 
  body: "Hello world" 
 };
var requete = new Request(
  `http://${domain}/${bucket}/${filename}`,
  contexteRequete,
);
fetch(requete,contexteRequete)
  .then(function(response) {
    console.log("réponse s3 ok", response.text().then( (c)=>console.log(c) )) 
  })
  .catch(function(erreur) {
    console.log("réponse s3 ko", erreur) 
  });

Quant aux journaux de CURL, je ne vois pas d'écart notable :

*   Trying 127.0.0.1:9000...
* Connected to localhost (127.0.0.1) port 9000 (#0)
> PUT /tout/test-text.json HTTP/1.1
> Host: localhost:9000
> User-Agent: curl/7.81.0
> Date: Sun, 04 Jun 2023 13:13:28 +0200
> Content-Type: text/plain
> Authorization: AWS F1vfymAX4V4yp25DbX33:GbddtmO6/A2PGse8ySAwEPNn3vk=
> Accept: */*
> Accept-Language: en-US,en;q=0.5
> Accept-Encoding: gzip, deflate, br
> Origin: moz-extension://183b3036-20b3-4bc3-82e2-46f80d95c2ca
> Connection: keep-alive
> Sec-Fetch-Dest: empty
> Sec-Fetch-Mode: cors
> Sec-Fetch-Site: same-origin
> Cache-Control: max-age=0
> Content-Length: 9547
> Expect: 100-continue
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 100 Continue
HTTP/1.1 100 Continue

* We are completely uploaded and fine
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
HTTP/1.1 200 OK
(...)

J'ai pensé que l'entête Date serait la source du problème (au détriment de x-amz-date), mais si je retire celui-ci dans Bash, l'erreur est bien différente d'une 403 (il lui faut une date) :

<Error><Code>AccessDenied</Code><Message>AWS authentication requires a valid Date or x-amz-date header</Message><Resource>/tout/test-text.json</Resource><RequestId></RequestId><HostId></HostId></Error>

Point intéressant, la requête n'est pas valide par défaut de signature (403, SignatureDoesNotMatch) si je rajoute x-amz-date dans les entêtes CURL (avec ou sans Date) - ici le bout de script pour tester avec les entêtes de Firefox que je teste :

curl -v -I -X PUT -T "${fileorigin}" \
  -H "Host: ${domain}" \
  -H "User-Agent: curl/7.81.0" \
  -H "Date: ${dateValue}" \
  -H "x-amz-date: ${dateValue}" \
  -H "Content-Type: ${contentType}" \
  -H "Authorization: AWS ${signature}" \
  -H "Accept: */*" \
  -H "Accept-Language: en-US,en;q=0.5" \
  -H "Accept-Encoding: gzip, deflate, br" \
  -H "Origin: moz-extension://183b3036-20b3-4bc3-82e2-46f80d95c2ca" \
  -H "Connection: keep-alive" \
  -H "Sec-Fetch-Dest: empty" \
  -H "Sec-Fetch-Mode: cors" \
  -H "Sec-Fetch-Site: same-origin" \
  -H "Cache-Control: max-age=0" \
  "http://${domain}/${bucket}/${filename}"

Notez bien que le contenu textuel qui sert à la signature (en plus du secret), est correct (sinon ça ne fonctionnerait jamais en Bash).

Selon vous, quelle serait ma méprise ? Est-ce seulement cette question de l'entête Date, mais si oui, comment "forcer" son envoi par le navigateur ?

PS : ce sont des clés bidon évidemment.

  • # Oups

    Posté par  . Évalué à 3.

    Je m'auto-réponds sur la partie entête "Date" : on ne peut pas l'envoyer.

    Mais du reste, ça ne devrait pas poser problème si on en croit la doc AWS :

    Some HTTP client libraries do not expose the ability to set the Date header for a request. If you have trouble including the value of the 'Date' header in the canonicalized headers, you can set the timestamp for the request by using an 'x-amz-date' header instead. The value of the x-amz-date header must be in one of the RFC 2616 formats (http://www.ietf.org/rfc/rfc2616.txt). When an x-amz-date header is present in a request, the system will ignore any Date header when computing the request signature. Therefore, if you include the x-amz-date header, use the empty string for the Date when constructing the StringToSign.

    Là je sèche vraiment…

    • [^] # Re: Oups

      Posté par  . Évalué à 2.

      Bon… il faut croire que mettre à plat mes idées dans un post de forum aide (beaucoup)… Finalement j'ai abandonné l'idée d'envoyer Date via une requête gérée par fetch. Et comme je suis dans une WebExtension, je peux aller modifier les entêtes juste avant l'envoi réel :

      function ajouter_entete_date(details) {
        console.log("ajouter_entete_date", details);
        const headers = details.requestHeaders;
        headers.push({ name: "Date", value: date });
        return { requestHeaders: headers };
      }
      browser.webRequest.onBeforeSendHeaders.addListener(
        ajouter_entete_date,
        { urls: ["<all_urls>"] },
        ["blocking", "requestHeaders"]
      );

      Et là plus de problème !

      Je vais voir pour avoir un entête bidon avec un jeton de type X-Date-{jeton} qui sera écouté. Si cet entête est trouvé, il est transformé en entête classique Date. Le jeton ne sera connu que de l'extension elle-même, et aléatoire.

      PS : jugez-moi.

      • [^] # Re: Oups

        Posté par  (site web personnel) . Évalué à 6.

        il faut croire que mettre à plat mes idées dans un post de forum aide (beaucoup)

        c'est le principe du « parle à ton canard » aussi appelé Méthode_du_canard_en_plastique, comme un gamin le fait dans son bain et exprime ainsi ses questions existentielles :-)

        • [^] # Re: Oups

          Posté par  . Évalué à 1.

          J'aime tellement cet article de Wikipedia… ! Merci.

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.