From e8a251ac4b43fd54c496496e3d915ba2cc2d6779 Mon Sep 17 00:00:00 2001 From: "Joey F. Poon" Date: Fri, 10 Mar 2023 09:15:23 -0600 Subject: [PATCH] [Fleet] add support for message signing without encryption key (#152624) --- .../security/message_signing_service.test.ts | 154 ++++++++++++------ .../security/message_signing_service.ts | 70 ++++++-- x-pack/plugins/fleet/server/services/setup.ts | 11 +- 3 files changed, 165 insertions(+), 70 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/security/message_signing_service.test.ts b/x-pack/plugins/fleet/server/services/security/message_signing_service.test.ts index 7416e2af49ec5..e3ad1d82bdee7 100644 --- a/x-pack/plugins/fleet/server/services/security/message_signing_service.test.ts +++ b/x-pack/plugins/fleet/server/services/security/message_signing_service.test.ts @@ -10,6 +10,7 @@ import { createVerify } from 'crypto'; import type { KibanaRequest } from '@kbn/core-http-server'; import type { SavedObjectsClientContract } from '@kbn/core/server'; import type { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server'; +import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks'; import { MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE } from '../../constants'; import { createAppContextStartContractMock } from '../../mocks'; @@ -24,17 +25,6 @@ describe('MessageSigningService', () => { let soClientMock: jest.Mocked; let esoClientMock: jest.Mocked; let messageSigningService: MessageSigningServiceInterface; - const keyPairObj = { - id: 'id1', - type: MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, - attributes: { - private_key: - 'MIHsMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAgtNcDFoj07+QICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEELajFPDz2bpD2qfPCRHphAgEgZCq0eUxTOEGrefdeNgHR2VVxXjWRZG+cGn+e8LW4auBCwwMiZsAZPKKvzLdlLi5sQhH+qWPM7Z9/OLbF/0ZKvyDM2/+4/9+5Iwna7vueTZtcdSIuGIFRjqUZbgNLejPSPcBMM9SP1V6I8TjDguGAQ3Nj95t7g7cbl0x48nQZ9bNDJyvy4ytHl+ubzdanLlFkLc=', - public_key: - 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6E5aKP8dAa+TlBuSKrrgl9UtkzHjn6YUQO+72vi3khGfUQIpD9qq9MsjsWz6Bvm6tnSOyyPXv+Koh80lNCKw5A==', - passphrase: 'eb35af2291344a51c9a8bb81e653281c38892d564db617a2cb0bc660f0ae96f2', - }, - }; function mockCreatePointInTimeFinderAsInternalUser(savedObjects: unknown[] = []) { esoClientMock.createPointInTimeFinderDecryptedAsInternalUser = jest.fn().mockResolvedValue({ @@ -45,8 +35,11 @@ describe('MessageSigningService', () => { }); } - beforeEach(() => { + function setupMocks(canEncrypt = true) { const mockContext = createAppContextStartContractMock(); + mockContext.encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup({ + canEncrypt, + }); appContextService.start(mockContext); esoClientMock = mockContext.encryptedSavedObjectsStart!.getClient() as jest.Mocked; @@ -55,48 +48,115 @@ describe('MessageSigningService', () => { .getScopedClient({} as unknown as KibanaRequest) as jest.Mocked; messageSigningService = new MessageSigningService(esoClientMock); - }); + } - afterEach(() => { - jest.resetAllMocks(); - }); + describe('with encryption key configured', () => { + const keyPairObj = { + id: 'id1', + type: MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, + attributes: { + private_key: + 'MIHsMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAgtNcDFoj07+QICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEELajFPDz2bpD2qfPCRHphAgEgZCq0eUxTOEGrefdeNgHR2VVxXjWRZG+cGn+e8LW4auBCwwMiZsAZPKKvzLdlLi5sQhH+qWPM7Z9/OLbF/0ZKvyDM2/+4/9+5Iwna7vueTZtcdSIuGIFRjqUZbgNLejPSPcBMM9SP1V6I8TjDguGAQ3Nj95t7g7cbl0x48nQZ9bNDJyvy4ytHl+ubzdanLlFkLc=', + public_key: + 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6E5aKP8dAa+TlBuSKrrgl9UtkzHjn6YUQO+72vi3khGfUQIpD9qq9MsjsWz6Bvm6tnSOyyPXv+Koh80lNCKw5A==', + passphrase: 'eb35af2291344a51c9a8bb81e653281c38892d564db617a2cb0bc660f0ae96f2', + }, + }; + + beforeEach(() => { + setupMocks(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); - it('can correctly generate key pair if none exist', async () => { - mockCreatePointInTimeFinderAsInternalUser(); + it('can correctly generate key pair if none exist', async () => { + mockCreatePointInTimeFinderAsInternalUser(); - await messageSigningService.generateKeyPair(); - expect(soClientMock.create).toBeCalledWith(MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, { - private_key: expect.any(String), - public_key: expect.any(String), - passphrase: expect.any(String), + await messageSigningService.generateKeyPair(); + expect(soClientMock.create).toBeCalledWith(MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, { + private_key: expect.any(String), + public_key: expect.any(String), + passphrase: expect.any(String), + }); }); - }); - it('does not generate key pair if one exists', async () => { - mockCreatePointInTimeFinderAsInternalUser([keyPairObj]); + it('does not generate key pair if one exists', async () => { + mockCreatePointInTimeFinderAsInternalUser([keyPairObj]); + + await messageSigningService.generateKeyPair(); + expect(soClientMock.create).not.toBeCalled(); + }); - await messageSigningService.generateKeyPair(); - expect(soClientMock.create).not.toBeCalled(); + it('can correctly sign messages', async () => { + mockCreatePointInTimeFinderAsInternalUser([keyPairObj]); + + const message = Buffer.from(JSON.stringify({ message: 'foobar' }), 'utf8'); + const { data, signature } = await messageSigningService.sign(message); + + const verifier = createVerify('SHA256'); + verifier.update(data); + verifier.end(); + + const serializedPublicKey = await messageSigningService.getPublicKey(); + const publicKey = Buffer.from(serializedPublicKey, 'base64'); + const isVerified = verifier.verify( + { key: publicKey, format: 'der', type: 'spki' }, + signature, + 'base64' + ); + expect(isVerified).toBe(true); + expect(data).toBe(message); + }); }); - it('can correctly sign messages', async () => { - mockCreatePointInTimeFinderAsInternalUser([keyPairObj]); - - const message = Buffer.from(JSON.stringify({ message: 'foobar' }), 'utf8'); - const { data, signature } = await messageSigningService.sign(message); - - const verifier = createVerify('SHA256'); - verifier.update(data); - verifier.end(); - - const serializedPublicKey = await messageSigningService.getPublicKey(); - const publicKey = Buffer.from(serializedPublicKey, 'base64'); - const isVerified = verifier.verify( - { key: publicKey, format: 'der', type: 'spki' }, - signature, - 'base64' - ); - expect(isVerified).toBe(true); - expect(data).toBe(message); + describe('with NO encryption key configured', () => { + const keyPairObj = { + id: 'id1', + type: MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, + attributes: { + private_key: + 'MIHsMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAgtNcDFoj07+QICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEELajFPDz2bpD2qfPCRHphAgEgZCq0eUxTOEGrefdeNgHR2VVxXjWRZG+cGn+e8LW4auBCwwMiZsAZPKKvzLdlLi5sQhH+qWPM7Z9/OLbF/0ZKvyDM2/+4/9+5Iwna7vueTZtcdSIuGIFRjqUZbgNLejPSPcBMM9SP1V6I8TjDguGAQ3Nj95t7g7cbl0x48nQZ9bNDJyvy4ytHl+ubzdanLlFkLc=', + public_key: + 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6E5aKP8dAa+TlBuSKrrgl9UtkzHjn6YUQO+72vi3khGfUQIpD9qq9MsjsWz6Bvm6tnSOyyPXv+Koh80lNCKw5A==', + passphrase_plain: 'eb35af2291344a51c9a8bb81e653281c38892d564db617a2cb0bc660f0ae96f2', + }, + }; + + beforeEach(() => { + setupMocks(false); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('can correctly generate key pair if none exist', async () => { + mockCreatePointInTimeFinderAsInternalUser(); + + await messageSigningService.generateKeyPair(); + expect(soClientMock.create).toBeCalledWith(MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, { + private_key: expect.any(String), + public_key: expect.any(String), + passphrase_plain: expect.any(String), + }); + }); + + it('encrypts passphrase when encryption key is newly configured', async () => { + setupMocks(); + mockCreatePointInTimeFinderAsInternalUser([keyPairObj]); + + await messageSigningService.generateKeyPair(); + expect(soClientMock.create).not.toBeCalled(); + expect(soClientMock.update).toBeCalledWith( + MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, + keyPairObj.id, + { + passphrase: expect.any(String), + passphrase_plain: '', + } + ); + }); }); }); diff --git a/x-pack/plugins/fleet/server/services/security/message_signing_service.ts b/x-pack/plugins/fleet/server/services/security/message_signing_service.ts index 590421113f339..443f35ec06ed2 100644 --- a/x-pack/plugins/fleet/server/services/security/message_signing_service.ts +++ b/x-pack/plugins/fleet/server/services/security/message_signing_service.ts @@ -19,6 +19,7 @@ interface MessageSigningKeys { private_key: string; public_key: string; passphrase: string; + passphrase_plain: string; } export interface MessageSigningServiceInterface { @@ -39,17 +40,39 @@ export class MessageSigningService implements MessageSigningServiceInterface { return appContextService.getEncryptedSavedObjectsSetup()?.canEncrypt ?? false; } - public async generateKeyPair( - providedPassphrase?: string - ): Promise<{ privateKey: string; publicKey: string; passphrase: string }> { - this.checkForEncryptionKey(); + public async generateKeyPair(providedPassphrase?: string): Promise<{ + privateKey: string; + publicKey: string; + passphrase: string; + }> { + let passphrase = providedPassphrase || this.generatePassphrase(); const currentKeyPair = await this.getCurrentKeyPair(); - if (currentKeyPair.privateKey && currentKeyPair.publicKey && currentKeyPair.passphrase) { - return currentKeyPair; - } + if ( + currentKeyPair.privateKey && + currentKeyPair.publicKey && + (currentKeyPair.passphrase || currentKeyPair.passphrasePlain) + ) { + passphrase = currentKeyPair.passphrase || currentKeyPair.passphrasePlain; + + // newly configured encryption key, encrypt the passphrase + if (currentKeyPair.passphrasePlain && this.isEncryptionAvailable) { + await this.soClient.update( + MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, + currentKeyPair.id, + { + passphrase, + passphrase_plain: '', + } + ); + } - const passphrase = providedPassphrase || this.generatePassphrase(); + return { + privateKey: currentKeyPair.privateKey, + publicKey: currentKeyPair.publicKey, + passphrase, + }; + } const keyPair = generateKeyPairSync('ec', { namedCurve: 'prime256v1', @@ -67,11 +90,21 @@ export class MessageSigningService implements MessageSigningServiceInterface { const privateKey = keyPair.privateKey.toString('base64'); const publicKey = keyPair.publicKey.toString('base64'); - await this.soClient.create(MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, { + let keypairSoObject: Partial = { private_key: privateKey, public_key: publicKey, - passphrase, - }); + }; + keypairSoObject = this.isEncryptionAvailable + ? { + ...keypairSoObject, + passphrase, + } + : { ...keypairSoObject, passphrase_plain: passphrase }; + + await this.soClient.create>( + MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, + keypairSoObject + ); return { privateKey, @@ -144,9 +177,11 @@ export class MessageSigningService implements MessageSigningServiceInterface { } private async getCurrentKeyPair(): Promise<{ + id: string; privateKey: string; publicKey: string; passphrase: string; + passphrasePlain: string; }> { const finder = await this.esoClient.createPointInTimeFinderDecryptedAsInternalUser({ @@ -156,19 +191,24 @@ export class MessageSigningService implements MessageSigningServiceInterface { sortOrder: 'desc', }); let keyPair = { + id: '', privateKey: '', publicKey: '', passphrase: '', + passphrasePlain: '', }; for await (const result of finder.find()) { - const attributes = result.saved_objects[0]?.attributes; + const savedObject = result.saved_objects[0]; + const attributes = savedObject?.attributes; if (!attributes?.private_key) { break; } keyPair = { + id: savedObject.id, privateKey: attributes.private_key, publicKey: attributes.public_key, passphrase: attributes.passphrase, + passphrasePlain: attributes.passphrase_plain, }; break; } @@ -179,10 +219,4 @@ export class MessageSigningService implements MessageSigningServiceInterface { private generatePassphrase(): string { return randomBytes(32).toString('hex'); } - - private checkForEncryptionKey(): void { - if (!this.isEncryptionAvailable) { - throw new Error('encryption key not set, message signing service is disabled'); - } - } } diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index db4a9155ba89e..29bd1772126ef 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -165,12 +165,13 @@ async function createSetupSideEffects( logger.debug('Upgrade Fleet package install versions'); await upgradePackageInstallVersion({ soClient, esClient, logger }); - if (appContextService.getMessageSigningService()?.isEncryptionAvailable) { - logger.debug('Generating key pair for message signing'); - await appContextService.getMessageSigningService()?.generateKeyPair(); - } else { - logger.info('No encryption key set, skipping key pair generation for message signing'); + logger.debug('Generating key pair for message signing'); + if (!appContextService.getMessageSigningService()?.isEncryptionAvailable) { + logger.warn( + 'xpack.encryptedSavedObjects.encryptionKey is not configured, private key passphrase is being stored in plain text' + ); } + await appContextService.getMessageSigningService()?.generateKeyPair(); logger.debug('Upgrade Agent policy schema version'); await upgradeAgentPolicySchemaVersion(soClient);