From e46dd121cdd2cbc0042c0aa578cd47f3d7b4e456 Mon Sep 17 00:00:00 2001 From: Ari Date: Tue, 23 Apr 2024 11:38:07 +0200 Subject: [PATCH] feat: allow using a local RSA key for machine keys (#7671) * Allow using a local RSA key for machine keys * Add check for key validity * Fix naming error * docs: provide translations of invalid key --------- Co-authored-by: Livio Spring --- internal/api/grpc/management/user.go | 15 ++++-- .../api/grpc/management/user_converter.go | 1 + internal/command/user_machine_key.go | 7 +++ internal/command/user_machine_key_test.go | 48 +++++++++++++++---- internal/static/i18n/bg.yaml | 1 + internal/static/i18n/cs.yaml | 1 + internal/static/i18n/de.yaml | 1 + internal/static/i18n/en.yaml | 1 + internal/static/i18n/es.yaml | 1 + internal/static/i18n/fr.yaml | 1 + internal/static/i18n/it.yaml | 1 + internal/static/i18n/ja.yaml | 1 + internal/static/i18n/mk.yaml | 1 + internal/static/i18n/nl.yaml | 1 + internal/static/i18n/pl.yaml | 1 + internal/static/i18n/pt.yaml | 1 + internal/static/i18n/ru.yaml | 1 + internal/static/i18n/zh.yaml | 1 + proto/zitadel/management.proto | 8 +++- 19 files changed, 80 insertions(+), 13 deletions(-) diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index eae4f40d3..6166bf4a5 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -774,13 +774,22 @@ func (s *Server) ListMachineKeys(ctx context.Context, req *mgmt_pb.ListMachineKe func (s *Server) AddMachineKey(ctx context.Context, req *mgmt_pb.AddMachineKeyRequest) (*mgmt_pb.AddMachineKeyResponse, error) { machineKey := AddMachineKeyRequestToCommand(req, authz.GetCtxData(ctx).OrgID) + // If there is no pubkey supplied, then AddUserMachineKey will generate a new one + pubkeySupplied := len(machineKey.PublicKey) > 0 details, err := s.command.AddUserMachineKey(ctx, machineKey) if err != nil { return nil, err } - keyDetails, err := machineKey.Detail() - if err != nil { - return nil, err + + // Return key details only if the pubkey wasn't supplied, otherwise the user already has + // private key locally + var keyDetails []byte + if !pubkeySupplied { + var err error + keyDetails, err = machineKey.Detail() + if err != nil { + return nil, err + } } return &mgmt_pb.AddMachineKeyResponse{ KeyId: machineKey.KeyID, diff --git a/internal/api/grpc/management/user_converter.go b/internal/api/grpc/management/user_converter.go index 32496b431..511e1a4f5 100644 --- a/internal/api/grpc/management/user_converter.go +++ b/internal/api/grpc/management/user_converter.go @@ -237,6 +237,7 @@ func AddMachineKeyRequestToCommand(req *mgmt_pb.AddMachineKeyRequest, resourceOw }, ExpirationDate: expDate, Type: authn.KeyTypeToDomain(req.Type), + PublicKey: req.PublicKey, } } diff --git a/internal/command/user_machine_key.go b/internal/command/user_machine_key.go index d092cc54e..d6abdd954 100644 --- a/internal/command/user_machine_key.go +++ b/internal/command/user_machine_key.go @@ -5,6 +5,7 @@ import ( "time" "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/eventstore/v1/models" @@ -78,6 +79,12 @@ func (key *MachineKey) valid() (err error) { if err := key.content(); err != nil { return err } + // If a key is supplied, it should be a valid public key + if len(key.PublicKey) > 0 { + if _, err := crypto.BytesToPublicKey(key.PublicKey); err != nil { + return zerrors.ThrowInvalidArgument(nil, "COMMAND-5F3h1", "Errors.User.Machine.Key.Invalid") + } + } key.ExpirationDate, err = domain.ValidateExpirationDate(key.ExpirationDate) return err } diff --git a/internal/command/user_machine_key_test.go b/internal/command/user_machine_key_test.go index 7e4a0f069..9e50f81e7 100644 --- a/internal/command/user_machine_key_test.go +++ b/internal/command/user_machine_key_test.go @@ -18,6 +18,16 @@ import ( "github.com/zitadel/zitadel/internal/zerrors" ) +const fakePubkey = `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp4qNBuUu/HekF2E5bOtA +oEL76zS0NQdZL3ByEJ3hZplJhE30ITPIOLW3+uaMMM+obl/LLapwG2vdhvutQtx/ +FOLJmXysbG3RL9zjXDBT5IE+nGFC7ctsi5FGbHQbAm45E3HHCSk7gfmTy9hxyk1K +GsyU8BDeOWasJO6aeXqpOnRM8vw/fY+6mHVC9CxcIroSfrIabFGe/mP6qpBGeFSn +APymBc/8lca4JaPv2/u/rBhnaAHZiUuCS1+MonWelOb+MSfq48VgtpiaYIVY9szI +esorA6EJ9pO17ROEUpX5wP5Oir+yGJU27jSvLCjvK6fOFX+OwUM9L8047JKoo+Nf +PwIDAQAB +-----END PUBLIC KEY-----` + func TestCommands_AddMachineKey(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore @@ -145,7 +155,7 @@ func TestCommands_AddMachineKey(t *testing.T) { "key1", domain.AuthNKeyTypeJSON, time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC), - []byte("public"), + []byte(fakePubkey), ), ), ), @@ -161,14 +171,14 @@ func TestCommands_AddMachineKey(t *testing.T) { }, Type: domain.AuthNKeyTypeJSON, ExpirationDate: time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC), - PublicKey: []byte("public"), + PublicKey: []byte(fakePubkey), }, }, res{ want: &domain.ObjectDetails{ ResourceOwner: "org1", }, - key: true, + key: false, }, }, { @@ -194,7 +204,7 @@ func TestCommands_AddMachineKey(t *testing.T) { "key1", domain.AuthNKeyTypeJSON, time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC), - []byte("public"), + []byte(fakePubkey), ), ), ), @@ -210,14 +220,35 @@ func TestCommands_AddMachineKey(t *testing.T) { KeyID: "key1", Type: domain.AuthNKeyTypeJSON, ExpirationDate: time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC), - PublicKey: []byte("public"), + PublicKey: []byte(fakePubkey), }, }, res{ want: &domain.ObjectDetails{ ResourceOwner: "org1", }, - key: true, + key: false, + }, + }, + { + "key added with invalid public key", + fields{ + eventstore: eventstoreExpect(t), + }, + args{ + ctx: context.Background(), + key: &MachineKey{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + KeyID: "key1", + Type: domain.AuthNKeyTypeJSON, + PublicKey: []byte("incorrect"), + }, + }, + res{ + err: zerrors.IsErrorInvalidArgument, }, }, } @@ -237,9 +268,8 @@ func TestCommands_AddMachineKey(t *testing.T) { } if tt.res.err == nil { assert.Equal(t, tt.res.want, got) - if tt.res.key { - assert.NotEqual(t, "", tt.args.key.PrivateKey) - } + receivedKey := len(tt.args.key.PrivateKey) > 0 + assert.Equal(t, tt.res.key, receivedKey) } }) } diff --git a/internal/static/i18n/bg.yaml b/internal/static/i18n/bg.yaml index 899fa9f0c..5fac59fb9 100644 --- a/internal/static/i18n/bg.yaml +++ b/internal/static/i18n/bg.yaml @@ -111,6 +111,7 @@ Errors: Key: NotFound: Машинният ключ не е намерен AlreadyExisting: Машинният ключ вече съществува + Invalid: Публичният ключ не е валиден RSA публичен ключ във формат PKIX с PEM кодиране Secret: NotExisting: Тайната не съществува Invalid: Тайната е невалидна diff --git a/internal/static/i18n/cs.yaml b/internal/static/i18n/cs.yaml index b1e83a6d1..d12a1b451 100644 --- a/internal/static/i18n/cs.yaml +++ b/internal/static/i18n/cs.yaml @@ -109,6 +109,7 @@ Errors: Key: NotFound: Klíč stroje nenalezen AlreadyExisting: Klíč stroje již existuje + Invalid: Veřejný klíč není platný veřejný klíč RSA ve formátu PKIX s kódováním PEM Secret: NotExisting: Tajemství neexistuje Invalid: Tajemství je neplatné diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 12bd70bc4..408a862b4 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -109,6 +109,7 @@ Errors: Key: NotFound: Maschinen Schlüssel nicht gefunden AlreadyExisting: Machine Schlüssel exisiert bereits + Invalid: Der öffentliche Schlüssel ist kein gültiger öffentlicher RSA-Schlüssel im PKIX-Format mit PEM-Kodierung Secret: NotExisting: Secret existiert nicht Invalid: Secret ist ungültig diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 511248da6..937820fdc 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -109,6 +109,7 @@ Errors: Key: NotFound: Machine key not found AlreadyExisting: Machine key already existing + Invalid: Public key is not a valid RSA public key in PKIX format with PEM encoding Secret: NotExisting: Secret doesn't exist Invalid: Secret is invalid diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index 60115ca86..102d99f08 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -109,6 +109,7 @@ Errors: Key: NotFound: Clave de máquina no encontrada AlreadyExisting: La clave de máquina ya existe + Invalid: La clave pública no es una clave pública RSA válida en formato PKIX con codificación PEM Secret: NotExisting: El secreto no existe Invalid: El secret no es válido diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index 8855ce5c4..631d5be47 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -109,6 +109,7 @@ Errors: Key: NotFound: Clé de la machine non trouvée AlreadyExisting: Clé de la machine déjà existante + Invalid: La clé publique n'est pas une clé publique RSA valide au format PKIX avec encodage PEM Secret: NotExisting: Secret n'existe pas Invalid: Secret n'est pas valide diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index 7005eaf31..064f3929e 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -109,6 +109,7 @@ Errors: Key: NotFound: Chiave macchina non trovato AlreadyExisting: Chiave macchina già esistente + Invalid: La chiave pubblica non è una chiave pubblica RSA valida in formato PKIX con codifica PEM Secret: NotExisting: Secret non esiste Invalid: Secret non è valido diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index 6eed3af3e..4ef7b8f98 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -102,6 +102,7 @@ Errors: Key: NotFound: マシーンキーが見つかりません AlreadyExisting: すでに存在しているマシーンキーです + Invalid: 公開キーは、PEM エンコードを使用した PKIX 形式の有効な RSA 公開キーではありません Secret: NotExisting: シークレットは存在しません Invalid: 無効なシークレットです diff --git a/internal/static/i18n/mk.yaml b/internal/static/i18n/mk.yaml index f964ce2d7..deab6d2e3 100644 --- a/internal/static/i18n/mk.yaml +++ b/internal/static/i18n/mk.yaml @@ -109,6 +109,7 @@ Errors: Key: NotFound: Machine key не е пронајден AlreadyExisting: Machine key веќе постои + Invalid: Јавниот клуч не е валиден јавен клуч RSA во формат PKIX со PEM кодирање Secret: NotExisting: Тајната не постои Invalid: Тајната е невалидна diff --git a/internal/static/i18n/nl.yaml b/internal/static/i18n/nl.yaml index 42ca88acf..a5ff77d71 100644 --- a/internal/static/i18n/nl.yaml +++ b/internal/static/i18n/nl.yaml @@ -108,6 +108,7 @@ Errors: Key: NotFound: Machine sleutel niet gevonden AlreadyExisting: Machine sleutel al bestaand + Invalid: De openbare sleutel is geen geldige openbare RSA-sleutel in PKIX-indeling met PEM-codering Secret: NotExisting: Geheim bestaat niet Invalid: Geheim is ongeldig diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index 33e7b7734..804018362 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -109,6 +109,7 @@ Errors: Key: NotFound: Klucz maszyny nie znaleziony AlreadyExisting: Klucz maszyny już istnieje + Invalid: Klucz publiczny nie jest prawidłowym kluczem publicznym RSA w formacie PKIX z kodowaniem PEM Secret: NotExisting: Sekret nie istnieje Invalid: Sekret jest nieprawidłowy diff --git a/internal/static/i18n/pt.yaml b/internal/static/i18n/pt.yaml index 89dab225c..3bcc981d3 100644 --- a/internal/static/i18n/pt.yaml +++ b/internal/static/i18n/pt.yaml @@ -109,6 +109,7 @@ Errors: Key: NotFound: Chave de máquina não encontrada AlreadyExisting: Chave de máquina já existe + Invalid: A chave pública não é uma chave pública RSA válida no formato PKIX com codificação PEM Secret: NotExisting: Segredo não existe Invalid: Segredo é inválido diff --git a/internal/static/i18n/ru.yaml b/internal/static/i18n/ru.yaml index 3bb3cd64d..5d4182d25 100644 --- a/internal/static/i18n/ru.yaml +++ b/internal/static/i18n/ru.yaml @@ -110,6 +110,7 @@ Errors: Key: NotFound: Машинный ключ не найден AlreadyExisting: Машинный ключ уже существует + Invalid: Открытый ключ не является допустимым открытым ключом RSA в формате PKIX с кодировкой PEM Secret: NotExisting: Ключ не существует Invalid: Ключ недействителен diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index ee7bfec97..4ec0e24cc 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -109,6 +109,7 @@ Errors: Key: NotFound: 未找到机器密钥 AlreadyExisting: 已有的机器钥匙 + Invalid: 公钥不是采用 PEM 编码的 PKIX 格式的有效 RSA 公钥 Secret: NotExisting: 秘密并不存在 Invalid: 秘密是无效的 diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 0b1a3b6a1..5a0f241ff 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -1734,7 +1734,7 @@ service ManagementService { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Create Key for machine user"; - description: "A new key is generated and will be returned in the response. Make sure to store the returned key. Machine keys are used to authenticate with jwt profile." + description: "If a public key is not supplied, a new key is generated and will be returned in the response. Make sure to store the returned key. If an RSA public key is supplied, the private key is omitted from the response. Machine keys are used to authenticate with jwt profile." tags: "Users"; tags: "User Machine"; responses: { @@ -8504,6 +8504,12 @@ message AddMachineKeyRequest { description: "The date the key will expire and no logins will be possible"; } ]; + bytes public_key = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1...\""; + description: "Optionally provide a public key of your own generated RSA private key."; + } + ]; } message AddMachineKeyResponse {