All Downloads are FREE. Search and download functionalities are using the official Maven repository.

go.signing.mustache Maven / Gradle / Ivy

There is a newer version: 7.8.0
Show newest version
{{>partial_header}}
package {{packageName}}

import (
	"bytes"
	"context"
	"crypto"
	"crypto/ecdsa"
	"crypto/ed25519"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/base64"
	"encoding/pem"
	"fmt"
	"io"
	"net/http"
	"net/textproto"
	"os"
	"strings"
	"time"
)

const (
	// Constants for HTTP signature parameters.
	// The '(request-target)' parameter concatenates the lowercased :method, an
	// ASCII space, and the :path pseudo-headers.
	HttpSignatureParameterRequestTarget string = "(request-target)"
	// The '(created)' parameter expresses when the signature was
	// created.  The value MUST be a Unix timestamp integer value.
	HttpSignatureParameterCreated string = "(created)"
	// The '(expires)' parameter expresses when the signature ceases to
	// be valid.  The value MUST be a Unix timestamp integer value.
	HttpSignatureParameterExpires string = "(expires)"
)

const (
	// Constants for HTTP headers.
	// The 'Host' header, as defined in RFC 2616, section 14.23.
	HttpHeaderHost string = "Host"
	// The 'Date' header.
	HttpHeaderDate string = "Date"
	// The digest header, as defined in RFC 3230, section 4.3.2.
	HttpHeaderDigest string = "Digest"
	// The HTTP Authorization header, as defined in RFC 7235, section 4.2.
	HttpHeaderAuthorization string = "Authorization"
)

const (
	// Specifies the Digital Signature Algorithm is derived from metadata
	// associated with 'keyId'. Supported DSA algorithms are RSASSA-PKCS1-v1_5,
	// RSASSA-PSS, and ECDSA.
	// The hash is SHA-512.
	// This is the default value.
	HttpSigningSchemeHs2019 string = "hs2019"
	// Use RSASSA-PKCS1-v1_5 with SHA-512 hash. Deprecated.
	HttpSigningSchemeRsaSha512 string = "rsa-sha512"
	// Use RSASSA-PKCS1-v1_5 with SHA-256 hash. Deprecated.
	HttpSigningSchemeRsaSha256 string = "rsa-sha256"

	// RFC 8017 section 7.2
	// Calculate the message signature using RSASSA-PKCS1-V1_5-SIGN from RSA PKCS#1 v1.5.
	// PKCSV1_5 is deterministic. The same message and key will produce an identical
	// signature value each time.
	HttpSigningAlgorithmRsaPKCS1v15 string = "RSASSA-PKCS1-v1_5"
	// Calculate the message signature using probabilistic signature scheme RSASSA-PSS.
	// PSS is randomized and will produce a different signature value each time.
	HttpSigningAlgorithmRsaPSS string = "RSASSA-PSS"

	// HashAlgorithm Sha256 for generating hash
	HttpHashAlgorithmSha256 string = "sha256"

	// HashAlgorithm Sha512 for generating hash
	HttpHashAlgorithmSha512 string = "sha512"
)

var supportedSigningSchemes = map[string]bool{
	HttpSigningSchemeHs2019: true,
	HttpSigningSchemeRsaSha512: true,
	HttpSigningSchemeRsaSha256: true,
}


// HttpSignatureAuth provides HTTP signature authentication to a request passed
// via context using ContextHttpSignatureAuth.
// An 'Authorization' header is calculated by creating a hash of select headers,
// and optionally the body of the HTTP request, then signing the hash value using
// a private key which is available to the client.
//
// SignedHeaders specifies the list of HTTP headers that are included when generating
// the message signature.
// The two special signature headers '(request-target)' and '(created)' SHOULD be
// included in SignedHeaders.
// The '(created)' header expresses when the signature was created.
// The '(request-target)' header is a concatenation of the lowercased :method, an
// ASCII space, and the :path pseudo-headers.
//
// For example, SignedHeaders can be set to:
//   (request-target) (created) date host digest
//
// When SignedHeaders is not specified, the client defaults to a single value, '(created)',
// in the list of HTTP headers.
// When SignedHeaders contains the 'Digest' value, the client performs the following operations:
// 1. Calculate a digest of request body, as specified in RFC3230, section 4.3.2.
// 2. Set the 'Digest' header in the request body.
// 3. Include the 'Digest' header and value in the HTTP signature.
type HttpSignatureAuth struct {
	KeyId             string            // A key identifier.
	PrivateKeyPath    string            // The path to the private key.
	Passphrase        string            // The passphrase to decrypt the private key, if the key is encrypted.
	SigningScheme     string            // The signature scheme, when signing HTTP requests. Supported value is 'hs2019'.
	// The signature algorithm, when signing HTTP requests.
	// Supported values are RSASSA-PKCS1-v1_5, RSASSA-PSS.
	SigningAlgorithm  string
	HashAlgorithm string                // supported values are sha256 and sha512. This also allows using sha256 with hs2019, which defaults to sha512.
	SignedHeaders     []string          // A list of HTTP headers included when generating the signature for the message.
	// SignatureMaxValidity specifies the maximum duration of the signature validity.
	// The value is used to set the '(expires)' signature parameter in the HTTP request.
	// '(expires)' is set to '(created)' plus the value of the SignatureMaxValidity field.
	// To specify the '(expires)' signature parameter, set 'SignatureMaxValidity' and add '(expires)' to 'SignedHeaders'.
	SignatureMaxValidity time.Duration
	privateKey        crypto.PrivateKey // The private key used to sign HTTP requests.
}

// SetPrivateKey accepts a private key string and sets it.
func (h *HttpSignatureAuth) SetPrivateKey(privateKey string) error {
	return h.parsePrivateKey([]byte(privateKey))
}

// ContextWithValue validates the HttpSignatureAuth configuration parameters and returns a context
// suitable for HTTP signature. An error is returned if the HttpSignatureAuth configuration parameters
// are invalid.
func (h *HttpSignatureAuth) ContextWithValue(ctx context.Context) (context.Context, error) {
	if h.KeyId == "" {
		return nil, fmt.Errorf("key ID must be specified")
	}
	if h.PrivateKeyPath == "" && h.privateKey == nil {
		return nil, fmt.Errorf("private key path must be specified")
	}
	if _, ok := supportedSigningSchemes[h.SigningScheme]; !ok {
		return nil, fmt.Errorf("invalid signing scheme: '%v'", h.SigningScheme)
	}
	m := make(map[string]bool)
	for _, h := range h.SignedHeaders {
		if strings.EqualFold(h, HttpHeaderAuthorization) {
			return nil, fmt.Errorf("signed headers cannot include the 'Authorization' header")
		}
		m[h] = true
	}
	if len(m) != len(h.SignedHeaders) {
		return nil, fmt.Errorf("list of signed headers cannot have duplicate names")
	}
	if h.SignatureMaxValidity < 0 {
		return nil, fmt.Errorf("signature max validity must be a positive value")
	}
	if err := h.loadPrivateKey(); err != nil {
		return nil, err
	}
	return context.WithValue(ctx, ContextHttpSignatureAuth, *h), nil
}

// GetPublicKey returns the public key associated with this HTTP signature configuration.
func (h *HttpSignatureAuth) GetPublicKey() (crypto.PublicKey, error) {
	if h.privateKey == nil {
		if err := h.loadPrivateKey(); err != nil {
			return nil, err
		}
	}
	switch key := h.privateKey.(type) {
	case *rsa.PrivateKey:
		return key.Public(), nil
	case *ecdsa.PrivateKey:
		return key.Public(), nil
	default:
		// Do not change '%T' to anything else such as '%v'!
		// The value of the private key must not be returned.
		return nil, fmt.Errorf("unsupported key: %T", h.privateKey)
	}
}

// loadPrivateKey reads the private key from the file specified in the HttpSignatureAuth.
// The key is loaded only when privateKey is not already set.
func (h *HttpSignatureAuth) loadPrivateKey() (err error) {
	if h.privateKey != nil {
		return nil
	}
	var file *os.File
	file, err = os.Open(h.PrivateKeyPath)
	if err != nil {
		return fmt.Errorf("cannot load private key '%s'. Error: %v", h.PrivateKeyPath, err)
	}
	defer func() {
		err = file.Close()
	}()
	var priv []byte
	priv, err = io.ReadAll(file)
	if err != nil {
		return err
	}
	return h.parsePrivateKey(priv)
}

// parsePrivateKey decodes privateKey byte array to crypto.PrivateKey type.
func (h *HttpSignatureAuth) parsePrivateKey(priv []byte) error {
	pemBlock, _ := pem.Decode(priv)
	if pemBlock == nil {
		// No PEM data has been found.
		return fmt.Errorf("file '%s' does not contain PEM data", h.PrivateKeyPath)
	}
	var privKey []byte
	var err error
	if x509.IsEncryptedPEMBlock(pemBlock) {
		// The PEM data is encrypted.
		privKey, err = x509.DecryptPEMBlock(pemBlock, []byte(h.Passphrase))
		if err != nil {
			// Failed to decrypt PEM block. Because of deficiencies in the encrypted-PEM format,
			// it's not always possible to detect an incorrect password.
			return err
		}
	} else {
		privKey = pemBlock.Bytes
	}
	switch pemBlock.Type {
	case "RSA PRIVATE KEY":
		if h.privateKey, err = x509.ParsePKCS1PrivateKey(privKey); err != nil {
			return err
		}
	case "EC PRIVATE KEY", "PRIVATE KEY":
		// https://tools.ietf.org/html/rfc5915 section 4.
		if h.privateKey, err = x509.ParsePKCS8PrivateKey(privKey); err != nil {
			return err
		}
	default:
		return fmt.Errorf("key '%s' is not supported", pemBlock.Type)
	}
	return nil
}

// SignRequest signs the request using HTTP signature.
// See https://datatracker.ietf.org/doc/draft-cavage-http-signatures/
//
// Do not add, remove or change headers that are included in the SignedHeaders
// after SignRequest has been invoked; this is because the header values are
// included in the signature. Any subsequent alteration will cause a signature
// verification failure.
// If there are multiple instances of the same header field, all
// header field values associated with the header field MUST be
// concatenated, separated by a ASCII comma and an ASCII space
// ', ', and used in the order in which they will appear in the
// transmitted HTTP message.
func SignRequest(
	ctx context.Context,
	r *http.Request,
	auth HttpSignatureAuth) error {

	if auth.privateKey == nil {
		return fmt.Errorf("private key is not set")
	}
	now := time.Now()
	date := now.UTC().Format(http.TimeFormat)
	// The 'created' field expresses when the signature was created.
	// The value MUST be a Unix timestamp integer value. See 'HTTP signature' section 2.1.4.
	created := now.Unix()

	var h crypto.Hash
	var err error
	var prefix string
	var expiresUnix float64

	if auth.SignatureMaxValidity < 0 {
		return fmt.Errorf("signature validity must be a positive value")
	}
	if auth.SignatureMaxValidity > 0 {
		e := now.Add(auth.SignatureMaxValidity)
		expiresUnix = float64(e.Unix()) + float64(e.Nanosecond()) / float64(time.Second)
	}
	// Determine the cryptographic hash to be used for the signature and the body digest.
	switch auth.SigningScheme {
	case HttpSigningSchemeRsaSha512:
		h = crypto.SHA512
		prefix = "SHA-512="
	case HttpSigningSchemeRsaSha256:
		// This is deprecated and should no longer be used.
		h = crypto.SHA256
		prefix = "SHA-256="
	case HttpSigningSchemeHs2019:
		if auth.HashAlgorithm == HttpHashAlgorithmSha256 {
			h = crypto.SHA256
			prefix = "SHA-256="
		} else {
			h = crypto.SHA512
			prefix = "SHA-512="
		}

	default:
		return fmt.Errorf("unsupported signature scheme: %v", auth.SigningScheme)
	}
	if !h.Available() {
		return fmt.Errorf("hash '%v' is not available", h)
	}

	// Build the "(request-target)" signature header.
	var sb bytes.Buffer
	fmt.Fprintf(&sb, "%s %s", strings.ToLower(r.Method), r.URL.EscapedPath())
	if r.URL.RawQuery != "" {
		// The ":path" pseudo-header field includes the path and query parts
		// of the target URI (the "path-absolute" production and optionally a
		// '?' character followed by the "query" production (see Sections 3.3
		// and 3.4 of [RFC3986]
		fmt.Fprintf(&sb, "?%s", r.URL.RawQuery)
	}
	requestTarget := sb.String()
	sb.Reset()

	// Build the string to be signed.
	signedHeaders := auth.SignedHeaders
	if len(signedHeaders) == 0 {
		signedHeaders = []string{HttpSignatureParameterCreated}
	}
	// Validate the list of signed headers has no duplicates.
	m := make(map[string]bool)
	for _, h := range signedHeaders {
		m[h] = true
	}
	if len(m) != len(signedHeaders) {
		return fmt.Errorf("list of signed headers must not have any duplicates")
	}
	hasCreatedParameter := false
	hasExpiresParameter := false
	for i, header := range signedHeaders {
		header = strings.ToLower(header)
		var value string
		switch header {
		case strings.ToLower(HttpHeaderAuthorization):
			return fmt.Errorf("cannot include the 'Authorization' header as a signed header")
		case HttpSignatureParameterRequestTarget:
			value = requestTarget
		case HttpSignatureParameterCreated:
			value = fmt.Sprintf("%d", created)
			hasCreatedParameter = true
		case HttpSignatureParameterExpires:
			if auth.SignatureMaxValidity.Nanoseconds() == 0 {
				return fmt.Errorf("cannot set '(expires)' signature parameter. SignatureMaxValidity is not configured")
			}
			value = fmt.Sprintf("%.3f", expiresUnix)
			hasExpiresParameter = true
		case "date":
			value = date
			r.Header.Set(HttpHeaderDate, date)
		case "digest":
			// Calculate the digest of the HTTP request body.
			// Calculate body digest per RFC 3230 section 4.3.2
			bodyHash := h.New()
			if r.Body != nil {
				// Make a copy of the body io.Reader so that we can read the body to calculate the hash,
				// then one more time when marshaling the request.
				var body io.Reader
				body, err = r.GetBody()
				if err != nil {
					return err
				}
				if _, err = io.Copy(bodyHash, body); err != nil {
					return err
				}
			}
			d := bodyHash.Sum(nil)
			value = prefix + base64.StdEncoding.EncodeToString(d)
			r.Header.Set(HttpHeaderDigest, value)
		case "host":
			value = r.Host
			r.Header.Set(HttpHeaderHost, r.Host)
		default:
			var ok bool
			var v []string
			canonicalHeader := textproto.CanonicalMIMEHeaderKey(header)
			if v, ok = r.Header[canonicalHeader]; !ok {
				// If a header specified in the headers parameter cannot be matched with
				// a provided header in the message, the implementation MUST produce an error.
				return fmt.Errorf("header '%s' does not exist in the request", canonicalHeader)
			}
			// If there are multiple instances of the same header field, all
			// header field values associated with the header field MUST be
			// concatenated, separated by a ASCII comma and an ASCII space
			// `, `, and used in the order in which they will appear in the
			// transmitted HTTP message.
			value = strings.Join(v, ", ")
		}
		if i > 0 {
			fmt.Fprintf(&sb, "\n")
		}
		fmt.Fprintf(&sb, "%s: %s", header, value)
	}
	if expiresUnix != 0 && !hasExpiresParameter {
		return fmt.Errorf("signatureMaxValidity is specified, but '(expired)' parameter is not present")
	}
	msg := []byte(sb.String())
	msgHash := h.New()
	if _, err = msgHash.Write(msg); err != nil {
		return err
	}
	d := msgHash.Sum(nil)

	var signature []byte
	switch key := auth.privateKey.(type) {
	case *rsa.PrivateKey:
		switch auth.SigningAlgorithm {
		case HttpSigningAlgorithmRsaPKCS1v15:
			signature, err = rsa.SignPKCS1v15(rand.Reader, key, h, d)
		case "", HttpSigningAlgorithmRsaPSS:
			signature, err = rsa.SignPSS(rand.Reader, key, h, d, nil)
		default:
			return fmt.Errorf("unsupported signing algorithm: '%s'", auth.SigningAlgorithm)
		}
	case *ecdsa.PrivateKey:
		signature, err = key.Sign(rand.Reader, d, h)
	case ed25519.PrivateKey: // requires go 1.13
	  signature, err = key.Sign(rand.Reader, msg, crypto.Hash(0))
	default:
		return fmt.Errorf("unsupported private key")
	}
	if err != nil {
		return err
	}

	sb.Reset()
	for i, header := range signedHeaders {
		if i > 0 {
			sb.WriteRune(' ')
		}
		sb.WriteString(strings.ToLower(header))
	}
	headers_list := sb.String()
	sb.Reset()
	fmt.Fprintf(&sb, `Signature keyId="%s",algorithm="%s",`, auth.KeyId, auth.SigningScheme)
	if hasCreatedParameter {
		fmt.Fprintf(&sb, "created=%d,", created)
	}
	if hasExpiresParameter {
		fmt.Fprintf(&sb, "expires=%.3f,", expiresUnix)
	}
	fmt.Fprintf(&sb, `headers="%s",signature="%s"`, headers_list, base64.StdEncoding.EncodeToString(signature))
	authStr := sb.String()
	r.Header.Set(HttpHeaderAuthorization, authStr)
	return nil
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy