gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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 }