snapshot of https://github.com/ory/keto.git (master, 2026-09-16; blobs > 131072B omitted)
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
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
This commit is contained in:
414
.schema/openapi/templates/go/signing.mustache
Normal file
414
.schema/openapi/templates/go/signing.mustache
Normal file
@@ -0,0 +1,414 @@
|
||||
{{>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
|
||||
}
|
||||
Reference in New Issue
Block a user