Files
zitadel/internal/command/action_v2_target_key.go
T
2025-12-30 09:13:05 +01:00

191 lines
6.1 KiB
Go

package command
import (
"context"
"crypto/ecdsa"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"time"
"github.com/zitadel/zitadel/internal/repository/target"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
const (
ErrTargetIDMissing = "ErrTargetIDMissing"
ErrPublicKeyFormat = "ErrPublicKeyFormat"
ErrExpirationDateBeforeNow = "ErrExpirationDateBeforeNow"
ErrPublicKeyDeleteActiveKey = "ErrPublicKeyDeleteActiveKey"
ErrTargetNotFound = "ErrTargetNotFound"
ErrKeyNotFound = "ErrKeyNotFound"
ErrPublicKeyExpired = "ErrPublicKeyExpired"
)
type TargetPublicKey struct {
TargetID string
PublicKey []byte
Expiration time.Time
// KeyID is generated and returned after adding the public key
KeyID string
}
func (c *Commands) AddTargetPublicKey(ctx context.Context, key *TargetPublicKey, resourceOwner string) (_ time.Time, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if key.TargetID == "" {
return time.Time{}, zerrors.ThrowInvalidArgument(nil, ErrTargetIDMissing, "Errors.IDMissing")
}
if !key.Expiration.IsZero() && key.Expiration.Before(time.Now()) {
return time.Time{}, zerrors.ThrowInvalidArgument(nil, ErrExpirationDateBeforeNow, "Errors.Target.InvalidExpirationDate")
}
fingerprint, err := checkPublicKeyAndComputeFingerprint(key.PublicKey)
if err != nil {
return time.Time{}, err
}
key.KeyID, err = c.idGenerator.Next()
if err != nil {
return time.Time{}, err
}
writeModel, err := c.getTargetKeyWriteModelByID(ctx, key.TargetID, key.KeyID, resourceOwner)
if err != nil {
return time.Time{}, err
}
if !writeModel.TargetExists {
return time.Time{}, zerrors.ThrowPreconditionFailed(nil, ErrTargetNotFound, "Errors.Target.NotFound")
}
err = c.pushAppendAndReduce(ctx, writeModel, target.NewKeyAddedEvent(
ctx,
TargetAggregateFromWriteModel(&writeModel.WriteModel),
key.KeyID,
key.PublicKey,
fingerprint,
key.Expiration,
))
if err != nil {
return time.Time{}, err
}
return writeModel.ChangeDate, nil
}
func (c *Commands) ActivateTargetPublicKey(ctx context.Context, targetID, keyID, resourceOwner string) (_ time.Time, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if targetID == "" || keyID == "" {
return time.Time{}, zerrors.ThrowInvalidArgument(nil, ErrTargetIDMissing, "Errors.IDMissing")
}
writeModel, err := c.getTargetKeyWriteModelByID(ctx, targetID, keyID, resourceOwner)
if err != nil {
return time.Time{}, err
}
if !writeModel.TargetExists || !writeModel.KeyExists {
return time.Time{}, zerrors.ThrowPreconditionFailed(nil, ErrKeyNotFound, "Errors.Target.NotFound")
}
if !writeModel.ExpirationDate.IsZero() && writeModel.ExpirationDate.Before(time.Now()) {
return time.Time{}, zerrors.ThrowPreconditionFailed(nil, ErrPublicKeyExpired, "Errors.Target.PublicKeyExpired")
}
if writeModel.Active {
return writeModel.ChangeDate, nil
}
err = c.pushAppendAndReduce(ctx, writeModel, target.NewKeyActivatedEvent(
ctx,
TargetAggregateFromWriteModel(&writeModel.WriteModel),
keyID,
))
if err != nil {
return time.Time{}, err
}
return writeModel.ChangeDate, nil
}
func (c *Commands) DeactivateTargetPublicKey(ctx context.Context, targetID, keyID, resourceOwner string) (_ time.Time, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if targetID == "" || keyID == "" {
return time.Time{}, zerrors.ThrowInvalidArgument(nil, ErrTargetIDMissing, "Errors.IDMissing")
}
writeModel, err := c.getTargetKeyWriteModelByID(ctx, targetID, keyID, resourceOwner)
if err != nil {
return time.Time{}, err
}
if !writeModel.TargetExists || !writeModel.KeyExists {
return time.Time{}, zerrors.ThrowPreconditionFailed(nil, ErrKeyNotFound, "Errors.Target.NotFound")
}
if !writeModel.Active {
return writeModel.ChangeDate, nil
}
err = c.pushAppendAndReduce(ctx, writeModel, target.NewKeyDeactivatedEvent(
ctx,
TargetAggregateFromWriteModel(&writeModel.WriteModel),
keyID,
))
if err != nil {
return time.Time{}, err
}
return writeModel.ChangeDate, nil
}
func (c *Commands) RemoveTargetPublicKey(ctx context.Context, targetID, keyID, resourceOwner string) (_ time.Time, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if targetID == "" || keyID == "" {
return time.Time{}, zerrors.ThrowInvalidArgument(nil, ErrTargetIDMissing, "Errors.IDMissing")
}
writeModel, err := c.getTargetKeyWriteModelByID(ctx, targetID, keyID, resourceOwner)
if err != nil {
return time.Time{}, err
}
if !writeModel.TargetExists || !writeModel.KeyExists {
return writeModel.ChangeDate, nil
}
if writeModel.Active {
return time.Time{}, zerrors.ThrowPreconditionFailed(nil, ErrPublicKeyDeleteActiveKey, "Errors.Target.PublicKeyActive")
}
err = c.pushAppendAndReduce(ctx, writeModel, target.NewKeyRemovedEvent(
ctx,
TargetAggregateFromWriteModel(&writeModel.WriteModel),
keyID,
))
if err != nil {
return time.Time{}, err
}
return writeModel.ChangeDate, nil
}
func checkPublicKeyAndComputeFingerprint(key []byte) (string, error) {
block, _ := pem.Decode(key)
if block == nil {
return "", zerrors.ThrowInvalidArgument(nil, ErrPublicKeyFormat, "Errors.Target.InvalidPublicKey")
}
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return "", zerrors.ThrowInvalidArgument(err, ErrPublicKeyFormat, "Errors.Target.InvalidPublicKey")
}
switch pub.(type) {
case *rsa.PublicKey,
*ecdsa.PublicKey:
fingerprint := sha256.Sum256(block.Bytes)
return fmt.Sprintf("SHA256:%s", base64.RawStdEncoding.EncodeToString(fingerprint[:])), nil
default:
return "", zerrors.ThrowInvalidArgument(nil, ErrPublicKeyFormat, "Errors.Target.InvalidPublicKey")
}
}
func (c *Commands) getTargetKeyWriteModelByID(ctx context.Context, targetID, keyID string, resourceOwner string) (*TargetKeyWriteModel, error) {
wm := NewTargetKeyWriteModel(targetID, keyID, resourceOwner)
err := c.eventstore.FilterToQueryReducer(ctx, wm)
if err != nil {
return nil, err
}
return wm, nil
}