mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 01:02:25 -05:00 
			
		
		
		
	
		
			
	
	
		
			414 lines
		
	
	
	
		
			17 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
		
		
			
		
	
	
			414 lines
		
	
	
	
		
			17 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
|  | // Implements HTTP request and response signing and verification. Supports the | ||
|  | // major MAC and asymmetric key signature algorithms. It has several safety | ||
|  | // restrictions: One, none of the widely known non-cryptographically safe | ||
|  | // algorithms are permitted; Two, the RSA SHA256 algorithms must be available in | ||
|  | // the binary (and it should, barring export restrictions); Finally, the library | ||
|  | // assumes either the 'Authorizationn' or 'Signature' headers are to be set (but | ||
|  | // not both). | ||
|  | package httpsig | ||
|  | 
 | ||
|  | import ( | ||
|  | 	"crypto" | ||
|  | 	"fmt" | ||
|  | 	"net/http" | ||
|  | 	"strings" | ||
|  | 	"time" | ||
|  | 
 | ||
|  | 	"golang.org/x/crypto/ssh" | ||
|  | ) | ||
|  | 
 | ||
|  | // Algorithm specifies a cryptography secure algorithm for signing HTTP requests | ||
|  | // and responses. | ||
|  | type Algorithm string | ||
|  | 
 | ||
|  | const ( | ||
|  | 	// MAC-based algoirthms. | ||
|  | 	HMAC_SHA224      Algorithm = hmacPrefix + "-" + sha224String | ||
|  | 	HMAC_SHA256      Algorithm = hmacPrefix + "-" + sha256String | ||
|  | 	HMAC_SHA384      Algorithm = hmacPrefix + "-" + sha384String | ||
|  | 	HMAC_SHA512      Algorithm = hmacPrefix + "-" + sha512String | ||
|  | 	HMAC_RIPEMD160   Algorithm = hmacPrefix + "-" + ripemd160String | ||
|  | 	HMAC_SHA3_224    Algorithm = hmacPrefix + "-" + sha3_224String | ||
|  | 	HMAC_SHA3_256    Algorithm = hmacPrefix + "-" + sha3_256String | ||
|  | 	HMAC_SHA3_384    Algorithm = hmacPrefix + "-" + sha3_384String | ||
|  | 	HMAC_SHA3_512    Algorithm = hmacPrefix + "-" + sha3_512String | ||
|  | 	HMAC_SHA512_224  Algorithm = hmacPrefix + "-" + sha512_224String | ||
|  | 	HMAC_SHA512_256  Algorithm = hmacPrefix + "-" + sha512_256String | ||
|  | 	HMAC_BLAKE2S_256 Algorithm = hmacPrefix + "-" + blake2s_256String | ||
|  | 	HMAC_BLAKE2B_256 Algorithm = hmacPrefix + "-" + blake2b_256String | ||
|  | 	HMAC_BLAKE2B_384 Algorithm = hmacPrefix + "-" + blake2b_384String | ||
|  | 	HMAC_BLAKE2B_512 Algorithm = hmacPrefix + "-" + blake2b_512String | ||
|  | 	BLAKE2S_256      Algorithm = blake2s_256String | ||
|  | 	BLAKE2B_256      Algorithm = blake2b_256String | ||
|  | 	BLAKE2B_384      Algorithm = blake2b_384String | ||
|  | 	BLAKE2B_512      Algorithm = blake2b_512String | ||
|  | 	// RSA-based algorithms. | ||
|  | 	RSA_SHA1   Algorithm = rsaPrefix + "-" + sha1String | ||
|  | 	RSA_SHA224 Algorithm = rsaPrefix + "-" + sha224String | ||
|  | 	// RSA_SHA256 is the default algorithm. | ||
|  | 	RSA_SHA256    Algorithm = rsaPrefix + "-" + sha256String | ||
|  | 	RSA_SHA384    Algorithm = rsaPrefix + "-" + sha384String | ||
|  | 	RSA_SHA512    Algorithm = rsaPrefix + "-" + sha512String | ||
|  | 	RSA_RIPEMD160 Algorithm = rsaPrefix + "-" + ripemd160String | ||
|  | 	// ECDSA algorithms | ||
|  | 	ECDSA_SHA224    Algorithm = ecdsaPrefix + "-" + sha224String | ||
|  | 	ECDSA_SHA256    Algorithm = ecdsaPrefix + "-" + sha256String | ||
|  | 	ECDSA_SHA384    Algorithm = ecdsaPrefix + "-" + sha384String | ||
|  | 	ECDSA_SHA512    Algorithm = ecdsaPrefix + "-" + sha512String | ||
|  | 	ECDSA_RIPEMD160 Algorithm = ecdsaPrefix + "-" + ripemd160String | ||
|  | 	// ED25519 algorithms | ||
|  | 	// can only be SHA512 | ||
|  | 	ED25519 Algorithm = ed25519Prefix | ||
|  | 
 | ||
|  | 	// Just because you can glue things together, doesn't mean they will | ||
|  | 	// work. The following options are not supported. | ||
|  | 	rsa_SHA3_224    Algorithm = rsaPrefix + "-" + sha3_224String | ||
|  | 	rsa_SHA3_256    Algorithm = rsaPrefix + "-" + sha3_256String | ||
|  | 	rsa_SHA3_384    Algorithm = rsaPrefix + "-" + sha3_384String | ||
|  | 	rsa_SHA3_512    Algorithm = rsaPrefix + "-" + sha3_512String | ||
|  | 	rsa_SHA512_224  Algorithm = rsaPrefix + "-" + sha512_224String | ||
|  | 	rsa_SHA512_256  Algorithm = rsaPrefix + "-" + sha512_256String | ||
|  | 	rsa_BLAKE2S_256 Algorithm = rsaPrefix + "-" + blake2s_256String | ||
|  | 	rsa_BLAKE2B_256 Algorithm = rsaPrefix + "-" + blake2b_256String | ||
|  | 	rsa_BLAKE2B_384 Algorithm = rsaPrefix + "-" + blake2b_384String | ||
|  | 	rsa_BLAKE2B_512 Algorithm = rsaPrefix + "-" + blake2b_512String | ||
|  | ) | ||
|  | 
 | ||
|  | // HTTP Signatures can be applied to different HTTP headers, depending on the | ||
|  | // expected application behavior. | ||
|  | type SignatureScheme string | ||
|  | 
 | ||
|  | const ( | ||
|  | 	// Signature will place the HTTP Signature into the 'Signature' HTTP | ||
|  | 	// header. | ||
|  | 	Signature SignatureScheme = "Signature" | ||
|  | 	// Authorization will place the HTTP Signature into the 'Authorization' | ||
|  | 	// HTTP header. | ||
|  | 	Authorization SignatureScheme = "Authorization" | ||
|  | ) | ||
|  | 
 | ||
|  | const ( | ||
|  | 	// The HTTP Signatures specification uses the "Signature" auth-scheme | ||
|  | 	// for the Authorization header. This is coincidentally named, but not | ||
|  | 	// semantically the same, as the "Signature" HTTP header value. | ||
|  | 	signatureAuthScheme = "Signature" | ||
|  | ) | ||
|  | 
 | ||
|  | // There are subtle differences to the values in the header. The Authorization | ||
|  | // header has an 'auth-scheme' value that must be prefixed to the rest of the | ||
|  | // key and values. | ||
|  | func (s SignatureScheme) authScheme() string { | ||
|  | 	switch s { | ||
|  | 	case Authorization: | ||
|  | 		return signatureAuthScheme | ||
|  | 	default: | ||
|  | 		return "" | ||
|  | 	} | ||
|  | } | ||
|  | 
 | ||
|  | type SignatureOption struct { | ||
|  | 	// ExcludeQueryStringFromPathPseudoHeader omits the query parameters from the | ||
|  | 	// `:path` pseudo-header in the HTTP signature. | ||
|  | 	// | ||
|  | 	// The query string is optional in the `:path` pseudo-header. | ||
|  | 	// https://www.rfc-editor.org/rfc/rfc9113#section-8.3.1-2.4.1 | ||
|  | 	ExcludeQueryStringFromPathPseudoHeader bool | ||
|  | } | ||
|  | 
 | ||
|  | // Signers will sign HTTP requests or responses based on the algorithms and | ||
|  | // headers selected at creation time. | ||
|  | // | ||
|  | // Signers are not safe to use between multiple goroutines. | ||
|  | // | ||
|  | // Note that signatures do set the deprecated 'algorithm' parameter for | ||
|  | // backwards compatibility. | ||
|  | type Signer interface { | ||
|  | 	// SignRequest signs the request using a private key. The public key id | ||
|  | 	// is used by the HTTP server to identify which key to use to verify the | ||
|  | 	// signature. | ||
|  | 	// | ||
|  | 	// If the Signer was created using a MAC based algorithm, then the key | ||
|  | 	// is expected to be of type []byte. If the Signer was created using an | ||
|  | 	// RSA based algorithm, then the private key is expected to be of type | ||
|  | 	// *rsa.PrivateKey. | ||
|  | 	// | ||
|  | 	// A Digest (RFC 3230) will be added to the request. The body provided | ||
|  | 	// must match the body used in the request, and is allowed to be nil. | ||
|  | 	// The Digest ensures the request body is not tampered with in flight, | ||
|  | 	// and if the signer is created to also sign the "Digest" header, the | ||
|  | 	// HTTP Signature will then ensure both the Digest and body are not both | ||
|  | 	// modified to maliciously represent different content. | ||
|  | 	SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte) error | ||
|  | 	// SignResponse signs the response using a private key. The public key | ||
|  | 	// id is used by the HTTP client to identify which key to use to verify | ||
|  | 	// the signature. | ||
|  | 	// | ||
|  | 	// If the Signer was created using a MAC based algorithm, then the key | ||
|  | 	// is expected to be of type []byte. If the Signer was created using an | ||
|  | 	// RSA based algorithm, then the private key is expected to be of type | ||
|  | 	// *rsa.PrivateKey. | ||
|  | 	// | ||
|  | 	// A Digest (RFC 3230) will be added to the response. The body provided | ||
|  | 	// must match the body written in the response, and is allowed to be | ||
|  | 	// nil. The Digest ensures the response body is not tampered with in | ||
|  | 	// flight, and if the signer is created to also sign the "Digest" | ||
|  | 	// header, the HTTP Signature will then ensure both the Digest and body | ||
|  | 	// are not both modified to maliciously represent different content. | ||
|  | 	SignResponse(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte) error | ||
|  | } | ||
|  | 
 | ||
|  | type SignerWithOptions interface { | ||
|  | 	Signer | ||
|  | 
 | ||
|  | 	// SignRequestWithOptions signs the request using a private key. The public key id | ||
|  | 	// is used by the HTTP server to identify which key to use to verify the | ||
|  | 	// signature. | ||
|  | 	// | ||
|  | 	// If the Signer was created using a MAC based algorithm, then the key | ||
|  | 	// is expected to be of type []byte. If the Signer was created using an | ||
|  | 	// RSA based algorithm, then the private key is expected to be of type | ||
|  | 	// *rsa.PrivateKey. | ||
|  | 	// | ||
|  | 	// A Digest (RFC 3230) will be added to the request. The body provided | ||
|  | 	// must match the body used in the request, and is allowed to be nil. | ||
|  | 	// The Digest ensures the request body is not tampered with in flight, | ||
|  | 	// and if the signer is created to also sign the "Digest" header, the | ||
|  | 	// HTTP Signature will then ensure both the Digest and body are not both | ||
|  | 	// modified to maliciously represent different content. | ||
|  | 	SignRequestWithOptions(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte, opts SignatureOption) error | ||
|  | 	// SignResponseWithOptions signs the response using a private key. The public key | ||
|  | 	// id is used by the HTTP client to identify which key to use to verify | ||
|  | 	// the signature. | ||
|  | 	// | ||
|  | 	// If the Signer was created using a MAC based algorithm, then the key | ||
|  | 	// is expected to be of type []byte. If the Signer was created using an | ||
|  | 	// RSA based algorithm, then the private key is expected to be of type | ||
|  | 	// *rsa.PrivateKey. | ||
|  | 	// | ||
|  | 	// A Digest (RFC 3230) will be added to the response. The body provided | ||
|  | 	// must match the body written in the response, and is allowed to be | ||
|  | 	// nil. The Digest ensures the response body is not tampered with in | ||
|  | 	// flight, and if the signer is created to also sign the "Digest" | ||
|  | 	// header, the HTTP Signature will then ensure both the Digest and body | ||
|  | 	// are not both modified to maliciously represent different content. | ||
|  | 	SignResponseWithOptions(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte, opts SignatureOption) error | ||
|  | } | ||
|  | 
 | ||
|  | // NewSigner creates a new Signer with the provided algorithm preferences to | ||
|  | // make HTTP signatures. Only the first available algorithm will be used, which | ||
|  | // is returned by this function along with the Signer. If none of the preferred | ||
|  | // algorithms were available, then the default algorithm is used. The headers | ||
|  | // specified will be included into the HTTP signatures. | ||
|  | // | ||
|  | // The Digest will also be calculated on a request's body using the provided | ||
|  | // digest algorithm, if "Digest" is one of the headers listed. | ||
|  | // | ||
|  | // The provided scheme determines which header is populated with the HTTP | ||
|  | // Signature. | ||
|  | // | ||
|  | // An error is returned if an unknown or a known cryptographically insecure | ||
|  | // Algorithm is provided. | ||
|  | func NewSigner(prefs []Algorithm, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme, expiresIn int64) (SignerWithOptions, Algorithm, error) { | ||
|  | 	for _, pref := range prefs { | ||
|  | 		s, err := newSigner(pref, dAlgo, headers, scheme, expiresIn) | ||
|  | 		if err != nil { | ||
|  | 			continue | ||
|  | 		} | ||
|  | 		return s, pref, err | ||
|  | 	} | ||
|  | 	s, err := newSigner(defaultAlgorithm, dAlgo, headers, scheme, expiresIn) | ||
|  | 	return s, defaultAlgorithm, err | ||
|  | } | ||
|  | 
 | ||
|  | // Signers will sign HTTP requests or responses based on the algorithms and | ||
|  | // headers selected at creation time. | ||
|  | // | ||
|  | // Signers are not safe to use between multiple goroutines. | ||
|  | // | ||
|  | // Note that signatures do set the deprecated 'algorithm' parameter for | ||
|  | // backwards compatibility. | ||
|  | type SSHSigner interface { | ||
|  | 	// SignRequest signs the request using ssh.Signer. | ||
|  | 	// The public key id is used by the HTTP server to identify which key to use | ||
|  | 	// to verify the signature. | ||
|  | 	// | ||
|  | 	// A Digest (RFC 3230) will be added to the request. The body provided | ||
|  | 	// must match the body used in the request, and is allowed to be nil. | ||
|  | 	// The Digest ensures the request body is not tampered with in flight, | ||
|  | 	// and if the signer is created to also sign the "Digest" header, the | ||
|  | 	// HTTP Signature will then ensure both the Digest and body are not both | ||
|  | 	// modified to maliciously represent different content. | ||
|  | 	SignRequest(pubKeyId string, r *http.Request, body []byte) error | ||
|  | 	// SignResponse signs the response using ssh.Signer. The public key | ||
|  | 	// id is used by the HTTP client to identify which key to use to verify | ||
|  | 	// the signature. | ||
|  | 	// | ||
|  | 	// A Digest (RFC 3230) will be added to the response. The body provided | ||
|  | 	// must match the body written in the response, and is allowed to be | ||
|  | 	// nil. The Digest ensures the response body is not tampered with in | ||
|  | 	// flight, and if the signer is created to also sign the "Digest" | ||
|  | 	// header, the HTTP Signature will then ensure both the Digest and body | ||
|  | 	// are not both modified to maliciously represent different content. | ||
|  | 	SignResponse(pubKeyId string, r http.ResponseWriter, body []byte) error | ||
|  | } | ||
|  | 
 | ||
|  | // NewwSSHSigner creates a new Signer using the specified ssh.Signer | ||
|  | // At the moment only ed25519 ssh keys are supported. | ||
|  | // The headers specified will be included into the HTTP signatures. | ||
|  | // | ||
|  | // The Digest will also be calculated on a request's body using the provided | ||
|  | // digest algorithm, if "Digest" is one of the headers listed. | ||
|  | // | ||
|  | // The provided scheme determines which header is populated with the HTTP | ||
|  | // Signature. | ||
|  | func NewSSHSigner(s ssh.Signer, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme, expiresIn int64) (SSHSigner, Algorithm, error) { | ||
|  | 	sshAlgo := getSSHAlgorithm(s.PublicKey().Type()) | ||
|  | 	if sshAlgo == "" { | ||
|  | 		return nil, "", fmt.Errorf("key type: %s not supported yet.", s.PublicKey().Type()) | ||
|  | 	} | ||
|  | 
 | ||
|  | 	signer, err := newSSHSigner(s, sshAlgo, dAlgo, headers, scheme, expiresIn) | ||
|  | 	if err != nil { | ||
|  | 		return nil, "", err | ||
|  | 	} | ||
|  | 
 | ||
|  | 	return signer, sshAlgo, nil | ||
|  | } | ||
|  | 
 | ||
|  | func getSSHAlgorithm(pkType string) Algorithm { | ||
|  | 	switch { | ||
|  | 	case strings.HasPrefix(pkType, sshPrefix+"-"+ed25519Prefix): | ||
|  | 		return ED25519 | ||
|  | 	case strings.HasPrefix(pkType, sshPrefix+"-"+rsaPrefix): | ||
|  | 		return RSA_SHA1 | ||
|  | 	} | ||
|  | 
 | ||
|  | 	return "" | ||
|  | } | ||
|  | 
 | ||
|  | // Verifier verifies HTTP Signatures. | ||
|  | // | ||
|  | // It will determine which of the supported headers has the parameters | ||
|  | // that define the signature. | ||
|  | // | ||
|  | // Verifiers are not safe to use between multiple goroutines. | ||
|  | // | ||
|  | // Note that verification ignores the deprecated 'algorithm' parameter. | ||
|  | type Verifier interface { | ||
|  | 	// KeyId gets the public key id that the signature is signed with. | ||
|  | 	// | ||
|  | 	// Note that the application is expected to determine the algorithm | ||
|  | 	// used based on metadata or out-of-band information for this key id. | ||
|  | 	KeyId() string | ||
|  | 	// Verify accepts the public key specified by KeyId and returns an | ||
|  | 	// error if verification fails or if the signature is malformed. The | ||
|  | 	// algorithm must be the one used to create the signature in order to | ||
|  | 	// pass verification. The algorithm is determined based on metadata or | ||
|  | 	// out-of-band information for the key id. | ||
|  | 	// | ||
|  | 	// If the signature was created using a MAC based algorithm, then the | ||
|  | 	// key is expected to be of type []byte. If the signature was created | ||
|  | 	// using an RSA based algorithm, then the public key is expected to be | ||
|  | 	// of type *rsa.PublicKey. | ||
|  | 	Verify(pKey crypto.PublicKey, algo Algorithm) error | ||
|  | } | ||
|  | 
 | ||
|  | type VerifierWithOptions interface { | ||
|  | 	Verifier | ||
|  | 
 | ||
|  | 	VerifyWithOptions(pKey crypto.PublicKey, algo Algorithm, opts SignatureOption) error | ||
|  | } | ||
|  | 
 | ||
|  | const ( | ||
|  | 	// host is treated specially because golang may not include it in the | ||
|  | 	// request header map on the server side of a request. | ||
|  | 	hostHeader = "Host" | ||
|  | ) | ||
|  | 
 | ||
|  | // NewVerifier verifies the given request. It returns an error if the HTTP | ||
|  | // Signature parameters are not present in any headers, are present in more than | ||
|  | // one header, are malformed, or are missing required parameters. It ignores | ||
|  | // unknown HTTP Signature parameters. | ||
|  | func NewVerifier(r *http.Request) (VerifierWithOptions, error) { | ||
|  | 	h := r.Header | ||
|  | 	if _, hasHostHeader := h[hostHeader]; len(r.Host) > 0 && !hasHostHeader { | ||
|  | 		h[hostHeader] = []string{r.Host} | ||
|  | 	} | ||
|  | 	return newVerifier(h, func(h http.Header, toInclude []string, created int64, expires int64, opts SignatureOption) (string, error) { | ||
|  | 		return signatureString(h, toInclude, addRequestTarget(r, opts), created, expires) | ||
|  | 	}) | ||
|  | } | ||
|  | 
 | ||
|  | // NewResponseVerifier verifies the given response. It returns errors under the | ||
|  | // same conditions as NewVerifier. | ||
|  | func NewResponseVerifier(r *http.Response) (Verifier, error) { | ||
|  | 	return newVerifier(r.Header, func(h http.Header, toInclude []string, created int64, expires int64, _ SignatureOption) (string, error) { | ||
|  | 		return signatureString(h, toInclude, requestTargetNotPermitted, created, expires) | ||
|  | 	}) | ||
|  | } | ||
|  | 
 | ||
|  | func newSSHSigner(sshSigner ssh.Signer, algo Algorithm, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme, expiresIn int64) (SSHSigner, error) { | ||
|  | 	var expires, created int64 = 0, 0 | ||
|  | 
 | ||
|  | 	if expiresIn != 0 { | ||
|  | 		created = time.Now().Unix() | ||
|  | 		expires = created + expiresIn | ||
|  | 	} | ||
|  | 
 | ||
|  | 	s, err := signerFromSSHSigner(sshSigner, string(algo)) | ||
|  | 	if err != nil { | ||
|  | 		return nil, fmt.Errorf("no crypto implementation available for ssh algo %q: %s", algo, err) | ||
|  | 	} | ||
|  | 
 | ||
|  | 	a := &asymmSSHSigner{ | ||
|  | 		asymmSigner: &asymmSigner{ | ||
|  | 			s:            s, | ||
|  | 			dAlgo:        dAlgo, | ||
|  | 			headers:      headers, | ||
|  | 			targetHeader: scheme, | ||
|  | 			prefix:       scheme.authScheme(), | ||
|  | 			created:      created, | ||
|  | 			expires:      expires, | ||
|  | 		}, | ||
|  | 	} | ||
|  | 
 | ||
|  | 	return a, nil | ||
|  | } | ||
|  | 
 | ||
|  | func newSigner(algo Algorithm, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme, expiresIn int64) (SignerWithOptions, error) { | ||
|  | 
 | ||
|  | 	var expires, created int64 = 0, 0 | ||
|  | 	if expiresIn != 0 { | ||
|  | 		created = time.Now().Unix() | ||
|  | 		expires = created + expiresIn | ||
|  | 	} | ||
|  | 
 | ||
|  | 	s, err := signerFromString(string(algo)) | ||
|  | 	if err == nil { | ||
|  | 		a := &asymmSigner{ | ||
|  | 			s:            s, | ||
|  | 			dAlgo:        dAlgo, | ||
|  | 			headers:      headers, | ||
|  | 			targetHeader: scheme, | ||
|  | 			prefix:       scheme.authScheme(), | ||
|  | 			created:      created, | ||
|  | 			expires:      expires, | ||
|  | 		} | ||
|  | 		return a, nil | ||
|  | 	} | ||
|  | 	m, err := macerFromString(string(algo)) | ||
|  | 	if err != nil { | ||
|  | 		return nil, fmt.Errorf("no crypto implementation available for %q: %s", algo, err) | ||
|  | 	} | ||
|  | 	c := &macSigner{ | ||
|  | 		m:            m, | ||
|  | 		dAlgo:        dAlgo, | ||
|  | 		headers:      headers, | ||
|  | 		targetHeader: scheme, | ||
|  | 		prefix:       scheme.authScheme(), | ||
|  | 		created:      created, | ||
|  | 		expires:      expires, | ||
|  | 	} | ||
|  | 	return c, nil | ||
|  | } |