Fresh repository history for elektrine/magpie hosted at https://git.elektrine.com/elektrine/magpie.
312 lines
8.2 KiB
Go
312 lines
8.2 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"encoding/hex"
|
|
"net/http"
|
|
"net/url"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const awsAlgorithm = "AWS4-HMAC-SHA256"
|
|
|
|
func (s *Server) authorizedS3(r *http.Request) bool {
|
|
if len(s.cfg.S3Keys) == 0 {
|
|
return s.authorized(r)
|
|
}
|
|
|
|
if r.URL.Query().Get("X-Amz-Algorithm") != "" {
|
|
return s.authorizedS3Presigned(r)
|
|
}
|
|
|
|
return s.authorizedS3Header(r)
|
|
}
|
|
|
|
func (s *Server) authorizedS3Presigned(r *http.Request) bool {
|
|
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
|
return false
|
|
}
|
|
query := r.URL.Query()
|
|
if query.Get("X-Amz-Algorithm") != awsAlgorithm {
|
|
return false
|
|
}
|
|
if query.Get("X-Amz-Signature") == "" {
|
|
return false
|
|
}
|
|
|
|
credential := query.Get("X-Amz-Credential")
|
|
accessKey, scope, ok := splitCredential(credential)
|
|
key, ok := s.accessKey(accessKey)
|
|
if !ok || !key.can(permissionForMethod(r.Method)) {
|
|
return false
|
|
}
|
|
|
|
amzDate := query.Get("X-Amz-Date")
|
|
if !s.presignedTimeValid(amzDate, query.Get("X-Amz-Expires")) {
|
|
return false
|
|
}
|
|
if !s.scopeAllowed(scope, amzDate) {
|
|
return false
|
|
}
|
|
|
|
signedHeaders := query.Get("X-Amz-SignedHeaders")
|
|
canonicalRequest := canonicalRequest(r, signedHeaders, "UNSIGNED-PAYLOAD", true)
|
|
expected := signature(key.Secret, amzDate, scope, canonicalRequest)
|
|
return hmac.Equal([]byte(expected), []byte(query.Get("X-Amz-Signature")))
|
|
}
|
|
|
|
func (s *Server) authorizedS3Header(r *http.Request) bool {
|
|
return s.authorizedS3HeaderPermission(r, permissionForMethod(r.Method))
|
|
}
|
|
|
|
func (s *Server) authorizedS3HeaderPermission(r *http.Request, permission string) bool {
|
|
auth := r.Header.Get("Authorization")
|
|
if !strings.HasPrefix(auth, awsAlgorithm+" ") {
|
|
return false
|
|
}
|
|
parts := parseAuthParams(strings.TrimPrefix(auth, awsAlgorithm+" "))
|
|
credential := parts["Credential"]
|
|
signedHeaders := parts["SignedHeaders"]
|
|
providedSignature := parts["Signature"]
|
|
accessKey, scope, ok := splitCredential(credential)
|
|
key, keyOK := s.accessKey(accessKey)
|
|
if !ok || !keyOK || !key.can(permission) || signedHeaders == "" || providedSignature == "" {
|
|
return false
|
|
}
|
|
|
|
amzDate := r.Header.Get("X-Amz-Date")
|
|
if !headerTimeValid(amzDate) {
|
|
return false
|
|
}
|
|
if !s.scopeAllowed(scope, amzDate) {
|
|
return false
|
|
}
|
|
payloadHash := r.Header.Get("X-Amz-Content-Sha256")
|
|
if payloadHash == "" {
|
|
payloadHash = "UNSIGNED-PAYLOAD"
|
|
}
|
|
if permission == "write" && r.Header.Get("X-Magpie-Replication") != "1" && payloadHash == "UNSIGNED-PAYLOAD" && !s.cfg.AllowUnsignedLocal {
|
|
return false
|
|
}
|
|
|
|
canonicalRequest := canonicalRequest(r, signedHeaders, payloadHash, false)
|
|
expected := signature(key.Secret, amzDate, scope, canonicalRequest)
|
|
return hmac.Equal([]byte(expected), []byte(providedSignature))
|
|
}
|
|
|
|
func (s *Server) accessKey(id string) (AccessKey, bool) {
|
|
for _, key := range s.cfg.S3Keys {
|
|
if subtle.ConstantTimeCompare([]byte(key.ID), []byte(id)) == 1 {
|
|
return key, true
|
|
}
|
|
}
|
|
return AccessKey{}, false
|
|
}
|
|
|
|
func (s *Server) requestAccessKey(r *http.Request) (AccessKey, bool) {
|
|
credential := r.URL.Query().Get("X-Amz-Credential")
|
|
if credential == "" {
|
|
auth := r.Header.Get("Authorization")
|
|
if !strings.HasPrefix(auth, awsAlgorithm+" ") {
|
|
return AccessKey{}, false
|
|
}
|
|
credential = parseAuthParams(strings.TrimPrefix(auth, awsAlgorithm+" "))["Credential"]
|
|
}
|
|
accessKey, _, ok := splitCredential(credential)
|
|
if !ok {
|
|
return AccessKey{}, false
|
|
}
|
|
return s.accessKey(accessKey)
|
|
}
|
|
|
|
func (key AccessKey) can(permission string) bool {
|
|
return key.Permissions["admin"] || key.Permissions[permission]
|
|
}
|
|
|
|
func permissionForMethod(method string) string {
|
|
switch method {
|
|
case http.MethodGet, http.MethodHead:
|
|
return "read"
|
|
case http.MethodPut, http.MethodPost, http.MethodDelete:
|
|
return "write"
|
|
default:
|
|
return "admin"
|
|
}
|
|
}
|
|
|
|
func signature(secret, amzDate, scope, canonicalRequest string) string {
|
|
date := strings.Split(scope, "/")[0]
|
|
stringToSign := strings.Join([]string{
|
|
awsAlgorithm,
|
|
amzDate,
|
|
scope,
|
|
hexSHA256(canonicalRequest),
|
|
}, "\n")
|
|
key := signingKey(secret, date, scope)
|
|
sig := hmacSHA256(key, []byte(stringToSign))
|
|
return hex.EncodeToString(sig)
|
|
}
|
|
|
|
func signingKey(secret, date, scope string) []byte {
|
|
parts := strings.Split(scope, "/")
|
|
region := "auto"
|
|
service := "s3"
|
|
if len(parts) >= 3 {
|
|
region = parts[1]
|
|
service = parts[2]
|
|
}
|
|
kDate := hmacSHA256([]byte("AWS4"+secret), []byte(date))
|
|
kRegion := hmacSHA256(kDate, []byte(region))
|
|
kService := hmacSHA256(kRegion, []byte(service))
|
|
return hmacSHA256(kService, []byte("aws4_request"))
|
|
}
|
|
|
|
func (s *Server) scopeAllowed(scope string, amzDate string) bool {
|
|
parts := strings.Split(scope, "/")
|
|
if len(parts) != 4 {
|
|
return false
|
|
}
|
|
if len(amzDate) < 8 || parts[0] != amzDate[:8] {
|
|
return false
|
|
}
|
|
if parts[1] != s.cfg.S3Region || parts[2] != "s3" || parts[3] != "aws4_request" {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func canonicalRequest(r *http.Request, signedHeaders, payloadHash string, presigned bool) string {
|
|
return strings.Join([]string{
|
|
r.Method,
|
|
canonicalURI(r.URL.Path),
|
|
canonicalQuery(r.URL.Query(), presigned),
|
|
canonicalHeaders(r, signedHeaders),
|
|
signedHeaders,
|
|
payloadHash,
|
|
}, "\n")
|
|
}
|
|
|
|
func canonicalURI(path string) string {
|
|
if path == "" {
|
|
return "/"
|
|
}
|
|
segments := strings.Split(path, "/")
|
|
for i, segment := range segments {
|
|
segments[i] = awsEscape(segment)
|
|
}
|
|
return strings.Join(segments, "/")
|
|
}
|
|
|
|
func canonicalQuery(values url.Values, presigned bool) string {
|
|
type pair struct{ key, value string }
|
|
pairs := make([]pair, 0)
|
|
for key, vals := range values {
|
|
if presigned && key == "X-Amz-Signature" {
|
|
continue
|
|
}
|
|
for _, value := range vals {
|
|
pairs = append(pairs, pair{awsEscape(key), awsEscape(value)})
|
|
}
|
|
}
|
|
sort.Slice(pairs, func(i, j int) bool {
|
|
if pairs[i].key == pairs[j].key {
|
|
return pairs[i].value < pairs[j].value
|
|
}
|
|
return pairs[i].key < pairs[j].key
|
|
})
|
|
parts := make([]string, len(pairs))
|
|
for i, pair := range pairs {
|
|
parts[i] = pair.key + "=" + pair.value
|
|
}
|
|
return strings.Join(parts, "&")
|
|
}
|
|
|
|
func canonicalHeaders(r *http.Request, signedHeaders string) string {
|
|
headers := strings.Split(signedHeaders, ";")
|
|
var b strings.Builder
|
|
for _, header := range headers {
|
|
name := strings.ToLower(strings.TrimSpace(header))
|
|
value := ""
|
|
if name == "host" {
|
|
value = r.Host
|
|
} else {
|
|
value = strings.Join(r.Header.Values(name), ",")
|
|
}
|
|
b.WriteString(name)
|
|
b.WriteString(":")
|
|
b.WriteString(compressSpaces(value))
|
|
b.WriteString("\n")
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func splitCredential(credential string) (string, string, bool) {
|
|
parts := strings.SplitN(credential, "/", 2)
|
|
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
|
return "", "", false
|
|
}
|
|
scopeParts := strings.Split(parts[1], "/")
|
|
if len(scopeParts) != 4 || scopeParts[3] != "aws4_request" {
|
|
return "", "", false
|
|
}
|
|
return parts[0], parts[1], true
|
|
}
|
|
|
|
func parseAuthParams(value string) map[string]string {
|
|
params := map[string]string{}
|
|
for _, part := range strings.Split(value, ",") {
|
|
keyValue := strings.SplitN(strings.TrimSpace(part), "=", 2)
|
|
if len(keyValue) == 2 {
|
|
params[keyValue[0]] = keyValue[1]
|
|
}
|
|
}
|
|
return params
|
|
}
|
|
|
|
func (s *Server) presignedTimeValid(amzDate, expiresRaw string) bool {
|
|
created, err := time.Parse("20060102T150405Z", amzDate)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
expires, err := time.ParseDuration(expiresRaw + "s")
|
|
if err != nil || expires <= 0 || expires > s.cfg.PresignedMaxExpiry {
|
|
return false
|
|
}
|
|
now := time.Now().UTC()
|
|
return now.After(created.Add(-15*time.Minute)) && now.Before(created.Add(expires))
|
|
}
|
|
|
|
func headerTimeValid(amzDate string) bool {
|
|
created, err := time.Parse("20060102T150405Z", amzDate)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
now := time.Now().UTC()
|
|
return now.After(created.Add(-15*time.Minute)) && now.Before(created.Add(15*time.Minute))
|
|
}
|
|
|
|
func awsEscape(value string) string {
|
|
escaped := url.QueryEscape(value)
|
|
escaped = strings.ReplaceAll(escaped, "+", "%20")
|
|
escaped = strings.ReplaceAll(escaped, "%7E", "~")
|
|
return escaped
|
|
}
|
|
|
|
func compressSpaces(value string) string {
|
|
return strings.Join(strings.Fields(strings.TrimSpace(value)), " ")
|
|
}
|
|
|
|
func hexSHA256(value string) string {
|
|
sum := sha256.Sum256([]byte(value))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func hmacSHA256(key []byte, value []byte) []byte {
|
|
mac := hmac.New(sha256.New, key)
|
|
_, _ = mac.Write(value)
|
|
return mac.Sum(nil)
|
|
}
|