mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Fleet] Add GPG verify function to fleet (#135223)
* add LGPL-3.0+ to list of allowed licenses * Add openpgp (LGPLv3 license)to kibana * Add jsdom TextEncoder and TextDecoder polyfills * Use opengpg to read gpg key * add basic verification function * add lgpl to license overrides for now * Revert "add lgpl to license overrides for now" This reverts commit 3730eb07540d8b537712267a5430085f54c088c0. * collect verification result * use Key ID of the verification key * add @openpgp/web-stream-tools as a dev dependency * verified -> isVerified * only allow LGPL-v3 for openpgp * fix: use @ as separator when checking license overrides * fix isValidIndexName test
This commit is contained in:
parent
3d4abda822
commit
4179903a2f
10 changed files with 174 additions and 17 deletions
|
@ -382,6 +382,7 @@
|
|||
"normalize-path": "^3.0.0",
|
||||
"object-hash": "^1.3.1",
|
||||
"object-path-immutable": "^3.1.1",
|
||||
"openpgp": "5.3.0",
|
||||
"opn": "^5.5.0",
|
||||
"ora": "^4.0.4",
|
||||
"p-limit": "^3.0.1",
|
||||
|
@ -575,6 +576,7 @@
|
|||
"@microsoft/api-documenter": "7.13.68",
|
||||
"@microsoft/api-extractor": "7.18.19",
|
||||
"@octokit/rest": "^16.35.0",
|
||||
"@openpgp/web-stream-tools": "^0.0.10",
|
||||
"@percy/agent": "^0.28.6",
|
||||
"@storybook/addon-a11y": "^6.4.22",
|
||||
"@storybook/addon-actions": "^6.4.22",
|
||||
|
|
|
@ -14,3 +14,10 @@ require('whatwg-fetch');
|
|||
if (!global.URL.hasOwnProperty('createObjectURL')) {
|
||||
Object.defineProperty(global.URL, 'createObjectURL', { value: () => '' });
|
||||
}
|
||||
|
||||
// https://github.com/jsdom/jsdom/issues/2524
|
||||
if (!global.hasOwnProperty('TextEncoder')) {
|
||||
const { TextEncoder, TextDecoder } = require('util');
|
||||
global.TextEncoder = TextEncoder;
|
||||
global.TextDecoder = TextDecoder;
|
||||
}
|
||||
|
|
|
@ -72,6 +72,11 @@ export const LICENSE_ALLOWED = [
|
|||
// we wanna allow in packages only used as dev dependencies
|
||||
export const DEV_ONLY_LICENSE_ALLOWED = ['MPL-2.0'];
|
||||
|
||||
// there are some licenses which should not be globally allowed
|
||||
// but can be brought in on a per-package basis
|
||||
export const PER_PACKAGE_ALLOWED_LICENSES = {
|
||||
'openpgp@5.3.0': ['LGPL-3.0+'],
|
||||
};
|
||||
// Globally overrides a license for a given package@version
|
||||
export const LICENSE_OVERRIDES = {
|
||||
'jsts@1.6.2': ['Eclipse Distribution License - v 1.0'], // cf. https://github.com/bjornharrtell/jsts
|
||||
|
|
|
@ -10,7 +10,12 @@ import { REPO_ROOT } from '@kbn/utils';
|
|||
import { run } from '@kbn/dev-cli-runner';
|
||||
import { getInstalledPackages } from '../npm';
|
||||
|
||||
import { LICENSE_ALLOWED, DEV_ONLY_LICENSE_ALLOWED, LICENSE_OVERRIDES } from './config';
|
||||
import {
|
||||
LICENSE_ALLOWED,
|
||||
DEV_ONLY_LICENSE_ALLOWED,
|
||||
LICENSE_OVERRIDES,
|
||||
PER_PACKAGE_ALLOWED_LICENSES,
|
||||
} from './config';
|
||||
import { assertLicensesValid } from './valid';
|
||||
|
||||
run(
|
||||
|
@ -26,6 +31,7 @@ run(
|
|||
assertLicensesValid({
|
||||
packages: packages.filter((pkg) => !pkg.isDevOnly),
|
||||
validLicenses: LICENSE_ALLOWED,
|
||||
perPackageOverrides: PER_PACKAGE_ALLOWED_LICENSES,
|
||||
});
|
||||
log.success('All production dependency licenses are allowed');
|
||||
|
||||
|
@ -35,6 +41,7 @@ run(
|
|||
assertLicensesValid({
|
||||
packages: packages.filter((pkg) => pkg.isDevOnly),
|
||||
validLicenses: LICENSE_ALLOWED.concat(DEV_ONLY_LICENSE_ALLOWED),
|
||||
perPackageOverrides: PER_PACKAGE_ALLOWED_LICENSES,
|
||||
});
|
||||
log.success('All development dependency licenses are allowed');
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ describe('tasks/lib/licenses', () => {
|
|||
packages: [PACKAGE],
|
||||
validLicenses: [`not ${PACKAGE.licenses[0]}`],
|
||||
perPackageOverrides: {
|
||||
[`${PACKAGE.name}-${PACKAGE.version}`]: [`also not ${PACKAGE.licenses[0]}`],
|
||||
[`${PACKAGE.name}@${PACKAGE.version}`]: [`also not ${PACKAGE.licenses[0]}`],
|
||||
},
|
||||
});
|
||||
}).toThrow(PACKAGE.name);
|
||||
|
@ -59,7 +59,7 @@ describe('tasks/lib/licenses', () => {
|
|||
packages: [PACKAGE],
|
||||
validLicenses: [`not ${PACKAGE.licenses[0]}`],
|
||||
perPackageOverrides: {
|
||||
[`${PACKAGE.name}-${PACKAGE.version}`]: PACKAGE.licenses,
|
||||
[`${PACKAGE.name}@${PACKAGE.version}`]: PACKAGE.licenses,
|
||||
},
|
||||
})
|
||||
).toEqual(undefined);
|
||||
|
|
|
@ -33,7 +33,7 @@ export function assertLicensesValid({
|
|||
const invalidMsgs = packages.reduce((acc, pkg) => {
|
||||
const isValidLicense = (license: string) => validLicenses.includes(license);
|
||||
const isValidLicenseForPackage = (license: string) =>
|
||||
(perPackageOverrides[`${pkg.name}-${pkg.version}`] || []).includes(license);
|
||||
(perPackageOverrides[`${pkg.name}@${pkg.version}`] || []).includes(license);
|
||||
|
||||
const invalidLicenses = pkg.licenses.filter(
|
||||
(license) => !isValidLicense(license) && !isValidLicenseForPackage(license)
|
||||
|
|
|
@ -13,6 +13,41 @@ const mockLoggerFactory = loggingSystemMock.create();
|
|||
const mockLogger = mockLoggerFactory.get('mock logger');
|
||||
import { getGpgKeyOrUndefined, _readGpgKey } from './package_verification';
|
||||
|
||||
const testGpgKeyId = 'd27d666cd88e42b4';
|
||||
const testGpgKey = `
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Version: GnuPG v2.0.14 (GNU/Linux)
|
||||
|
||||
mQENBFI3HsoBCADXDtbNJnxbPqB1vDNtCsqhe49vFYsZN9IOZsZXgp7aHjh6CJBD
|
||||
A+bGFOwyhbd7at35jQjWAw1O3cfYsKAmFy+Ar3LHCMkV3oZspJACTIgCrwnkic/9
|
||||
CUliQe324qvObU2QRtP4Fl0zWcfb/S8UYzWXWIFuJqMvE9MaRY1bwUBvzoqavLGZ
|
||||
j3SF1SPO+TB5QrHkrQHBsmX+Jda6d4Ylt8/t6CvMwgQNlrlzIO9WT+YN6zS+sqHd
|
||||
1YK/aY5qhoLNhp9G/HxhcSVCkLq8SStj1ZZ1S9juBPoXV1ZWNbxFNGwOh/NYGldD
|
||||
2kmBf3YgCqeLzHahsAEpvAm8TBa7Q9W21C8vABEBAAG0RUVsYXN0aWNzZWFyY2gg
|
||||
KEVsYXN0aWNzZWFyY2ggU2lnbmluZyBLZXkpIDxkZXZfb3BzQGVsYXN0aWNzZWFy
|
||||
Y2gub3JnPokBOAQTAQIAIgUCUjceygIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgEC
|
||||
F4AACgkQ0n1mbNiOQrRzjAgAlTUQ1mgo3nK6BGXbj4XAJvuZDG0HILiUt+pPnz75
|
||||
nsf0NWhqR4yGFlmpuctgCmTD+HzYtV9fp9qW/bwVuJCNtKXk3sdzYABY+Yl0Cez/
|
||||
7C2GuGCOlbn0luCNT9BxJnh4mC9h/cKI3y5jvZ7wavwe41teqG14V+EoFSn3NPKm
|
||||
TxcDTFrV7SmVPxCBcQze00cJhprKxkuZMPPVqpBS+JfDQtzUQD/LSFfhHj9eD+Xe
|
||||
8d7sw+XvxB2aN4gnTlRzjL1nTRp0h2/IOGkqYfIG9rWmSLNlxhB2t+c0RsjdGM4/
|
||||
eRlPWylFbVMc5pmDpItrkWSnzBfkmXL3vO2X3WvwmSFiQbkBDQRSNx7KAQgA5JUl
|
||||
zcMW5/cuyZR8alSacKqhSbvoSqqbzHKcUQZmlzNMKGTABFG1yRx9r+wa/fvqP6OT
|
||||
RzRDvVS/cycws8YX7Ddum7x8uI95b9ye1/Xy5noPEm8cD+hplnpU+PBQZJ5XJ2I+
|
||||
1l9Nixx47wPGXeClLqcdn0ayd+v+Rwf3/XUJrvccG2YZUiQ4jWZkoxsA07xx7Bj+
|
||||
Lt8/FKG7sHRFvePFU0ZS6JFx9GJqjSBbHRRkam+4emW3uWgVfZxuwcUCn1ayNgRt
|
||||
KiFv9jQrg2TIWEvzYx9tywTCxc+FFMWAlbCzi+m4WD+QUWWfDQ009U/WM0ks0Kww
|
||||
EwSk/UDuToxGnKU2dQARAQABiQEfBBgBAgAJBQJSNx7KAhsMAAoJENJ9ZmzYjkK0
|
||||
c3MIAIE9hAR20mqJWLcsxLtrRs6uNF1VrpB+4n/55QU7oxA1iVBO6IFu4qgsF12J
|
||||
TavnJ5MLaETlggXY+zDef9syTPXoQctpzcaNVDmedwo1SiL03uMoblOvWpMR/Y0j
|
||||
6rm7IgrMWUDXDPvoPGjMl2q1iTeyHkMZEyUJ8SKsaHh4jV9wp9KmC8C+9CwMukL7
|
||||
vM5w8cgvJoAwsp3Fn59AxWthN3XJYcnMfStkIuWgR7U2r+a210W6vnUxU4oN0PmM
|
||||
cursYPyeV0NX/KQeUeNMwGTFB6QHS/anRaGQewijkrYYoTNtfllxIu9XYmiBERQ/
|
||||
qPDlGRlOgVTd9xUfHFkzB52c70E=
|
||||
=92oX
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
`;
|
||||
const testGpgKeyFileContent = Buffer.from(testGpgKey);
|
||||
const mockGetConfig = jest.fn();
|
||||
jest.mock('../../app_context', () => ({
|
||||
appContextService: {
|
||||
|
@ -32,11 +67,12 @@ beforeEach(() => {
|
|||
});
|
||||
describe('getGpgKeyOrUndefined', () => {
|
||||
it('should cache the gpg key after reading file once', async () => {
|
||||
const keyContent = 'this is the gpg key';
|
||||
mockedReadFile.mockResolvedValue(Buffer.from(keyContent));
|
||||
mockedReadFile.mockResolvedValue(testGpgKeyFileContent);
|
||||
mockGetConfig.mockReturnValue({ packageVerification: { gpgKeyPath: 'somePath' } });
|
||||
expect(await getGpgKeyOrUndefined()).toEqual(keyContent);
|
||||
expect(await getGpgKeyOrUndefined()).toEqual(keyContent);
|
||||
const key1 = await getGpgKeyOrUndefined();
|
||||
const key2 = await getGpgKeyOrUndefined();
|
||||
expect(key1?.getKeyID().toHex()).toEqual(testGpgKeyId);
|
||||
expect(key2?.getKeyID().toHex()).toEqual(testGpgKeyId);
|
||||
expect(mockedReadFile).toHaveBeenCalledWith('somePath');
|
||||
expect(mockedReadFile).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
@ -44,7 +80,7 @@ describe('getGpgKeyOrUndefined', () => {
|
|||
|
||||
describe('_readGpgKey', () => {
|
||||
it('should return undefined if the key file isnt configured', async () => {
|
||||
mockedReadFile.mockResolvedValue(Buffer.from('this is the gpg key'));
|
||||
mockedReadFile.mockResolvedValue(testGpgKeyFileContent);
|
||||
mockGetConfig.mockReturnValue({ packageVerification: {} });
|
||||
|
||||
expect(await _readGpgKey()).toEqual(undefined);
|
||||
|
|
|
@ -7,22 +7,23 @@
|
|||
|
||||
import { readFile } from 'fs/promises';
|
||||
|
||||
import * as openpgp from 'openpgp';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
|
||||
import { appContextService } from '../../app_context';
|
||||
let cachedKey: openpgp.Key | undefined | null = null;
|
||||
|
||||
let cachedKey: string | undefined | null = null;
|
||||
|
||||
export async function getGpgKeyOrUndefined(): Promise<string | undefined> {
|
||||
export async function getGpgKeyOrUndefined(): Promise<openpgp.Key | undefined> {
|
||||
if (cachedKey !== null) return cachedKey;
|
||||
|
||||
cachedKey = await _readGpgKey();
|
||||
return cachedKey;
|
||||
}
|
||||
|
||||
export async function _readGpgKey(): Promise<string | undefined> {
|
||||
export async function _readGpgKey(): Promise<openpgp.Key | undefined> {
|
||||
const config = appContextService.getConfig();
|
||||
const logger = appContextService.getLogger();
|
||||
const gpgKeyPath = config?.packageVerification?.gpgKeyPath;
|
||||
|
||||
if (!gpgKeyPath) {
|
||||
logger.warn('GPG key path not configured at "xpack.fleet.packageVerification.gpgKeyPath"');
|
||||
return undefined;
|
||||
|
@ -35,8 +36,53 @@ export async function _readGpgKey(): Promise<string | undefined> {
|
|||
logger.warn(`Unable to retrieve GPG key from '${gpgKeyPath}': ${e.code}`);
|
||||
return undefined;
|
||||
}
|
||||
let key;
|
||||
try {
|
||||
key = await openpgp.readKey({ armoredKey: buffer.toString() });
|
||||
} catch (e) {
|
||||
logger.warn(`Unable to parse GPG key from '${gpgKeyPath}': ${e}`);
|
||||
}
|
||||
|
||||
const key = buffer.toString();
|
||||
cachedKey = key;
|
||||
return key;
|
||||
}
|
||||
|
||||
export interface PackageVerificationResult {
|
||||
isVerified: boolean;
|
||||
keyId: string;
|
||||
}
|
||||
|
||||
export async function verifyPackageSignature({
|
||||
pkgZipBuffer,
|
||||
pkgZipSignature,
|
||||
verificationKey,
|
||||
logger,
|
||||
}: {
|
||||
pkgZipBuffer: Buffer;
|
||||
pkgZipSignature: string;
|
||||
verificationKey: openpgp.Key;
|
||||
logger: Logger;
|
||||
}): Promise<PackageVerificationResult> {
|
||||
const signature = await openpgp.readSignature({
|
||||
armoredSignature: pkgZipSignature,
|
||||
});
|
||||
|
||||
const message = await openpgp.createMessage({
|
||||
binary: pkgZipBuffer,
|
||||
});
|
||||
|
||||
const verificationResult = await openpgp.verify({
|
||||
verificationKeys: verificationKey,
|
||||
signature,
|
||||
message,
|
||||
});
|
||||
|
||||
const signatureVerificationResult = verificationResult.signatures[0];
|
||||
|
||||
let isVerified = false;
|
||||
try {
|
||||
isVerified = await signatureVerificationResult.verified;
|
||||
} catch (e) {
|
||||
logger.error(`Error verifying package signature: ${e}`);
|
||||
}
|
||||
return { isVerified, keyId: verificationKey.getKeyID().toHex() };
|
||||
}
|
||||
|
|
|
@ -52,7 +52,26 @@ describe('Util: isValidIndexName()', () => {
|
|||
expect(isValidIndexName('a'.repeat(255))).toBe(true);
|
||||
expect(isValidIndexName('a'.repeat(256))).toBe(false);
|
||||
// multi-byte character test
|
||||
// because jest doesn't have TextEncoder this will still be true
|
||||
// this test relies on TextEncoder being mocked here 'packages/kbn-test/src/jest/setup/polyfills.jsdom.js'
|
||||
expect(isValidIndexName('あ'.repeat(255))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Util isValidIndexName() with no TextEncoder avaialble', () => {
|
||||
const TextEncoder = global.TextEncoder;
|
||||
|
||||
beforeAll(() => {
|
||||
// @ts-ignore The operand of a 'delete' operator must be optional
|
||||
delete global.TextEncoder;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
global.TextEncoder = TextEncoder;
|
||||
});
|
||||
// this test is in it's own describe block to prevent ordering issues as it deletes with global scope
|
||||
test('Multi-byte characters should not count extra towards character limit', () => {
|
||||
// @ts-ignore The operand of a 'delete' operator must be optional
|
||||
delete global.TextEncoder;
|
||||
expect(isValidIndexName('あ'.repeat(255))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
35
yarn.lock
35
yarn.lock
|
@ -3816,6 +3816,11 @@
|
|||
"@types/proj4" "^2.5.0"
|
||||
proj4 "2.6.2"
|
||||
|
||||
"@mattiasbuelens/web-streams-adapter@~0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@mattiasbuelens/web-streams-adapter/-/web-streams-adapter-0.1.0.tgz#607b5a25682f4ae2741da7ba6df39302505336b3"
|
||||
integrity sha512-oV4PyZfwJNtmFWhvlJLqYIX1Nn22ML8FZpS16ZUKv0hg7414xV1fjsGqxQzLT2dyK92TKxsJSwMOd7VNHAtPmA==
|
||||
|
||||
"@mdx-js/loader@^1.6.22":
|
||||
version "1.6.22"
|
||||
resolved "https://registry.yarnpkg.com/@mdx-js/loader/-/loader-1.6.22.tgz#d9e8fe7f8185ff13c9c8639c048b123e30d322c4"
|
||||
|
@ -4289,6 +4294,14 @@
|
|||
dependencies:
|
||||
"@octokit/openapi-types" "^11.2.0"
|
||||
|
||||
"@openpgp/web-stream-tools@^0.0.10":
|
||||
version "0.0.10"
|
||||
resolved "https://registry.yarnpkg.com/@openpgp/web-stream-tools/-/web-stream-tools-0.0.10.tgz#4496390da9715c9bfc581ad144f9fb8a36a37775"
|
||||
integrity sha512-1ONZADML0fb0RJR5UiGYPnRf9VaYBYUBc1gF9jyq57sHkr58cp5/BQHS+ivrqbRw21Sb70FKTssmJbRe71V+kw==
|
||||
dependencies:
|
||||
"@mattiasbuelens/web-streams-adapter" "~0.1.0"
|
||||
web-streams-polyfill "~3.0.3"
|
||||
|
||||
"@opentelemetry/api@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.1.0.tgz#563539048255bbe1a5f4f586a4a10a1bb737f44a"
|
||||
|
@ -9131,6 +9144,16 @@ asn1.js@^4.0.0:
|
|||
inherits "^2.0.1"
|
||||
minimalistic-assert "^1.0.0"
|
||||
|
||||
asn1.js@^5.0.0:
|
||||
version "5.4.1"
|
||||
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
|
||||
integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==
|
||||
dependencies:
|
||||
bn.js "^4.0.0"
|
||||
inherits "^2.0.1"
|
||||
minimalistic-assert "^1.0.0"
|
||||
safer-buffer "^2.1.0"
|
||||
|
||||
asn1@~0.2.3:
|
||||
version "0.2.4"
|
||||
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
|
||||
|
@ -22092,6 +22115,13 @@ opener@^1.5.2:
|
|||
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
|
||||
integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==
|
||||
|
||||
openpgp@5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/openpgp/-/openpgp-5.3.0.tgz#e8fc97e538865b8c095dbd91c7be4203bd1dd1df"
|
||||
integrity sha512-qjCj0vYpV3dmmkE+vURiJ5kVAJwrMk8BPukvpWJiHcTNWKwPVsRS810plIe4klIcHVf1ScgUQwqtBbv99ff+kQ==
|
||||
dependencies:
|
||||
asn1.js "^5.0.0"
|
||||
|
||||
opentracing@^0.14.3:
|
||||
version "0.14.4"
|
||||
resolved "https://registry.yarnpkg.com/opentracing/-/opentracing-0.14.4.tgz#a113408ea740da3a90fde5b3b0011a375c2e4268"
|
||||
|
@ -30244,6 +30274,11 @@ web-streams-polyfill@^3.2.0:
|
|||
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz#a6b74026b38e4885869fb5c589e90b95ccfc7965"
|
||||
integrity sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA==
|
||||
|
||||
web-streams-polyfill@~3.0.3:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.0.3.tgz#f49e487eedeca47a207c1aee41ee5578f884b42f"
|
||||
integrity sha512-d2H/t0eqRNM4w2WvmTdoeIvzAUSpK7JmATB8Nr2lb7nQ9BTIJVjbQ/TRFVEh2gUH1HwclPdoPtfMoFfetXaZnA==
|
||||
|
||||
webidl-conversions@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue