-
Notifications
You must be signed in to change notification settings - Fork 351
/
v2.go
256 lines (234 loc) · 7.55 KB
/
v2.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
package sig
import (
"context"
"crypto/hmac"
"crypto/sha1" //nolint:gosec
"encoding/base64"
"fmt"
"net/http"
"net/url"
"regexp"
"sort"
"strings"
"github.com/treeverse/lakefs/pkg/auth/model"
"github.com/treeverse/lakefs/pkg/gateway/errors"
"github.com/treeverse/lakefs/pkg/httputil"
"github.com/treeverse/lakefs/pkg/logging"
)
const (
v2authHeaderName = "Authorization"
)
var (
V2AuthHeaderRegexp = regexp.MustCompile(`AWS (?P<AccessKeyId>.{3,20}):(?P<Signature>[A-Za-z0-9+/=]+)`)
// Both "interesting" arrays are sorted. so once we extract relevant items by looping on them = the result is sorted
interestingHeaders = [...]string{"content-md5", "content-type", "date"}
interestingResources []string // initialized and sorted by the init function
)
//nolint:gochecknoinits
func init() {
interestingResourcesContainer := []string{
"accelerate", "acl", "cors", "defaultObjectAcl",
"location", "logging", "partNumber", "policy",
"requestPayment", "torrent",
"versioning", "versionId", "versions", "website",
"uploads", "uploadId", "response-content-type",
"response-content-language", "response-expires",
"response-cache-control", "response-content-disposition",
"response-content-encoding", "delete", "lifecycle",
"tagging", "restore", "storageClass", "notification",
"replication", "analytics", "metrics",
"inventory", "select", "select-type",
}
sort.Strings(interestingResourcesContainer)
// check for duplicates in the array - if it happens it is a programmer error that will happen only when that
// query parameter is used - may be very hard to find.
tempMap := map[string]bool{}
var sortedArray []string
for _, word := range interestingResourcesContainer {
if _, ok := tempMap[word]; ok {
logging.ContextUnavailable().
WithField("word", word).
Warn("appears twice in sig\v2.go array interestingResourcesContainer. a programmer error")
} else {
tempMap[word] = true
}
}
for key := range tempMap {
sortedArray = append(sortedArray, key)
}
sort.Strings(sortedArray)
interestingResources = sortedArray
}
type v2Context struct {
accessKeyID string
signature []byte
}
func (a v2Context) GetAccessKeyID() string {
return a.accessKeyID
}
type V2SigAuthenticator struct {
r *http.Request
sigCtx v2Context
}
func NewV2SigAuthenticator(r *http.Request) *V2SigAuthenticator {
return &V2SigAuthenticator{
r: r,
}
}
func (a *V2SigAuthenticator) Parse(ctx context.Context) (SigContext, error) {
var sigCtx v2Context
headerValue := a.r.Header.Get(v2authHeaderName)
if len(headerValue) > 0 {
match := V2AuthHeaderRegexp.FindStringSubmatch(headerValue)
if len(match) == 0 {
logging.FromContext(ctx).WithField("header", v2authHeaderName).Error("log header does not match v2 structure")
return sigCtx, ErrHeaderMalformed
}
result := make(map[string]string)
for i, name := range V2AuthHeaderRegexp.SubexpNames() {
if i != 0 && name != "" {
result[name] = match[i]
}
}
sigCtx.accessKeyID = result["AccessKeyId"]
// parse signature
sig, err := base64.StdEncoding.DecodeString(result["Signature"])
if err != nil {
logging.FromContext(ctx).WithField("header", v2authHeaderName).Error("log header does not match v2 structure (isn't proper base64)")
return sigCtx, ErrHeaderMalformed
}
sigCtx.signature = sig
}
a.sigCtx = sigCtx
return sigCtx, nil
}
func headerValueToString(val []string) string {
var returnStr string
for i, item := range val {
if i == 0 {
returnStr = strings.TrimSpace(item)
} else {
returnStr += "," + strings.TrimSpace(item)
}
}
return returnStr
}
func canonicalStandardHeaders(headers http.Header) string {
var returnStr string
for _, hoi := range interestingHeaders {
foundHoi := false
for key, val := range headers {
if len(val) > 0 && strings.ToLower(key) == hoi {
returnStr += headerValueToString(val) + "\n"
foundHoi = true
break
}
}
if !foundHoi {
returnStr += "\n"
}
}
return returnStr
}
func canonicalCustomHeaders(headers http.Header) string {
var returnStr string
var foundKeys []string
for key := range headers {
if strings.HasPrefix(strings.ToLower(key), "x-amz-") {
foundKeys = append(foundKeys, key)
}
}
if len(foundKeys) == 0 {
return returnStr
}
sort.Strings(foundKeys)
for _, key := range foundKeys {
returnStr += fmt.Sprint(strings.ToLower(key), ":", headerValueToString(headers[key]), "\n")
}
return returnStr
}
func canonicalResources(query url.Values, authPath string) string {
var foundResources []string
var foundResourcesStr string
lowercaseQuery := make(url.Values)
if len(query) > 0 {
for key, val := range query {
lowercaseQuery[strings.ToLower(key)] = val
}
for _, r := range interestingResources { // the resulting array will be sorted by resource name, because interesting resources array is sorted
val, ok := lowercaseQuery[r]
if ok {
newValue := r
if len(strings.Join(val, "")) > 0 {
newValue += "=" + strings.Join(val, ",")
}
foundResources = append(foundResources, newValue)
}
}
if len(foundResources) > 0 {
foundResourcesStr = "?" + strings.Join(foundResources, "&")
}
}
return authPath + foundResourcesStr
}
func canonicalString(method string, query url.Values, path string, headers http.Header) string {
cs := strings.ToUpper(method) + "\n"
cs += canonicalStandardHeaders(headers)
cs += canonicalCustomHeaders(headers)
cs += canonicalResources(query, path)
return cs
}
func signCanonicalString(msg string, signature []byte) (digest []byte) {
h := hmac.New(sha1.New, signature)
_, _ = h.Write([]byte(msg))
digest = h.Sum(nil)
return
}
func buildPath(host string, bareDomain string, path string) string {
h := httputil.HostOnly(host)
b := httputil.HostOnly(bareDomain)
if h == b {
return path
}
bareSuffix := "." + b
if strings.HasSuffix(h, bareSuffix) {
prePath := strings.TrimSuffix(h, bareSuffix)
return "/" + prePath + path
}
// bareDomain is not suffix of the path probably a bug
logging.ContextUnavailable().
WithFields(logging.Fields{"request_host": host, "bare_domain": bareDomain}).
Error("request host mismatch")
return ""
}
func (a *V2SigAuthenticator) Verify(creds *model.Credential, bareDomain string) error {
/*
s3 sigV2 implementation:
the s3 signature is somewhat different from general aws signature implementation.
in boto3 configuration their value is 's3' and 's3v4' respectively, while the general aws signatures are
'v2' and 'v4'.
in 2020, the GO aws sdk does not implement 's3' signature, So I will "translate" it from boto3.
source is class botocore.auth.HmacV1Auth
steps in building the string to be signed:
1. create initial string, with uppercase http method + '\n'
2. collect all required headers(in order):
- standard headers - 'content-md5', 'content-type', 'date' - if one of those does not appear, it is replaces with an
empty line '\n'. sorted and stringify
- custom headers - any header that starts with 'x-amz-'. if the header appears more than once - the values
are joined with ',' separator. sorted and stringify.
- path of the object
- QSA(Query String Arguments) - query arguments are searched for "interesting Resources".
*/
// Prefer the raw path if it exists -- *this* is what SigV2 signs
rawPath := a.r.URL.EscapedPath()
path := buildPath(a.r.Host, bareDomain, rawPath)
stringToSign := canonicalString(a.r.Method, a.r.URL.Query(), path, a.r.Header)
digest := signCanonicalString(stringToSign, []byte(creds.SecretAccessKey))
if !Equal(digest, a.sigCtx.signature) {
return errors.ErrSignatureDoesNotMatch
}
return nil
}
func (a *V2SigAuthenticator) String() string {
return "sigv2"
}