mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 14:47:33 +00:00
feat: action v2 signing (#8779)
# Which Problems Are Solved The action v2 messages were didn't contain anything providing security for the sent content. # How the Problems Are Solved Each Target now has a SigningKey, which can also be newly generated through the API and returned at creation and through the Get-Endpoints. There is now a HTTP header "Zitadel-Signature", which is generated with the SigningKey and Payload, and also contains a timestamp to check with a tolerance if the message took to long to sent. # Additional Changes The functionality to create and check the signature is provided in the pkg/actions package, and can be reused in the SDK. # Additional Context Closes #7924 --------- Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
115
pkg/actions/signing.go
Normal file
115
pkg/actions/signing.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package actions
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoValidSignature = errors.New("no valid signature")
|
||||
ErrInvalidHeader = errors.New("webhook has invalid Zitadel-Signature header")
|
||||
ErrNotSigned = errors.New("webhook has no Zitadel-Signature header")
|
||||
ErrTooOld = errors.New("timestamp wasn't within tolerance")
|
||||
)
|
||||
|
||||
const (
|
||||
SigningHeader = "ZITADEL-Signature"
|
||||
signingTimestamp = "t"
|
||||
signingVersion string = "v1"
|
||||
DefaultTolerance = 300 * time.Second
|
||||
partSeparator = ","
|
||||
)
|
||||
|
||||
func ComputeSignatureHeader(t time.Time, payload []byte, signingKey ...string) string {
|
||||
parts := []string{
|
||||
fmt.Sprintf("%s=%d", signingTimestamp, t.Unix()),
|
||||
}
|
||||
for _, k := range signingKey {
|
||||
parts = append(parts, fmt.Sprintf("%s=%s", signingVersion, hex.EncodeToString(computeSignature(t, payload, k))))
|
||||
}
|
||||
return strings.Join(parts, partSeparator)
|
||||
}
|
||||
|
||||
func computeSignature(t time.Time, payload []byte, signingKey string) []byte {
|
||||
mac := hmac.New(sha256.New, []byte(signingKey))
|
||||
mac.Write([]byte(fmt.Sprintf("%d", t.Unix())))
|
||||
mac.Write([]byte("."))
|
||||
mac.Write(payload)
|
||||
return mac.Sum(nil)
|
||||
}
|
||||
|
||||
func ValidatePayload(payload []byte, header string, signingKey string) error {
|
||||
return ValidatePayloadWithTolerance(payload, header, signingKey, DefaultTolerance)
|
||||
}
|
||||
|
||||
func ValidatePayloadWithTolerance(payload []byte, header string, signingKey string, tolerance time.Duration) error {
|
||||
return validatePayload(payload, header, signingKey, tolerance, true)
|
||||
}
|
||||
|
||||
func validatePayload(payload []byte, sigHeader string, signingKey string, tolerance time.Duration, enforceTolerance bool) error {
|
||||
header, err := parseSignatureHeader(sigHeader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expectedSignature := computeSignature(header.timestamp, payload, signingKey)
|
||||
expiredTimestamp := time.Since(header.timestamp) > tolerance
|
||||
if enforceTolerance && expiredTimestamp {
|
||||
return ErrTooOld
|
||||
}
|
||||
|
||||
for _, sig := range header.signatures {
|
||||
if hmac.Equal(expectedSignature, sig) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return ErrNoValidSignature
|
||||
}
|
||||
|
||||
type signedHeader struct {
|
||||
timestamp time.Time
|
||||
signatures [][]byte
|
||||
}
|
||||
|
||||
func parseSignatureHeader(header string) (*signedHeader, error) {
|
||||
sh := &signedHeader{}
|
||||
if header == "" {
|
||||
return sh, ErrNotSigned
|
||||
}
|
||||
|
||||
pairs := strings.Split(header, ",")
|
||||
for _, pair := range pairs {
|
||||
parts := strings.Split(pair, "=")
|
||||
if len(parts) != 2 {
|
||||
return sh, ErrInvalidHeader
|
||||
}
|
||||
switch parts[0] {
|
||||
case signingTimestamp:
|
||||
timestamp, err := strconv.ParseInt(parts[1], 10, 64)
|
||||
if err != nil {
|
||||
return sh, ErrInvalidHeader
|
||||
}
|
||||
sh.timestamp = time.Unix(timestamp, 0)
|
||||
|
||||
case signingVersion:
|
||||
sig, err := hex.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
sh.signatures = append(sh.signatures, sig)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(sh.signatures) == 0 {
|
||||
return sh, ErrNoValidSignature
|
||||
}
|
||||
return sh, nil
|
||||
}
|
Reference in New Issue
Block a user