[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:
Mark Hopkin 2022-06-30 11:00:27 +01:00 committed by GitHub
parent 3d4abda822
commit 4179903a2f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 174 additions and 17 deletions

View file

@ -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",

View file

@ -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;
}

View file

@ -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

View file

@ -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');
}

View file

@ -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);

View file

@ -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)

View file

@ -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);

View file

@ -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() };
}

View file

@ -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);
});
});

View file

@ -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"