github.com/mailgun/holster/v4@v4.20.0/httpsign/signer.go (about) 1 /* 2 Package httpsign provides tools for signing and authenticating HTTP requests between 3 web services. See README.md for more details. 4 */ 5 package httpsign 6 7 import ( 8 "bytes" 9 "crypto/hmac" 10 "crypto/sha256" 11 "encoding/hex" 12 "errors" 13 "fmt" 14 "io" 15 "net/http" 16 "os" 17 "strconv" 18 19 "github.com/mailgun/holster/v4/clock" 20 ) 21 22 const ( 23 XMailgunSignature = "X-Mailgun-Signature" 24 XMailgunSignatureVersion = "X-Mailgun-Signature-Version" 25 XMailgunNonce = "X-Mailgun-Nonce" 26 XMailgunTimestamp = "X-Mailgun-Timestamp" 27 28 defaultCacheTimeout = 100 // 100 sec 29 defaultCacheCapacity = 5000 * defaultCacheTimeout // 5,000 msg/sec * 100 sec = 500,000 elements 30 31 maxSkewSec = 5 // 5 sec 32 ) 33 34 var ( 35 randomPrv randomProvider = &realRandom{} 36 ) 37 38 // Modify NonceCacheCapacity and NonceCacheTimeout if your service needs to 39 // authenticate more than 5,000 requests per second. For example, if you need 40 // to handle 10,000 requests per second and timeout after one minute, you may 41 // want to set NonceCacheTimeout to 60 and NonceCacheCapacity to 42 // 10000 * cacheTimeout = 600000. 43 type Config struct { 44 // KeyPath is a path to a file that contains the key to sign requests. If 45 // it is an empty string then the key should be provided in `KeyBytes`. 46 KeyPath string 47 48 // KeyBytes is a key that is used by lemma to sign requests. Ignored if 49 // `KeyPath` is not an empty string. 50 KeyBytes []byte 51 52 HeadersToSign []string // list of headers to sign 53 SignVerbAndURI bool // include the http verb and uri in request 54 55 NonceCacheCapacity int // capacity of the nonce cache 56 NonceCacheTimeout int // nonce cache timeout 57 58 EmitStats bool // toggle emitting metrics or not 59 StatsdHost string // hostname of statsd server 60 StatsdPort int // port of statsd server 61 StatsdPrefix string // prefix to prepend to metrics 62 63 NonceHeaderName string // default: X-Mailgun-Nonce 64 TimestampHeaderName string // default: X-Mailgun-Timestamp 65 SignatureHeaderName string // default: X-Mailgun-Signature 66 SignatureVersionHeaderName string // default: X-Mailgun-Signature-Version 67 } 68 69 // Represents an entity that can be used to sign and authenticate requests. 70 type Signer struct { 71 config *Config 72 nonceCache *nonceCache 73 secretKey []byte 74 } 75 76 // Return a new Signer. Config can not be nil. If you need control over 77 // setting time and random providers, use NewWithProviders. 78 func New(config *Config) (*Signer, error) { 79 // config is required! 80 if config == nil { 81 return nil, fmt.Errorf("config is required.") //nolint:stylecheck // TODO(v5): ST1005: error strings should not end with punctuation or newlines 82 } 83 84 // set defaults if not set 85 if config.NonceCacheCapacity < 1 { 86 config.NonceCacheCapacity = defaultCacheCapacity 87 } 88 if config.NonceCacheTimeout < 1 { 89 config.NonceCacheTimeout = defaultCacheTimeout 90 } 91 if config.NonceHeaderName == "" { 92 config.NonceHeaderName = XMailgunNonce 93 } 94 if config.TimestampHeaderName == "" { 95 config.TimestampHeaderName = XMailgunTimestamp 96 } 97 if config.SignatureHeaderName == "" { 98 config.SignatureHeaderName = XMailgunSignature 99 } 100 if config.SignatureVersionHeaderName == "" { 101 config.SignatureVersionHeaderName = XMailgunSignatureVersion 102 } 103 104 // Commented out this code because it does effectively nothing. 105 // // setup metrics service 106 // if config.EmitStats { 107 // // get hostname of box 108 // hostname, err := os.Hostname() 109 // if err != nil { 110 // return nil, fmt.Errorf("failed to obtain hostname: %v", err) 111 // } 112 // 113 // // build lemma prefix 114 // prefix := "lemma." + strings.ReplaceAll(hostname, ".", "_") 115 // if config.StatsdPrefix != "" { 116 // prefix += "." + config.StatsdPrefix 117 // } 118 // } 119 120 // Read in key from KeyPath or if not given, try getting them from KeyBytes. 121 var keyBytes []byte 122 var err error 123 if config.KeyPath != "" { 124 if keyBytes, err = readKeyFromDisk(config.KeyPath); err != nil { 125 return nil, err 126 } 127 } else { 128 if config.KeyBytes == nil { 129 return nil, errors.New("no key bytes provided") 130 } 131 keyBytes = config.KeyBytes 132 } 133 134 // setup nonce cache 135 ncache, err := newNonceCache(config.NonceCacheCapacity, config.NonceCacheTimeout) 136 if err != nil { 137 return nil, err 138 } 139 140 return &Signer{ 141 config: config, 142 nonceCache: ncache, 143 secretKey: keyBytes, 144 }, nil 145 } 146 147 // Signs a given HTTP request with signature, nonce, and timestamp. 148 func (s *Signer) SignRequest(r *http.Request) error { 149 if s.secretKey == nil { 150 return fmt.Errorf("service not loaded with key.") //nolint:stylecheck // TODO(v5): ST1005: error strings should not end with punctuation or newlines 151 } 152 return s.SignRequestWithKey(r, s.secretKey) 153 } 154 155 // Signs a given HTTP request with signature, nonce, and timestamp. Signs the 156 // message with the passed in key not the one initialized with. 157 func (s *Signer) SignRequestWithKey(r *http.Request, secretKey []byte) error { 158 // extract request body bytes 159 bodyBytes, err := readBody(r) 160 if err != nil { 161 return err 162 } 163 164 // extract any headers if requested 165 headerValues, err := extractHeaderValues(r, s.config.HeadersToSign) 166 if err != nil { 167 return err 168 } 169 170 // get 128-bit random number from /dev/urandom and base16 encode it 171 nonce, err := randomPrv.hexDigest(16) 172 if err != nil { 173 return fmt.Errorf("unable to get random : %v", err) 174 } 175 176 // get current timestamp 177 timestamp := strconv.FormatInt(clock.Now().Unix(), 10) 178 179 // compute the hmac and base16 encode it 180 computedMAC := computeMAC(secretKey, s.config.SignVerbAndURI, r.Method, r.URL.RequestURI(), 181 timestamp, nonce, bodyBytes, headerValues) 182 signature := hex.EncodeToString(computedMAC) 183 184 // set headers 185 r.Header.Set(s.config.NonceHeaderName, nonce) 186 r.Header.Set(s.config.TimestampHeaderName, timestamp) 187 r.Header.Set(s.config.SignatureHeaderName, signature) 188 r.Header.Set(s.config.SignatureVersionHeaderName, "2") 189 return nil 190 } 191 192 // VerifyRequest checks that an HTTP request was sent by an authorized sender. 193 func (s *Signer) VerifyRequest(r *http.Request) error { 194 if s.secretKey == nil { 195 return fmt.Errorf("service not loaded with key.") //nolint:stylecheck // TODO(v5): ST1005: error strings should not end with punctuation or newlines 196 } 197 return s.VerifyRequestWithKey(r, s.secretKey) 198 } 199 200 // VerifyRequestWithKey checks that an HTTP request was sent by an authorized 201 // sender. The check is performed against the passed in key. 202 func (s *Signer) VerifyRequestWithKey(r *http.Request, secretKey []byte) (err error) { 203 // extract parameters 204 signature := r.Header.Get(s.config.SignatureHeaderName) 205 if signature == "" { 206 return fmt.Errorf("header not found: %v", s.config.SignatureHeaderName) 207 } 208 nonce := r.Header.Get(s.config.NonceHeaderName) 209 if nonce == "" { 210 return fmt.Errorf("header not found: %v", s.config.NonceHeaderName) 211 } 212 timestamp := r.Header.Get(s.config.TimestampHeaderName) 213 if timestamp == "" { 214 return fmt.Errorf("header not found: %v", s.config.TimestampHeaderName) 215 } 216 217 // extract request body bytes 218 bodyBytes, err := readBody(r) 219 if err != nil { 220 return err 221 } 222 223 // extract any headers if requested 224 headerValues, err := extractHeaderValues(r, s.config.HeadersToSign) 225 if err != nil { 226 return err 227 } 228 229 // check the hmac 230 isValid, err := checkMAC(secretKey, s.config.SignVerbAndURI, r.Method, r.URL.RequestURI(), 231 timestamp, nonce, bodyBytes, headerValues, signature) 232 if !isValid { 233 return err 234 } 235 236 // check timestamp 237 isValid, err = s.checkTimestamp(timestamp) 238 if !isValid { 239 return err 240 } 241 242 // check to see if we have seen nonce before 243 inCache, err := s.nonceCache.inCache(nonce) 244 if err != nil { 245 return fmt.Errorf("while reading nonce cache for value %v: %w", nonce, err) 246 } 247 if inCache { 248 return fmt.Errorf("nonce already in cache: %v", nonce) 249 } 250 251 return nil 252 } 253 254 func (s *Signer) checkTimestamp(timestampHeader string) (bool, error) { 255 // convert unix timestamp string into time struct 256 timestamp, err := strconv.ParseInt(timestampHeader, 10, 0) 257 if err != nil { 258 return false, fmt.Errorf("unable to parse %v: %v", s.config.TimestampHeaderName, timestampHeader) 259 } 260 261 now := clock.Now().Unix() 262 263 // if timestamp is from the future, it's invalid 264 if timestamp >= now+maxSkewSec { 265 return false, fmt.Errorf("timestamp header from the future; now: %v; %v: %v; difference: %v", 266 now, s.config.TimestampHeaderName, timestamp, timestamp-now) 267 } 268 269 // if the timestamp is older than ttl - skew, it's invalid 270 if timestamp <= now-int64(s.nonceCache.cacheTTL-maxSkewSec) { 271 return false, fmt.Errorf("timestamp header too old; now: %v; %v: %v; difference: %v", 272 now, s.config.TimestampHeaderName, timestamp, now-timestamp) 273 } 274 275 return true, nil 276 } 277 278 func computeMAC(secretKey []byte, signVerbAndURI bool, httpVerb string, httpResourceURI string, 279 timestamp string, nonce string, body []byte, headerValues []string) []byte { 280 281 // use hmac-sha256 282 mac := hmac.New(sha256.New, secretKey) 283 284 // required parameters (timestamp, nonce, body) 285 fmt.Fprintf(mac, "%v|", len(timestamp)) 286 mac.Write([]byte(timestamp)) 287 fmt.Fprintf(mac, "|%v|", len(nonce)) 288 mac.Write([]byte(nonce)) 289 fmt.Fprintf(mac, "|%v|", len(body)) 290 mac.Write(body) 291 292 // optional parameters (httpVerb, httpResourceUri) 293 if signVerbAndURI { 294 fmt.Fprintf(mac, "|%v|", len(httpVerb)) 295 mac.Write([]byte(httpVerb)) 296 fmt.Fprintf(mac, "|%v|", len(httpResourceURI)) 297 mac.Write([]byte(httpResourceURI)) 298 } 299 300 // optional parameters (headers) 301 for _, headerValue := range headerValues { 302 fmt.Fprintf(mac, "|%v|", len(headerValue)) 303 mac.Write([]byte(headerValue)) 304 } 305 306 return mac.Sum(nil) 307 } 308 309 func checkMAC(secretKey []byte, signVerbAndURI bool, httpVerb string, httpResourceURI string, 310 timestamp string, nonce string, body []byte, headerValues []string, signature string) (bool, error) { 311 312 // the hmac we get is a hexdigest (string representation of hex values) 313 // which needs to be decoded before before we can use it 314 expectedMAC, err := hex.DecodeString(signature) 315 if err != nil { 316 return false, err 317 } 318 319 // compute the hmac 320 computedMAC := computeMAC(secretKey, signVerbAndURI, httpVerb, httpResourceURI, timestamp, nonce, body, headerValues) 321 322 // constant time compare 323 isEqual := hmac.Equal(expectedMAC, computedMAC) 324 if !isEqual { 325 return false, fmt.Errorf("signature header value %v does not match computed value", expectedMAC) 326 } 327 328 return true, nil 329 } 330 331 // readBody will read in the request body, return a byte slice, and also restore it 332 // within the *http.Request so it can be read later. Tries to be smart and initialize 333 // a buffer based off content-length. 334 // 335 // See for more details: 336 // https://github.com/golang/go/blob/release-branch.go1.5/src/io/ioutil/ioutil.go#L16-L43 337 func readBody(r *http.Request) (b []byte, err error) { 338 // if we have no body, like a GET request, set it to "" 339 if r.Body == nil { 340 return []byte(""), nil 341 } 342 343 // try and be smart and pre-allocate buffer 344 var n int64 = bytes.MinRead 345 if r.ContentLength > int64(n) { 346 n = r.ContentLength 347 } 348 buf := bytes.NewBuffer(make([]byte, 0, n)) 349 350 // If the buffer overflows, we will get bytes.ErrTooLarge. 351 // Return that as an error. Any other panic remains. 352 defer func() { 353 e := recover() 354 if e == nil { 355 return 356 } 357 if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge { 358 err = panicErr 359 } else { 360 panic(e) 361 } 362 }() 363 _, err = buf.ReadFrom(r.Body) 364 365 // restore the body back to the request 366 b = buf.Bytes() 367 r.Body = io.NopCloser(bytes.NewReader(b)) 368 369 return b, err 370 } 371 372 func extractHeaderValues(r *http.Request, headerNames []string) ([]string, error) { 373 if len(headerNames) < 1 { 374 return nil, nil 375 } 376 377 headerValues := make([]string, len(headerNames)) 378 for i, headerName := range headerNames { 379 _, ok := r.Header[headerName] 380 if !ok { 381 return nil, fmt.Errorf("header %s not found in request", headerName) 382 } 383 headerValues[i] = r.Header.Get(headerName) 384 } 385 386 return headerValues, nil 387 } 388 389 func readKeyFromDisk(keypath string) ([]byte, error) { 390 // load key from disk 391 keyBytes, err := os.ReadFile(keypath) 392 if err != nil { 393 return nil, err 394 } 395 396 // strip newline (\n or 0x0a) if it's at the end 397 keyBytes = bytes.TrimSuffix(keyBytes, []byte("\n")) 398 399 return keyBytes, nil 400 }