Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
415 lines
14 KiB
Plaintext
415 lines
14 KiB
Plaintext
{{>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"
|
|
)
|
|
|
|
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
|
|
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.
|
|
}
|
|
|
|
// 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 == "" {
|
|
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.ToLower(h) == strings.ToLower(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.
|
|
func (h *HttpSignatureAuth) loadPrivateKey() (err error) {
|
|
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
|
|
}
|
|
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
|
|
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 possibleto 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, HttpSigningSchemeHs2019:
|
|
h = crypto.SHA512
|
|
prefix = "SHA-512="
|
|
case HttpSigningSchemeRsaSha256:
|
|
// This is deprecated and should no longer be used.
|
|
h = crypto.SHA256
|
|
prefix = "SHA-256="
|
|
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
|
|
}
|