mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
# 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 <[email protected]>
116 lines
2.9 KiB
Go
116 lines
2.9 KiB
Go
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
|
|
}
|