Skip to content

Commit

Permalink
[Fleet] add support for message signing without encryption key (#152624)
Browse files Browse the repository at this point in the history
  • Loading branch information
joeypoon authored Mar 10, 2023
1 parent 206a9d1 commit e8a251a
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -24,17 +25,6 @@ describe('MessageSigningService', () => {
let soClientMock: jest.Mocked<SavedObjectsClientContract>;
let esoClientMock: jest.Mocked<EncryptedSavedObjectsClient>;
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({
Expand All @@ -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<EncryptedSavedObjectsClient>;
Expand All @@ -55,48 +48,115 @@ describe('MessageSigningService', () => {
.getScopedClient({} as unknown as KibanaRequest) as jest.Mocked<SavedObjectsClientContract>;

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: '',
}
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface MessageSigningKeys {
private_key: string;
public_key: string;
passphrase: string;
passphrase_plain: string;
}

export interface MessageSigningServiceInterface {
Expand All @@ -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<MessageSigningKeys>(
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',
Expand All @@ -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<MessageSigningKeys>(MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, {
let keypairSoObject: Partial<MessageSigningKeys> = {
private_key: privateKey,
public_key: publicKey,
passphrase,
});
};
keypairSoObject = this.isEncryptionAvailable
? {
...keypairSoObject,
passphrase,
}
: { ...keypairSoObject, passphrase_plain: passphrase };

await this.soClient.create<Partial<MessageSigningKeys>>(
MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE,
keypairSoObject
);

return {
privateKey,
Expand Down Expand Up @@ -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<MessageSigningKeys>({
Expand All @@ -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;
}
Expand All @@ -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');
}
}
}
11 changes: 6 additions & 5 deletions x-pack/plugins/fleet/server/services/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit e8a251a

Please sign in to comment.