eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3// πŸ€– Minimal AWS Signature Version 4 signer β€” just enough to talk to S3.
4// Hand-rolled on purpose (#550 Phase 1): home has two dependencies total and
5// the s3Store needs ~6 operations, so pulling in aws-sdk-go-v2 would multiply
6// the dependency surface for one HMAC chain. Pinned offline against the
7// official AWS worked examples in sigv4_test.go; exercised for real by
8// TestS3VaultStore.
9
10import (
11 "crypto/hmac"
12 "crypto/sha256"
13 "encoding/hex"
14 "fmt"
15 "net/http"
16 "net/url"
17 "sort"
18 "strings"
19 "time"
20)
21
22// emptyPayloadHash is sha256("") β€” the x-amz-content-sha256 for bodyless
23// requests (GET/HEAD/DELETE).
24const emptyPayloadHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
25
26// awsSignV4 computes the SigV4 signature for req and sets its Authorization
27// header. The caller must already have set x-amz-date (matching t) and
28// x-amz-content-sha256 (the hex payload hash β€” signed, so the body can't be
29// swapped in flight). Signed headers are host plus every x-amz-* header plus
30// range/content-type/content-md5 when present; conditional headers (If-Match
31// etc.) ride unsigned, which S3 permits β€” only x-amz-* headers MUST be signed.
32func awsSignV4(req *http.Request, accessKey, secretKey, region, service string, t time.Time) {
33 amzDate := t.UTC().Format("20060102T150405Z")
34 scope := t.UTC().Format("20060102") + "/" + region + "/" + service + "/aws4_request"
35
36 host := req.Host
37 if host == "" {
38 host = req.URL.Host
39 }
40 type hdr struct{ name, value string }
41 signed := []hdr{{"host", host}}
42 for name, vals := range req.Header {
43 ln := strings.ToLower(name)
44 if strings.HasPrefix(ln, "x-amz-") || ln == "range" || ln == "content-type" || ln == "content-md5" {
45 signed = append(signed, hdr{ln, strings.TrimSpace(strings.Join(vals, ","))})
46 }
47 }
48 sort.Slice(signed, func(i, j int) bool { return signed[i].name < signed[j].name })
49 names := make([]string, len(signed))
50 var canonHdrs strings.Builder
51 for i, h := range signed {
52 canonHdrs.WriteString(h.name)
53 canonHdrs.WriteByte(':')
54 canonHdrs.WriteString(h.value)
55 canonHdrs.WriteByte('\n')
56 names[i] = h.name
57 }
58 signedHeaders := strings.Join(names, ";")
59
60 path := req.URL.EscapedPath()
61 if path == "" {
62 path = "/"
63 }
64 canonical := req.Method + "\n" +
65 path + "\n" +
66 canonicalQuery(req.URL.Query()) + "\n" +
67 canonHdrs.String() + "\n" +
68 signedHeaders + "\n" +
69 req.Header.Get("x-amz-content-sha256")
70
71 stringToSign := "AWS4-HMAC-SHA256\n" + amzDate + "\n" + scope + "\n" + hexSHA256([]byte(canonical))
72
73 // The HMAC chain: secret β†’ date β†’ region β†’ service β†’ "aws4_request".
74 key := hmacSHA256([]byte("AWS4"+secretKey), t.UTC().Format("20060102"))
75 key = hmacSHA256(key, region)
76 key = hmacSHA256(key, service)
77 key = hmacSHA256(key, "aws4_request")
78 sig := hex.EncodeToString(hmacSHA256(key, stringToSign))
79
80 req.Header.Set("Authorization",
81 "AWS4-HMAC-SHA256 Credential="+accessKey+"/"+scope+
82 ",SignedHeaders="+signedHeaders+",Signature="+sig)
83}
84
85// canonicalQuery renders url.Values in SigV4 canonical form: keys sorted,
86// values sorted within a key, strict RFC 3986 escaping (%20 for space, never
87// '+'), and flag parameters ("?versions") rendered with a trailing '='.
88func canonicalQuery(q url.Values) string {
89 keys := make([]string, 0, len(q))
90 for k := range q {
91 keys = append(keys, k)
92 }
93 sort.Strings(keys)
94 parts := make([]string, 0, len(q))
95 for _, k := range keys {
96 vs := append([]string(nil), q[k]...)
97 sort.Strings(vs)
98 for _, v := range vs {
99 parts = append(parts, awsURIEncode(k, true)+"="+awsURIEncode(v, true))
100 }
101 }
102 return strings.Join(parts, "&")
103}
104
105// awsURIEncode percent-encodes per the SigV4 rules: unreserved characters
106// (A-Za-z0-9, '-', '.', '_', '~') pass through; '/' passes through only in
107// paths (encodeSlash=false); everything else β€” including '+' and space β€”
108// becomes %XX with uppercase hex.
109func awsURIEncode(s string, encodeSlash bool) string {
110 var b strings.Builder
111 for i := 0; i < len(s); i++ {
112 c := s[i]
113 switch {
114 case c >= 'A' && c <= 'Z', c >= 'a' && c <= 'z', c >= '0' && c <= '9',
115 c == '-', c == '.', c == '_', c == '~':
116 b.WriteByte(c)
117 case c == '/' && !encodeSlash:
118 b.WriteByte(c)
119 default:
120 fmt.Fprintf(&b, "%%%02X", c)
121 }
122 }
123 return b.String()
124}
125
126func hexSHA256(b []byte) string {
127 h := sha256.Sum256(b)
128 return hex.EncodeToString(h[:])
129}
130
131func hmacSHA256(key []byte, msg string) []byte {
132 m := hmac.New(sha256.New, key)
133 m.Write([]byte(msg))
134 return m.Sum(nil)
135}
136