| 1 | package main |
| 2 | |
| 3 | // đŸ¤– Pins the hand-rolled signer against the official worked examples in the |
| 4 | // AWS docs ("Signature Calculations for the Authorization Header" — the |
| 5 | // examplebucket vectors, signing key AKIAIOSFODNN7EXAMPLE, 2013-05-24), so the |
| 6 | // signer is testable offline. TestS3VaultStore exercises it for real. |
| 7 | |
| 8 | import ( |
| 9 | "net/http" |
| 10 | "strings" |
| 11 | "testing" |
| 12 | "time" |
| 13 | ) |
| 14 | |
| 15 | func TestSigV4Vectors(t *testing.T) { |
| 16 | const ( |
| 17 | accessKey = "AKIAIOSFODNN7EXAMPLE" |
| 18 | secretKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" |
| 19 | amzDate = "20130524T000000Z" |
| 20 | ) |
| 21 | ts := time.Date(2013, 5, 24, 0, 0, 0, 0, time.UTC) |
| 22 | |
| 23 | cases := []struct { |
| 24 | name, method, url string |
| 25 | headers map[string]string |
| 26 | wantSignedHeaders string |
| 27 | wantSig string |
| 28 | }{ |
| 29 | { |
| 30 | // GET Object: GET /test.txt with a Range header. |
| 31 | name: "GetObject", |
| 32 | method: "GET", |
| 33 | url: "https://examplebucket.s3.amazonaws.com/test.txt", |
| 34 | headers: map[string]string{ |
| 35 | "Range": "bytes=0-9", |
| 36 | "x-amz-content-sha256": emptyPayloadHash, |
| 37 | "x-amz-date": amzDate, |
| 38 | }, |
| 39 | wantSignedHeaders: "host;range;x-amz-content-sha256;x-amz-date", |
| 40 | wantSig: "f0e8bdb87c964420e857bd35b5d6ed310bd44f0170aba48dd91039c6036bdb41", |
| 41 | }, |
| 42 | { |
| 43 | // GET Bucket Lifecycle: a valueless subresource must canonicalize |
| 44 | // as "lifecycle=" — the same shape our ListObjectVersions uses. |
| 45 | name: "GetBucketLifecycle", |
| 46 | method: "GET", |
| 47 | url: "https://examplebucket.s3.amazonaws.com/?lifecycle", |
| 48 | headers: map[string]string{ |
| 49 | "x-amz-content-sha256": emptyPayloadHash, |
| 50 | "x-amz-date": amzDate, |
| 51 | }, |
| 52 | wantSignedHeaders: "host;x-amz-content-sha256;x-amz-date", |
| 53 | wantSig: "fea454ca298b7da1c68078a5d1bdbfbbe0d65c699e0f91ac7a200a0136783543", |
| 54 | }, |
| 55 | { |
| 56 | // List Objects: multiple query params, sorted canonically. |
| 57 | name: "ListObjects", |
| 58 | method: "GET", |
| 59 | url: "https://examplebucket.s3.amazonaws.com/?max-keys=2&prefix=J", |
| 60 | headers: map[string]string{ |
| 61 | "x-amz-content-sha256": emptyPayloadHash, |
| 62 | "x-amz-date": amzDate, |
| 63 | }, |
| 64 | wantSignedHeaders: "host;x-amz-content-sha256;x-amz-date", |
| 65 | wantSig: "34b48302e7b5fa45bde8084f4b7868a86f0a534bc59db6670ed5711ef69dc6f7", |
| 66 | }, |
| 67 | } |
| 68 | |
| 69 | for _, tc := range cases { |
| 70 | t.Run(tc.name, func(t *testing.T) { |
| 71 | req, err := http.NewRequest(tc.method, tc.url, nil) |
| 72 | if err != nil { |
| 73 | t.Fatalf("NewRequest: %v", err) |
| 74 | } |
| 75 | for k, v := range tc.headers { |
| 76 | req.Header.Set(k, v) |
| 77 | } |
| 78 | awsSignV4(req, accessKey, secretKey, "us-east-1", "s3", ts) |
| 79 | auth := req.Header.Get("Authorization") |
| 80 | if !strings.Contains(auth, "SignedHeaders="+tc.wantSignedHeaders+",") { |
| 81 | t.Errorf("signed headers: want %q in %q", tc.wantSignedHeaders, auth) |
| 82 | } |
| 83 | if !strings.HasSuffix(auth, "Signature="+tc.wantSig) { |
| 84 | t.Errorf("signature mismatch:\n got %q\nwant …Signature=%s", auth, tc.wantSig) |
| 85 | } |
| 86 | }) |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | // The SigV4 path/query encoders have their own rules (uppercase hex, '+' is |
| 91 | // %2B never a space, '/' survives only in paths); a wrong encoder signs |
| 92 | // requests S3 will reject, so pin the corners. |
| 93 | func TestAWSURIEncode(t *testing.T) { |
| 94 | for _, tc := range []struct { |
| 95 | in string |
| 96 | encodeSlash bool |
| 97 | want string |
| 98 | }{ |
| 99 | {"blobs/u1", false, "blobs/u1"}, |
| 100 | {"blobs/u1", true, "blobs%2Fu1"}, |
| 101 | {"a b+c", true, "a%20b%2Bc"}, |
| 102 | {"ok-._~", true, "ok-._~"}, |
| 103 | {"Ă©", true, "%C3%A9"}, |
| 104 | } { |
| 105 | if got := awsURIEncode(tc.in, tc.encodeSlash); got != tc.want { |
| 106 | t.Errorf("awsURIEncode(%q, %v) = %q, want %q", tc.in, tc.encodeSlash, got, tc.want) |
| 107 | } |
| 108 | } |
| 109 | } |
| 110 | |