-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #84 from SenseUnit/hmac_auth
HMAC authorization
- Loading branch information
Showing
6 changed files
with
248 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package auth | ||
|
||
import ( | ||
"crypto/subtle" | ||
"net" | ||
"net/http" | ||
"strconv" | ||
"strings" | ||
) | ||
|
||
func matchHiddenDomain(host, hidden_domain string) bool { | ||
if h, _, err := net.SplitHostPort(host); err == nil { | ||
host = h | ||
} | ||
host = strings.ToLower(host) | ||
return subtle.ConstantTimeCompare([]byte(host), []byte(hidden_domain)) == 1 | ||
} | ||
|
||
func requireBasicAuth(wr http.ResponseWriter, req *http.Request, hidden_domain string) { | ||
if hidden_domain != "" && | ||
!matchHiddenDomain(req.URL.Host, hidden_domain) && | ||
!matchHiddenDomain(req.Host, hidden_domain) { | ||
http.Error(wr, BAD_REQ_MSG, http.StatusBadRequest) | ||
} else { | ||
wr.Header().Set("Proxy-Authenticate", `Basic realm="dumbproxy"`) | ||
wr.Header().Set("Content-Length", strconv.Itoa(len([]byte(AUTH_REQUIRED_MSG)))) | ||
wr.WriteHeader(407) | ||
wr.Write([]byte(AUTH_REQUIRED_MSG)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
package auth | ||
|
||
import ( | ||
"crypto/hmac" | ||
"crypto/sha256" | ||
"encoding/base64" | ||
"encoding/binary" | ||
"encoding/hex" | ||
"errors" | ||
"fmt" | ||
"net/http" | ||
"net/url" | ||
"os" | ||
"strconv" | ||
"strings" | ||
"time" | ||
|
||
clog "github.com/SenseUnit/dumbproxy/log" | ||
) | ||
|
||
const ( | ||
HMACSignaturePrefix = "dumbproxy grant token v1" | ||
HMACSignatureSize = 32 | ||
HMACTimestampSize = 8 | ||
EnvVarHMACSecret = "DUMBPROXY_HMAC_SECRET" | ||
) | ||
|
||
type HMACAuth struct { | ||
secret []byte | ||
hiddenDomain string | ||
logger *clog.CondLogger | ||
} | ||
|
||
func NewHMACAuth(param_url *url.URL, logger *clog.CondLogger) (*HMACAuth, error) { | ||
values, err := url.ParseQuery(param_url.RawQuery) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
hexSecret := os.Getenv(EnvVarHMACSecret) | ||
if hs := values.Get("secret"); hs != "" { | ||
hexSecret = hs | ||
} | ||
|
||
if hexSecret == "" { | ||
return nil, errors.New("no HMAC secret specified. Please specify \"secret\" parameter for auth provider or set " + EnvVarHMACSecret + " environment variable.") | ||
} | ||
|
||
secret, err := hex.DecodeString(hexSecret) | ||
if err != nil { | ||
return nil, fmt.Errorf("can't hex-decode HMAC secret: %w", err) | ||
} | ||
|
||
return &HMACAuth{ | ||
secret: secret, | ||
logger: logger, | ||
hiddenDomain: strings.ToLower(values.Get("hidden_domain")), | ||
}, nil | ||
} | ||
|
||
type HMACToken struct { | ||
Expire int64 | ||
Signature [HMACSignatureSize]byte | ||
} | ||
|
||
func (auth *HMACAuth) validateToken(login, password string) bool { | ||
marshaledToken, err := base64.RawURLEncoding.DecodeString(password) | ||
if err != nil { | ||
return false | ||
} | ||
|
||
var token HMACToken | ||
_, err = binary.Decode(marshaledToken, binary.BigEndian, &token) | ||
if err != nil { | ||
return false | ||
} | ||
|
||
if time.Unix(token.Expire, 0).Before(time.Now()) { | ||
return false | ||
} | ||
|
||
expectedMAC := CalculateHMACSignature(auth.secret, login, token.Expire) | ||
return hmac.Equal(token.Signature[:], expectedMAC) | ||
} | ||
|
||
func (auth *HMACAuth) Validate(wr http.ResponseWriter, req *http.Request) (string, bool) { | ||
hdr := req.Header.Get("Proxy-Authorization") | ||
if hdr == "" { | ||
requireBasicAuth(wr, req, auth.hiddenDomain) | ||
return "", false | ||
} | ||
hdr_parts := strings.SplitN(hdr, " ", 2) | ||
if len(hdr_parts) != 2 || strings.ToLower(hdr_parts[0]) != "basic" { | ||
requireBasicAuth(wr, req, auth.hiddenDomain) | ||
return "", false | ||
} | ||
|
||
token := hdr_parts[1] | ||
data, err := base64.StdEncoding.DecodeString(token) | ||
if err != nil { | ||
requireBasicAuth(wr, req, auth.hiddenDomain) | ||
return "", false | ||
} | ||
|
||
pair := strings.SplitN(string(data), ":", 2) | ||
if len(pair) != 2 { | ||
requireBasicAuth(wr, req, auth.hiddenDomain) | ||
return "", false | ||
} | ||
|
||
login := pair[0] | ||
password := pair[1] | ||
|
||
if auth.validateToken(login, password) { | ||
if auth.hiddenDomain != "" && | ||
(req.Host == auth.hiddenDomain || req.URL.Host == auth.hiddenDomain) { | ||
wr.Header().Set("Content-Length", strconv.Itoa(len([]byte(AUTH_TRIGGERED_MSG)))) | ||
wr.Header().Set("Pragma", "no-cache") | ||
wr.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") | ||
wr.Header().Set("Expires", EPOCH_EXPIRE) | ||
wr.Header()["Date"] = nil | ||
wr.WriteHeader(http.StatusOK) | ||
wr.Write([]byte(AUTH_TRIGGERED_MSG)) | ||
return "", false | ||
} else { | ||
return login, true | ||
} | ||
} | ||
requireBasicAuth(wr, req, auth.hiddenDomain) | ||
return "", false | ||
} | ||
|
||
func (auth *HMACAuth) Stop() { | ||
} | ||
|
||
func CalculateHMACSignature(secret []byte, username string, expire int64) []byte { | ||
mac := hmac.New(sha256.New, secret) | ||
mac.Write([]byte(HMACSignaturePrefix)) | ||
mac.Write([]byte(username)) | ||
binary.Write(mac, binary.BigEndian, expire) | ||
return mac.Sum(nil) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,13 @@ | ||
package main | ||
|
||
import ( | ||
"bytes" | ||
"crypto/rand" | ||
"crypto/tls" | ||
"crypto/x509" | ||
"encoding/base64" | ||
"encoding/binary" | ||
"encoding/hex" | ||
"errors" | ||
"flag" | ||
"fmt" | ||
|
@@ -139,6 +144,8 @@ type CLIArgs struct { | |
autocertHTTP string | ||
passwd string | ||
passwdCost int | ||
hmacSign bool | ||
hmacGenKey bool | ||
positionalArgs []string | ||
proxy []string | ||
sourceIPHints string | ||
|
@@ -173,6 +180,9 @@ func parse_args() CLIArgs { | |
flag.StringVar(&args.passwd, "passwd", "", "update given htpasswd file and add/set password for username. "+ | ||
"Username and password can be passed as positional arguments or requested interactively") | ||
flag.IntVar(&args.passwdCost, "passwd-cost", bcrypt.MinCost, "bcrypt password cost (for -passwd mode)") | ||
flag.BoolVar(&args.hmacSign, "hmac-sign", false, "sign username with specified key for given validity period. "+ | ||
"Positional arguments are: hex-encoded HMAC key, username, validity duration.") | ||
flag.BoolVar(&args.hmacGenKey, "hmac-genkey", false, "generate hex-encoded HMAC signing key of optimal length") | ||
flag.Func("proxy", "upstream proxy URL. Can be repeated multiple times to chain proxies. Examples: socks5h://127.0.0.1:9050; https://user:[email protected]:443", func(p string) error { | ||
args.proxy = append(args.proxy, p) | ||
return nil | ||
|
@@ -206,6 +216,20 @@ func run() int { | |
return 0 | ||
} | ||
|
||
if args.hmacSign { | ||
if err := hmacSign(args.positionalArgs...); err != nil { | ||
log.Fatalf("can't sign: %v", err) | ||
} | ||
return 0 | ||
} | ||
|
||
if args.hmacGenKey { | ||
if err := hmacGenKey(); err != nil { | ||
log.Fatalf("can't generate key: %v", err) | ||
} | ||
return 0 | ||
} | ||
|
||
logWriter := clog.NewLogWriter(os.Stderr) | ||
defer logWriter.Close() | ||
|
||
|
@@ -458,6 +482,53 @@ func passwd(filename string, cost int, args ...string) error { | |
return nil | ||
} | ||
|
||
func hmacSign(args ...string) error { | ||
if len(args) != 3 { | ||
fmt.Fprintln(os.Stderr, "Usage:") | ||
fmt.Fprintln(os.Stderr, "") | ||
fmt.Fprintln(os.Stderr, "dumbproxy -hmac-sign <HMAC key> <username> <validity duration>") | ||
fmt.Fprintln(os.Stderr, "") | ||
return errors.New("bad command line arguments") | ||
} | ||
|
||
secret, err := hex.DecodeString(args[0]) | ||
if err != nil { | ||
return fmt.Errorf("unable to hex-decode HMAC secret: %w", err) | ||
} | ||
|
||
validity, err := time.ParseDuration(args[2]) | ||
if err != nil { | ||
return fmt.Errorf("unable to parse validity duration: %w", err) | ||
} | ||
|
||
expire := time.Now().Add(validity).Unix() | ||
mac := auth.CalculateHMACSignature(secret, args[1], expire) | ||
token := auth.HMACToken{ | ||
Expire: expire, | ||
} | ||
copy(token.Signature[:], mac) | ||
|
||
var resBuf bytes.Buffer | ||
enc := base64.NewEncoder(base64.RawURLEncoding, &resBuf) | ||
if err := binary.Write(enc, binary.BigEndian, &token); err != nil { | ||
return fmt.Errorf("token encoding failed: %w", err) | ||
} | ||
enc.Close() | ||
|
||
fmt.Println("Username:", args[1]) | ||
fmt.Println("Password:", resBuf.String()) | ||
return nil | ||
} | ||
|
||
func hmacGenKey(args ...string) error { | ||
buf := make([]byte, auth.HMACSignatureSize) | ||
if _, err := rand.Read(buf); err != nil { | ||
return fmt.Errorf("CSPRNG failure: %w", err) | ||
} | ||
fmt.Println(hex.EncodeToString(buf)) | ||
return nil | ||
} | ||
|
||
func prompt(prompt string, secure bool) (string, error) { | ||
var input string | ||
fmt.Print(prompt) | ||
|