httpsig.go (14435B)
1 // Implements HTTP request and response signing and verification. Supports the 2 // major MAC and asymmetric key signature algorithms. It has several safety 3 // restrictions: One, none of the widely known non-cryptographically safe 4 // algorithms are permitted; Two, the RSA SHA256 algorithms must be available in 5 // the binary (and it should, barring export restrictions); Finally, the library 6 // assumes either the 'Authorizationn' or 'Signature' headers are to be set (but 7 // not both). 8 package httpsig 9 10 import ( 11 "crypto" 12 "fmt" 13 "net/http" 14 "strings" 15 "time" 16 17 "golang.org/x/crypto/ssh" 18 ) 19 20 // Algorithm specifies a cryptography secure algorithm for signing HTTP requests 21 // and responses. 22 type Algorithm string 23 24 const ( 25 // MAC-based algoirthms. 26 HMAC_SHA224 Algorithm = hmacPrefix + "-" + sha224String 27 HMAC_SHA256 Algorithm = hmacPrefix + "-" + sha256String 28 HMAC_SHA384 Algorithm = hmacPrefix + "-" + sha384String 29 HMAC_SHA512 Algorithm = hmacPrefix + "-" + sha512String 30 HMAC_RIPEMD160 Algorithm = hmacPrefix + "-" + ripemd160String 31 HMAC_SHA3_224 Algorithm = hmacPrefix + "-" + sha3_224String 32 HMAC_SHA3_256 Algorithm = hmacPrefix + "-" + sha3_256String 33 HMAC_SHA3_384 Algorithm = hmacPrefix + "-" + sha3_384String 34 HMAC_SHA3_512 Algorithm = hmacPrefix + "-" + sha3_512String 35 HMAC_SHA512_224 Algorithm = hmacPrefix + "-" + sha512_224String 36 HMAC_SHA512_256 Algorithm = hmacPrefix + "-" + sha512_256String 37 HMAC_BLAKE2S_256 Algorithm = hmacPrefix + "-" + blake2s_256String 38 HMAC_BLAKE2B_256 Algorithm = hmacPrefix + "-" + blake2b_256String 39 HMAC_BLAKE2B_384 Algorithm = hmacPrefix + "-" + blake2b_384String 40 HMAC_BLAKE2B_512 Algorithm = hmacPrefix + "-" + blake2b_512String 41 BLAKE2S_256 Algorithm = blake2s_256String 42 BLAKE2B_256 Algorithm = blake2b_256String 43 BLAKE2B_384 Algorithm = blake2b_384String 44 BLAKE2B_512 Algorithm = blake2b_512String 45 // RSA-based algorithms. 46 RSA_SHA1 Algorithm = rsaPrefix + "-" + sha1String 47 RSA_SHA224 Algorithm = rsaPrefix + "-" + sha224String 48 // RSA_SHA256 is the default algorithm. 49 RSA_SHA256 Algorithm = rsaPrefix + "-" + sha256String 50 RSA_SHA384 Algorithm = rsaPrefix + "-" + sha384String 51 RSA_SHA512 Algorithm = rsaPrefix + "-" + sha512String 52 RSA_RIPEMD160 Algorithm = rsaPrefix + "-" + ripemd160String 53 // ECDSA algorithms 54 ECDSA_SHA224 Algorithm = ecdsaPrefix + "-" + sha224String 55 ECDSA_SHA256 Algorithm = ecdsaPrefix + "-" + sha256String 56 ECDSA_SHA384 Algorithm = ecdsaPrefix + "-" + sha384String 57 ECDSA_SHA512 Algorithm = ecdsaPrefix + "-" + sha512String 58 ECDSA_RIPEMD160 Algorithm = ecdsaPrefix + "-" + ripemd160String 59 // ED25519 algorithms 60 // can only be SHA512 61 ED25519 Algorithm = ed25519Prefix 62 63 // Just because you can glue things together, doesn't mean they will 64 // work. The following options are not supported. 65 rsa_SHA3_224 Algorithm = rsaPrefix + "-" + sha3_224String 66 rsa_SHA3_256 Algorithm = rsaPrefix + "-" + sha3_256String 67 rsa_SHA3_384 Algorithm = rsaPrefix + "-" + sha3_384String 68 rsa_SHA3_512 Algorithm = rsaPrefix + "-" + sha3_512String 69 rsa_SHA512_224 Algorithm = rsaPrefix + "-" + sha512_224String 70 rsa_SHA512_256 Algorithm = rsaPrefix + "-" + sha512_256String 71 rsa_BLAKE2S_256 Algorithm = rsaPrefix + "-" + blake2s_256String 72 rsa_BLAKE2B_256 Algorithm = rsaPrefix + "-" + blake2b_256String 73 rsa_BLAKE2B_384 Algorithm = rsaPrefix + "-" + blake2b_384String 74 rsa_BLAKE2B_512 Algorithm = rsaPrefix + "-" + blake2b_512String 75 ) 76 77 // HTTP Signatures can be applied to different HTTP headers, depending on the 78 // expected application behavior. 79 type SignatureScheme string 80 81 const ( 82 // Signature will place the HTTP Signature into the 'Signature' HTTP 83 // header. 84 Signature SignatureScheme = "Signature" 85 // Authorization will place the HTTP Signature into the 'Authorization' 86 // HTTP header. 87 Authorization SignatureScheme = "Authorization" 88 ) 89 90 const ( 91 // The HTTP Signatures specification uses the "Signature" auth-scheme 92 // for the Authorization header. This is coincidentally named, but not 93 // semantically the same, as the "Signature" HTTP header value. 94 signatureAuthScheme = "Signature" 95 ) 96 97 // There are subtle differences to the values in the header. The Authorization 98 // header has an 'auth-scheme' value that must be prefixed to the rest of the 99 // key and values. 100 func (s SignatureScheme) authScheme() string { 101 switch s { 102 case Authorization: 103 return signatureAuthScheme 104 default: 105 return "" 106 } 107 } 108 109 // Signers will sign HTTP requests or responses based on the algorithms and 110 // headers selected at creation time. 111 // 112 // Signers are not safe to use between multiple goroutines. 113 // 114 // Note that signatures do set the deprecated 'algorithm' parameter for 115 // backwards compatibility. 116 type Signer interface { 117 // SignRequest signs the request using a private key. The public key id 118 // is used by the HTTP server to identify which key to use to verify the 119 // signature. 120 // 121 // If the Signer was created using a MAC based algorithm, then the key 122 // is expected to be of type []byte. If the Signer was created using an 123 // RSA based algorithm, then the private key is expected to be of type 124 // *rsa.PrivateKey. 125 // 126 // A Digest (RFC 3230) will be added to the request. The body provided 127 // must match the body used in the request, and is allowed to be nil. 128 // The Digest ensures the request body is not tampered with in flight, 129 // and if the signer is created to also sign the "Digest" header, the 130 // HTTP Signature will then ensure both the Digest and body are not both 131 // modified to maliciously represent different content. 132 SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte) error 133 // SignResponse signs the response using a private key. The public key 134 // id is used by the HTTP client to identify which key to use to verify 135 // the signature. 136 // 137 // If the Signer was created using a MAC based algorithm, then the key 138 // is expected to be of type []byte. If the Signer was created using an 139 // RSA based algorithm, then the private key is expected to be of type 140 // *rsa.PrivateKey. 141 // 142 // A Digest (RFC 3230) will be added to the response. The body provided 143 // must match the body written in the response, and is allowed to be 144 // nil. The Digest ensures the response body is not tampered with in 145 // flight, and if the signer is created to also sign the "Digest" 146 // header, the HTTP Signature will then ensure both the Digest and body 147 // are not both modified to maliciously represent different content. 148 SignResponse(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte) error 149 } 150 151 // NewSigner creates a new Signer with the provided algorithm preferences to 152 // make HTTP signatures. Only the first available algorithm will be used, which 153 // is returned by this function along with the Signer. If none of the preferred 154 // algorithms were available, then the default algorithm is used. The headers 155 // specified will be included into the HTTP signatures. 156 // 157 // The Digest will also be calculated on a request's body using the provided 158 // digest algorithm, if "Digest" is one of the headers listed. 159 // 160 // The provided scheme determines which header is populated with the HTTP 161 // Signature. 162 // 163 // An error is returned if an unknown or a known cryptographically insecure 164 // Algorithm is provided. 165 func NewSigner(prefs []Algorithm, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme, expiresIn int64) (Signer, Algorithm, error) { 166 for _, pref := range prefs { 167 s, err := newSigner(pref, dAlgo, headers, scheme, expiresIn) 168 if err != nil { 169 continue 170 } 171 return s, pref, err 172 } 173 s, err := newSigner(defaultAlgorithm, dAlgo, headers, scheme, expiresIn) 174 return s, defaultAlgorithm, err 175 } 176 177 // Signers will sign HTTP requests or responses based on the algorithms and 178 // headers selected at creation time. 179 // 180 // Signers are not safe to use between multiple goroutines. 181 // 182 // Note that signatures do set the deprecated 'algorithm' parameter for 183 // backwards compatibility. 184 type SSHSigner interface { 185 // SignRequest signs the request using ssh.Signer. 186 // The public key id is used by the HTTP server to identify which key to use 187 // to verify the signature. 188 // 189 // A Digest (RFC 3230) will be added to the request. The body provided 190 // must match the body used in the request, and is allowed to be nil. 191 // The Digest ensures the request body is not tampered with in flight, 192 // and if the signer is created to also sign the "Digest" header, the 193 // HTTP Signature will then ensure both the Digest and body are not both 194 // modified to maliciously represent different content. 195 SignRequest(pubKeyId string, r *http.Request, body []byte) error 196 // SignResponse signs the response using ssh.Signer. The public key 197 // id is used by the HTTP client to identify which key to use to verify 198 // the signature. 199 // 200 // A Digest (RFC 3230) will be added to the response. The body provided 201 // must match the body written in the response, and is allowed to be 202 // nil. The Digest ensures the response body is not tampered with in 203 // flight, and if the signer is created to also sign the "Digest" 204 // header, the HTTP Signature will then ensure both the Digest and body 205 // are not both modified to maliciously represent different content. 206 SignResponse(pubKeyId string, r http.ResponseWriter, body []byte) error 207 } 208 209 // NewwSSHSigner creates a new Signer using the specified ssh.Signer 210 // At the moment only ed25519 ssh keys are supported. 211 // The headers specified will be included into the HTTP signatures. 212 // 213 // The Digest will also be calculated on a request's body using the provided 214 // digest algorithm, if "Digest" is one of the headers listed. 215 // 216 // The provided scheme determines which header is populated with the HTTP 217 // Signature. 218 func NewSSHSigner(s ssh.Signer, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme, expiresIn int64) (SSHSigner, Algorithm, error) { 219 sshAlgo := getSSHAlgorithm(s.PublicKey().Type()) 220 if sshAlgo == "" { 221 return nil, "", fmt.Errorf("key type: %s not supported yet.", s.PublicKey().Type()) 222 } 223 224 signer, err := newSSHSigner(s, sshAlgo, dAlgo, headers, scheme, expiresIn) 225 if err != nil { 226 return nil, "", err 227 } 228 229 return signer, sshAlgo, nil 230 } 231 232 func getSSHAlgorithm(pkType string) Algorithm { 233 switch { 234 case strings.HasPrefix(pkType, sshPrefix+"-"+ed25519Prefix): 235 return ED25519 236 case strings.HasPrefix(pkType, sshPrefix+"-"+rsaPrefix): 237 return RSA_SHA1 238 } 239 240 return "" 241 } 242 243 // Verifier verifies HTTP Signatures. 244 // 245 // It will determine which of the supported headers has the parameters 246 // that define the signature. 247 // 248 // Verifiers are not safe to use between multiple goroutines. 249 // 250 // Note that verification ignores the deprecated 'algorithm' parameter. 251 type Verifier interface { 252 // KeyId gets the public key id that the signature is signed with. 253 // 254 // Note that the application is expected to determine the algorithm 255 // used based on metadata or out-of-band information for this key id. 256 KeyId() string 257 // Verify accepts the public key specified by KeyId and returns an 258 // error if verification fails or if the signature is malformed. The 259 // algorithm must be the one used to create the signature in order to 260 // pass verification. The algorithm is determined based on metadata or 261 // out-of-band information for the key id. 262 // 263 // If the signature was created using a MAC based algorithm, then the 264 // key is expected to be of type []byte. If the signature was created 265 // using an RSA based algorithm, then the public key is expected to be 266 // of type *rsa.PublicKey. 267 Verify(pKey crypto.PublicKey, algo Algorithm) error 268 } 269 270 const ( 271 // host is treated specially because golang may not include it in the 272 // request header map on the server side of a request. 273 hostHeader = "Host" 274 ) 275 276 // NewVerifier verifies the given request. It returns an error if the HTTP 277 // Signature parameters are not present in any headers, are present in more than 278 // one header, are malformed, or are missing required parameters. It ignores 279 // unknown HTTP Signature parameters. 280 func NewVerifier(r *http.Request) (Verifier, error) { 281 h := r.Header 282 if _, hasHostHeader := h[hostHeader]; len(r.Host) > 0 && !hasHostHeader { 283 h[hostHeader] = []string{r.Host} 284 } 285 return newVerifier(h, func(h http.Header, toInclude []string, created int64, expires int64) (string, error) { 286 return signatureString(h, toInclude, addRequestTarget(r), created, expires) 287 }) 288 } 289 290 // NewResponseVerifier verifies the given response. It returns errors under the 291 // same conditions as NewVerifier. 292 func NewResponseVerifier(r *http.Response) (Verifier, error) { 293 return newVerifier(r.Header, func(h http.Header, toInclude []string, created int64, expires int64) (string, error) { 294 return signatureString(h, toInclude, requestTargetNotPermitted, created, expires) 295 }) 296 } 297 298 func newSSHSigner(sshSigner ssh.Signer, algo Algorithm, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme, expiresIn int64) (SSHSigner, error) { 299 var expires, created int64 = 0, 0 300 301 if expiresIn != 0 { 302 created = time.Now().Unix() 303 expires = created + expiresIn 304 } 305 306 s, err := signerFromSSHSigner(sshSigner, string(algo)) 307 if err != nil { 308 return nil, fmt.Errorf("no crypto implementation available for ssh algo %q", algo) 309 } 310 311 a := &asymmSSHSigner{ 312 asymmSigner: &asymmSigner{ 313 s: s, 314 dAlgo: dAlgo, 315 headers: headers, 316 targetHeader: scheme, 317 prefix: scheme.authScheme(), 318 created: created, 319 expires: expires, 320 }, 321 } 322 323 return a, nil 324 } 325 326 func newSigner(algo Algorithm, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme, expiresIn int64) (Signer, error) { 327 328 var expires, created int64 = 0, 0 329 if expiresIn != 0 { 330 created = time.Now().Unix() 331 expires = created + expiresIn 332 } 333 334 s, err := signerFromString(string(algo)) 335 if err == nil { 336 a := &asymmSigner{ 337 s: s, 338 dAlgo: dAlgo, 339 headers: headers, 340 targetHeader: scheme, 341 prefix: scheme.authScheme(), 342 created: created, 343 expires: expires, 344 } 345 return a, nil 346 } 347 m, err := macerFromString(string(algo)) 348 if err != nil { 349 return nil, fmt.Errorf("no crypto implementation available for %q", algo) 350 } 351 c := &macSigner{ 352 m: m, 353 dAlgo: dAlgo, 354 headers: headers, 355 targetHeader: scheme, 356 prefix: scheme.authScheme(), 357 created: created, 358 expires: expires, 359 } 360 return c, nil 361 }