URL:     https://linuxfr.org/forums/programmationweb/posts/signature-s3-et-comportement-du-navigateur-web
Title:   Signature S3 et comportement du navigateur Web
Authors: JulienG
Date:    2023-06-04T13:56:57+02:00
License: CC By-SA
Tags:    aws, s3, bash et javascript
Score:   2


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`](https://docs.aws.amazon.com/AmazonS3/latest/userguide/RESTAuthentication.html#RESTAuthenticationExamples). 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
# bash
signature=`echo -en "PUT\n\n${type}\n${date}\n${chemin}" | openssl sha1 -hmac ${s3Secret} -binary | base64`
```

```javascript
// 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
<?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` : 

```javascript
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) : 

```xml
<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 :

```bash
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._ 



