feat(v3alpha): web key resource (#8262)

# Which Problems Are Solved

Implement a new API service that allows management of OIDC signing web
keys.
This allows users to manage rotation of the instance level keys. which
are currently managed based on expiry.

The API accepts the generation of the following key types and
parameters:

- RSA keys with 2048, 3072 or 4096 bit in size and:
  - Signing with SHA-256 (RS256)
  - Signing with SHA-384 (RS384)
  - Signing with SHA-512 (RS512)
- ECDSA keys with
  - P256 curve
  - P384 curve
  - P512 curve
- ED25519 keys

# How the Problems Are Solved

Keys are serialized for storage using the JSON web key format from the
`jose` library. This is the format that will be used by OIDC for
signing, verification and publication.

Each instance can have a number of key pairs. All existing public keys
are meant to be used for token verification and publication the keys
endpoint. Keys can be activated and the active private key is meant to
sign new tokens. There is always exactly 1 active signing key:

1. When the first key for an instance is generated, it is automatically
activated.
2. Activation of the next key automatically deactivates the previously
active key.
3. Keys cannot be manually deactivated from the API
4. Active keys cannot be deleted

# Additional Changes

- Query methods that later will be used by the OIDC package are already
implemented. Preparation for #8031
- Fix indentation in french translation for instance event
- Move user_schema translations to consistent positions in all
translation files

# Additional Context

- Closes #8030
- Part of #7809

---------

Co-authored-by: Elio Bischof <elio@zitadel.com>
This commit is contained in:
Tim Möhlmann 2024-08-14 17:18:14 +03:00 committed by GitHub
parent e2e1100124
commit 64a3bb3149
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
91 changed files with 5133 additions and 256 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,7 @@
package initialise
import (
"github.com/mitchellh/mapstructure"
"github.com/spf13/viper"
"github.com/zitadel/logging"
@ -17,7 +18,10 @@ type Config struct {
func MustNewConfig(v *viper.Viper) *Config {
config := new(Config)
err := v.Unmarshal(config,
viper.DecodeHook(database.DecodeHook),
viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
database.DecodeHook,
mapstructure.TextUnmarshallerHookFunc(),
)),
)
logging.OnError(err).Fatal("unable to read config")

View File

@ -74,6 +74,7 @@ func mustNewConfig(v *viper.Viper, config any) {
database.DecodeHook,
actions.HTTPConfigDecodeHook,
hook.EnumHookFunc(internal_authz.MemberTypeString),
mapstructure.TextUnmarshallerHookFunc(),
)),
)
logging.OnError(err).Fatal("unable to read default config")

View File

@ -27,6 +27,7 @@ func MustNewConfig(v *viper.Viper) *Config {
mapstructure.StringToTimeHookFunc(time.RFC3339),
mapstructure.StringToSliceHookFunc(","),
hook.EnumHookFunc(internal_authz.MemberTypeString),
mapstructure.TextUnmarshallerHookFunc(),
)),
)
logging.OnError(err).Fatal("unable to read default config")

View File

@ -74,6 +74,7 @@ func MustNewConfig(v *viper.Viper) *Config {
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToTimeHookFunc(time.RFC3339),
mapstructure.StringToSliceHookFunc(","),
mapstructure.TextUnmarshallerHookFunc(),
)),
)
logging.OnError(err).Fatal("unable to read default config")
@ -139,6 +140,7 @@ func MustNewSteps(v *viper.Viper) *Steps {
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToTimeHookFunc(time.RFC3339),
mapstructure.StringToSliceHookFunc(","),
mapstructure.TextUnmarshallerHookFunc(),
)),
)
logging.OnError(err).Fatal("unable to read steps")

View File

@ -100,6 +100,7 @@ func MustNewConfig(v *viper.Viper) *Config {
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToTimeHookFunc(time.RFC3339),
mapstructure.StringToSliceHookFunc(","),
mapstructure.TextUnmarshallerHookFunc(),
)),
)
logging.OnError(err).Fatal("unable to read config")

View File

@ -44,6 +44,7 @@ import (
org_v2 "github.com/zitadel/zitadel/internal/api/grpc/org/v2"
org_v2beta "github.com/zitadel/zitadel/internal/api/grpc/org/v2beta"
action_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/action/v3alpha"
"github.com/zitadel/zitadel/internal/api/grpc/resources/webkey/v3"
session_v2 "github.com/zitadel/zitadel/internal/api/grpc/session/v2"
session_v2beta "github.com/zitadel/zitadel/internal/api/grpc/session/v2beta"
settings_v2 "github.com/zitadel/zitadel/internal/api/grpc/settings/v2"
@ -442,6 +443,9 @@ func startAPIs(
if err := apis.RegisterService(ctx, user_schema_v3_alpha.CreateServer(commands, queries)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, webkey.CreateServer(commands, queries)); err != nil {
return nil, err
}
instanceInterceptor := middleware.InstanceInterceptor(queries, config.ExternalDomain, login.IgnoreInstanceEndpoints...)
assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge)
apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle))

View File

@ -340,6 +340,14 @@ module.exports = {
categoryLinkSource: "auto",
},
},
webkey_v3: {
specPath: ".artifacts/openapi/zitadel/resources/webkey/v3alpha/webkey_service.swagger.json",
outputDir: "docs/apis/resources/webkey_service_v3",
sidebarOptions: {
groupPathsBy: "tag",
categoryLinkSource: "auto",
},
},
feature_v2: {
specPath: ".artifacts/openapi/zitadel/feature/v2/feature_service.swagger.json",
outputDir: "docs/apis/resources/feature_service_v2",

View File

@ -732,6 +732,20 @@ module.exports = {
},
items: require("./docs/apis/resources/action_service_v3/sidebar.ts"),
},
{
type: "category",
label: "Web key Lifecycle (Preview)",
link: {
type: "generated-index",
title: "Action Service API (Preview)",
slug: "/apis/resources/action_service_v3",
description:
"This API is intended to manage web keys for a ZITADEL instance, used to sign and validate OIDC tokens.\n" +
"\n" +
"This project is in preview state. It can AND will continue breaking until a stable version is released.",
},
items: require("./docs/apis/resources/webkey_service_v3/sidebar.ts"),
},
],
},
{

View File

@ -42,6 +42,7 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *comm
TokenExchange: req.OidcTokenExchange,
Actions: req.Actions,
ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance),
WebKey: req.WebKey,
}
}
@ -55,6 +56,7 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat
OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange),
Actions: featureSourceToFlagPb(&f.Actions),
ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance),
WebKey: featureSourceToFlagPb(&f.WebKey),
}
}

View File

@ -123,6 +123,7 @@ func Test_instanceFeaturesToCommand(t *testing.T) {
OidcTokenExchange: gu.Ptr(true),
Actions: gu.Ptr(true),
ImprovedPerformance: nil,
WebKey: gu.Ptr(true),
}
want := &command.InstanceFeatures{
LoginDefaultOrg: gu.Ptr(true),
@ -132,6 +133,7 @@ func Test_instanceFeaturesToCommand(t *testing.T) {
TokenExchange: gu.Ptr(true),
Actions: gu.Ptr(true),
ImprovedPerformance: nil,
WebKey: gu.Ptr(true),
}
got := instanceFeaturesToCommand(arg)
assert.Equal(t, want, got)
@ -172,6 +174,10 @@ func Test_instanceFeaturesToPb(t *testing.T) {
Level: feature.LevelSystem,
Value: []feature.ImprovedPerformanceType{feature.ImprovedPerformanceTypeOrgByID},
},
WebKey: query.FeatureSource[bool]{
Level: feature.LevelInstance,
Value: true,
},
}
want := &feature_pb.GetInstanceFeaturesResponse{
Details: &object.Details{
@ -207,6 +213,10 @@ func Test_instanceFeaturesToPb(t *testing.T) {
ExecutionPaths: []feature_pb.ImprovedPerformance{feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID},
Source: feature_pb.Source_SOURCE_SYSTEM,
},
WebKey: &feature_pb.FeatureFlag{
Enabled: true,
Source: feature_pb.Source_SOURCE_INSTANCE,
},
}
got := instanceFeaturesToPb(arg)
assert.Equal(t, want, got)

View File

@ -42,6 +42,7 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *comm
TokenExchange: req.OidcTokenExchange,
Actions: req.Actions,
ImprovedPerformance: improvedPerformanceListToDomain(req.ImprovedPerformance),
WebKey: req.WebKey,
}
}
@ -55,6 +56,7 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat
OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange),
Actions: featureSourceToFlagPb(&f.Actions),
ImprovedPerformance: featureSourceToImprovedPerformanceFlagPb(&f.ImprovedPerformance),
WebKey: featureSourceToFlagPb(&f.WebKey),
}
}

View File

@ -123,6 +123,7 @@ func Test_instanceFeaturesToCommand(t *testing.T) {
OidcTokenExchange: gu.Ptr(true),
Actions: gu.Ptr(true),
ImprovedPerformance: nil,
WebKey: gu.Ptr(true),
}
want := &command.InstanceFeatures{
LoginDefaultOrg: gu.Ptr(true),
@ -132,6 +133,7 @@ func Test_instanceFeaturesToCommand(t *testing.T) {
TokenExchange: gu.Ptr(true),
Actions: gu.Ptr(true),
ImprovedPerformance: nil,
WebKey: gu.Ptr(true),
}
got := instanceFeaturesToCommand(arg)
assert.Equal(t, want, got)
@ -172,6 +174,10 @@ func Test_instanceFeaturesToPb(t *testing.T) {
Level: feature.LevelSystem,
Value: []feature.ImprovedPerformanceType{feature.ImprovedPerformanceTypeOrgByID},
},
WebKey: query.FeatureSource[bool]{
Level: feature.LevelInstance,
Value: true,
},
}
want := &feature_pb.GetInstanceFeaturesResponse{
Details: &object.Details{
@ -207,6 +213,10 @@ func Test_instanceFeaturesToPb(t *testing.T) {
ExecutionPaths: []feature_pb.ImprovedPerformance{feature_pb.ImprovedPerformance_IMPROVED_PERFORMANCE_ORG_BY_ID},
Source: feature_pb.Source_SOURCE_SYSTEM,
},
WebKey: &feature_pb.FeatureFlag{
Enabled: true,
Source: feature_pb.Source_SOURCE_INSTANCE,
},
}
got := instanceFeaturesToPb(arg)
assert.Equal(t, want, got)

View File

@ -188,8 +188,8 @@ func TestServer_SetExecution_Request(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// We want to have the same response no matter how often we call the function
Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
got, err := Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
got, err := Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
@ -326,8 +326,8 @@ func TestServer_SetExecution_Request_Include(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// We want to have the same response no matter how often we call the function
Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
got, err := Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
got, err := Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
@ -506,8 +506,8 @@ func TestServer_SetExecution_Response(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// We want to have the same response no matter how often we call the function
Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
got, err := Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
got, err := Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
@ -692,8 +692,8 @@ func TestServer_SetExecution_Event(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// We want to have the same response no matter how often we call the function
Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
got, err := Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
got, err := Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return
@ -791,8 +791,8 @@ func TestServer_SetExecution_Function(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// We want to have the same response no matter how often we call the function
Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
got, err := Tester.Client.ActionV3.SetExecution(tt.ctx, tt.req)
Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
got, err := Tester.Client.ActionV3Alpha.SetExecution(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return

View File

@ -248,7 +248,7 @@ func TestServer_ExecutionTarget(t *testing.T) {
defer close()
}
got, err := Tester.Client.ActionV3.GetTarget(tt.ctx, tt.req)
got, err := Tester.Client.ActionV3Alpha.GetTarget(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return

View File

@ -214,7 +214,7 @@ func TestServer_GetTarget(t *testing.T) {
err := tt.args.dep(tt.args.ctx, tt.args.req, tt.want)
require.NoError(t, err)
}
got, getErr := Tester.Client.ActionV3.GetTarget(tt.args.ctx, tt.args.req)
got, getErr := Tester.Client.ActionV3Alpha.GetTarget(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(t, getErr, "Error: "+getErr.Error())
} else {
@ -476,7 +476,7 @@ func TestServer_ListTargets(t *testing.T) {
}
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, listErr := Tester.Client.ActionV3.SearchTargets(tt.args.ctx, tt.args.req)
got, listErr := Tester.Client.ActionV3Alpha.SearchTargets(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(ttt, listErr, "Error: "+listErr.Error())
} else {
@ -864,7 +864,7 @@ func TestServer_SearchExecutions(t *testing.T) {
}
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
got, listErr := Tester.Client.ActionV3.SearchExecutions(tt.args.ctx, tt.args.req)
got, listErr := Tester.Client.ActionV3Alpha.SearchExecutions(tt.args.ctx, tt.args.req)
if tt.wantErr {
assert.Error(t, listErr, "Error: "+listErr.Error())
} else {

View File

@ -197,7 +197,7 @@ func TestServer_CreateTarget(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Tester.Client.ActionV3.CreateTarget(tt.ctx, &action.CreateTargetRequest{Target: tt.req})
got, err := Tester.Client.ActionV3Alpha.CreateTarget(tt.ctx, &action.CreateTargetRequest{Target: tt.req})
if tt.wantErr {
require.Error(t, err)
return
@ -382,8 +382,8 @@ func TestServer_PatchTarget(t *testing.T) {
err := tt.prepare(tt.args.req)
require.NoError(t, err)
// We want to have the same response no matter how often we call the function
Tester.Client.ActionV3.PatchTarget(tt.args.ctx, tt.args.req)
got, err := Tester.Client.ActionV3.PatchTarget(tt.args.ctx, tt.args.req)
Tester.Client.ActionV3Alpha.PatchTarget(tt.args.ctx, tt.args.req)
got, err := Tester.Client.ActionV3Alpha.PatchTarget(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
@ -438,7 +438,7 @@ func TestServer_DeleteTarget(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Tester.Client.ActionV3.DeleteTarget(tt.ctx, tt.req)
got, err := Tester.Client.ActionV3Alpha.DeleteTarget(tt.ctx, tt.req)
if tt.wantErr {
require.Error(t, err)
return

View File

@ -0,0 +1,47 @@
package webkey
import (
"google.golang.org/grpc"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/query"
webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
)
type Server struct {
webkey.UnimplementedZITADELWebKeysServer
command *command.Commands
query *query.Queries
}
func CreateServer(
command *command.Commands,
query *query.Queries,
) *Server {
return &Server{
command: command,
query: query,
}
}
func (s *Server) RegisterServer(grpcServer *grpc.Server) {
webkey.RegisterZITADELWebKeysServer(grpcServer, s)
}
func (s *Server) AppName() string {
return webkey.ZITADELWebKeys_ServiceDesc.ServiceName
}
func (s *Server) MethodPrefix() string {
return webkey.ZITADELWebKeys_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {
return webkey.ZITADELWebKeys_AuthMethods
}
func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
return webkey.RegisterZITADELWebKeysHandler
}

View File

@ -0,0 +1,87 @@
package webkey
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
)
func (s *Server) CreateWebKey(ctx context.Context, req *webkey.CreateWebKeyRequest) (_ *webkey.CreateWebKeyResponse, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if err = checkWebKeyFeature(ctx); err != nil {
return nil, err
}
webKey, err := s.command.CreateWebKey(ctx, createWebKeyRequestToConfig(req))
if err != nil {
return nil, err
}
return &webkey.CreateWebKeyResponse{
Details: resource_object.DomainToDetailsPb(webKey.ObjectDetails, object.OwnerType_OWNER_TYPE_INSTANCE, authz.GetInstance(ctx).InstanceID()),
}, nil
}
func (s *Server) ActivateWebKey(ctx context.Context, req *webkey.ActivateWebKeyRequest) (_ *webkey.ActivateWebKeyResponse, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if err = checkWebKeyFeature(ctx); err != nil {
return nil, err
}
details, err := s.command.ActivateWebKey(ctx, req.GetId())
if err != nil {
return nil, err
}
return &webkey.ActivateWebKeyResponse{
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, authz.GetInstance(ctx).InstanceID()),
}, nil
}
func (s *Server) DeleteWebKey(ctx context.Context, req *webkey.DeleteWebKeyRequest) (_ *webkey.DeleteWebKeyResponse, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if err = checkWebKeyFeature(ctx); err != nil {
return nil, err
}
details, err := s.command.DeleteWebKey(ctx, req.GetId())
if err != nil {
return nil, err
}
return &webkey.DeleteWebKeyResponse{
Details: resource_object.DomainToDetailsPb(details, object.OwnerType_OWNER_TYPE_INSTANCE, authz.GetInstance(ctx).InstanceID()),
}, nil
}
func (s *Server) ListWebKeys(ctx context.Context, _ *webkey.ListWebKeysRequest) (_ *webkey.ListWebKeysResponse, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if err = checkWebKeyFeature(ctx); err != nil {
return nil, err
}
list, err := s.query.ListWebKeys(ctx)
if err != nil {
return nil, err
}
return &webkey.ListWebKeysResponse{
WebKeys: webKeyDetailsListToPb(list, authz.GetInstance(ctx).InstanceID()),
}, nil
}
func checkWebKeyFeature(ctx context.Context) error {
if !authz.GetFeatures(ctx).WebKey {
return zerrors.ThrowPreconditionFailed(nil, "WEBKEY-Ohx6E", "Errors.WebKey.FeatureDisabled")
}
return nil
}

View File

@ -0,0 +1,173 @@
package webkey
import (
resource_object "github.com/zitadel/zitadel/internal/api/grpc/resources/object/v3alpha"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
)
func createWebKeyRequestToConfig(req *webkey.CreateWebKeyRequest) crypto.WebKeyConfig {
switch config := req.GetKey().GetConfig().(type) {
case *webkey.WebKey_Rsa:
return webKeyRSAConfigToCrypto(config.Rsa)
case *webkey.WebKey_Ecdsa:
return webKeyECDSAConfigToCrypto(config.Ecdsa)
case *webkey.WebKey_Ed25519:
return new(crypto.WebKeyED25519Config)
default:
return webKeyRSAConfigToCrypto(nil)
}
}
func webKeyRSAConfigToCrypto(config *webkey.WebKeyRSAConfig) *crypto.WebKeyRSAConfig {
out := new(crypto.WebKeyRSAConfig)
switch config.GetBits() {
case webkey.WebKeyRSAConfig_RSA_BITS_UNSPECIFIED:
out.Bits = crypto.RSABits2048
case webkey.WebKeyRSAConfig_RSA_BITS_2048:
out.Bits = crypto.RSABits2048
case webkey.WebKeyRSAConfig_RSA_BITS_3072:
out.Bits = crypto.RSABits3072
case webkey.WebKeyRSAConfig_RSA_BITS_4096:
out.Bits = crypto.RSABits4096
default:
out.Bits = crypto.RSABits2048
}
switch config.GetHasher() {
case webkey.WebKeyRSAConfig_RSA_HASHER_UNSPECIFIED:
out.Hasher = crypto.RSAHasherSHA256
case webkey.WebKeyRSAConfig_RSA_HASHER_SHA256:
out.Hasher = crypto.RSAHasherSHA256
case webkey.WebKeyRSAConfig_RSA_HASHER_SHA384:
out.Hasher = crypto.RSAHasherSHA384
case webkey.WebKeyRSAConfig_RSA_HASHER_SHA512:
out.Hasher = crypto.RSAHasherSHA512
default:
out.Hasher = crypto.RSAHasherSHA256
}
return out
}
func webKeyECDSAConfigToCrypto(config *webkey.WebKeyECDSAConfig) *crypto.WebKeyECDSAConfig {
out := new(crypto.WebKeyECDSAConfig)
switch config.GetCurve() {
case webkey.WebKeyECDSAConfig_ECDSA_CURVE_UNSPECIFIED:
out.Curve = crypto.EllipticCurveP256
case webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256:
out.Curve = crypto.EllipticCurveP256
case webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384:
out.Curve = crypto.EllipticCurveP384
case webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512:
out.Curve = crypto.EllipticCurveP512
default:
out.Curve = crypto.EllipticCurveP256
}
return out
}
func webKeyDetailsListToPb(list []query.WebKeyDetails, instanceID string) []*webkey.GetWebKey {
out := make([]*webkey.GetWebKey, len(list))
for i := range list {
out[i] = webKeyDetailsToPb(&list[i], instanceID)
}
return out
}
func webKeyDetailsToPb(details *query.WebKeyDetails, instanceID string) *webkey.GetWebKey {
out := &webkey.GetWebKey{
Details: resource_object.DomainToDetailsPb(&domain.ObjectDetails{
ID: details.KeyID,
CreationDate: details.CreationDate,
EventDate: details.ChangeDate,
}, object.OwnerType_OWNER_TYPE_INSTANCE, instanceID),
State: webKeyStateToPb(details.State),
Config: &webkey.WebKey{},
}
switch config := details.Config.(type) {
case *crypto.WebKeyRSAConfig:
out.Config.Config = &webkey.WebKey_Rsa{
Rsa: webKeyRSAConfigToPb(config),
}
case *crypto.WebKeyECDSAConfig:
out.Config.Config = &webkey.WebKey_Ecdsa{
Ecdsa: webKeyECDSAConfigToPb(config),
}
case *crypto.WebKeyED25519Config:
out.Config.Config = &webkey.WebKey_Ed25519{
Ed25519: new(webkey.WebKeyED25519Config),
}
}
return out
}
func webKeyStateToPb(state domain.WebKeyState) webkey.WebKeyState {
switch state {
case domain.WebKeyStateUnspecified:
return webkey.WebKeyState_STATE_UNSPECIFIED
case domain.WebKeyStateInitial:
return webkey.WebKeyState_STATE_INITIAL
case domain.WebKeyStateActive:
return webkey.WebKeyState_STATE_ACTIVE
case domain.WebKeyStateInactive:
return webkey.WebKeyState_STATE_INACTIVE
case domain.WebKeyStateRemoved:
return webkey.WebKeyState_STATE_REMOVED
default:
return webkey.WebKeyState_STATE_UNSPECIFIED
}
}
func webKeyRSAConfigToPb(config *crypto.WebKeyRSAConfig) *webkey.WebKeyRSAConfig {
out := new(webkey.WebKeyRSAConfig)
switch config.Bits {
case crypto.RSABitsUnspecified:
out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_UNSPECIFIED
case crypto.RSABits2048:
out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_2048
case crypto.RSABits3072:
out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_3072
case crypto.RSABits4096:
out.Bits = webkey.WebKeyRSAConfig_RSA_BITS_4096
}
switch config.Hasher {
case crypto.RSAHasherUnspecified:
out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_UNSPECIFIED
case crypto.RSAHasherSHA256:
out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_SHA256
case crypto.RSAHasherSHA384:
out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_SHA384
case crypto.RSAHasherSHA512:
out.Hasher = webkey.WebKeyRSAConfig_RSA_HASHER_SHA512
}
return out
}
func webKeyECDSAConfigToPb(config *crypto.WebKeyECDSAConfig) *webkey.WebKeyECDSAConfig {
out := new(webkey.WebKeyECDSAConfig)
switch config.Curve {
case crypto.EllipticCurveUnspecified:
out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_UNSPECIFIED
case crypto.EllipticCurveP256:
out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256
case crypto.EllipticCurveP384:
out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384
case crypto.EllipticCurveP512:
out.Curve = webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512
}
return out
}

View File

@ -0,0 +1,529 @@
package webkey
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
)
func Test_createWebKeyRequestToConfig(t *testing.T) {
type args struct {
req *webkey.CreateWebKeyRequest
}
tests := []struct {
name string
args args
want crypto.WebKeyConfig
}{
{
name: "RSA",
args: args{&webkey.CreateWebKeyRequest{
Key: &webkey.WebKey{
Config: &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384,
},
},
},
}},
want: &crypto.WebKeyRSAConfig{
Bits: crypto.RSABits3072,
Hasher: crypto.RSAHasherSHA384,
},
},
{
name: "ECDSA",
args: args{&webkey.CreateWebKeyRequest{
Key: &webkey.WebKey{
Config: &webkey.WebKey_Ecdsa{
Ecdsa: &webkey.WebKeyECDSAConfig{
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384,
},
},
},
}},
want: &crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
},
{
name: "ED25519",
args: args{&webkey.CreateWebKeyRequest{
Key: &webkey.WebKey{
Config: &webkey.WebKey_Ed25519{
Ed25519: &webkey.WebKeyED25519Config{},
},
},
}},
want: &crypto.WebKeyED25519Config{},
},
{
name: "default",
args: args{&webkey.CreateWebKeyRequest{}},
want: &crypto.WebKeyRSAConfig{
Bits: crypto.RSABits2048,
Hasher: crypto.RSAHasherSHA256,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := createWebKeyRequestToConfig(tt.args.req)
assert.Equal(t, tt.want, got)
})
}
}
func Test_webKeyRSAConfigToCrypto(t *testing.T) {
type args struct {
config *webkey.WebKeyRSAConfig
}
tests := []struct {
name string
args args
want *crypto.WebKeyRSAConfig
}{
{
name: "unspecified",
args: args{&webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_UNSPECIFIED,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_UNSPECIFIED,
}},
want: &crypto.WebKeyRSAConfig{
Bits: crypto.RSABits2048,
Hasher: crypto.RSAHasherSHA256,
},
},
{
name: "2048, RSA256",
args: args{&webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
}},
want: &crypto.WebKeyRSAConfig{
Bits: crypto.RSABits2048,
Hasher: crypto.RSAHasherSHA256,
},
},
{
name: "3072, RSA384",
args: args{&webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384,
}},
want: &crypto.WebKeyRSAConfig{
Bits: crypto.RSABits3072,
Hasher: crypto.RSAHasherSHA384,
},
},
{
name: "4096, RSA512",
args: args{&webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_4096,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA512,
}},
want: &crypto.WebKeyRSAConfig{
Bits: crypto.RSABits4096,
Hasher: crypto.RSAHasherSHA512,
},
},
{
name: "invalid",
args: args{&webkey.WebKeyRSAConfig{
Bits: 99,
Hasher: 99,
}},
want: &crypto.WebKeyRSAConfig{
Bits: crypto.RSABits2048,
Hasher: crypto.RSAHasherSHA256,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := webKeyRSAConfigToCrypto(tt.args.config)
assert.Equal(t, tt.want, got)
})
}
}
func Test_webKeyECDSAConfigToCrypto(t *testing.T) {
type args struct {
config *webkey.WebKeyECDSAConfig
}
tests := []struct {
name string
args args
want *crypto.WebKeyECDSAConfig
}{
{
name: "unspecified",
args: args{&webkey.WebKeyECDSAConfig{
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_UNSPECIFIED,
}},
want: &crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP256,
},
},
{
name: "P256",
args: args{&webkey.WebKeyECDSAConfig{
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256,
}},
want: &crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP256,
},
},
{
name: "P384",
args: args{&webkey.WebKeyECDSAConfig{
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384,
}},
want: &crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
},
{
name: "P512",
args: args{&webkey.WebKeyECDSAConfig{
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512,
}},
want: &crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP512,
},
},
{
name: "invalid",
args: args{&webkey.WebKeyECDSAConfig{
Curve: 99,
}},
want: &crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP256,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := webKeyECDSAConfigToCrypto(tt.args.config)
assert.Equal(t, tt.want, got)
})
}
}
func Test_webKeyDetailsListToPb(t *testing.T) {
instanceID := "ownerid"
list := []query.WebKeyDetails{
{
KeyID: "key1",
CreationDate: time.Unix(123, 456),
ChangeDate: time.Unix(789, 0),
Sequence: 123,
State: domain.WebKeyStateActive,
Config: &crypto.WebKeyRSAConfig{
Bits: crypto.RSABits3072,
Hasher: crypto.RSAHasherSHA384,
},
},
{
KeyID: "key2",
CreationDate: time.Unix(123, 456),
ChangeDate: time.Unix(789, 0),
Sequence: 123,
State: domain.WebKeyStateActive,
Config: &crypto.WebKeyED25519Config{},
},
}
want := []*webkey.GetWebKey{
{
Details: &resource_object.Details{
Id: "key1",
Created: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
Changed: &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID},
},
State: webkey.WebKeyState_STATE_ACTIVE,
Config: &webkey.WebKey{
Config: &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384,
},
},
},
},
{
Details: &resource_object.Details{
Id: "key2",
Created: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
Changed: &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID},
},
State: webkey.WebKeyState_STATE_ACTIVE,
Config: &webkey.WebKey{
Config: &webkey.WebKey_Ed25519{
Ed25519: &webkey.WebKeyED25519Config{},
},
},
},
}
got := webKeyDetailsListToPb(list, instanceID)
assert.Equal(t, want, got)
}
func Test_webKeyDetailsToPb(t *testing.T) {
instanceID := "ownerid"
type args struct {
details *query.WebKeyDetails
}
tests := []struct {
name string
args args
want *webkey.GetWebKey
}{
{
name: "RSA",
args: args{&query.WebKeyDetails{
KeyID: "keyID",
CreationDate: time.Unix(123, 456),
ChangeDate: time.Unix(789, 0),
Sequence: 123,
State: domain.WebKeyStateActive,
Config: &crypto.WebKeyRSAConfig{
Bits: crypto.RSABits3072,
Hasher: crypto.RSAHasherSHA384,
},
}},
want: &webkey.GetWebKey{
Details: &resource_object.Details{
Id: "keyID",
Created: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
Changed: &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID},
},
State: webkey.WebKeyState_STATE_ACTIVE,
Config: &webkey.WebKey{
Config: &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384,
},
},
},
},
},
{
name: "ECDSA",
args: args{&query.WebKeyDetails{
KeyID: "keyID",
CreationDate: time.Unix(123, 456),
ChangeDate: time.Unix(789, 0),
Sequence: 123,
State: domain.WebKeyStateActive,
Config: &crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
}},
want: &webkey.GetWebKey{
Details: &resource_object.Details{
Id: "keyID",
Created: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
Changed: &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID},
},
State: webkey.WebKeyState_STATE_ACTIVE,
Config: &webkey.WebKey{
Config: &webkey.WebKey_Ecdsa{
Ecdsa: &webkey.WebKeyECDSAConfig{
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384,
},
},
},
},
},
{
name: "ED25519",
args: args{&query.WebKeyDetails{
KeyID: "keyID",
CreationDate: time.Unix(123, 456),
ChangeDate: time.Unix(789, 0),
Sequence: 123,
State: domain.WebKeyStateActive,
Config: &crypto.WebKeyED25519Config{},
}},
want: &webkey.GetWebKey{
Details: &resource_object.Details{
Id: "keyID",
Created: &timestamppb.Timestamp{Seconds: 123, Nanos: 456},
Changed: &timestamppb.Timestamp{Seconds: 789, Nanos: 0},
Owner: &object.Owner{Type: object.OwnerType_OWNER_TYPE_INSTANCE, Id: instanceID},
},
State: webkey.WebKeyState_STATE_ACTIVE,
Config: &webkey.WebKey{
Config: &webkey.WebKey_Ed25519{
Ed25519: &webkey.WebKeyED25519Config{},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := webKeyDetailsToPb(tt.args.details, instanceID)
assert.Equal(t, tt.want, got)
})
}
}
func Test_webKeyStateToPb(t *testing.T) {
type args struct {
state domain.WebKeyState
}
tests := []struct {
name string
args args
want webkey.WebKeyState
}{
{
name: "unspecified",
args: args{domain.WebKeyStateUnspecified},
want: webkey.WebKeyState_STATE_UNSPECIFIED,
},
{
name: "initial",
args: args{domain.WebKeyStateInitial},
want: webkey.WebKeyState_STATE_INITIAL,
},
{
name: "active",
args: args{domain.WebKeyStateActive},
want: webkey.WebKeyState_STATE_ACTIVE,
},
{
name: "inactive",
args: args{domain.WebKeyStateInactive},
want: webkey.WebKeyState_STATE_INACTIVE,
},
{
name: "removed",
args: args{domain.WebKeyStateRemoved},
want: webkey.WebKeyState_STATE_REMOVED,
},
{
name: "invalid",
args: args{99},
want: webkey.WebKeyState_STATE_UNSPECIFIED,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := webKeyStateToPb(tt.args.state)
assert.Equal(t, tt.want, got)
})
}
}
func Test_webKeyRSAConfigToPb(t *testing.T) {
type args struct {
config *crypto.WebKeyRSAConfig
}
tests := []struct {
name string
args args
want *webkey.WebKeyRSAConfig
}{
{
name: "2048, RSA256",
args: args{&crypto.WebKeyRSAConfig{
Bits: crypto.RSABits2048,
Hasher: crypto.RSAHasherSHA256,
}},
want: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
},
},
{
name: "3072, RSA384",
args: args{&crypto.WebKeyRSAConfig{
Bits: crypto.RSABits3072,
Hasher: crypto.RSAHasherSHA384,
}},
want: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_3072,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA384,
},
},
{
name: "4096, RSA512",
args: args{&crypto.WebKeyRSAConfig{
Bits: crypto.RSABits4096,
Hasher: crypto.RSAHasherSHA512,
}},
want: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_4096,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA512,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := webKeyRSAConfigToPb(tt.args.config)
assert.Equal(t, tt.want, got)
})
}
}
func Test_webKeyECDSAConfigToPb(t *testing.T) {
type args struct {
config *crypto.WebKeyECDSAConfig
}
tests := []struct {
name string
args args
want *webkey.WebKeyECDSAConfig
}{
{
name: "P256",
args: args{&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP256,
}},
want: &webkey.WebKeyECDSAConfig{
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P256,
},
},
{
name: "P384",
args: args{&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
}},
want: &webkey.WebKeyECDSAConfig{
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P384,
},
},
{
name: "P512",
args: args{&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP512,
}},
want: &webkey.WebKeyECDSAConfig{
Curve: webkey.WebKeyECDSAConfig_ECDSA_CURVE_P512,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := webKeyECDSAConfigToPb(tt.args.config)
assert.Equal(t, tt.want, got)
})
}
}

View File

@ -0,0 +1,245 @@
//go:build integration
package webkey_test
import (
"context"
"net"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
object "github.com/zitadel/zitadel/pkg/grpc/object/v3alpha"
resource_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
webkey "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
)
var (
CTX context.Context
SystemCTX context.Context
Tester *integration.Tester
)
func TestMain(m *testing.M) {
os.Exit(func() int {
ctx, _, cancel := integration.Contexts(time.Hour)
defer cancel()
Tester = integration.NewTester(ctx)
defer Tester.Done()
SystemCTX = Tester.WithAuthorization(ctx, integration.SystemUser)
CTX = Tester.WithAuthorization(ctx, integration.IAMOwner)
return m.Run()
}())
}
func TestServer_Feature_Disabled(t *testing.T) {
client, iamCTX := createInstanceAndClients(t, false)
t.Run("CreateWebKey", func(t *testing.T) {
_, err := client.CreateWebKey(iamCTX, &webkey.CreateWebKeyRequest{})
assertFeatureDisabledError(t, err)
})
t.Run("ActivateWebKey", func(t *testing.T) {
_, err := client.ActivateWebKey(iamCTX, &webkey.ActivateWebKeyRequest{
Id: "1",
})
assertFeatureDisabledError(t, err)
})
t.Run("DeleteWebKey", func(t *testing.T) {
_, err := client.DeleteWebKey(iamCTX, &webkey.DeleteWebKeyRequest{
Id: "1",
})
assertFeatureDisabledError(t, err)
})
t.Run("ListWebKeys", func(t *testing.T) {
_, err := client.ListWebKeys(iamCTX, &webkey.ListWebKeysRequest{})
assertFeatureDisabledError(t, err)
})
}
func TestServer_ListWebKeys(t *testing.T) {
client, iamCtx := createInstanceAndClients(t, true)
// After the feature is first enabled, we can expect 2 generated keys with the default config.
checkWebKeyListState(iamCtx, t, client, 2, "", &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
},
})
}
func TestServer_CreateWebKey(t *testing.T) {
client, iamCtx := createInstanceAndClients(t, true)
_, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{
Key: &webkey.WebKey{
Config: &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
},
},
},
})
require.NoError(t, err)
checkWebKeyListState(iamCtx, t, client, 3, "", &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
},
})
}
func TestServer_ActivateWebKey(t *testing.T) {
client, iamCtx := createInstanceAndClients(t, true)
resp, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{
Key: &webkey.WebKey{
Config: &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
},
},
},
})
require.NoError(t, err)
_, err = client.ActivateWebKey(iamCtx, &webkey.ActivateWebKeyRequest{
Id: resp.GetDetails().GetId(),
})
require.NoError(t, err)
checkWebKeyListState(iamCtx, t, client, 3, resp.GetDetails().GetId(), &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
},
})
}
func TestServer_DeleteWebKey(t *testing.T) {
client, iamCtx := createInstanceAndClients(t, true)
keyIDs := make([]string, 2)
for i := 0; i < 2; i++ {
resp, err := client.CreateWebKey(iamCtx, &webkey.CreateWebKeyRequest{
Key: &webkey.WebKey{
Config: &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
},
},
},
})
require.NoError(t, err)
keyIDs[i] = resp.GetDetails().GetId()
}
_, err := client.ActivateWebKey(iamCtx, &webkey.ActivateWebKeyRequest{
Id: keyIDs[0],
})
require.NoError(t, err)
ok := t.Run("cannot delete active key", func(t *testing.T) {
_, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{
Id: keyIDs[0],
})
require.Error(t, err)
s := status.Convert(err)
assert.Equal(t, codes.FailedPrecondition, s.Code())
assert.Contains(t, s.Message(), "COMMAND-Chai1")
})
if !ok {
return
}
ok = t.Run("delete inactive key", func(t *testing.T) {
_, err := client.DeleteWebKey(iamCtx, &webkey.DeleteWebKeyRequest{
Id: keyIDs[1],
})
require.NoError(t, err)
})
if !ok {
return
}
// There are 2 keys from feature setup, +2 created, -1 deleted = 3
checkWebKeyListState(iamCtx, t, client, 3, keyIDs[0], &webkey.WebKey_Rsa{
Rsa: &webkey.WebKeyRSAConfig{
Bits: webkey.WebKeyRSAConfig_RSA_BITS_2048,
Hasher: webkey.WebKeyRSAConfig_RSA_HASHER_SHA256,
},
})
}
func createInstanceAndClients(t *testing.T, enableFeature bool) (webkey.ZITADELWebKeysClient, context.Context) {
domain, _, _, iamCTX := Tester.UseIsolatedInstance(t, CTX, SystemCTX)
cc, err := grpc.NewClient(
net.JoinHostPort(domain, "8080"),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
require.NoError(t, err)
if enableFeature {
features := feature.NewFeatureServiceClient(cc)
_, err = features.SetInstanceFeatures(iamCTX, &feature.SetInstanceFeaturesRequest{
WebKey: proto.Bool(true),
})
require.NoError(t, err)
time.Sleep(time.Second)
}
return webkey.NewZITADELWebKeysClient(cc), iamCTX
}
func assertFeatureDisabledError(t *testing.T, err error) {
t.Helper()
require.Error(t, err)
s := status.Convert(err)
assert.Equal(t, codes.FailedPrecondition, s.Code())
assert.Contains(t, s.Message(), "WEBKEY-Ohx6E")
}
func checkWebKeyListState(ctx context.Context, t *testing.T, client webkey.ZITADELWebKeysClient, nKeys int, expectActiveKeyID string, config any) {
resp, err := client.ListWebKeys(ctx, &webkey.ListWebKeysRequest{})
require.NoError(t, err)
list := resp.GetWebKeys()
require.Len(t, list, nKeys)
now := time.Now()
var gotActiveKeyID string
for _, key := range list {
integration.AssertResourceDetails(t, &resource_object.Details{
Created: timestamppb.Now(),
Changed: timestamppb.Now(),
Owner: &object.Owner{
Type: object.OwnerType_OWNER_TYPE_INSTANCE,
Id: Tester.Instance.InstanceID(),
},
}, key.GetDetails())
assert.WithinRange(t, key.GetDetails().GetChanged().AsTime(), now.Add(-time.Minute), now.Add(time.Minute))
assert.NotEqual(t, webkey.WebKeyState_STATE_UNSPECIFIED, key.GetState())
assert.NotEqual(t, webkey.WebKeyState_STATE_REMOVED, key.GetState())
assert.Equal(t, config, key.GetConfig().GetConfig())
if key.GetState() == webkey.WebKeyState_STATE_ACTIVE {
gotActiveKeyID = key.GetDetails().GetId()
}
}
assert.NotEmpty(t, gotActiveKeyID)
if expectActiveKeyID != "" {
assert.Equal(t, expectActiveKeyID, gotActiveKeyID)
}
}

View File

@ -14,7 +14,6 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/repository/instance"
@ -208,7 +207,7 @@ func (k keySetMap) getKey(keyID string) (*jose.JSONWebKey, error) {
return &jose.JSONWebKey{
Key: pubKey,
KeyID: keyID,
Use: domain.KeyUsageSigning.String(),
Use: crypto.KeyUsageSigning.String(),
}, nil
}

View File

@ -12,14 +12,14 @@ import (
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/query"
)
type publicKey struct {
id string
alg string
use domain.KeyUsage
use crypto.KeyUsage
seq uint64
expiry time.Time
key any
@ -33,7 +33,7 @@ func (k *publicKey) Algorithm() string {
return k.alg
}
func (k *publicKey) Use() domain.KeyUsage {
func (k *publicKey) Use() crypto.KeyUsage {
return k.use
}
@ -55,21 +55,21 @@ var (
"key1": {
id: "key1",
alg: "alg",
use: domain.KeyUsageSigning,
use: crypto.KeyUsageSigning,
seq: 1,
expiry: clock.Now().Add(time.Minute),
},
"key2": {
id: "key2",
alg: "alg",
use: domain.KeyUsageSigning,
use: crypto.KeyUsageSigning,
seq: 3,
expiry: clock.Now().Add(10 * time.Hour),
},
"exp1": {
id: "key2",
alg: "alg",
use: domain.KeyUsageSigning,
use: crypto.KeyUsageSigning,
seq: 4,
expiry: clock.Now().Add(-time.Hour),
},

View File

@ -11,7 +11,6 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/repository/instance"
@ -53,7 +52,7 @@ func (c *CertificateAndKey) ID() string {
return c.id
}
func (p *Storage) GetCertificateAndKey(ctx context.Context, usage domain.KeyUsage) (certAndKey *key.CertificateAndKey, err error) {
func (p *Storage) GetCertificateAndKey(ctx context.Context, usage crypto.KeyUsage) (certAndKey *key.CertificateAndKey, err error) {
err = retry(func() error {
certAndKey, err = p.getCertificateAndKey(ctx, usage)
if err != nil {
@ -67,7 +66,7 @@ func (p *Storage) GetCertificateAndKey(ctx context.Context, usage domain.KeyUsag
return certAndKey, err
}
func (p *Storage) getCertificateAndKey(ctx context.Context, usage domain.KeyUsage) (*key.CertificateAndKey, error) {
func (p *Storage) getCertificateAndKey(ctx context.Context, usage crypto.KeyUsage) (*key.CertificateAndKey, error) {
certs, err := p.query.ActiveCertificates(ctx, time.Now().Add(gracefulPeriod), usage)
if err != nil {
return nil, err
@ -87,7 +86,7 @@ func (p *Storage) getCertificateAndKey(ctx context.Context, usage domain.KeyUsag
func (p *Storage) refreshCertificate(
ctx context.Context,
usage domain.KeyUsage,
usage crypto.KeyUsage,
position float64,
) error {
ok, err := p.ensureIsLatestCertificate(ctx, position)
@ -112,7 +111,7 @@ func (p *Storage) ensureIsLatestCertificate(ctx context.Context, position float6
return position >= maxSequence, nil
}
func (p *Storage) lockAndGenerateCertificateAndKey(ctx context.Context, usage domain.KeyUsage) error {
func (p *Storage) lockAndGenerateCertificateAndKey(ctx context.Context, usage crypto.KeyUsage) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
ctx = setSAMLCtx(ctx)
@ -128,8 +127,8 @@ func (p *Storage) lockAndGenerateCertificateAndKey(ctx context.Context, usage do
}
switch usage {
case domain.KeyUsageSAMLMetadataSigning, domain.KeyUsageSAMLResponseSinging:
certAndKey, err := p.GetCertificateAndKey(ctx, domain.KeyUsageSAMLCA)
case crypto.KeyUsageSAMLMetadataSigning, crypto.KeyUsageSAMLResponseSinging:
certAndKey, err := p.GetCertificateAndKey(ctx, crypto.KeyUsageSAMLCA)
if err != nil {
return fmt.Errorf("error while reading ca certificate: %w", err)
}
@ -138,14 +137,14 @@ func (p *Storage) lockAndGenerateCertificateAndKey(ctx context.Context, usage do
}
switch usage {
case domain.KeyUsageSAMLMetadataSigning:
case crypto.KeyUsageSAMLMetadataSigning:
return p.command.GenerateSAMLMetadataCertificate(setSAMLCtx(ctx), p.certificateAlgorithm, certAndKey.Key, certAndKey.Certificate)
case domain.KeyUsageSAMLResponseSinging:
case crypto.KeyUsageSAMLResponseSinging:
return p.command.GenerateSAMLResponseCertificate(setSAMLCtx(ctx), p.certificateAlgorithm, certAndKey.Key, certAndKey.Certificate)
default:
return fmt.Errorf("unknown usage")
}
case domain.KeyUsageSAMLCA:
case crypto.KeyUsageSAMLCA:
return p.command.GenerateSAMLCACertificate(setSAMLCtx(ctx), p.certificateAlgorithm)
default:
return fmt.Errorf("unknown certificate usage")

View File

@ -87,15 +87,15 @@ func (p *Storage) Health(context.Context) error {
}
func (p *Storage) GetCA(ctx context.Context) (*key.CertificateAndKey, error) {
return p.GetCertificateAndKey(ctx, domain.KeyUsageSAMLCA)
return p.GetCertificateAndKey(ctx, crypto.KeyUsageSAMLCA)
}
func (p *Storage) GetMetadataSigningKey(ctx context.Context) (*key.CertificateAndKey, error) {
return p.GetCertificateAndKey(ctx, domain.KeyUsageSAMLMetadataSigning)
return p.GetCertificateAndKey(ctx, crypto.KeyUsageSAMLMetadataSigning)
}
func (p *Storage) GetResponseSigningKey(ctx context.Context) (*key.CertificateAndKey, error) {
return p.GetCertificateAndKey(ctx, domain.KeyUsageSAMLResponseSinging)
return p.GetCertificateAndKey(ctx, crypto.KeyUsageSAMLResponseSinging)
}
func (p *Storage) CreateAuthRequest(ctx context.Context, req *samlp.AuthnRequestType, acsUrl, protocolBinding, relayState, applicationID string) (_ models.AuthRequestInt, err error) {

View File

@ -13,6 +13,7 @@ import (
"sync"
"time"
"github.com/go-jose/go-jose/v4"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/api/authz"
@ -76,6 +77,7 @@ type Commands struct {
defaultSecretGenerators *SecretGenerators
samlCertificateAndKeyGenerator func(id string) ([]byte, []byte, error)
webKeyGenerator func(keyID string, alg crypto.EncryptionAlgorithm, genConfig crypto.WebKeyConfig) (encryptedPrivate *crypto.CryptoValue, public *jose.JSONWebKey, err error)
GrpcMethodExisting func(method string) bool
GrpcServiceExisting func(method string) bool
@ -157,6 +159,7 @@ func StartCommands(
defaultRefreshTokenIdleLifetime: defaultRefreshTokenIdleLifetime,
defaultSecretGenerators: defaultSecretGenerators,
samlCertificateAndKeyGenerator: samlCertificateAndKeyGenerator(defaults.KeyConfig.CertificateSize, defaults.KeyConfig.CertificateLifetime),
webKeyGenerator: crypto.GenerateEncryptedWebKey,
// always true for now until we can check with an eventlist
EventExisting: func(event string) bool { return true },
// always true for now until we can check with an eventlist

View File

@ -40,6 +40,14 @@ type InstanceSetup struct {
DefaultLanguage language.Tag
Org InstanceOrgSetup
SecretGenerators *SecretGenerators
WebKeys struct {
Type crypto.WebKeyConfigType
Config struct {
RSABits crypto.RSABits
RSAHasher crypto.RSAHasher
EllipticCurve crypto.EllipticCurve
}
}
PasswordComplexityPolicy struct {
MinLength uint64
HasLowercase bool
@ -267,6 +275,9 @@ func setUpInstance(ctx context.Context, c *Commands, setup *InstanceSetup) (vali
return nil, nil, nil, err
}
setupSMTPSettings(c, &validations, setup.SMTPConfiguration, instanceAgg)
if err := setupWebKeys(c, &validations, setup.zitadel.instanceID, setup); err != nil {
return nil, nil, nil, err
}
setupOIDCSettings(c, &validations, setup.OIDCSettings, instanceAgg)
setupFeatures(&validations, setup.Features, setup.zitadel.instanceID)
setupLimits(c, &validations, limits.NewAggregate(setup.zitadel.limitsID, setup.zitadel.instanceID), setup.Limits)
@ -390,6 +401,29 @@ func setupFeatures(validations *[]preparation.Validation, features *InstanceFeat
}
}
func setupWebKeys(c *Commands, validations *[]preparation.Validation, instanceID string, setup *InstanceSetup) error {
var conf crypto.WebKeyConfig
switch setup.WebKeys.Type {
case crypto.WebKeyConfigTypeUnspecified:
return nil // config disabled, skip
case crypto.WebKeyConfigTypeRSA:
conf = &crypto.WebKeyRSAConfig{
Bits: setup.WebKeys.Config.RSABits,
Hasher: setup.WebKeys.Config.RSAHasher,
}
case crypto.WebKeyConfigTypeECDSA:
conf = &crypto.WebKeyECDSAConfig{
Curve: setup.WebKeys.Config.EllipticCurve,
}
case crypto.WebKeyConfigTypeED25519:
conf = &crypto.WebKeyED25519Config{}
default:
return zerrors.ThrowInternalf(nil, "COMMAND-sieX0", "Errors.Internal unknown web key type %q", setup.WebKeys.Type)
}
*validations = append(*validations, c.prepareGenerateInitialWebKeys(instanceID, conf))
return nil
}
func setupOIDCSettings(commands *Commands, validations *[]preparation.Validation, oidcSettings *OIDCSettings, instanceAgg *instance.Aggregate) {
if oidcSettings == nil {
return

View File

@ -3,8 +3,11 @@ package command
import (
"context"
"github.com/muhlemmer/gu"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/feature"
@ -20,6 +23,7 @@ type InstanceFeatures struct {
TokenExchange *bool
Actions *bool
ImprovedPerformance []feature.ImprovedPerformanceType
WebKey *bool
}
func (m *InstanceFeatures) isEmpty() bool {
@ -30,7 +34,8 @@ func (m *InstanceFeatures) isEmpty() bool {
m.TokenExchange == nil &&
m.Actions == nil &&
// nil check to allow unset improvements
m.ImprovedPerformance == nil
m.ImprovedPerformance == nil &&
m.WebKey == nil
}
func (c *Commands) SetInstanceFeatures(ctx context.Context, f *InstanceFeatures) (*domain.ObjectDetails, error) {
@ -41,6 +46,9 @@ func (c *Commands) SetInstanceFeatures(ctx context.Context, f *InstanceFeatures)
if err := c.eventstore.FilterToQueryReducer(ctx, wm); err != nil {
return nil, err
}
if err := c.setupWebKeyFeature(ctx, wm, f); err != nil {
return nil, err
}
commands := wm.setCommands(ctx, f)
if len(commands) == 0 {
return writeModelToObjectDetails(wm.WriteModel), nil
@ -61,6 +69,21 @@ func prepareSetFeatures(instanceID string, f *InstanceFeatures) preparation.Vali
}
}
// setupWebKeyFeature generates the initial web keys for the instance,
// if the feature is enabled in the request and the feature wasn't enabled already in the writeModel.
// [Commands.GenerateInitialWebKeys] checks if keys already exist and does nothing if that's the case.
// The default config of a RSA key with 2048 and the SHA256 hasher is assumed.
// Users can customize this after using the webkey/v3 API.
func (c *Commands) setupWebKeyFeature(ctx context.Context, wm *InstanceFeaturesWriteModel, f *InstanceFeatures) error {
if !gu.Value(f.WebKey) || gu.Value(wm.WebKey) {
return nil
}
return c.GenerateInitialWebKeys(ctx, &crypto.WebKeyRSAConfig{
Bits: crypto.RSABits2048,
Hasher: crypto.RSAHasherSHA256,
})
}
func (c *Commands) ResetInstanceFeatures(ctx context.Context) (*domain.ObjectDetails, error) {
instanceID := authz.GetInstance(ctx).InstanceID()
wm := NewInstanceFeaturesWriteModel(instanceID)

View File

@ -67,6 +67,7 @@ func (m *InstanceFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
feature_v2.InstanceTokenExchangeEventType,
feature_v2.InstanceActionsEventType,
feature_v2.InstanceImprovedPerformanceEventType,
feature_v2.InstanceWebKeyEventType,
).
Builder().ResourceOwner(m.ResourceOwner)
}
@ -100,6 +101,9 @@ func reduceInstanceFeature(features *InstanceFeatures, key feature.Key, value an
case feature.KeyImprovedPerformance:
v := value.([]feature.ImprovedPerformanceType)
features.ImprovedPerformance = v
case feature.KeyWebKey:
v := value.(bool)
features.WebKey = &v
}
}
@ -113,5 +117,6 @@ func (wm *InstanceFeaturesWriteModel) setCommands(ctx context.Context, f *Instan
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.UserSchema, f.UserSchema, feature_v2.InstanceUserSchemaEventType)
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.Actions, f.Actions, feature_v2.InstanceActionsEventType)
cmds = appendFeatureSliceUpdate(ctx, cmds, aggregate, wm.ImprovedPerformance, f.ImprovedPerformance, feature_v2.InstanceImprovedPerformanceEventType)
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.WebKey, f.WebKey, feature_v2.InstanceWebKeyEventType)
return cmds
}

View File

@ -10,7 +10,6 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/repository/keypair"
)
@ -32,7 +31,7 @@ func (c *Commands) GenerateSigningKeyPair(ctx context.Context, algorithm string)
_, err = c.eventstore.Push(ctx, keypair.NewAddedEvent(
ctx,
keyAgg,
domain.KeyUsageSigning,
crypto.KeyUsageSigning,
algorithm,
privateCrypto, publicCrypto,
privateKeyExp, publicKeyExp))
@ -69,7 +68,7 @@ func (c *Commands) GenerateSAMLCACertificate(ctx context.Context, algorithm stri
keypair.NewAddedEvent(
ctx,
keyAgg,
domain.KeyUsageSAMLCA,
crypto.KeyUsageSAMLCA,
algorithm,
privateCrypto, publicCrypto,
after, after,
@ -115,7 +114,7 @@ func (c *Commands) GenerateSAMLResponseCertificate(ctx context.Context, algorith
keypair.NewAddedEvent(
ctx,
keyAgg,
domain.KeyUsageSAMLResponseSinging,
crypto.KeyUsageSAMLResponseSinging,
algorithm,
privateCrypto, publicCrypto,
after, after,
@ -160,7 +159,7 @@ func (c *Commands) GenerateSAMLMetadataCertificate(ctx context.Context, algorith
keypair.NewAddedEvent(
ctx,
keyAgg,
domain.KeyUsageSAMLMetadataSigning,
crypto.KeyUsageSAMLMetadataSigning,
algorithm,
privateCrypto, publicCrypto,
after, after),

View File

@ -1,6 +1,7 @@
package command
import (
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/keypair"
@ -9,7 +10,7 @@ import (
type KeyPairWriteModel struct {
eventstore.WriteModel
Usage domain.KeyUsage
Usage crypto.KeyUsage
Algorithm string
PrivateKey *domain.Key
PublicKey *domain.Key

188
internal/command/web_key.go Normal file
View File

@ -0,0 +1,188 @@
package command
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command/preparation"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/webkey"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
type WebKeyDetails struct {
KeyID string
ObjectDetails *domain.ObjectDetails
}
// CreateWebKey creates one web key pair for the instance.
// If the instance does not have an active key, the new key is activated.
func (c *Commands) CreateWebKey(ctx context.Context, conf crypto.WebKeyConfig) (_ *WebKeyDetails, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
_, activeID, err := c.getAllWebKeys(ctx)
if err != nil {
return nil, err
}
addedCmd, aggregate, err := c.generateWebKeyCommand(ctx, authz.GetInstance(ctx).InstanceID(), conf)
if err != nil {
return nil, err
}
commands := []eventstore.Command{addedCmd}
if activeID == "" {
commands = append(commands, webkey.NewActivatedEvent(ctx, aggregate))
}
model := NewWebKeyWriteModel(aggregate.ID, authz.GetInstance(ctx).InstanceID())
err = c.pushAppendAndReduce(ctx, model, commands...)
if err != nil {
return nil, err
}
return &WebKeyDetails{
KeyID: aggregate.ID,
ObjectDetails: writeModelToObjectDetails(&model.WriteModel),
}, nil
}
// GenerateInitialWebKeys creates 2 web key pairs for the instance.
// The first key is activated for signing use.
// If the instance already has keys, this is noop.
func (c *Commands) GenerateInitialWebKeys(ctx context.Context, conf crypto.WebKeyConfig) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
keys, _, err := c.getAllWebKeys(ctx)
if err != nil {
return err
}
if len(keys) != 0 {
return nil
}
commands, err := c.generateInitialWebKeysCommands(ctx, authz.GetInstance(ctx).InstanceID(), conf)
if err != nil {
return err
}
_, err = c.eventstore.Push(ctx, commands...)
return err
}
func (c *Commands) generateInitialWebKeysCommands(ctx context.Context, instanceID string, conf crypto.WebKeyConfig) (_ []eventstore.Command, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
commands := make([]eventstore.Command, 0, 3)
for i := 0; i < 2; i++ {
addedCmd, aggregate, err := c.generateWebKeyCommand(ctx, instanceID, conf)
if err != nil {
return nil, err
}
commands = append(commands, addedCmd)
if i == 0 {
commands = append(commands, webkey.NewActivatedEvent(ctx, aggregate))
}
}
return commands, nil
}
func (c *Commands) generateWebKeyCommand(ctx context.Context, instanceID string, conf crypto.WebKeyConfig) (_ eventstore.Command, _ *eventstore.Aggregate, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
keyID, err := c.idGenerator.Next()
if err != nil {
return nil, nil, err
}
encryptedPrivate, public, err := c.webKeyGenerator(keyID, c.keyAlgorithm, conf)
if err != nil {
return nil, nil, err
}
aggregate := webkey.NewAggregate(keyID, instanceID)
addedCmd, err := webkey.NewAddedEvent(ctx, aggregate, encryptedPrivate, public, conf)
if err != nil {
return nil, nil, err
}
return addedCmd, aggregate, nil
}
// ActivateWebKey activates the key identified by keyID.
// Any previously activated key on the current instance is deactivated.
func (c *Commands) ActivateWebKey(ctx context.Context, keyID string) (_ *domain.ObjectDetails, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
keys, activeID, err := c.getAllWebKeys(ctx)
if err != nil {
return nil, err
}
if activeID == keyID {
return writeModelToObjectDetails(
&keys[activeID].WriteModel,
), nil
}
nextActive, ok := keys[keyID]
if !ok {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-teiG3", "Errors.WebKey.NotFound")
}
commands := make([]eventstore.Command, 0, 2)
commands = append(commands, webkey.NewActivatedEvent(ctx,
webkey.AggregateFromWriteModel(ctx, &nextActive.WriteModel),
))
if activeID != "" {
commands = append(commands, webkey.NewDeactivatedEvent(ctx,
webkey.AggregateFromWriteModel(ctx, &keys[activeID].WriteModel),
))
}
err = c.pushAppendAndReduce(ctx, nextActive, commands...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&nextActive.WriteModel), nil
}
// getAllWebKeys searches for all web keys on the instance and returns a map of key IDs.
// activeID is the id of the currently active key.
func (c *Commands) getAllWebKeys(ctx context.Context) (_ map[string]*WebKeyWriteModel, activeID string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
models := newWebKeyWriteModels(authz.GetInstance(ctx).InstanceID())
if err = c.eventstore.FilterToQueryReducer(ctx, models); err != nil {
return nil, "", err
}
return models.keys, models.activeID, nil
}
func (c *Commands) DeleteWebKey(ctx context.Context, keyID string) (_ *domain.ObjectDetails, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
model := NewWebKeyWriteModel(keyID, authz.GetInstance(ctx).InstanceID())
if err = c.eventstore.FilterToQueryReducer(ctx, model); err != nil {
return nil, err
}
if model.State == domain.WebKeyStateUnspecified {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-ooCa7", "Errors.WebKey.NotFound")
}
if model.State == domain.WebKeyStateActive {
return nil, zerrors.ThrowPreconditionFailed(nil, "COMMAND-Chai1", "Errors.WebKey.ActiveDelete")
}
err = c.pushAppendAndReduce(ctx, model, webkey.NewRemovedEvent(ctx,
webkey.AggregateFromWriteModel(ctx, &model.WriteModel),
))
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&model.WriteModel), nil
}
func (c *Commands) prepareGenerateInitialWebKeys(instanceID string, conf crypto.WebKeyConfig) preparation.Validation {
return func() (preparation.CreateCommands, error) {
return func(ctx context.Context, _ preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
return c.generateInitialWebKeysCommands(ctx, instanceID, conf)
}, nil
}
}

View File

@ -0,0 +1,131 @@
package command
import (
"github.com/go-jose/go-jose/v4"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/webkey"
)
type WebKeyWriteModel struct {
eventstore.WriteModel
State domain.WebKeyState
PrivateKey *crypto.CryptoValue
PublicKey *jose.JSONWebKey
}
func NewWebKeyWriteModel(keyID, resourceOwner string) *WebKeyWriteModel {
return &WebKeyWriteModel{
WriteModel: eventstore.WriteModel{
AggregateID: keyID,
ResourceOwner: resourceOwner,
},
}
}
func (wm *WebKeyWriteModel) AppendEvents(events ...eventstore.Event) {
wm.WriteModel.AppendEvents(events...)
}
func (wm *WebKeyWriteModel) Reduce() error {
for _, event := range wm.Events {
if event.Aggregate().ID != wm.AggregateID {
continue
}
switch e := event.(type) {
case *webkey.AddedEvent:
wm.State = domain.WebKeyStateInitial
wm.PrivateKey = e.PrivateKey
wm.PublicKey = e.PublicKey
case *webkey.ActivatedEvent:
wm.State = domain.WebKeyStateActive
case *webkey.DeactivatedEvent:
wm.State = domain.WebKeyStateInactive
case *webkey.RemovedEvent:
wm.State = domain.WebKeyStateRemoved
wm.PrivateKey = nil
wm.PublicKey = nil
}
}
return wm.WriteModel.Reduce()
}
func (wm *WebKeyWriteModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(webkey.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(
webkey.AddedEventType,
webkey.ActivatedEventType,
webkey.DeactivatedEventType,
webkey.RemovedEventType,
).
Builder()
}
type webKeyWriteModels struct {
resourceOwner string
events []eventstore.Event
keys map[string]*WebKeyWriteModel
activeID string
}
func newWebKeyWriteModels(resourceOwner string) *webKeyWriteModels {
return &webKeyWriteModels{
resourceOwner: resourceOwner,
keys: make(map[string]*WebKeyWriteModel),
}
}
func (models *webKeyWriteModels) AppendEvents(events ...eventstore.Event) {
models.events = append(models.events, events...)
}
func (models *webKeyWriteModels) Reduce() error {
for _, event := range models.events {
aggregate := event.Aggregate()
if models.keys[aggregate.ID] == nil {
models.keys[aggregate.ID] = NewWebKeyWriteModel(aggregate.ID, aggregate.ResourceOwner)
}
switch event.(type) {
case *webkey.AddedEvent:
break
case *webkey.ActivatedEvent:
models.activeID = aggregate.ID
case *webkey.DeactivatedEvent:
if models.activeID == aggregate.ID {
models.activeID = ""
}
case *webkey.RemovedEvent:
delete(models.keys, aggregate.ID)
continue
}
model := models.keys[aggregate.ID]
model.AppendEvents(event)
if err := model.Reduce(); err != nil {
return err
}
}
models.events = models.events[0:0]
return nil
}
func (models *webKeyWriteModels) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(models.resourceOwner).
AddQuery().
AggregateTypes(webkey.AggregateType).
EventTypes(
webkey.AddedEventType,
webkey.ActivatedEventType,
webkey.DeactivatedEventType,
webkey.RemovedEventType,
).
Builder()
}

View File

@ -0,0 +1,754 @@
package command
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"io"
"testing"
"github.com/go-jose/go-jose/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/id"
id_mock "github.com/zitadel/zitadel/internal/id/mock"
"github.com/zitadel/zitadel/internal/repository/webkey"
"github.com/zitadel/zitadel/internal/zerrors"
)
func TestCommands_CreateWebKey(t *testing.T) {
ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil)
key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
require.NoError(t, err)
type fields struct {
eventstore func(*testing.T) *eventstore.Eventstore
idGenerator id.Generator
webKeyGenerator func(keyID string, alg crypto.EncryptionAlgorithm, genConfig crypto.WebKeyConfig) (encryptedPrivate *crypto.CryptoValue, public *jose.JSONWebKey, err error)
}
type args struct {
conf crypto.WebKeyConfig
}
tests := []struct {
name string
fields fields
args args
want *WebKeyDetails
wantErr error
}{
{
name: "filter error",
fields: fields{
eventstore: expectEventstore(
expectFilterError(io.ErrClosedPipe),
),
},
args: args{
&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
},
wantErr: io.ErrClosedPipe,
},
{
name: "generate error",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key1"),
webKeyGenerator: func(string, crypto.EncryptionAlgorithm, crypto.WebKeyConfig) (*crypto.CryptoValue, *jose.JSONWebKey, error) {
return nil, nil, io.ErrClosedPipe
},
},
args: args{
&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
},
wantErr: io.ErrClosedPipe,
},
{
name: "generate key, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(mustNewWebkeyAddedEvent(ctx,
webkey.NewAggregate("key1", "instance1"),
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
},
&jose.JSONWebKey{
Key: &key.PublicKey,
KeyID: "key1",
Algorithm: string(jose.ES384),
Use: crypto.KeyUsageSigning.String(),
},
&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
)),
eventFromEventPusher(webkey.NewActivatedEvent(ctx,
webkey.NewAggregate("key1", "instance1"),
)),
),
expectPush(
mustNewWebkeyAddedEvent(ctx,
webkey.NewAggregate("key2", "instance1"),
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
},
&jose.JSONWebKey{
Key: &key.PublicKey,
KeyID: "key2",
Algorithm: string(jose.ES384),
Use: crypto.KeyUsageSigning.String(),
},
&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key2"),
webKeyGenerator: func(keyID string, _ crypto.EncryptionAlgorithm, _ crypto.WebKeyConfig) (*crypto.CryptoValue, *jose.JSONWebKey, error) {
return &crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
}, &jose.JSONWebKey{
Key: &key.PublicKey,
KeyID: keyID,
Algorithm: string(jose.ES384),
Use: crypto.KeyUsageSigning.String(),
}, nil
},
},
args: args{
&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
},
want: &WebKeyDetails{
KeyID: "key2",
ObjectDetails: &domain.ObjectDetails{
ResourceOwner: "instance1",
ID: "key2",
},
},
},
{
name: "generate and activate key, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
expectPush(
mustNewWebkeyAddedEvent(ctx,
webkey.NewAggregate("key1", "instance1"),
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
},
&jose.JSONWebKey{
Key: &key.PublicKey,
KeyID: "key1",
Algorithm: string(jose.ES384),
Use: crypto.KeyUsageSigning.String(),
},
&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
),
webkey.NewActivatedEvent(ctx,
webkey.NewAggregate("key1", "instance1"),
),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key1"),
webKeyGenerator: func(keyID string, _ crypto.EncryptionAlgorithm, _ crypto.WebKeyConfig) (*crypto.CryptoValue, *jose.JSONWebKey, error) {
return &crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
}, &jose.JSONWebKey{
Key: &key.PublicKey,
KeyID: keyID,
Algorithm: string(jose.ES384),
Use: crypto.KeyUsageSigning.String(),
}, nil
},
},
args: args{
&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
},
want: &WebKeyDetails{
KeyID: "key1",
ObjectDetails: &domain.ObjectDetails{
ResourceOwner: "instance1",
ID: "key1",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
webKeyGenerator: tt.fields.webKeyGenerator,
}
got, err := c.CreateWebKey(ctx, tt.args.conf)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
}
}
func TestCommands_GenerateInitialWebKeys(t *testing.T) {
ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil)
key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
require.NoError(t, err)
type fields struct {
eventstore func(*testing.T) *eventstore.Eventstore
idGenerator id.Generator
webKeyGenerator func(keyID string, alg crypto.EncryptionAlgorithm, genConfig crypto.WebKeyConfig) (encryptedPrivate *crypto.CryptoValue, public *jose.JSONWebKey, err error)
}
type args struct {
conf crypto.WebKeyConfig
}
tests := []struct {
name string
fields fields
args args
wantErr error
}{
{
name: "filter error",
fields: fields{
eventstore: expectEventstore(
expectFilterError(io.ErrClosedPipe),
),
},
args: args{
&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
},
wantErr: io.ErrClosedPipe,
},
{
name: "key found, noop",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(mustNewWebkeyAddedEvent(ctx,
webkey.NewAggregate("key1", "instance1"),
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
},
&jose.JSONWebKey{
Key: &key.PublicKey,
KeyID: "key1",
Algorithm: string(jose.ES384),
Use: crypto.KeyUsageSigning.String(),
},
&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
)),
eventFromEventPusher(webkey.NewActivatedEvent(ctx,
webkey.NewAggregate("key1", "instance1"),
)),
),
),
},
args: args{
&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
},
wantErr: nil,
},
{
name: "id generator error",
fields: fields{
eventstore: expectEventstore(expectFilter()),
idGenerator: id_mock.NewIDGeneratorExpectError(t, io.ErrUnexpectedEOF),
},
args: args{
&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
},
wantErr: io.ErrUnexpectedEOF,
},
{
name: "keys generated and activated",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
expectPush(
mustNewWebkeyAddedEvent(ctx,
webkey.NewAggregate("key1", "instance1"),
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
},
&jose.JSONWebKey{
Key: &key.PublicKey,
KeyID: "key1",
Algorithm: string(jose.ES384),
Use: crypto.KeyUsageSigning.String(),
},
&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
),
webkey.NewActivatedEvent(ctx,
webkey.NewAggregate("key1", "instance1"),
),
mustNewWebkeyAddedEvent(ctx,
webkey.NewAggregate("key2", "instance1"),
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
},
&jose.JSONWebKey{
Key: &key.PublicKey,
KeyID: "key2",
Algorithm: string(jose.ES384),
Use: crypto.KeyUsageSigning.String(),
},
&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
),
),
),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "key1", "key2"),
webKeyGenerator: func(keyID string, _ crypto.EncryptionAlgorithm, _ crypto.WebKeyConfig) (*crypto.CryptoValue, *jose.JSONWebKey, error) {
return &crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
}, &jose.JSONWebKey{
Key: &key.PublicKey,
KeyID: keyID,
Algorithm: string(jose.ES384),
Use: crypto.KeyUsageSigning.String(),
}, nil
},
},
args: args{
&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
},
wantErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
webKeyGenerator: tt.fields.webKeyGenerator,
}
err := c.GenerateInitialWebKeys(ctx, tt.args.conf)
require.ErrorIs(t, err, tt.wantErr)
})
}
}
func TestCommands_ActivateWebKey(t *testing.T) {
ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil)
key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
require.NoError(t, err)
type fields struct {
eventstore func(*testing.T) *eventstore.Eventstore
webKeyGenerator func(keyID string, alg crypto.EncryptionAlgorithm, genConfig crypto.WebKeyConfig) (encryptedPrivate *crypto.CryptoValue, public *jose.JSONWebKey, err error)
}
type args struct {
keyID string
}
tests := []struct {
name string
fields fields
args args
want *domain.ObjectDetails
wantErr error
}{
{
name: "filter error",
fields: fields{
eventstore: expectEventstore(
expectFilterError(io.ErrClosedPipe),
),
},
args: args{"key2"},
wantErr: io.ErrClosedPipe,
},
{
name: "no changes",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(mustNewWebkeyAddedEvent(ctx,
webkey.NewAggregate("key1", "instance1"),
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
},
&jose.JSONWebKey{
Key: &key.PublicKey,
KeyID: "key1",
Algorithm: string(jose.ES384),
Use: crypto.KeyUsageSigning.String(),
},
&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
)),
eventFromEventPusher(webkey.NewActivatedEvent(ctx,
webkey.NewAggregate("key1", "instance1"),
)),
),
),
},
args: args{"key1"},
want: &domain.ObjectDetails{
ResourceOwner: "instance1",
ID: "key1",
},
},
{
name: "not found error",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(mustNewWebkeyAddedEvent(ctx,
webkey.NewAggregate("key1", "instance1"),
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
},
&jose.JSONWebKey{
Key: &key.PublicKey,
KeyID: "key1",
Algorithm: string(jose.ES384),
Use: crypto.KeyUsageSigning.String(),
},
&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
)),
eventFromEventPusher(webkey.NewActivatedEvent(ctx,
webkey.NewAggregate("key1", "instance1"),
)),
),
),
},
args: args{"key2"},
wantErr: zerrors.ThrowNotFound(nil, "COMMAND-teiG3", "Errors.WebKey.NotFound"),
},
{
name: "activate next, de-activate old, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(mustNewWebkeyAddedEvent(ctx,
webkey.NewAggregate("key1", "instance1"),
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
},
&jose.JSONWebKey{
Key: &key.PublicKey,
KeyID: "key1",
Algorithm: string(jose.ES384),
Use: crypto.KeyUsageSigning.String(),
},
&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
)),
eventFromEventPusher(webkey.NewActivatedEvent(ctx,
webkey.NewAggregate("key1", "instance1"),
)),
eventFromEventPusher(mustNewWebkeyAddedEvent(ctx,
webkey.NewAggregate("key2", "instance1"),
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
},
&jose.JSONWebKey{
Key: &key.PublicKey,
KeyID: "key2",
Algorithm: string(jose.ES384),
Use: crypto.KeyUsageSigning.String(),
},
&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
)),
),
expectPush(
webkey.NewActivatedEvent(ctx,
webkey.NewAggregate("key2", "instance1"),
),
webkey.NewDeactivatedEvent(ctx,
webkey.NewAggregate("key1", "instance1"),
),
),
),
},
args: args{"key2"},
want: &domain.ObjectDetails{
ResourceOwner: "instance1",
ID: "key2",
},
},
{
name: "activate next, ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(mustNewWebkeyAddedEvent(ctx,
webkey.NewAggregate("key1", "instance1"),
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
},
&jose.JSONWebKey{
Key: &key.PublicKey,
KeyID: "key1",
Algorithm: string(jose.ES384),
Use: crypto.KeyUsageSigning.String(),
},
&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
)),
),
expectPush(
webkey.NewActivatedEvent(ctx,
webkey.NewAggregate("key1", "instance1"),
),
),
),
},
args: args{"key1"},
want: &domain.ObjectDetails{
ResourceOwner: "instance1",
ID: "key1",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore(t),
webKeyGenerator: tt.fields.webKeyGenerator,
}
got, err := c.ActivateWebKey(ctx, tt.args.keyID)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
}
}
func TestCommands_DeleteWebKey(t *testing.T) {
ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil)
key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
require.NoError(t, err)
type fields struct {
eventstore func(*testing.T) *eventstore.Eventstore
}
type args struct {
keyID string
}
tests := []struct {
name string
fields fields
args args
want *domain.ObjectDetails
wantErr error
}{
{
name: "filter error",
fields: fields{
eventstore: expectEventstore(
expectFilterError(io.ErrClosedPipe),
),
},
args: args{"key1"},
wantErr: io.ErrClosedPipe,
},
{
name: "not found error",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
),
},
args: args{"key1"},
wantErr: zerrors.ThrowNotFound(nil, "COMMAND-ooCa7", "Errors.WebKey.NotFound"),
},
{
name: "key active error",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(mustNewWebkeyAddedEvent(ctx,
webkey.NewAggregate("key1", "instance1"),
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
},
&jose.JSONWebKey{
Key: &key.PublicKey,
KeyID: "key1",
Algorithm: string(jose.ES384),
Use: crypto.KeyUsageSigning.String(),
},
&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
)),
eventFromEventPusher(webkey.NewActivatedEvent(ctx,
webkey.NewAggregate("key1", "instance1"),
)),
),
),
},
args: args{"key1"},
wantErr: zerrors.ThrowPreconditionFailed(nil, "COMMAND-Chai1", "Errors.WebKey.ActiveDelete"),
},
{
name: "delete deactivated key",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(mustNewWebkeyAddedEvent(ctx,
webkey.NewAggregate("key1", "instance1"),
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
},
&jose.JSONWebKey{
Key: &key.PublicKey,
KeyID: "key1",
Algorithm: string(jose.ES384),
Use: crypto.KeyUsageSigning.String(),
},
&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
)),
eventFromEventPusher(webkey.NewActivatedEvent(ctx,
webkey.NewAggregate("key1", "instance1"),
)),
eventFromEventPusher(mustNewWebkeyAddedEvent(ctx,
webkey.NewAggregate("key2", "instance1"),
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
},
&jose.JSONWebKey{
Key: &key.PublicKey,
KeyID: "key2",
Algorithm: string(jose.ES384),
Use: crypto.KeyUsageSigning.String(),
},
&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
)),
eventFromEventPusher(webkey.NewActivatedEvent(ctx,
webkey.NewAggregate("key2", "instance1"),
)),
eventFromEventPusher(webkey.NewDeactivatedEvent(ctx,
webkey.NewAggregate("key1", "instance1"),
)),
),
expectPush(
webkey.NewRemovedEvent(ctx, webkey.NewAggregate("key1", "instance1")),
),
),
},
args: args{"key1"},
want: &domain.ObjectDetails{
ResourceOwner: "instance1",
ID: "key1",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore(t),
}
got, err := c.DeleteWebKey(ctx, tt.args.keyID)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
}
}
func mustNewWebkeyAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
privateKey *crypto.CryptoValue,
publicKey *jose.JSONWebKey,
config crypto.WebKeyConfig) *webkey.AddedEvent {
event, err := webkey.NewAddedEvent(ctx, aggregate, privateKey, publicKey, config)
if err != nil {
panic(err)
}
return event
}

View File

@ -68,6 +68,14 @@ func Encrypt(value []byte, alg EncryptionAlgorithm) (*CryptoValue, error) {
}, nil
}
func EncryptJSON(obj any, alg EncryptionAlgorithm) (*CryptoValue, error) {
data, err := json.Marshal(obj)
if err != nil {
return nil, zerrors.ThrowInternal(err, "CRYPT-Ei6doF", "error encrypting value")
}
return Encrypt(data, alg)
}
func Decrypt(value *CryptoValue, alg EncryptionAlgorithm) ([]byte, error) {
if err := checkEncryptionAlgorithm(value, alg); err != nil {
return nil, err
@ -75,6 +83,17 @@ func Decrypt(value *CryptoValue, alg EncryptionAlgorithm) ([]byte, error) {
return alg.Decrypt(value.Crypted, value.KeyID)
}
func DecryptJSON(value *CryptoValue, dst any, alg EncryptionAlgorithm) error {
data, err := Decrypt(value, alg)
if err != nil {
return err
}
if err = json.Unmarshal(data, dst); err != nil {
return zerrors.ThrowInternal(err, "CRYPT-Jaik2R", "error decrypting value")
}
return nil
}
// DecryptString decrypts the value using the key identified by keyID.
// When the decrypted value contains non-UTF8 characters an error is returned.
func DecryptString(value *CryptoValue, alg EncryptionAlgorithm) (string, error) {

View File

@ -0,0 +1,116 @@
// Code generated by "enumer -type EllipticCurve -trimprefix EllipticCurve -text -json -linecomment"; DO NOT EDIT.
package crypto
import (
"encoding/json"
"fmt"
"strings"
)
const _EllipticCurveName = "P256P384P512"
var _EllipticCurveIndex = [...]uint8{0, 0, 4, 8, 12}
const _EllipticCurveLowerName = "p256p384p512"
func (i EllipticCurve) String() string {
if i < 0 || i >= EllipticCurve(len(_EllipticCurveIndex)-1) {
return fmt.Sprintf("EllipticCurve(%d)", i)
}
return _EllipticCurveName[_EllipticCurveIndex[i]:_EllipticCurveIndex[i+1]]
}
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
func _EllipticCurveNoOp() {
var x [1]struct{}
_ = x[EllipticCurveUnspecified-(0)]
_ = x[EllipticCurveP256-(1)]
_ = x[EllipticCurveP384-(2)]
_ = x[EllipticCurveP512-(3)]
}
var _EllipticCurveValues = []EllipticCurve{EllipticCurveUnspecified, EllipticCurveP256, EllipticCurveP384, EllipticCurveP512}
var _EllipticCurveNameToValueMap = map[string]EllipticCurve{
_EllipticCurveName[0:0]: EllipticCurveUnspecified,
_EllipticCurveLowerName[0:0]: EllipticCurveUnspecified,
_EllipticCurveName[0:4]: EllipticCurveP256,
_EllipticCurveLowerName[0:4]: EllipticCurveP256,
_EllipticCurveName[4:8]: EllipticCurveP384,
_EllipticCurveLowerName[4:8]: EllipticCurveP384,
_EllipticCurveName[8:12]: EllipticCurveP512,
_EllipticCurveLowerName[8:12]: EllipticCurveP512,
}
var _EllipticCurveNames = []string{
_EllipticCurveName[0:0],
_EllipticCurveName[0:4],
_EllipticCurveName[4:8],
_EllipticCurveName[8:12],
}
// EllipticCurveString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func EllipticCurveString(s string) (EllipticCurve, error) {
if val, ok := _EllipticCurveNameToValueMap[s]; ok {
return val, nil
}
if val, ok := _EllipticCurveNameToValueMap[strings.ToLower(s)]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to EllipticCurve values", s)
}
// EllipticCurveValues returns all values of the enum
func EllipticCurveValues() []EllipticCurve {
return _EllipticCurveValues
}
// EllipticCurveStrings returns a slice of all String values of the enum
func EllipticCurveStrings() []string {
strs := make([]string, len(_EllipticCurveNames))
copy(strs, _EllipticCurveNames)
return strs
}
// IsAEllipticCurve returns "true" if the value is listed in the enum definition. "false" otherwise
func (i EllipticCurve) IsAEllipticCurve() bool {
for _, v := range _EllipticCurveValues {
if i == v {
return true
}
}
return false
}
// MarshalJSON implements the json.Marshaler interface for EllipticCurve
func (i EllipticCurve) MarshalJSON() ([]byte, error) {
return json.Marshal(i.String())
}
// UnmarshalJSON implements the json.Unmarshaler interface for EllipticCurve
func (i *EllipticCurve) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return fmt.Errorf("EllipticCurve should be a string, got %s", data)
}
var err error
*i, err = EllipticCurveString(s)
return err
}
// MarshalText implements the encoding.TextMarshaler interface for EllipticCurve
func (i EllipticCurve) MarshalText() ([]byte, error) {
return []byte(i.String()), nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface for EllipticCurve
func (i *EllipticCurve) UnmarshalText(text []byte) error {
var err error
*i, err = EllipticCurveString(string(text))
return err
}

View File

@ -0,0 +1,136 @@
// Code generated by "enumer -type RSABits -trimprefix RSABits -text -json -linecomment"; DO NOT EDIT.
package crypto
import (
"encoding/json"
"fmt"
"strings"
)
const (
_RSABitsName_0 = ""
_RSABitsLowerName_0 = ""
_RSABitsName_1 = "2048"
_RSABitsLowerName_1 = "2048"
_RSABitsName_2 = "3072"
_RSABitsLowerName_2 = "3072"
_RSABitsName_3 = "4096"
_RSABitsLowerName_3 = "4096"
)
var (
_RSABitsIndex_0 = [...]uint8{0, 0}
_RSABitsIndex_1 = [...]uint8{0, 4}
_RSABitsIndex_2 = [...]uint8{0, 4}
_RSABitsIndex_3 = [...]uint8{0, 4}
)
func (i RSABits) String() string {
switch {
case i == 0:
return _RSABitsName_0
case i == 2048:
return _RSABitsName_1
case i == 3072:
return _RSABitsName_2
case i == 4096:
return _RSABitsName_3
default:
return fmt.Sprintf("RSABits(%d)", i)
}
}
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
func _RSABitsNoOp() {
var x [1]struct{}
_ = x[RSABitsUnspecified-(0)]
_ = x[RSABits2048-(2048)]
_ = x[RSABits3072-(3072)]
_ = x[RSABits4096-(4096)]
}
var _RSABitsValues = []RSABits{RSABitsUnspecified, RSABits2048, RSABits3072, RSABits4096}
var _RSABitsNameToValueMap = map[string]RSABits{
_RSABitsName_0[0:0]: RSABitsUnspecified,
_RSABitsLowerName_0[0:0]: RSABitsUnspecified,
_RSABitsName_1[0:4]: RSABits2048,
_RSABitsLowerName_1[0:4]: RSABits2048,
_RSABitsName_2[0:4]: RSABits3072,
_RSABitsLowerName_2[0:4]: RSABits3072,
_RSABitsName_3[0:4]: RSABits4096,
_RSABitsLowerName_3[0:4]: RSABits4096,
}
var _RSABitsNames = []string{
_RSABitsName_0[0:0],
_RSABitsName_1[0:4],
_RSABitsName_2[0:4],
_RSABitsName_3[0:4],
}
// RSABitsString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func RSABitsString(s string) (RSABits, error) {
if val, ok := _RSABitsNameToValueMap[s]; ok {
return val, nil
}
if val, ok := _RSABitsNameToValueMap[strings.ToLower(s)]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to RSABits values", s)
}
// RSABitsValues returns all values of the enum
func RSABitsValues() []RSABits {
return _RSABitsValues
}
// RSABitsStrings returns a slice of all String values of the enum
func RSABitsStrings() []string {
strs := make([]string, len(_RSABitsNames))
copy(strs, _RSABitsNames)
return strs
}
// IsARSABits returns "true" if the value is listed in the enum definition. "false" otherwise
func (i RSABits) IsARSABits() bool {
for _, v := range _RSABitsValues {
if i == v {
return true
}
}
return false
}
// MarshalJSON implements the json.Marshaler interface for RSABits
func (i RSABits) MarshalJSON() ([]byte, error) {
return json.Marshal(i.String())
}
// UnmarshalJSON implements the json.Unmarshaler interface for RSABits
func (i *RSABits) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return fmt.Errorf("RSABits should be a string, got %s", data)
}
var err error
*i, err = RSABitsString(s)
return err
}
// MarshalText implements the encoding.TextMarshaler interface for RSABits
func (i RSABits) MarshalText() ([]byte, error) {
return []byte(i.String()), nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface for RSABits
func (i *RSABits) UnmarshalText(text []byte) error {
var err error
*i, err = RSABitsString(string(text))
return err
}

View File

@ -0,0 +1,116 @@
// Code generated by "enumer -type RSAHasher -trimprefix RSAHasher -text -json -linecomment"; DO NOT EDIT.
package crypto
import (
"encoding/json"
"fmt"
"strings"
)
const _RSAHasherName = "SHA256SHA384SHA512"
var _RSAHasherIndex = [...]uint8{0, 0, 6, 12, 18}
const _RSAHasherLowerName = "sha256sha384sha512"
func (i RSAHasher) String() string {
if i < 0 || i >= RSAHasher(len(_RSAHasherIndex)-1) {
return fmt.Sprintf("RSAHasher(%d)", i)
}
return _RSAHasherName[_RSAHasherIndex[i]:_RSAHasherIndex[i+1]]
}
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
func _RSAHasherNoOp() {
var x [1]struct{}
_ = x[RSAHasherUnspecified-(0)]
_ = x[RSAHasherSHA256-(1)]
_ = x[RSAHasherSHA384-(2)]
_ = x[RSAHasherSHA512-(3)]
}
var _RSAHasherValues = []RSAHasher{RSAHasherUnspecified, RSAHasherSHA256, RSAHasherSHA384, RSAHasherSHA512}
var _RSAHasherNameToValueMap = map[string]RSAHasher{
_RSAHasherName[0:0]: RSAHasherUnspecified,
_RSAHasherLowerName[0:0]: RSAHasherUnspecified,
_RSAHasherName[0:6]: RSAHasherSHA256,
_RSAHasherLowerName[0:6]: RSAHasherSHA256,
_RSAHasherName[6:12]: RSAHasherSHA384,
_RSAHasherLowerName[6:12]: RSAHasherSHA384,
_RSAHasherName[12:18]: RSAHasherSHA512,
_RSAHasherLowerName[12:18]: RSAHasherSHA512,
}
var _RSAHasherNames = []string{
_RSAHasherName[0:0],
_RSAHasherName[0:6],
_RSAHasherName[6:12],
_RSAHasherName[12:18],
}
// RSAHasherString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func RSAHasherString(s string) (RSAHasher, error) {
if val, ok := _RSAHasherNameToValueMap[s]; ok {
return val, nil
}
if val, ok := _RSAHasherNameToValueMap[strings.ToLower(s)]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to RSAHasher values", s)
}
// RSAHasherValues returns all values of the enum
func RSAHasherValues() []RSAHasher {
return _RSAHasherValues
}
// RSAHasherStrings returns a slice of all String values of the enum
func RSAHasherStrings() []string {
strs := make([]string, len(_RSAHasherNames))
copy(strs, _RSAHasherNames)
return strs
}
// IsARSAHasher returns "true" if the value is listed in the enum definition. "false" otherwise
func (i RSAHasher) IsARSAHasher() bool {
for _, v := range _RSAHasherValues {
if i == v {
return true
}
}
return false
}
// MarshalJSON implements the json.Marshaler interface for RSAHasher
func (i RSAHasher) MarshalJSON() ([]byte, error) {
return json.Marshal(i.String())
}
// UnmarshalJSON implements the json.Unmarshaler interface for RSAHasher
func (i *RSAHasher) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return fmt.Errorf("RSAHasher should be a string, got %s", data)
}
var err error
*i, err = RSAHasherString(s)
return err
}
// MarshalText implements the encoding.TextMarshaler interface for RSAHasher
func (i RSAHasher) MarshalText() ([]byte, error) {
return []byte(i.String()), nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface for RSAHasher
func (i *RSAHasher) UnmarshalText(text []byte) error {
var err error
*i, err = RSAHasherString(string(text))
return err
}

238
internal/crypto/web_key.go Normal file
View File

@ -0,0 +1,238 @@
package crypto
import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"encoding/json"
"github.com/go-jose/go-jose/v4"
"github.com/muhlemmer/gu"
"github.com/zitadel/zitadel/internal/zerrors"
)
type KeyUsage int32
const (
KeyUsageSigning KeyUsage = iota
KeyUsageSAMLMetadataSigning
KeyUsageSAMLResponseSinging
KeyUsageSAMLCA
)
func (u KeyUsage) String() string {
switch u {
case KeyUsageSigning:
return "sig"
case KeyUsageSAMLCA:
return "saml_ca"
case KeyUsageSAMLResponseSinging:
return "saml_response_sig"
case KeyUsageSAMLMetadataSigning:
return "saml_metadata_sig"
}
return ""
}
//go:generate enumer -type WebKeyConfigType -trimprefix WebKeyConfigType -text -json -linecomment
type WebKeyConfigType int
const (
WebKeyConfigTypeUnspecified WebKeyConfigType = iota //
WebKeyConfigTypeRSA
WebKeyConfigTypeECDSA
WebKeyConfigTypeED25519
)
//go:generate enumer -type RSABits -trimprefix RSABits -text -json -linecomment
type RSABits int
const (
RSABitsUnspecified RSABits = 0 //
RSABits2048 RSABits = 2048
RSABits3072 RSABits = 3072
RSABits4096 RSABits = 4096
)
type RSAHasher int
//go:generate enumer -type RSAHasher -trimprefix RSAHasher -text -json -linecomment
const (
RSAHasherUnspecified RSAHasher = iota //
RSAHasherSHA256
RSAHasherSHA384
RSAHasherSHA512
)
type EllipticCurve int
//go:generate enumer -type EllipticCurve -trimprefix EllipticCurve -text -json -linecomment
const (
EllipticCurveUnspecified EllipticCurve = iota //
EllipticCurveP256
EllipticCurveP384
EllipticCurveP512
)
type WebKeyConfig interface {
Alg() jose.SignatureAlgorithm
Type() WebKeyConfigType // Type is needed to make Unmarshal work
IsValid() error
}
func UnmarshalWebKeyConfig(data []byte, configType WebKeyConfigType) (config WebKeyConfig, err error) {
switch configType {
case WebKeyConfigTypeUnspecified:
return nil, zerrors.ThrowInternal(nil, "CRYPT-Ii3AiH", "Errors.Internal")
case WebKeyConfigTypeRSA:
config = new(WebKeyRSAConfig)
case WebKeyConfigTypeECDSA:
config = new(WebKeyECDSAConfig)
case WebKeyConfigTypeED25519:
config = new(WebKeyED25519Config)
default:
return nil, zerrors.ThrowInternal(nil, "CRYPT-Eig8ho", "Errors.Internal")
}
if err = json.Unmarshal(data, config); err != nil {
return nil, zerrors.ThrowInternal(err, "CRYPT-waeR0N", "Errors.Internal")
}
return config, nil
}
type WebKeyRSAConfig struct {
Bits RSABits
Hasher RSAHasher
}
func (c WebKeyRSAConfig) Alg() jose.SignatureAlgorithm {
switch c.Hasher {
case RSAHasherUnspecified:
return ""
case RSAHasherSHA256:
return jose.RS256
case RSAHasherSHA384:
return jose.RS384
case RSAHasherSHA512:
return jose.RS512
default:
return ""
}
}
func (WebKeyRSAConfig) Type() WebKeyConfigType {
return WebKeyConfigTypeRSA
}
func (c WebKeyRSAConfig) IsValid() error {
if !c.Bits.IsARSABits() || c.Bits == RSABitsUnspecified {
return zerrors.ThrowInvalidArgument(nil, "CRYPTO-eaz3T", "Errors.WebKey.Config")
}
if !c.Hasher.IsARSAHasher() || c.Hasher == RSAHasherUnspecified {
return zerrors.ThrowInvalidArgument(nil, "CRYPTO-ODie7", "Errors.WebKey.Config")
}
return nil
}
type WebKeyECDSAConfig struct {
Curve EllipticCurve
}
func (c WebKeyECDSAConfig) Alg() jose.SignatureAlgorithm {
switch c.Curve {
case EllipticCurveUnspecified:
return ""
case EllipticCurveP256:
return jose.ES256
case EllipticCurveP384:
return jose.ES384
case EllipticCurveP512:
return jose.ES512
default:
return ""
}
}
func (WebKeyECDSAConfig) Type() WebKeyConfigType {
return WebKeyConfigTypeECDSA
}
func (c WebKeyECDSAConfig) IsValid() error {
if !c.Curve.IsAEllipticCurve() || c.Curve == EllipticCurveUnspecified {
return zerrors.ThrowInvalidArgument(nil, "CRYPTO-Ii2ai", "Errors.WebKey.Config")
}
return nil
}
func (c WebKeyECDSAConfig) GetCurve() elliptic.Curve {
switch c.Curve {
case EllipticCurveUnspecified:
return nil
case EllipticCurveP256:
return elliptic.P256()
case EllipticCurveP384:
return elliptic.P384()
case EllipticCurveP512:
return elliptic.P521()
default:
return nil
}
}
type WebKeyED25519Config struct{}
func (WebKeyED25519Config) Alg() jose.SignatureAlgorithm {
return jose.EdDSA
}
func (WebKeyED25519Config) Type() WebKeyConfigType {
return WebKeyConfigTypeED25519
}
func (WebKeyED25519Config) IsValid() error {
return nil
}
func GenerateEncryptedWebKey(keyID string, alg EncryptionAlgorithm, genConfig WebKeyConfig) (encryptedPrivate *CryptoValue, public *jose.JSONWebKey, err error) {
private, public, err := generateWebKey(keyID, genConfig)
if err != nil {
return nil, nil, err
}
encryptedPrivate, err = EncryptJSON(private, alg)
if err != nil {
return nil, nil, err
}
return encryptedPrivate, public, nil
}
func generateWebKey(keyID string, genConfig WebKeyConfig) (private, public *jose.JSONWebKey, err error) {
if err = genConfig.IsValid(); err != nil {
return nil, nil, err
}
var key any
switch conf := genConfig.(type) {
case *WebKeyRSAConfig:
key, err = rsa.GenerateKey(rand.Reader, int(conf.Bits))
case *WebKeyECDSAConfig:
key, err = ecdsa.GenerateKey(conf.GetCurve(), rand.Reader)
case *WebKeyED25519Config:
_, key, err = ed25519.GenerateKey(rand.Reader)
}
if err != nil {
return nil, nil, err
}
private = newJSONWebkey(key, keyID, genConfig.Alg())
return private, gu.Ptr(private.Public()), err
}
func newJSONWebkey(key any, keyID string, algorithm jose.SignatureAlgorithm) *jose.JSONWebKey {
return &jose.JSONWebKey{
Key: key,
KeyID: keyID,
Algorithm: string(algorithm),
Use: KeyUsageSigning.String(),
}
}

View File

@ -0,0 +1,269 @@
package crypto
import (
"crypto/elliptic"
"testing"
"github.com/go-jose/go-jose/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/zerrors"
)
func TestUnmarshalWebKeyConfig(t *testing.T) {
type args struct {
data []byte
configType WebKeyConfigType
}
tests := []struct {
name string
args args
wantConfig WebKeyConfig
wantErr error
}{
{
name: "unspecified",
args: args{
[]byte(`{}`),
WebKeyConfigTypeUnspecified,
},
wantErr: zerrors.ThrowInternal(nil, "CRYPT-Ii3AiH", "Errors.Internal"),
},
{
name: "rsa",
args: args{
[]byte(`{"bits":"2048", "hasher":"sha256"}`),
WebKeyConfigTypeRSA,
},
wantConfig: &WebKeyRSAConfig{
Bits: RSABits2048,
Hasher: RSAHasherSHA256,
},
},
{
name: "ecdsa",
args: args{
[]byte(`{"curve":"p256"}`),
WebKeyConfigTypeECDSA,
},
wantConfig: &WebKeyECDSAConfig{
Curve: EllipticCurveP256,
},
},
{
name: "ed25519",
args: args{
[]byte(`{}`),
WebKeyConfigTypeED25519,
},
wantConfig: &WebKeyED25519Config{},
},
{
name: "unknown type error",
args: args{
[]byte(`{"curve":0}`),
99,
},
wantErr: zerrors.ThrowInternal(nil, "CRYPT-Eig8ho", "Errors.Internal"),
},
{
name: "unmarshal error",
args: args{
[]byte(`~~`),
WebKeyConfigTypeED25519,
},
wantErr: zerrors.ThrowInternal(nil, "CRYPT-waeR0N", "Errors.Internal"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotConfig, err := UnmarshalWebKeyConfig(tt.args.data, tt.args.configType)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, gotConfig, tt.wantConfig)
})
}
}
func TestWebKeyECDSAConfig_Alg(t *testing.T) {
type fields struct {
Curve EllipticCurve
}
tests := []struct {
name string
fields fields
want jose.SignatureAlgorithm
}{
{
name: "unspecified",
fields: fields{
Curve: EllipticCurveUnspecified,
},
want: "",
},
{
name: "P256",
fields: fields{
Curve: EllipticCurveP256,
},
want: jose.ES256,
},
{
name: "P384",
fields: fields{
Curve: EllipticCurveP384,
},
want: jose.ES384,
},
{
name: "P512",
fields: fields{
Curve: EllipticCurveP512,
},
want: jose.ES512,
},
{
name: "default",
fields: fields{
Curve: 99,
},
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := WebKeyECDSAConfig{
Curve: tt.fields.Curve,
}
got := c.Alg()
assert.Equal(t, tt.want, got)
})
}
}
func TestWebKeyECDSAConfig_GetCurve(t *testing.T) {
type fields struct {
Curve EllipticCurve
}
tests := []struct {
name string
fields fields
want elliptic.Curve
}{
{
name: "unspecified",
fields: fields{EllipticCurveUnspecified},
want: nil,
},
{
name: "P256",
fields: fields{EllipticCurveP256},
want: elliptic.P256(),
},
{
name: "P384",
fields: fields{EllipticCurveP384},
want: elliptic.P384(),
},
{
name: "P512",
fields: fields{EllipticCurveP512},
want: elliptic.P521(),
},
{
name: "default",
fields: fields{99},
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := WebKeyECDSAConfig{
Curve: tt.fields.Curve,
}
got := c.GetCurve()
assert.Equal(t, tt.want, got)
})
}
}
func Test_generateEncryptedWebKey(t *testing.T) {
type args struct {
keyID string
genConfig WebKeyConfig
}
tests := []struct {
name string
args args
assertPrivate func(t *testing.T, got *jose.JSONWebKey)
assertPublic func(t *testing.T, got *jose.JSONWebKey)
wantErr error
}{
{
name: "invalid",
args: args{
keyID: "keyID",
genConfig: &WebKeyRSAConfig{
Bits: RSABitsUnspecified,
Hasher: RSAHasherSHA256,
},
},
wantErr: zerrors.ThrowInvalidArgument(nil, "CRYPTO-eaz3T", "Errors.WebKey.Config"),
},
{
name: "RSA",
args: args{
keyID: "keyID",
genConfig: &WebKeyRSAConfig{
Bits: RSABits2048,
Hasher: RSAHasherSHA256,
},
},
assertPrivate: assertJSONWebKey("keyID", "RS256", "sig", false),
assertPublic: assertJSONWebKey("keyID", "RS256", "sig", true),
},
{
name: "ECDSA",
args: args{
keyID: "keyID",
genConfig: &WebKeyECDSAConfig{
Curve: EllipticCurveP256,
},
},
assertPrivate: assertJSONWebKey("keyID", "ES256", "sig", false),
assertPublic: assertJSONWebKey("keyID", "ES256", "sig", true),
},
{
name: "ED25519",
args: args{
keyID: "keyID",
genConfig: &WebKeyED25519Config{},
},
assertPrivate: assertJSONWebKey("keyID", "EdDSA", "sig", false),
assertPublic: assertJSONWebKey("keyID", "EdDSA", "sig", true),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotPrivate, gotPublic, err := generateWebKey(tt.args.keyID, tt.args.genConfig)
require.ErrorIs(t, err, tt.wantErr)
if tt.assertPrivate != nil {
tt.assertPrivate(t, gotPrivate)
}
if tt.assertPublic != nil {
tt.assertPublic(t, gotPublic)
}
})
}
}
func assertJSONWebKey(keyID, algorithm, use string, isPublic bool) func(t *testing.T, got *jose.JSONWebKey) {
return func(t *testing.T, got *jose.JSONWebKey) {
assert.NotNil(t, got)
assert.NotNil(t, got.Key, "key")
assert.Equal(t, keyID, got.KeyID, "keyID")
assert.Equal(t, algorithm, got.Algorithm, "algorithm")
assert.Equal(t, use, got.Use, "user")
assert.Equal(t, isPublic, got.IsPublic(), "isPublic")
}
}

View File

@ -0,0 +1,116 @@
// Code generated by "enumer -type WebKeyConfigType -trimprefix WebKeyConfigType -text -json -linecomment"; DO NOT EDIT.
package crypto
import (
"encoding/json"
"fmt"
"strings"
)
const _WebKeyConfigTypeName = "RSAECDSAED25519"
var _WebKeyConfigTypeIndex = [...]uint8{0, 0, 3, 8, 15}
const _WebKeyConfigTypeLowerName = "rsaecdsaed25519"
func (i WebKeyConfigType) String() string {
if i < 0 || i >= WebKeyConfigType(len(_WebKeyConfigTypeIndex)-1) {
return fmt.Sprintf("WebKeyConfigType(%d)", i)
}
return _WebKeyConfigTypeName[_WebKeyConfigTypeIndex[i]:_WebKeyConfigTypeIndex[i+1]]
}
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
func _WebKeyConfigTypeNoOp() {
var x [1]struct{}
_ = x[WebKeyConfigTypeUnspecified-(0)]
_ = x[WebKeyConfigTypeRSA-(1)]
_ = x[WebKeyConfigTypeECDSA-(2)]
_ = x[WebKeyConfigTypeED25519-(3)]
}
var _WebKeyConfigTypeValues = []WebKeyConfigType{WebKeyConfigTypeUnspecified, WebKeyConfigTypeRSA, WebKeyConfigTypeECDSA, WebKeyConfigTypeED25519}
var _WebKeyConfigTypeNameToValueMap = map[string]WebKeyConfigType{
_WebKeyConfigTypeName[0:0]: WebKeyConfigTypeUnspecified,
_WebKeyConfigTypeLowerName[0:0]: WebKeyConfigTypeUnspecified,
_WebKeyConfigTypeName[0:3]: WebKeyConfigTypeRSA,
_WebKeyConfigTypeLowerName[0:3]: WebKeyConfigTypeRSA,
_WebKeyConfigTypeName[3:8]: WebKeyConfigTypeECDSA,
_WebKeyConfigTypeLowerName[3:8]: WebKeyConfigTypeECDSA,
_WebKeyConfigTypeName[8:15]: WebKeyConfigTypeED25519,
_WebKeyConfigTypeLowerName[8:15]: WebKeyConfigTypeED25519,
}
var _WebKeyConfigTypeNames = []string{
_WebKeyConfigTypeName[0:0],
_WebKeyConfigTypeName[0:3],
_WebKeyConfigTypeName[3:8],
_WebKeyConfigTypeName[8:15],
}
// WebKeyConfigTypeString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func WebKeyConfigTypeString(s string) (WebKeyConfigType, error) {
if val, ok := _WebKeyConfigTypeNameToValueMap[s]; ok {
return val, nil
}
if val, ok := _WebKeyConfigTypeNameToValueMap[strings.ToLower(s)]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to WebKeyConfigType values", s)
}
// WebKeyConfigTypeValues returns all values of the enum
func WebKeyConfigTypeValues() []WebKeyConfigType {
return _WebKeyConfigTypeValues
}
// WebKeyConfigTypeStrings returns a slice of all String values of the enum
func WebKeyConfigTypeStrings() []string {
strs := make([]string, len(_WebKeyConfigTypeNames))
copy(strs, _WebKeyConfigTypeNames)
return strs
}
// IsAWebKeyConfigType returns "true" if the value is listed in the enum definition. "false" otherwise
func (i WebKeyConfigType) IsAWebKeyConfigType() bool {
for _, v := range _WebKeyConfigTypeValues {
if i == v {
return true
}
}
return false
}
// MarshalJSON implements the json.Marshaler interface for WebKeyConfigType
func (i WebKeyConfigType) MarshalJSON() ([]byte, error) {
return json.Marshal(i.String())
}
// UnmarshalJSON implements the json.Unmarshaler interface for WebKeyConfigType
func (i *WebKeyConfigType) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return fmt.Errorf("WebKeyConfigType should be a string, got %s", data)
}
var err error
*i, err = WebKeyConfigTypeString(s)
return err
}
// MarshalText implements the encoding.TextMarshaler interface for WebKeyConfigType
func (i WebKeyConfigType) MarshalText() ([]byte, error) {
return []byte(i.String()), nil
}
// UnmarshalText implements the encoding.TextUnmarshaler interface for WebKeyConfigType
func (i *WebKeyConfigType) UnmarshalText(text []byte) error {
var err error
*i, err = WebKeyConfigTypeString(string(text))
return err
}

View File

@ -10,36 +10,13 @@ import (
type KeyPair struct {
es_models.ObjectRoot
Usage KeyUsage
Usage crypto.KeyUsage
Algorithm string
PrivateKey *Key
PublicKey *Key
Certificate *Key
}
type KeyUsage int32
const (
KeyUsageSigning KeyUsage = iota
KeyUsageSAMLMetadataSigning
KeyUsageSAMLResponseSinging
KeyUsageSAMLCA
)
func (u KeyUsage) String() string {
switch u {
case KeyUsageSigning:
return "sig"
case KeyUsageSAMLCA:
return "saml_ca"
case KeyUsageSAMLResponseSinging:
return "saml_response_sig"
case KeyUsageSAMLMetadataSigning:
return "saml_metadata_sig"
}
return ""
}
type Key struct {
Key *crypto.CryptoValue
Expiry time.Time

View File

@ -0,0 +1,11 @@
package domain
type WebKeyState int
const (
WebKeyStateUnspecified WebKeyState = iota
WebKeyStateInitial
WebKeyStateActive
WebKeyStateInactive
WebKeyStateRemoved
)

View File

@ -48,15 +48,25 @@ func WithInstanceID(id string) aggregateOpt {
}
}
// AggregateFromWriteModel maps the given WriteModel to an Aggregate
// AggregateFromWriteModel maps the given WriteModel to an Aggregate.
// Deprecated: Creates linter errors on missing context. Use [AggregateFromWriteModelCtx] instead.
func AggregateFromWriteModel(
wm *WriteModel,
typ AggregateType,
version Version,
) *Aggregate {
return AggregateFromWriteModelCtx(context.Background(), wm, typ, version)
}
// AggregateFromWriteModelCtx maps the given WriteModel to an Aggregate.
func AggregateFromWriteModelCtx(
ctx context.Context,
wm *WriteModel,
typ AggregateType,
version Version,
) *Aggregate {
return NewAggregate(
// TODO: the linter complains if this function is called without passing a context
context.Background(),
ctx,
wm.AggregateID,
typ,
version,

View File

@ -166,7 +166,7 @@ func (e *mockEvent) DataAsBytes() []byte {
}
payload, err := json.Marshal(e.Payload())
if err != nil {
panic("unable to unmarshal")
panic(err)
}
return payload
}

View File

@ -14,6 +14,7 @@ const (
KeyTokenExchange
KeyActions
KeyImprovedPerformance
KeyWebKey
)
//go:generate enumer -type Level -transform snake -trimprefix Level
@ -37,6 +38,7 @@ type Features struct {
TokenExchange bool `json:"token_exchange,omitempty"`
Actions bool `json:"actions,omitempty"`
ImprovedPerformance []ImprovedPerformanceType `json:"improved_performance,omitempty"`
WebKey bool `json:"web_key,omitempty"`
}
type ImprovedPerformanceType int32

View File

@ -7,11 +7,11 @@ import (
"strings"
)
const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performance"
const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_key"
var _KeyIndex = [...]uint8{0, 11, 28, 61, 81, 92, 106, 113, 133}
var _KeyIndex = [...]uint8{0, 11, 28, 61, 81, 92, 106, 113, 133, 140}
const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performance"
const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchangeactionsimproved_performanceweb_key"
func (i Key) String() string {
if i < 0 || i >= Key(len(_KeyIndex)-1) {
@ -32,9 +32,10 @@ func _KeyNoOp() {
_ = x[KeyTokenExchange-(5)]
_ = x[KeyActions-(6)]
_ = x[KeyImprovedPerformance-(7)]
_ = x[KeyWebKey-(8)]
}
var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection, KeyUserSchema, KeyTokenExchange, KeyActions, KeyImprovedPerformance}
var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection, KeyUserSchema, KeyTokenExchange, KeyActions, KeyImprovedPerformance, KeyWebKey}
var _KeyNameToValueMap = map[string]Key{
_KeyName[0:11]: KeyUnspecified,
@ -53,6 +54,8 @@ var _KeyNameToValueMap = map[string]Key{
_KeyLowerName[106:113]: KeyActions,
_KeyName[113:133]: KeyImprovedPerformance,
_KeyLowerName[113:133]: KeyImprovedPerformance,
_KeyName[133:140]: KeyWebKey,
_KeyLowerName[133:140]: KeyWebKey,
}
var _KeyNames = []string{
@ -64,6 +67,7 @@ var _KeyNames = []string{
_KeyName[92:106],
_KeyName[106:113],
_KeyName[113:133],
_KeyName[133:140],
}
// KeyString retrieves an enum value from the enum constants string name.

View File

@ -36,6 +36,7 @@ import (
org "github.com/zitadel/zitadel/pkg/grpc/org/v2"
org_v2beta "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
webkey_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
session "github.com/zitadel/zitadel/pkg/grpc/session/v2"
session_v2beta "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2"
@ -63,10 +64,11 @@ type Client struct {
OrgV2beta org_v2beta.OrganizationServiceClient
OrgV2 org.OrganizationServiceClient
System system.SystemServiceClient
ActionV3 action.ZITADELActionsClient
ActionV3Alpha action.ZITADELActionsClient
FeatureV2beta feature_v2beta.FeatureServiceClient
FeatureV2 feature.FeatureServiceClient
UserSchemaV3 schema.UserSchemaServiceClient
WebKeyV3Alpha webkey_v3alpha.ZITADELWebKeysClient
}
func newClient(cc *grpc.ClientConn) Client {
@ -86,10 +88,11 @@ func newClient(cc *grpc.ClientConn) Client {
OrgV2beta: org_v2beta.NewOrganizationServiceClient(cc),
OrgV2: org.NewOrganizationServiceClient(cc),
System: system.NewSystemServiceClient(cc),
ActionV3: action.NewZITADELActionsClient(cc),
ActionV3Alpha: action.NewZITADELActionsClient(cc),
FeatureV2beta: feature_v2beta.NewFeatureServiceClient(cc),
FeatureV2: feature.NewFeatureServiceClient(cc),
UserSchemaV3: schema.NewUserSchemaServiceClient(cc),
WebKeyV3Alpha: webkey_v3alpha.NewZITADELWebKeysClient(cc),
}
}
@ -649,20 +652,20 @@ func (s *Tester) CreateTarget(ctx context.Context, t *testing.T, name, endpoint
RestAsync: &action.SetRESTAsync{},
}
}
target, err := s.Client.ActionV3.CreateTarget(ctx, &action.CreateTargetRequest{Target: reqTarget})
target, err := s.Client.ActionV3Alpha.CreateTarget(ctx, &action.CreateTargetRequest{Target: reqTarget})
require.NoError(t, err)
return target
}
func (s *Tester) DeleteExecution(ctx context.Context, t *testing.T, cond *action.Condition) {
_, err := s.Client.ActionV3.SetExecution(ctx, &action.SetExecutionRequest{
_, err := s.Client.ActionV3Alpha.SetExecution(ctx, &action.SetExecutionRequest{
Condition: cond,
})
require.NoError(t, err)
}
func (s *Tester) SetExecution(ctx context.Context, t *testing.T, cond *action.Condition, targets []*action.ExecutionTargetType) *action.SetExecutionResponse {
target, err := s.Client.ActionV3.SetExecution(ctx, &action.SetExecutionRequest{
target, err := s.Client.ActionV3Alpha.SetExecution(ctx, &action.SetExecutionRequest{
Condition: cond,
Execution: &action.Execution{
Targets: targets,

View File

@ -10,7 +10,6 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/call"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query/projection"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
@ -66,7 +65,7 @@ var (
}
)
func (q *Queries) ActiveCertificates(ctx context.Context, t time.Time, usage domain.KeyUsage) (certs *Certificates, err error) {
func (q *Queries) ActiveCertificates(ctx context.Context, t time.Time, usage crypto.KeyUsage) (certs *Certificates, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()

View File

@ -9,7 +9,6 @@ import (
"testing"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/zerrors"
)
@ -109,7 +108,7 @@ func Test_CertificatePrepares(t *testing.T) {
sequence: 20211109,
resourceOwner: "ro",
algorithm: "",
use: domain.KeyUsageSAMLMetadataSigning,
use: crypto.KeyUsageSAMLMetadataSigning,
},
expiry: testNow,
certificate: []byte("privateKey"),

View File

@ -16,6 +16,7 @@ type InstanceFeatures struct {
TokenExchange FeatureSource[bool]
Actions FeatureSource[bool]
ImprovedPerformance FeatureSource[[]feature.ImprovedPerformanceType]
WebKey FeatureSource[bool]
}
func (q *Queries) GetInstanceFeatures(ctx context.Context, cascade bool) (_ *InstanceFeatures, err error) {

View File

@ -67,6 +67,7 @@ func (m *InstanceFeaturesReadModel) Query() *eventstore.SearchQueryBuilder {
feature_v2.InstanceTokenExchangeEventType,
feature_v2.InstanceActionsEventType,
feature_v2.InstanceImprovedPerformanceEventType,
feature_v2.InstanceWebKeyEventType,
).
Builder().ResourceOwner(m.ResourceOwner)
}
@ -115,6 +116,8 @@ func reduceInstanceFeatureSet[T any](features *InstanceFeatures, event *feature_
features.Actions.set(level, event.Value)
case feature.KeyImprovedPerformance:
features.ImprovedPerformance.set(level, event.Value)
case feature.KeyWebKey:
features.WebKey.set(level, event.Value)
}
return nil
}

View File

@ -11,7 +11,6 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/call"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/query/projection"
"github.com/zitadel/zitadel/internal/repository/keypair"
@ -22,7 +21,7 @@ import (
type Key interface {
ID() string
Algorithm() string
Use() domain.KeyUsage
Use() crypto.KeyUsage
Sequence() uint64
}
@ -55,7 +54,7 @@ type key struct {
sequence uint64
resourceOwner string
algorithm string
use domain.KeyUsage
use crypto.KeyUsage
}
func (k *key) ID() string {
@ -66,7 +65,7 @@ func (k *key) Algorithm() string {
return k.algorithm
}
func (k *key) Use() domain.KeyUsage {
func (k *key) Use() crypto.KeyUsage {
return k.use
}
@ -222,7 +221,7 @@ func (q *Queries) ActivePrivateSigningKey(ctx context.Context, t time.Time) (key
query, args, err := stmt.Where(
sq.And{
sq.Eq{
KeyColUse.identifier(): domain.KeyUsageSigning,
KeyColUse.identifier(): crypto.KeyUsageSigning,
KeyColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
},
sq.Gt{KeyPrivateColExpiry.identifier(): t},
@ -358,7 +357,7 @@ type PublicKeyReadModel struct {
Algorithm string
Key *crypto.CryptoValue
Expiry time.Time
Usage domain.KeyUsage
Usage crypto.KeyUsage
}
func NewPublicKeyReadModel(keyID, resourceOwner string) *PublicKeyReadModel {

View File

@ -19,7 +19,6 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
key_repo "github.com/zitadel/zitadel/internal/repository/keypair"
"github.com/zitadel/zitadel/internal/zerrors"
@ -131,7 +130,7 @@ func Test_KeyPrepares(t *testing.T) {
sequence: 20211109,
resourceOwner: "ro",
algorithm: "RS256",
use: domain.KeyUsageSigning,
use: crypto.KeyUsageSigning,
},
expiry: testNow,
publicKey: &rsa.PublicKey{
@ -212,7 +211,7 @@ func Test_KeyPrepares(t *testing.T) {
sequence: 20211109,
resourceOwner: "ro",
algorithm: "RS256",
use: domain.KeyUsageSigning,
use: crypto.KeyUsageSigning,
},
expiry: testNow,
privateKey: &crypto.CryptoValue{
@ -306,7 +305,7 @@ func TestQueries_GetPublicKeyByID(t *testing.T) {
InstanceID: "instanceID",
Version: key_repo.AggregateVersion,
},
domain.KeyUsageSigning, "alg",
crypto.KeyUsageSigning, "alg",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
@ -345,7 +344,7 @@ func TestQueries_GetPublicKeyByID(t *testing.T) {
InstanceID: "instanceID",
Version: key_repo.AggregateVersion,
},
domain.KeyUsageSigning, "alg",
crypto.KeyUsageSigning, "alg",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
@ -385,7 +384,7 @@ func TestQueries_GetPublicKeyByID(t *testing.T) {
InstanceID: "instanceID",
Version: key_repo.AggregateVersion,
},
domain.KeyUsageSigning, "alg",
crypto.KeyUsageSigning, "alg",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
@ -416,7 +415,7 @@ func TestQueries_GetPublicKeyByID(t *testing.T) {
id: "keyID",
resourceOwner: "instanceID",
algorithm: "alg",
use: domain.KeyUsageSigning,
use: crypto.KeyUsageSigning,
},
expiry: future,
publicKey: func() *rsa.PublicKey {

View File

@ -88,6 +88,10 @@ func (*instanceFeatureProjection) Reducers() []handler.AggregateReducer {
Event: feature_v2.InstanceImprovedPerformanceEventType,
Reduce: reduceInstanceSetFeature[[]feature.ImprovedPerformanceType],
},
{
Event: feature_v2.InstanceWebKeyEventType,
Reduce: reduceInstanceSetFeature[bool],
},
{
Event: instance.InstanceRemovedEventType,
Reduce: reduceInstanceRemovedHelper(InstanceDomainInstanceIDCol),

View File

@ -8,7 +8,6 @@ import (
"go.uber.org/mock/gomock"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
"github.com/zitadel/zitadel/internal/repository/instance"
@ -33,7 +32,7 @@ func TestKeyProjection_reduces(t *testing.T) {
testEvent(
keypair.AddedEventType,
keypair.AggregateType,
keypairAddedEventData(domain.KeyUsageSigning, time.Now().Add(time.Hour)),
keypairAddedEventData(crypto.KeyUsageSigning, time.Now().Add(time.Hour)),
), keypair.AddedEventMapper),
},
reduce: (&keyProjection{encryptionAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t))}).reduceKeyPairAdded,
@ -52,7 +51,7 @@ func TestKeyProjection_reduces(t *testing.T) {
"instance-id",
uint64(15),
"algorithm",
domain.KeyUsageSigning,
crypto.KeyUsageSigning,
},
},
{
@ -89,7 +88,7 @@ func TestKeyProjection_reduces(t *testing.T) {
testEvent(
keypair.AddedEventType,
keypair.AggregateType,
keypairAddedEventData(domain.KeyUsageSigning, time.Now().Add(-time.Hour)),
keypairAddedEventData(crypto.KeyUsageSigning, time.Now().Add(-time.Hour)),
), keypair.AddedEventMapper),
},
reduce: (&keyProjection{}).reduceKeyPairAdded,
@ -132,7 +131,7 @@ func TestKeyProjection_reduces(t *testing.T) {
testEvent(
keypair.AddedCertificateEventType,
keypair.AggregateType,
certificateAddedEventData(domain.KeyUsageSAMLMetadataSigning, time.Now().Add(time.Hour)),
certificateAddedEventData(crypto.KeyUsageSAMLMetadataSigning, time.Now().Add(time.Hour)),
), keypair.AddedCertificateEventMapper),
},
reduce: (&keyProjection{certEncryptionAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t))}).reduceCertificateAdded,
@ -170,10 +169,10 @@ func TestKeyProjection_reduces(t *testing.T) {
}
}
func keypairAddedEventData(usage domain.KeyUsage, t time.Time) []byte {
func keypairAddedEventData(usage crypto.KeyUsage, t time.Time) []byte {
return []byte(`{"algorithm": "algorithm", "usage": ` + fmt.Sprintf("%d", usage) + `, "privateKey": {"key": {"cryptoType": 0, "algorithm": "enc", "keyID": "id", "crypted": "cHJpdmF0ZUtleQ=="}, "expiry": "` + t.Format(time.RFC3339) + `"}, "publicKey": {"key": {"cryptoType": 0, "algorithm": "enc", "keyID": "id", "crypted": "cHVibGljS2V5"}, "expiry": "` + t.Format(time.RFC3339) + `"}}`)
}
func certificateAddedEventData(usage domain.KeyUsage, t time.Time) []byte {
func certificateAddedEventData(usage crypto.KeyUsage, t time.Time) []byte {
return []byte(`{"algorithm": "algorithm", "usage": ` + fmt.Sprintf("%d", usage) + `, "certificate": {"key": {"cryptoType": 0, "algorithm": "enc", "keyID": "id", "crypted": "cHJpdmF0ZUtleQ=="}, "expiry": "` + t.Format(time.RFC3339) + `"}}`)
}

View File

@ -78,6 +78,7 @@ var (
TargetProjection *handler.Handler
ExecutionProjection *handler.Handler
UserSchemaProjection *handler.Handler
WebKeyProjection *handler.Handler
ProjectGrantFields *handler.FieldHandler
OrgDomainVerifiedFields *handler.FieldHandler
@ -163,6 +164,7 @@ func Create(ctx context.Context, sqlClient *database.DB, es handler.EventStore,
TargetProjection = newTargetProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["targets"]))
ExecutionProjection = newExecutionProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["executions"]))
UserSchemaProjection = newUserSchemaProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["user_schemas"]))
WebKeyProjection = newWebKeyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["web_keys"]))
ProjectGrantFields = newFillProjectGrantFields(applyCustomConfig(projectionConfig, config.Customizations[fieldsProjectGrant]))
OrgDomainVerifiedFields = newFillOrgDomainVerifiedFields(applyCustomConfig(projectionConfig, config.Customizations[fieldsOrgDomainVerified]))
@ -292,5 +294,6 @@ func newProjectionsList() {
TargetProjection,
ExecutionProjection,
UserSchemaProjection,
WebKeyProjection,
}
}

View File

@ -0,0 +1,165 @@
package projection
import (
"context"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
old_handler "github.com/zitadel/zitadel/internal/eventstore/handler"
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
"github.com/zitadel/zitadel/internal/repository/instance"
"github.com/zitadel/zitadel/internal/repository/webkey"
"github.com/zitadel/zitadel/internal/zerrors"
)
const (
WebKeyTable = "projections.web_keys"
WebKeyInstanceIDCol = "instance_id"
WebKeyKeyIDCol = "key_id"
WebKeyCreationDateCol = "creation_date"
WebKeyChangeDateCol = "change_date"
WebKeySequenceCol = "sequence"
WebKeyStateCol = "state"
WebKeyPrivateKeyCol = "private_key"
WebKeyPublicKeyCol = "public_key"
WebKeyConfigCol = "config"
WebKeyConfigTypeCol = "config_type"
)
type webKeyProjection struct{}
func newWebKeyProjection(ctx context.Context, config handler.Config) *handler.Handler {
return handler.NewHandler(ctx, &config, new(webKeyProjection))
}
func (*webKeyProjection) Name() string {
return WebKeyTable
}
func (*webKeyProjection) Init() *old_handler.Check {
return handler.NewTableCheck(
handler.NewTable(
[]*handler.InitColumn{
handler.NewColumn(WebKeyInstanceIDCol, handler.ColumnTypeText),
handler.NewColumn(WebKeyKeyIDCol, handler.ColumnTypeText),
handler.NewColumn(WebKeyCreationDateCol, handler.ColumnTypeTimestamp),
handler.NewColumn(WebKeyChangeDateCol, handler.ColumnTypeTimestamp),
handler.NewColumn(WebKeySequenceCol, handler.ColumnTypeInt64),
handler.NewColumn(WebKeyStateCol, handler.ColumnTypeInt64),
handler.NewColumn(WebKeyPrivateKeyCol, handler.ColumnTypeJSONB),
handler.NewColumn(WebKeyPublicKeyCol, handler.ColumnTypeJSONB),
handler.NewColumn(WebKeyConfigCol, handler.ColumnTypeJSONB),
handler.NewColumn(WebKeyConfigTypeCol, handler.ColumnTypeInt64),
},
handler.NewPrimaryKey(WebKeyInstanceIDCol, WebKeyKeyIDCol),
// index to find the current active private key for an instance.
handler.WithIndex(handler.NewIndex(
"web_key_state",
[]string{WebKeyInstanceIDCol, WebKeyStateCol},
handler.WithInclude(
WebKeyPrivateKeyCol,
),
)),
),
)
}
func (p *webKeyProjection) Reducers() []handler.AggregateReducer {
return []handler.AggregateReducer{{
Aggregate: webkey.AggregateType,
EventReducers: []handler.EventReducer{
{
Event: webkey.AddedEventType,
Reduce: p.reduceWebKeyAdded,
},
{
Event: webkey.ActivatedEventType,
Reduce: p.reduceWebKeyActivated,
},
{
Event: webkey.DeactivatedEventType,
Reduce: p.reduceWebKeyDeactivated,
},
{
Event: webkey.RemovedEventType,
Reduce: p.reduceWebKeyRemoved,
},
{
Event: instance.InstanceRemovedEventType,
Reduce: reduceInstanceRemovedHelper(WebKeyInstanceIDCol),
},
},
}}
}
func (p *webKeyProjection) reduceWebKeyAdded(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*webkey.AddedEvent)
if !ok {
return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-jei2K", "reduce.wrong.event.type %s", webkey.AddedEventType)
}
return handler.NewCreateStatement(e,
[]handler.Column{
handler.NewCol(WebKeyInstanceIDCol, e.Agg.InstanceID),
handler.NewCol(WebKeyKeyIDCol, e.Agg.ID),
handler.NewCol(WebKeyCreationDateCol, e.CreationDate()),
handler.NewCol(WebKeyChangeDateCol, e.CreationDate()),
handler.NewCol(WebKeySequenceCol, e.Sequence()),
handler.NewCol(WebKeyStateCol, domain.WebKeyStateInitial),
handler.NewCol(WebKeyPrivateKeyCol, e.PrivateKey),
handler.NewCol(WebKeyPublicKeyCol, e.PublicKey),
handler.NewCol(WebKeyConfigCol, e.Config),
handler.NewCol(WebKeyConfigTypeCol, e.ConfigType),
},
), nil
}
func (p *webKeyProjection) reduceWebKeyActivated(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*webkey.ActivatedEvent)
if !ok {
return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-iiQu2", "reduce.wrong.event.type %s", webkey.ActivatedEventType)
}
return handler.NewUpdateStatement(e,
[]handler.Column{
handler.NewCol(WebKeyChangeDateCol, e.CreationDate()),
handler.NewCol(WebKeySequenceCol, e.Sequence()),
handler.NewCol(WebKeyStateCol, domain.WebKeyStateActive),
},
[]handler.Condition{
handler.NewCond(WebKeyInstanceIDCol, e.Agg.InstanceID),
handler.NewCond(WebKeyKeyIDCol, e.Agg.ID),
},
), nil
}
func (p *webKeyProjection) reduceWebKeyDeactivated(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*webkey.DeactivatedEvent)
if !ok {
return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-zei3E", "reduce.wrong.event.type %s", webkey.DeactivatedEventType)
}
return handler.NewUpdateStatement(e,
[]handler.Column{
handler.NewCol(WebKeyChangeDateCol, e.CreationDate()),
handler.NewCol(WebKeySequenceCol, e.Sequence()),
handler.NewCol(WebKeyStateCol, domain.WebKeyStateInactive),
},
[]handler.Condition{
handler.NewCond(WebKeyInstanceIDCol, e.Agg.InstanceID),
handler.NewCond(WebKeyKeyIDCol, e.Agg.ID),
},
), nil
}
func (p *webKeyProjection) reduceWebKeyRemoved(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*webkey.RemovedEvent)
if !ok {
return nil, zerrors.ThrowInvalidArgumentf(nil, "PROJE-Zei6f", "reduce.wrong.event.type %s", webkey.RemovedEventType)
}
return handler.NewDeleteStatement(e,
[]handler.Condition{
handler.NewCond(WebKeyInstanceIDCol, e.Agg.InstanceID),
handler.NewCond(WebKeyKeyIDCol, e.Agg.ID),
},
), nil
}

154
internal/query/web_key.go Normal file
View File

@ -0,0 +1,154 @@
package query
import (
"context"
"database/sql"
_ "embed"
"encoding/json"
"errors"
"time"
"github.com/go-jose/go-jose/v4"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
var (
//go:embed web_key_by_state.sql
webKeyByStateQuery string
//go:embed web_key_list.sql
webKeyListQuery string
//go:embed web_key_public_keys.sql
webKeyPublicKeysQuery string
)
// GetPublicWebKeyByID gets a public key by it's keyID directly from the eventstore.
func (q *Queries) GetPublicWebKeyByID(ctx context.Context, keyID string) (webKey *jose.JSONWebKey, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
model := NewWebKeyReadModel(keyID, authz.GetInstance(ctx).InstanceID())
if err = q.eventstore.FilterToQueryReducer(ctx, model); err != nil {
return nil, err
}
if model.State == domain.WebKeyStateUnspecified || model.State == domain.WebKeyStateRemoved {
return nil, zerrors.ThrowNotFound(nil, "QUERY-AiCh0", "Errors.WebKey.NotFound")
}
return model.PublicKey, nil
}
// GetActiveSigningWebKey gets the current active signing key from the web_keys projection.
// The active signing key is eventual consistent.
func (q *Queries) GetActiveSigningWebKey(ctx context.Context) (webKey *jose.JSONWebKey, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
var keyValue *crypto.CryptoValue
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
return row.Scan(&keyValue)
},
webKeyByStateQuery,
authz.GetInstance(ctx).InstanceID(),
domain.WebKeyStateActive,
)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, zerrors.ThrowInternal(err, "QUERY-Opoh7", "Errors.WebKey.NoActive")
}
return nil, zerrors.ThrowInternal(err, "QUERY-Shoo0", "Errors.Internal")
}
if err = crypto.DecryptJSON(keyValue, &webKey, q.keyEncryptionAlgorithm); err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-Iuk0s", "Errors.Internal")
}
return webKey, nil
}
type WebKeyDetails struct {
KeyID string
CreationDate time.Time
ChangeDate time.Time
Sequence int64
State domain.WebKeyState
Config crypto.WebKeyConfig
}
type WebKeyList struct {
Keys []WebKeyDetails
}
// ListWebKeys gets a list of [WebKeyDetails] for the complete instance from the web_keys projection.
// The list is eventual consistent.
func (q *Queries) ListWebKeys(ctx context.Context) (list []WebKeyDetails, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
err = q.client.QueryContext(ctx, func(rows *sql.Rows) error {
for rows.Next() {
var (
configData []byte
configType crypto.WebKeyConfigType
)
var details WebKeyDetails
if err = rows.Scan(
&details.KeyID,
&details.CreationDate,
&details.ChangeDate,
&details.Sequence,
&details.State,
&configData,
&configType,
); err != nil {
return err
}
details.Config, err = crypto.UnmarshalWebKeyConfig(configData, configType)
if err != nil {
return err
}
list = append(list, details)
}
return rows.Err()
},
webKeyListQuery,
authz.GetInstance(ctx).InstanceID(),
)
if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-Ohl3A", "Errors.Internal")
}
return list, nil
}
// GetWebKeySet gets a JSON Web Key set from the web_keys projection.
// The set contains all existing public keys for the instance.
// The set is eventual consistent.
func (q *Queries) GetWebKeySet(ctx context.Context) (_ *jose.JSONWebKeySet, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
var keys []jose.JSONWebKey
err = q.client.QueryContext(ctx, func(rows *sql.Rows) error {
for rows.Next() {
var webKeyData []byte
if err = rows.Scan(&webKeyData); err != nil {
return err
}
var webKey jose.JSONWebKey
if err = json.Unmarshal(webKeyData, &webKey); err != nil {
return err
}
keys = append(keys, webKey)
}
return rows.Err()
},
webKeyPublicKeysQuery,
authz.GetInstance(ctx).InstanceID(),
)
if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-Eeng7", "Errors.Internal")
}
return &jose.JSONWebKeySet{Keys: keys}, nil
}

View File

@ -0,0 +1,5 @@
select private_key
from projections.web_keys
where instance_id = $1
and state = $2
limit 1;

View File

@ -0,0 +1,4 @@
select key_id, creation_date, change_date, sequence, state, config, config_type
from projections.web_keys
where instance_id = $1
order by creation_date asc;

View File

@ -0,0 +1,74 @@
package query
import (
"github.com/go-jose/go-jose/v4"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/webkey"
)
type WebKeyReadModel struct {
eventstore.ReadModel
State domain.WebKeyState
PrivateKey *crypto.CryptoValue
PublicKey *jose.JSONWebKey
Config crypto.WebKeyConfig
}
func NewWebKeyReadModel(keyID, resourceOwner string) *WebKeyReadModel {
return &WebKeyReadModel{
ReadModel: eventstore.ReadModel{
AggregateID: keyID,
ResourceOwner: resourceOwner,
},
}
}
func (wm *WebKeyReadModel) AppendEvents(events ...eventstore.Event) {
wm.ReadModel.AppendEvents(events...)
}
func (wm *WebKeyReadModel) Reduce() error {
for _, event := range wm.Events {
switch e := event.(type) {
case *webkey.AddedEvent:
if err := wm.reduceAdded(e); err != nil {
return err
}
case *webkey.ActivatedEvent:
wm.State = domain.WebKeyStateActive
case *webkey.DeactivatedEvent:
wm.State = domain.WebKeyStateInactive
case *webkey.RemovedEvent:
wm.State = domain.WebKeyStateRemoved
wm.PrivateKey = nil
wm.PublicKey = nil
}
}
return wm.ReadModel.Reduce()
}
func (wm *WebKeyReadModel) reduceAdded(e *webkey.AddedEvent) (err error) {
wm.State = domain.WebKeyStateInitial
wm.PrivateKey = e.PrivateKey
wm.PublicKey = e.PublicKey
wm.Config, err = crypto.UnmarshalWebKeyConfig(e.Config, e.ConfigType)
return err
}
func (wm *WebKeyReadModel) Query() *eventstore.SearchQueryBuilder {
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
ResourceOwner(wm.ResourceOwner).
AddQuery().
AggregateTypes(webkey.AggregateType).
AggregateIDs(wm.AggregateID).
EventTypes(
webkey.AddedEventType,
webkey.ActivatedEventType,
webkey.DeactivatedEventType,
webkey.RemovedEventType,
).
Builder()
}

View File

@ -0,0 +1,3 @@
select public_key
from projections.web_keys
where instance_id = $1;

View File

@ -0,0 +1,382 @@
package query
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"database/sql"
"database/sql/driver"
"encoding/json"
"io"
"regexp"
"strconv"
"testing"
"time"
"github.com/go-jose/go-jose/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/repository/webkey"
"github.com/zitadel/zitadel/internal/zerrors"
)
func TestQueries_GetPublicWebKeyByID(t *testing.T) {
ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil)
key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
require.NoError(t, err)
type fields struct {
eventstore func(*testing.T) *eventstore.Eventstore
}
type args struct {
keyID string
}
tests := []struct {
name string
fields fields
args args
want *jose.JSONWebKey
wantErr error
}{
{
name: "filter error",
fields: fields{
eventstore: expectEventstore(
expectFilterError(io.ErrClosedPipe),
),
},
args: args{"key1"},
wantErr: io.ErrClosedPipe,
},
{
name: "not found error",
fields: fields{
eventstore: expectEventstore(
expectFilter(),
),
},
args: args{"key1"},
wantErr: zerrors.ThrowNotFound(nil, "QUERY-AiCh0", "Errors.WebKey.NotFound"),
},
{
name: "removed, not found error",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(mustNewWebkeyAddedEvent(ctx,
webkey.NewAggregate("key1", "instance1"),
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
},
&jose.JSONWebKey{
Key: &key.PublicKey,
KeyID: "key1",
Algorithm: string(jose.ES384),
Use: crypto.KeyUsageSigning.String(),
},
&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
)),
eventFromEventPusher(webkey.NewRemovedEvent(ctx,
webkey.NewAggregate("key1", "instance1"),
)),
),
),
},
args: args{"key1"},
wantErr: zerrors.ThrowNotFound(nil, "QUERY-AiCh0", "Errors.WebKey.NotFound"),
},
{
name: "ok",
fields: fields{
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(mustNewWebkeyAddedEvent(ctx,
webkey.NewAggregate("key1", "instance1"),
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "alg",
KeyID: "encKey",
Crypted: []byte("crypted"),
},
&jose.JSONWebKey{
Key: &key.PublicKey,
KeyID: "key1",
Algorithm: string(jose.ES384),
Use: crypto.KeyUsageSigning.String(),
},
&crypto.WebKeyECDSAConfig{
Curve: crypto.EllipticCurveP384,
},
)),
),
),
},
args: args{"key1"},
want: &jose.JSONWebKey{
Key: &key.PublicKey,
KeyID: "key1",
Algorithm: string(jose.ES384),
Use: crypto.KeyUsageSigning.String(),
Certificates: []*x509.Certificate{},
CertificateThumbprintSHA1: []byte{},
CertificateThumbprintSHA256: []byte{},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
q := &Queries{
eventstore: tt.fields.eventstore(t),
}
got, err := q.GetPublicWebKeyByID(ctx, tt.args.keyID)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
}
}
func mustNewWebkeyAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
privateKey *crypto.CryptoValue,
publicKey *jose.JSONWebKey,
config crypto.WebKeyConfig) *webkey.AddedEvent {
event, err := webkey.NewAddedEvent(ctx, aggregate, privateKey, publicKey, config)
if err != nil {
panic(err)
}
return event
}
func TestQueries_GetActiveSigningWebKey(t *testing.T) {
ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil)
expQuery := regexp.QuoteMeta(webKeyByStateQuery)
queryArgs := []driver.Value{"instance1", domain.WebKeyStateActive}
cols := []string{"private_key"}
alg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
encryptedPrivate, _, err := crypto.GenerateEncryptedWebKey("key1", alg, &crypto.WebKeyED25519Config{})
require.NoError(t, err)
var expectedWebKey *jose.JSONWebKey
err = crypto.DecryptJSON(encryptedPrivate, &expectedWebKey, alg)
require.NoError(t, err)
tests := []struct {
name string
mock sqlExpectation
want *jose.JSONWebKey
wantErr error
}{
{
name: "no active error",
mock: mockQueryErr(expQuery, sql.ErrNoRows, queryArgs...),
wantErr: zerrors.ThrowInternal(sql.ErrNoRows, "QUERY-Opoh7", "Errors.WebKey.NoActive"),
},
{
name: "internal error",
mock: mockQueryErr(expQuery, sql.ErrConnDone, queryArgs...),
wantErr: zerrors.ThrowInternal(sql.ErrConnDone, "QUERY-Shoo0", "Errors.Internal"),
},
{
name: "invalid crypto value error",
mock: mockQuery(expQuery, cols, []driver.Value{&crypto.CryptoValue{}}, queryArgs...),
wantErr: zerrors.ThrowInvalidArgument(nil, "CRYPT-Nx7XlT", "value was encrypted with a different key"),
},
{
name: "found, ok",
mock: mockQuery(expQuery, cols, []driver.Value{encryptedPrivate}, queryArgs...),
want: expectedWebKey,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
execMock(t, tt.mock, func(db *sql.DB) {
q := &Queries{
client: &database.DB{
DB: db,
Database: &prepareDB{},
},
keyEncryptionAlgorithm: alg,
}
got, err := q.GetActiveSigningWebKey(ctx)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
})
}
}
func TestQueries_ListWebKeys(t *testing.T) {
ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil)
expQuery := regexp.QuoteMeta(webKeyListQuery)
queryArgs := []driver.Value{"instance1"}
cols := []string{"key_id", "creation_date", "change_date", "sequence", "state", "config", "config_type"}
webKeyConfig := &crypto.WebKeyRSAConfig{
Bits: crypto.RSABits4096,
Hasher: crypto.RSAHasherSHA512,
}
webKeyConfigJSON, err := json.Marshal(webKeyConfig)
require.NoError(t, err)
tests := []struct {
name string
mock sqlExpectation
want []WebKeyDetails
wantErr error
}{
{
name: "internal error",
mock: mockQueryErr(expQuery, sql.ErrConnDone, queryArgs...),
wantErr: zerrors.ThrowInternal(sql.ErrConnDone, "QUERY-Ohl3A", "Errors.Internal"),
},
{
name: "invalid json error",
mock: mockQueriesScanErr(expQuery, cols, [][]driver.Value{
{
"key1",
time.Unix(1, 2),
time.Unix(3, 4),
1,
domain.WebKeyStateActive,
"~~~~~",
crypto.WebKeyConfigTypeRSA,
},
}, queryArgs...),
wantErr: zerrors.ThrowInternal(err, "QUERY-Ohl3A", "Errors.Internal"),
},
{
name: "ok",
mock: mockQueries(expQuery, cols, [][]driver.Value{
{
"key1",
time.Unix(1, 2),
time.Unix(3, 4),
1,
domain.WebKeyStateActive,
webKeyConfigJSON,
crypto.WebKeyConfigTypeRSA,
},
{
"key2",
time.Unix(5, 6),
time.Unix(7, 8),
2,
domain.WebKeyStateInitial,
webKeyConfigJSON,
crypto.WebKeyConfigTypeRSA,
},
}, queryArgs...),
want: []WebKeyDetails{
{
KeyID: "key1",
CreationDate: time.Unix(1, 2),
ChangeDate: time.Unix(3, 4),
Sequence: 1,
State: domain.WebKeyStateActive,
Config: webKeyConfig,
},
{
KeyID: "key2",
CreationDate: time.Unix(5, 6),
ChangeDate: time.Unix(7, 8),
Sequence: 2,
State: domain.WebKeyStateInitial,
Config: webKeyConfig,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
execMock(t, tt.mock, func(db *sql.DB) {
q := &Queries{
client: &database.DB{
DB: db,
Database: &prepareDB{},
},
}
got, err := q.ListWebKeys(ctx)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
})
}
}
func TestQueries_GetWebKeySet(t *testing.T) {
ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil)
expQuery := regexp.QuoteMeta(webKeyPublicKeysQuery)
queryArgs := []driver.Value{"instance1"}
cols := []string{"public_key"}
alg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
conf := &crypto.WebKeyED25519Config{}
expectedKeySet := &jose.JSONWebKeySet{
Keys: make([]jose.JSONWebKey, 3),
}
expectedRows := make([][]driver.Value, 3)
for i := 0; i < 3; i++ {
_, pubKey, err := crypto.GenerateEncryptedWebKey(strconv.Itoa(i), alg, conf)
require.NoError(t, err)
pubKeyJSON, err := json.Marshal(pubKey)
require.NoError(t, err)
err = json.Unmarshal(pubKeyJSON, &expectedKeySet.Keys[i])
require.NoError(t, err)
expectedRows[i] = []driver.Value{pubKeyJSON}
}
tests := []struct {
name string
mock sqlExpectation
want *jose.JSONWebKeySet
wantErr error
}{
{
name: "internal error",
mock: mockQueryErr(expQuery, sql.ErrConnDone, queryArgs...),
wantErr: zerrors.ThrowInternal(sql.ErrConnDone, "QUERY-Eeng7", "Errors.Internal"),
},
{
name: "invalid json error",
mock: mockQueriesScanErr(expQuery, cols, [][]driver.Value{{"~~~"}}, queryArgs...),
wantErr: zerrors.ThrowInternal(nil, "QUERY-Eeng7", "Errors.Internal"),
},
{
name: "ok",
mock: mockQueries(expQuery, cols, expectedRows, queryArgs...),
want: expectedKeySet,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
execMock(t, tt.mock, func(db *sql.DB) {
q := &Queries{
client: &database.DB{
DB: db,
Database: &prepareDB{},
},
}
got, err := q.GetWebKeySet(ctx)
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.want, got)
})
})
}
}

View File

@ -23,4 +23,5 @@ func init() {
eventstore.RegisterFilterEventMapper(AggregateType, InstanceTokenExchangeEventType, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, InstanceActionsEventType, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, InstanceImprovedPerformanceEventType, eventstore.GenericEventMapper[SetEvent[[]feature.ImprovedPerformanceType]])
eventstore.RegisterFilterEventMapper(AggregateType, InstanceWebKeyEventType, eventstore.GenericEventMapper[SetEvent[bool]])
}

View File

@ -28,6 +28,7 @@ var (
InstanceTokenExchangeEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyTokenExchange)
InstanceActionsEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyActions)
InstanceImprovedPerformanceEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyImprovedPerformance)
InstanceWebKeyEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyWebKey)
)
const (

View File

@ -5,7 +5,6 @@ import (
"time"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/zerrors"
)
@ -18,7 +17,7 @@ const (
type AddedEvent struct {
eventstore.BaseEvent `json:"-"`
Usage domain.KeyUsage `json:"usage"`
Usage crypto.KeyUsage `json:"usage"`
Algorithm string `json:"algorithm"`
PrivateKey *Key `json:"privateKey"`
PublicKey *Key `json:"publicKey"`
@ -40,7 +39,7 @@ func (e *AddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
func NewAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
usage domain.KeyUsage,
usage crypto.KeyUsage,
algorithm string,
privateCrypto,
publicCrypto *crypto.CryptoValue,

View File

@ -0,0 +1,25 @@
package webkey
import (
"context"
"github.com/zitadel/zitadel/internal/eventstore"
)
const (
AggregateType = "web_key"
AggregateVersion = "v1"
)
func NewAggregate(id, resourceOwner string) *eventstore.Aggregate {
return &eventstore.Aggregate{
Type: AggregateType,
Version: AggregateVersion,
ID: id,
ResourceOwner: resourceOwner,
}
}
func AggregateFromWriteModel(ctx context.Context, wm *eventstore.WriteModel) *eventstore.Aggregate {
return eventstore.AggregateFromWriteModelCtx(ctx, wm, AggregateType, AggregateVersion)
}

View File

@ -0,0 +1,12 @@
package webkey
import (
"github.com/zitadel/zitadel/internal/eventstore"
)
func init() {
eventstore.RegisterFilterEventMapper(AggregateType, AddedEventType, eventstore.GenericEventMapper[AddedEvent])
eventstore.RegisterFilterEventMapper(AggregateType, ActivatedEventType, eventstore.GenericEventMapper[ActivatedEvent])
eventstore.RegisterFilterEventMapper(AggregateType, DeactivatedEventType, eventstore.GenericEventMapper[DeactivatedEvent])
eventstore.RegisterFilterEventMapper(AggregateType, RemovedEventType, eventstore.GenericEventMapper[RemovedEvent])
}

View File

@ -0,0 +1,160 @@
package webkey
import (
"context"
"encoding/json"
"github.com/go-jose/go-jose/v4"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/zerrors"
)
const (
UniqueWebKeyType = "web_key"
)
const (
eventTypePrefix = eventstore.EventType("web_key.")
AddedEventType = eventTypePrefix + "added"
ActivatedEventType = eventTypePrefix + "activated"
DeactivatedEventType = eventTypePrefix + "deactivated"
RemovedEventType = eventTypePrefix + "removed"
)
type AddedEvent struct {
*eventstore.BaseEvent `json:"-"`
PrivateKey *crypto.CryptoValue `json:"privateKey"`
PublicKey *jose.JSONWebKey `json:"publicKey"`
Config json.RawMessage `json:"config"`
ConfigType crypto.WebKeyConfigType `json:"configType"`
}
func (e *AddedEvent) Payload() interface{} {
return e
}
func (e *AddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return []*eventstore.UniqueConstraint{
eventstore.NewAddEventUniqueConstraint(UniqueWebKeyType, e.Agg.ID, "Errors.WebKey.Duplicate"),
}
}
func (e *AddedEvent) SetBaseEvent(base *eventstore.BaseEvent) {
e.BaseEvent = base
}
func NewAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
privateKey *crypto.CryptoValue,
publicKey *jose.JSONWebKey,
config crypto.WebKeyConfig,
) (*AddedEvent, error) {
configJson, err := json.Marshal(config)
if err != nil {
return nil, zerrors.ThrowInternal(err, "WEBKEY-IY9fa", "Errors.Internal")
}
return &AddedEvent{
BaseEvent: eventstore.NewBaseEventForPush(
ctx,
aggregate,
AddedEventType,
),
PrivateKey: privateKey,
PublicKey: publicKey,
Config: configJson,
ConfigType: config.Type(),
}, nil
}
type ActivatedEvent struct {
*eventstore.BaseEvent `json:"-"`
}
func (e *ActivatedEvent) Payload() interface{} {
return e
}
func (e *ActivatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func (e *ActivatedEvent) SetBaseEvent(base *eventstore.BaseEvent) {
e.BaseEvent = base
}
func NewActivatedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
) *ActivatedEvent {
return &ActivatedEvent{
BaseEvent: eventstore.NewBaseEventForPush(
ctx,
aggregate,
ActivatedEventType,
),
}
}
type DeactivatedEvent struct {
*eventstore.BaseEvent `json:"-"`
}
func (e *DeactivatedEvent) Payload() interface{} {
return e
}
func (e *DeactivatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func (e *DeactivatedEvent) SetBaseEvent(base *eventstore.BaseEvent) {
e.BaseEvent = base
}
func NewDeactivatedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
) *DeactivatedEvent {
return &DeactivatedEvent{
BaseEvent: eventstore.NewBaseEventForPush(
ctx,
aggregate,
DeactivatedEventType,
),
}
}
type RemovedEvent struct {
*eventstore.BaseEvent `json:"-"`
}
func (e *RemovedEvent) Payload() interface{} {
return e
}
func (e *RemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return []*eventstore.UniqueConstraint{
eventstore.NewRemoveUniqueConstraint(UniqueWebKeyType, e.Agg.ID),
}
}
func (e *RemovedEvent) SetBaseEvent(base *eventstore.BaseEvent) {
e.BaseEvent = base
}
func NewRemovedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
) *RemovedEvent {
return &RemovedEvent{
BaseEvent: eventstore.NewBaseEventForPush(
ctx,
aggregate,
RemovedEventType,
),
}
}

View File

@ -603,6 +603,13 @@ Errors:
NotForAPI: Имитирани токени не са разрешени за API
Impersonation:
PolicyDisabled: Имитирането е деактивирано в политиката за сигурност на екземпляра
WebKey:
ActiveDelete: Не може да се изтрие активен уеб ключ
Config: Невалидна конфигурация на уеб ключ
Duplicate: ID на уеб ключ не е уникален
FeatureDisabled: Ключовата уеб функция е деактивирана
NoActive: Не е намерен активен уеб ключ
NotFound: Уеб ключът не е намерен
AggregateTypes:
action: Действие
@ -626,6 +633,7 @@ AggregateTypes:
restrictions: Ограничения
system: Система
session: Сесия
web_key: Уеб ключ
EventTypes:
execution:
@ -1342,6 +1350,12 @@ EventTypes:
deactivated: Потребителската схема е деактивирана
reactivated: Потребителската схема е активирана отново
deleted: Потребителската схема е изтрита
web_key:
added: Добавен уеб ключ
activated: Уеб ключът е активиран
deactivated: Уеб ключът е деактивиран
removed: Уеб ключът е премахнат
Application:
OIDC:
UnsupportedVersion: Вашата OIDC версия не се поддържа

View File

@ -584,6 +584,13 @@ Errors:
NotForAPI: Zosobněné tokeny nejsou pro API povoleny
Impersonation:
PolicyDisabled: Zosobnění je zakázáno v zásadách zabezpečení instance
WebKey:
ActiveDelete: Aktivní webový klíč nelze smazat
Config: Neplatná konfigurace webového klíče
Duplicate: ID webového klíče není jedinečné
FeatureDisabled: Funkce webového klíče je zakázána
NoActive: Nebyl nalezen žádný aktivní webový klíč
NotFound: Webový klíč nebyl nalezen
AggregateTypes:
action: Akce
@ -607,6 +614,7 @@ AggregateTypes:
restrictions: Omezení
system: Systém
session: Sezení
web_key: Webový klíč
EventTypes:
execution:
@ -1308,6 +1316,11 @@ EventTypes:
deactivated: Uživatelské schéma deaktivováno
reactivated: Uživatelské schéma bylo znovu aktivováno
deleted: Uživatelské schéma bylo smazáno
web_key:
added: Přidán webový klíč
activated: Web Key aktivován
deactivated: Web Key deaktivován
removed: Odstraňte webový klíč
Application:
OIDC:

View File

@ -586,6 +586,13 @@ Errors:
NotForAPI: Imitierte Token sind für die API nicht zulässig
Impersonation:
PolicyDisabled: Der Identitätswechsel ist in der Sicherheitsrichtlinie der Instanz deaktiviert
WebKey:
ActiveDelete: Aktiver Webschlüssel kann nicht gelöscht werden
Config: Ungültige Webschlüsselkonfiguration
Duplicate: Webschlüssel-ID nicht eindeutig
FeatureDisabled: Webschlüsselfunktion deaktiviert
NoActive: Kein aktiver Webschlüssel gefunden
NotFound: Webschlüssel nicht gefunden
AggregateTypes:
action: Action
@ -609,6 +616,7 @@ AggregateTypes:
restrictions: Restriktionen
system: System
session: Session
web_key: Webschlüssel
EventTypes:
execution:
@ -1310,6 +1318,11 @@ EventTypes:
deactivated: Benutzerschema deaktiviert
reactivated: Benutzerschema reaktiviert
deleted: Benutzerschema gelöscht
web_key:
added: Web Key hinzugefügt
activated: Web Key aktiviert
deactivated: Web Key deaktiviert
removed: Web Key entfernen
Application:
OIDC:

View File

@ -586,6 +586,13 @@ Errors:
NotForAPI: Impersonated tokens not allowed for API
Impersonation:
PolicyDisabled: Impersonation is disabled in the instance security policy
WebKey:
ActiveDelete: Cannot delete active web key
Config: Invalid web key config
Duplicate: Web key ID not unique
FeatureDisabled: Web key feature disabled
NoActive: No active web key found
NotFound: Web key not found
AggregateTypes:
@ -610,6 +617,7 @@ AggregateTypes:
restrictions: Restrictions
system: System
session: Session
web_key: Web Key
EventTypes:
execution:
@ -1311,6 +1319,11 @@ EventTypes:
deactivated: User Schema deactivated
reactivated: User Schema reactivated
deleted: User Schema deleted
web_key:
added: Web Key added
activated: Web Key activated
deactivated: Web Key deactivated
removed: Web Key removed
Application:
OIDC:

View File

@ -586,6 +586,13 @@ Errors:
NotForAPI: Tokens suplantados no permitidos para API
Impersonation:
PolicyDisabled: La suplantación está deshabilitada en la política de seguridad de la instancia.
WebKey:
ActiveDelete: No se puede eliminar la clave web activa
Config: Configuración de clave web no válida
Duplicate: ID de clave web no único
FeatureDisabled: Función de clave web deshabilitada
NoActive: No se encontró ninguna clave web activa
NotFound: Clave web no encontrada
AggregateTypes:
action: Acción
@ -609,6 +616,7 @@ AggregateTypes:
restrictions: Restricciones
system: Sistema
session: Sesión
web_key: Clave web
EventTypes:
execution:
@ -1310,6 +1318,11 @@ EventTypes:
deactivated: Esquema de usuario desactivado
reactivated: Esquema de usuario reactivado
deleted: Esquema de usuario eliminado
web_key:
added: Clave web añadida
activated: Clave web activada
deactivated: Clave web desactivada
removed: Clave web eliminada
Application:
OIDC:

View File

@ -586,6 +586,13 @@ Errors:
NotForAPI: Les jetons usurpés d'identité ne sont pas autorisés pour l'API
Impersonation:
PolicyDisabled: L'usurpation d'identité est désactivée dans la politique de sécurité de l'instance
WebKey:
ActiveDelete: Impossible de supprimer la clé Web active
Config: Configuration de clé Web non valide
Duplicate: L'ID de clé Web n'est pas unique
FeatureDisabled: Fonctionnalité de clé Web désactivée
NoActive: Aucune clé Web active trouvée
NotFound: Clé Web introuvable
AggregateTypes:
action: Action
@ -609,6 +616,7 @@ AggregateTypes:
restrictions: Restrictions
system: Système
session: Session
web_key: Clé Web
EventTypes:
execution:
@ -1171,13 +1179,7 @@ EventTypes:
deactivated: Action désactivée
reactivated: Action réactivée
removed: Action supprimée
user_schema:
created: Schéma utilisateur créé
updated: Schéma utilisateur mis à jour
deactivated: Schéma utilisateur désactivé
reactivated: Schéma utilisateur réactivé
deleted: Schéma utilisateur supprimé
instance:
instance:
added: Instance ajoutée
changed: Instance modifiée
customtext:
@ -1305,6 +1307,18 @@ instance:
password:
changed: Mot de passe de configuration SMTP modifié
removed: Configuration SMTP supprimée
user_schema:
created: Schéma utilisateur créé
updated: Schéma utilisateur mis à jour
deactivated: Schéma utilisateur désactivé
reactivated: Schéma utilisateur réactivé
deleted: Schéma utilisateur supprimé
web_key:
added: Clé Web ajoutée
activated: Clé Web activée
deactivated: Clé Web désactivée
removed: Clé Web supprimée
Application:
OIDC:
UnsupportedVersion: Votre version de l'OIDC n'est pas prise en charge

View File

@ -586,6 +586,13 @@ Errors:
NotForAPI: Token rappresentati non consentiti per l'API
Impersonation:
PolicyDisabled: La rappresentazione è disabilitata nella policy di sicurezza dell'istanza
WebKey:
ActiveDelete: Impossibile eliminare la chiave Web attiva
Config: Configurazione chiave Web non valida
Duplicate: ID chiave Web non univoco
FeatureDisabled: Funzione chiave Web disabilitata
NoActive: Nessuna chiave Web attiva trovata
NotFound: Chiave Web non trovata
AggregateTypes:
action: Azione
@ -609,6 +616,7 @@ AggregateTypes:
restrictions: Restrizioni
system: Sistema
session: Sessione
web_key: Chiave Web
EventTypes:
execution:
@ -1172,12 +1180,6 @@ EventTypes:
deactivated: Azione disattivata
reactivated: Azione riattivata
removed: Azione rimossa
user_schema:
created: Schema utente creato
updated: Schema utente aggiornato
deactivated: Schema utente disattivato
reactivated: Schema utente riattivato
deleted: Schema utente eliminato
instance:
added: Istanza aggiunta
changed: L'istanza è cambiata
@ -1306,6 +1308,17 @@ EventTypes:
password:
changed: La password della configurazione SMTP è cambiata
removed: Configurazione SMTP rimossa
user_schema:
created: Schema utente creato
updated: Schema utente aggiornato
deactivated: Schema utente disattivato
reactivated: Schema utente riattivato
deleted: Schema utente eliminato
web_key:
added: Web Key aggiunto
activated: Web Key attivato
deactivated: Web Key disattivato
removed: Web Key rimosso
Application:
OIDC:

View File

@ -575,6 +575,13 @@ Errors:
NotForAPI: 偽装されたトークンは API では許可されません
Impersonation:
PolicyDisabled: インスタンスのセキュリティ ポリシーで偽装が無効になっています
WebKey:
ActiveDelete: アクティブな Web キーを削除できません
Config: 無効な Web キー設定
Duplicate: Web キー ID が一意ではありません
FeatureDisabled: Web キー機能が無効です
NoActive: アクティブな Web キーが見つかりません
NotFound: Web キーが見つかりません
AggregateTypes:
action: アクション
@ -598,6 +605,7 @@ AggregateTypes:
restrictions: 制限
system: システム
session: セッション
web_key: Web キー
EventTypes:
execution:
@ -1296,6 +1304,11 @@ EventTypes:
deactivated: ユーザースキーマが非アクティブ化されました
reactivated: ユーザースキーマが再アクティブ化されました
deleted: ユーザースキーマが削除されました
web_key:
added: Web キーが追加されました
activated: Web キーが有効化されました
deactivated: Web キーが無効化されました
removed: Web キーが削除されました
Application:
OIDC:

View File

@ -585,6 +585,13 @@ Errors:
NotForAPI: Имитирани токени не се дозволени за API
Impersonation:
PolicyDisabled: Имитирањето е оневозможено во политиката за безбедност на примерот
WebKey:
ActiveDelete: Не може да се избрише активниот веб-клуч
Config: Неважечка конфигурација на веб-клуч
Duplicate: ID на веб-клучот не е единствен
FeatureDisabled: Функцијата за веб-клуч е оневозможена
NoActive: Не е пронајден активен веб-клуч
NotFound: Веб-клучот не е пронајден
AggregateTypes:
action: Акција
@ -608,6 +615,7 @@ AggregateTypes:
restrictions: Ограничувања
system: Систем
session: Сесија
web_key: Веб клуч
EventTypes:
execution:
@ -1308,6 +1316,11 @@ EventTypes:
deactivated: Корисничката шема е деактивирана
reactivated: Корисничката шема е реактивирана
deleted: Корисничката шема е избришана
web_key:
added: Додаден е веб-клуч
activated: Веб-клучот е активиран
deactivated: Веб-клучот е деактивиран
removed: Веб-клучот е отстранет
Application:
OIDC:

View File

@ -586,6 +586,13 @@ Errors:
NotForAPI: Nagebootste tokens zijn niet toegestaan voor API
Impersonation:
PolicyDisabled: Nabootsing van identiteit is uitgeschakeld in het beveiligingsbeleid van de instantie.
WebKey:
ActiveDelete: Kan actieve websleutel niet verwijderen
Config: Ongeldige websleutelconfiguratie
Duplicate: Websleutel-ID niet uniek
FeatureDisabled: Websleutelfunctie uitgeschakeld
NoActive: Geen actieve websleutel gevonden
NotFound: Websleutel niet gevonden
AggregateTypes:
action: Actie
@ -609,6 +616,7 @@ AggregateTypes:
restrictions: Beperkingen
system: Systeem
session: Sessie
web_key: Websleutel
EventTypes:
execution:
@ -1305,6 +1313,11 @@ EventTypes:
deactivated: Gebruikersschema gedeactiveerd
reactivated: Gebruikersschema opnieuw geactiveerd
deleted: Gebruikersschema verwijderd
web_key:
added: Web Key toegevoegd
activated: Web Key geactiveerd
deactivated: Web Key gedeactiveerd
removed: Web Key verwijderd
Application:
OIDC:

View File

@ -586,6 +586,13 @@ Errors:
NotForAPI: Podrabiane tokeny nie są dozwolone w interfejsie API
Impersonation:
PolicyDisabled: Podszywanie się jest wyłączone w polityce bezpieczeństwa instancji
WebKey:
ActiveDelete: Nie można usunąć aktywnego klucza internetowego
Config: Nieprawidłowa konfiguracja klucza internetowego
Duplicate: Identyfikator klucza internetowego nie jest unikalny
FeatureDisabled: Funkcja klucza internetowego jest wyłączona
NoActive: Nie znaleziono aktywnego klucza internetowego
NotFound: Nie znaleziono klucza internetowego
AggregateTypes:
action: Działanie
@ -609,6 +616,7 @@ AggregateTypes:
restrictions: Ograniczenia
system: System
session: Sesja
web_key: Klucz internetowy
EventTypes:
execution:
@ -1310,6 +1318,11 @@ EventTypes:
deactivated: Schemat użytkownika dezaktywowany
reactivated: Schemat użytkownika został ponownie aktywowany
deleted: Schemat użytkownika został usunięty
web_key:
added: Dodano klucz internetowy
activated: Klucz internetowy aktywowano
deactivated: Klucz internetowy dezaktywowano
removed: Klucz internetowy usunięto
Application:
OIDC:

View File

@ -581,6 +581,13 @@ Errors:
NotForAPI: Tokens personificados não permitidos para API
Impersonation:
PolicyDisabled: A representação está desativada na política de segurança da instância
WebKey:
ActiveDelete: Não é possível eliminar a chave web ativa
Config: Configuração de chave web inválida
Duplicate: ID da chave Web não exclusivo
FeatureDisabled: Recurso chave da Web desativado
NoActive: Nenhuma chave web ativa encontrada
NotFound: Chave Web não encontrada
AggregateTypes:
action: Ação
@ -604,6 +611,7 @@ AggregateTypes:
restrictions: Restrições
system: Sistema
session: Sessão
web_key: Chave da Web
EventTypes:
execution:
@ -1302,6 +1310,11 @@ EventTypes:
deactivated: Esquema de usuário desativado
reactivated: Esquema do usuário reativado
deleted: Esquema do usuário excluído
web_key:
added: Chave Web adicionada
activated: Chave Web ativada
deactivated: Chave Web desativada
removed: Chave Web removida
Application:
OIDC:

View File

@ -575,6 +575,13 @@ Errors:
NotForAPI: Олицетворенные токены не разрешены для API.
Impersonation:
PolicyDisabled: Олицетворение отключено в политике безопасности экземпляра.
WebKey:
ActiveDelete: Невозможно удалить активный веб-ключ
Config: Неверная конфигурация веб-ключа
Duplicate: Идентификатор веб-ключа не уникален
FeatureDisabled: Функция веб-ключа отключена
NoActive: Активный веб-ключ не найден
NotFound: Веб-ключ не найден
AggregateTypes:
action: Действие
@ -598,6 +605,7 @@ AggregateTypes:
restrictions: Ограничения
system: Система
session: Сеанс
web_key: Веб-ключ
EventTypes:
execution:
@ -1296,6 +1304,12 @@ EventTypes:
deactivated: Пользовательская схема деактивирована
reactivated: Пользовательская схема повторно активирована
deleted: Пользовательская схема удалена
web_key:
added: Добавлен веб-ключ
activated: Веб-ключ активирован
deactivated: Веб-ключ деактивирован
removed: Веб-ключ удален
Application:
OIDC:
UnsupportedVersion: Ваша версия OIDC не поддерживается

View File

@ -586,6 +586,13 @@ Errors:
NotForAPI: Imitationstoken tillåts inte för API
Impersonation:
PolicyDisabled: Imitation är inaktiverad i instansens säkerhetspolicy
WebKey:
ActiveDelete: Det går inte att ta bort aktiv webbnyckel
Config: Ogiltig webbnyckelkonfiguration
Duplicate: Webnyckel-ID är inte unikt
FeatureDisabled: Webnyckelfunktion inaktiverad
NoActive: Ingen aktiv webbnyckel hittades
NotFound: Webnyckel hittades inte
AggregateTypes:
action: Åtgärd
@ -609,6 +616,7 @@ AggregateTypes:
restrictions: Restriktioner
system: System
session: Session
web_key: Webbnyckel
EventTypes:
execution:
@ -1310,6 +1318,11 @@ EventTypes:
deactivated: Användarschema avaktiverat
reactivated: Användarschema återaktiverat
deleted: Användarschema borttaget
web_key:
added: Webbnyckel har lagts till
activated: Webbnyckel aktiverad
deactivated: Webnyckel avaktiverad
removed: Webbnyckeln har tagits bort
Application:
OIDC:

View File

@ -586,6 +586,13 @@ Errors:
NotForAPI: API 不允许使用模拟令牌
Impersonation:
PolicyDisabled: 实例安全策略中禁用模拟
WebKey:
ActiveDelete: 无法删除活动 Web 密钥
Config: 无效的 Web 密钥配置
Duplicate: Web 密钥 ID 不唯一
FeatureDisabled: Web 密钥功能已禁用
NoActive: 未找到活动 Web 密钥
NotFound: 未找到 Web 密钥
AggregateTypes:
action: 动作
@ -609,6 +616,7 @@ AggregateTypes:
restrictions: 限制
system: 系统
session: 会话
web_key: Web 密钥
EventTypes:
execution:
@ -1175,12 +1183,6 @@ EventTypes:
deactivated: 停用动作
reactivated: 启用动作
removed: 删除动作
user_schema:
created: 已创建用户架构
updated: 用户架构已更新
deactivated: 用户架构已停用
reactivated: 用户架构已重新激活
deleted: 用户架构已删除
instance:
added: 实例已添加
changed: 实例已更改
@ -1309,6 +1311,17 @@ EventTypes:
password:
changed: SMTP 配置密码已更改
removed: SMTP 配置已删除
user_schema:
created: 已创建用户架构
updated: 用户架构已更新
deactivated: 用户架构已停用
reactivated: 用户架构已重新激活
deleted: 用户架构已删除
web_key:
added: 已添加 Web Key
activated: 已激活 Web Key
deactivated: 已停用 Web Key
removed: 已删除 Web Key
Application:
OIDC:

View File

@ -58,6 +58,13 @@ message SetInstanceFeaturesRequest{
description: "Improves performance of specified execution paths.";
}
];
optional bool web_key = 8 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "true";
description: "Enable the webkey/v3alpha API. The first time this feature is enabled, web keys are generated and activated.";
}
];
}
message SetInstanceFeaturesResponse {
@ -129,4 +136,11 @@ message GetInstanceFeaturesResponse {
description: "Improves performance of specified execution paths.";
}
];
FeatureFlag web_key = 9 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "true";
description: "Enable the webkey/v3alpha API. The first time this feature is enabled, web keys are generated and activated.";
}
];
}

View File

@ -58,6 +58,13 @@ message SetInstanceFeaturesRequest{
description: "Improves performance of specified execution paths.";
}
];
optional bool web_key = 8 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "true";
description: "Enable the webkey/v3alpha API. The first time this feature is enabled, web keys are generated and activated.";
}
];
}
message SetInstanceFeaturesResponse {
@ -129,4 +136,11 @@ message GetInstanceFeaturesResponse {
description: "Improves performance of specified execution paths.";
}
];
FeatureFlag web_key = 9 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "true";
description: "Enable the webkey/v3alpha API. The first time this feature is enabled, web keys are generated and activated.";
}
];
}

View File

@ -0,0 +1,41 @@
syntax = "proto3";
package zitadel.resources.webkey.v3alpha;
import "validate/validate.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha;webkey";
message WebKeyRSAConfig {
enum RSABits {
RSA_BITS_UNSPECIFIED = 0;
RSA_BITS_2048 = 1;
RSA_BITS_3072 = 2;
RSA_BITS_4096 = 3;
}
enum RSAHasher {
RSA_HASHER_UNSPECIFIED = 0;
RSA_HASHER_SHA256 = 1;
RSA_HASHER_SHA384 = 2;
RSA_HASHER_SHA512 = 3;
}
// bit size of the RSA key
RSABits bits = 1 [(validate.rules).enum = {defined_only: true, not_in: [0]}];
// signing algrithm used
RSAHasher hasher = 2 [(validate.rules).enum = {defined_only: true, not_in: [0]}];
}
message WebKeyECDSAConfig {
enum ECDSACurve {
ECDSA_CURVE_UNSPECIFIED = 0;
ECDSA_CURVE_P256 = 1;
ECDSA_CURVE_P384 = 2;
ECDSA_CURVE_P512 = 3;
}
ECDSACurve curve = 1 [(validate.rules).enum = {defined_only: true, not_in: [0]}];
}
message WebKeyED25519Config {}

View File

@ -0,0 +1,31 @@
syntax = "proto3";
package zitadel.resources.webkey.v3alpha;
import "google/protobuf/timestamp.proto";
import "zitadel/resources/webkey/v3alpha/config.proto";
import "zitadel/resources/object/v3alpha/object.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha;webkey";
enum WebKeyState {
STATE_UNSPECIFIED = 0;
STATE_INITIAL = 1;
STATE_ACTIVE = 2;
STATE_INACTIVE = 3;
STATE_REMOVED = 4;
}
message GetWebKey {
zitadel.resources.object.v3alpha.Details details = 1;
WebKey config = 2;
WebKeyState state = 3;
}
message WebKey {
oneof config {
WebKeyRSAConfig rsa = 6;
WebKeyECDSAConfig ecdsa = 7;
WebKeyED25519Config ed25519 = 8;
}
}

View File

@ -0,0 +1,278 @@
syntax = "proto3";
package zitadel.resources.webkey.v3alpha;
import "google/api/annotations.proto";
import "google/api/field_behavior.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "validate/validate.proto";
import "zitadel/protoc_gen_zitadel/v2/options.proto";
import "zitadel/resources/webkey/v3alpha/key.proto";
import "zitadel/resources/object/v3alpha/object.proto";
import "zitadel/object/v3alpha/object.proto";
option go_package = "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha;webkey";
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
info: {
title: "Web key Service";
version: "3.0-preview";
description: "This API is intended to manage web keys for a ZITADEL instance, used to sign and validate OIDC tokens. This project is in preview state. It can AND will continue breaking until a stable version is released.";
contact:{
name: "ZITADEL"
url: "https://zitadel.com"
email: "hi@zitadel.com"
}
license: {
name: "Apache 2.0",
url: "https://github.com/zitadel/zitadel/blob/main/LICENSE";
};
};
schemes: HTTPS;
schemes: HTTP;
consumes: "application/json";
produces: "application/json";
consumes: "application/grpc";
produces: "application/grpc";
consumes: "application/grpc-web+proto";
produces: "application/grpc-web+proto";
host: "${ZITADEL_DOMAIN}";
base_path: "/resources/v3alpha/web_keys";
external_docs: {
description: "Detailed information about ZITADEL",
url: "https://zitadel.com/docs"
}
security_definitions: {
security: {
key: "OAuth2";
value: {
type: TYPE_OAUTH2;
flow: FLOW_ACCESS_CODE;
authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize";
token_url: "$CUSTOM-DOMAIN/oauth/v2/token";
scopes: {
scope: {
key: "openid";
value: "openid";
}
scope: {
key: "urn:zitadel:iam:org:project:id:zitadel:aud";
value: "urn:zitadel:iam:org:project:id:zitadel:aud";
}
}
}
}
}
security: {
security_requirement: {
key: "OAuth2";
value: {
scope: "openid";
scope: "urn:zitadel:iam:org:project:id:zitadel:aud";
}
}
}
responses: {
key: "403";
value: {
description: "Returned when the user does not have permission to access the resource.";
schema: {
json_schema: {
ref: "#/definitions/rpcStatus";
}
}
}
}
responses: {
key: "404";
value: {
description: "Returned when the resource does not exist.";
schema: {
json_schema: {
ref: "#/definitions/rpcStatus";
}
}
}
}
};
service ZITADELWebKeys {
rpc CreateWebKey(CreateWebKeyRequest) returns (CreateWebKeyResponse) {
option (google.api.http) = {
post: "/"
body: "key"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "iam.web_key.write"
}
http_response: {
success_code: 201
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Generate a web key pair for the instance";
description: "Generate a private and public key pair. The private key can be used to sign OIDC tokens after activation. The public key can be used to valite OIDC tokens."
responses: {
key: "200"
value: {
description: "OK";
}
};
};
}
rpc ActivateWebKey(ActivateWebKeyRequest) returns (ActivateWebKeyResponse) {
option (google.api.http) = {
post: "/{id}/_activate"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "iam.web_key.write"
}
http_response: {
success_code: 200
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Activate a signing key for the instance";
description: "Switch the active signing web key. The previously active key will be deactivated."
responses: {
key: "200"
value: {
description: "OK";
}
};
};
}
rpc DeleteWebKey(DeleteWebKeyRequest) returns (DeleteWebKeyResponse) {
option (google.api.http) = {
delete: "/{id}"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "iam.web_key.delete"
}
http_response: {
success_code: 200
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Generate a web key pair for the instance";
description: "Delete a web key. Only inactive keys can be deleted. Once a key is deleted, any tokens signed by this key will be invalid."
responses: {
key: "200"
value: {
description: "OK";
}
};
};
}
rpc ListWebKeys(ListWebKeysRequest) returns (ListWebKeysResponse) {
option (google.api.http) = {
get: "/"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "iam.web_key.read"
}
http_response: {
success_code: 200
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "Generate a web key pair for the instance";
description: "List web key details for the instance"
responses: {
key: "200"
value: {
description: "OK";
}
};
};
}
}
message CreateWebKeyRequest {
optional zitadel.object.v3alpha.Instance instance = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
default: "\"domain from HOST or :authority header\""
}
];
WebKey key = 2;
}
message CreateWebKeyResponse {
zitadel.resources.object.v3alpha.Details details = 1;
}
message ActivateWebKeyRequest {
optional zitadel.object.v3alpha.Instance instance = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
default: "\"domain from HOST or :authority header\""
}
];
string id = 2 [
(validate.rules).string = {min_len: 1, max_len: 200},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 200,
example: "\"69629026806489455\"";
}
];
}
message ActivateWebKeyResponse {
zitadel.resources.object.v3alpha.Details details = 1;
}
message DeleteWebKeyRequest {
optional zitadel.object.v3alpha.Instance instance = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
default: "\"domain from HOST or :authority header\""
}
];
string id = 2 [
(validate.rules).string = {min_len: 1, max_len: 200},
(google.api.field_behavior) = REQUIRED,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
min_length: 1,
max_length: 200,
example: "\"69629026806489455\"";
}
];
}
message DeleteWebKeyResponse {
zitadel.resources.object.v3alpha.Details details = 1;
}
message ListWebKeysRequest {
optional zitadel.object.v3alpha.Instance instance = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
default: "\"domain from HOST or :authority header\""
}
];
}
message ListWebKeysResponse {
repeated GetWebKey web_keys = 1;
}