backport of commit 96bb63442204a1c8981947f3182640d04f29ff85 (#21931)

Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>
This commit is contained in:
hc-github-team-secure-vault-core 2023-07-19 12:01:28 -04:00 committed by GitHub
parent af69b579b5
commit 8bde02c699
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 96 additions and 9 deletions

3
changelog/21926.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
ui: Fixes problem displaying certificates issued with unsupported signature algorithms (i.e. ed25519)
```

View File

@ -24,7 +24,7 @@ export const EXTENSION_OIDs = {
};
// these are allowed ext oids, but not parsed and passed to cross-signed certs
export const IGNORED_OIDs = {
export const OTHER_OIDs = {
// These two extensions are controlled by the parent authority.
authority_key_identifier: '2.5.29.35',
authority_access_info: '1.3.6.1.5.5.7.1.1',

View File

@ -9,7 +9,7 @@ import { Certificate } from 'pkijs';
import { differenceInHours, getUnixTime } from 'date-fns';
import {
EXTENSION_OIDs,
IGNORED_OIDs,
OTHER_OIDs,
KEY_USAGE_BITS,
SAN_TYPES,
SIGNATURE_ALGORITHM_OIDs,
@ -131,15 +131,34 @@ export async function verifyCertificates(certA, certB, leaf) {
const parsedCertB = jsonToCertObject(certB);
if (leaf) {
const parsedLeaf = jsonToCertObject(leaf);
const chainA = await parsedLeaf.verify(parsedCertA);
const chainB = await parsedLeaf.verify(parsedCertB);
const chainA = await verifySignature(parsedCertA, parsedLeaf);
const chainB = await verifySignature(parsedCertB, parsedLeaf);
// the leaf's issuer should be equal to the subject data of the intermediate certs
const isEqualA = parsedLeaf.issuer.isEqual(parsedCertA.subject);
const isEqualB = parsedLeaf.issuer.isEqual(parsedCertB.subject);
return chainA && chainB && isEqualA && isEqualB;
}
// can be used to validate if a certificate is self-signed (i.e. a root cert), by passing it as both certA and B
return (await parsedCertA.verify(parsedCertB)) && parsedCertA.issuer.isEqual(parsedCertB.subject);
return (await verifySignature(parsedCertA, parsedCertB)) && parsedCertA.issuer.isEqual(parsedCertB.subject);
}
export async function verifySignature(parent, child) {
try {
return await child.verify(parent);
} catch (error) {
// ed25519 is an unsupported signature algorithm and so verify() errors
// SKID (subject key ID) is the byte array of the key identifier
// AKID (authority key ID) is a SEQUENCE-type extension that includes the key identifier and potentially other information.
const skidExtension = parent.extensions.find((ext) => ext.extnID === OTHER_OIDs.subject_key_identifier);
const akidExtension = parent.extensions.find((ext) => ext.extnID === OTHER_OIDs.authority_key_identifier);
// return false if either extension is missing
// this could mean a false-negative but that's okay for our use-case
if (!skidExtension || !akidExtension) return false;
const skid = new Uint8Array(skidExtension.parsedValue.valueBlock.valueHex);
const akid = new Uint8Array(akidExtension.extnValue.valueBlock.valueHex);
// Check that AKID includes the SKID, which saves us from parsing the AKID and is unlikely to return false-positives.
return akid.toString().includes(skid.toString());
}
}
//* PARSING HELPERS
@ -182,7 +201,7 @@ export function parseExtensions(extensions) {
if (!extensions) return null;
const values = {};
const errors = [];
const allowedOids = Object.values({ ...EXTENSION_OIDs, ...IGNORED_OIDs });
const allowedOids = Object.values({ ...EXTENSION_OIDs, ...OTHER_OIDs });
const isUnknownExtension = (ext) => !allowedOids.includes(ext.extnID);
if (extensions.any(isUnknownExtension)) {
const unknown = extensions.filter(isUnknownExtension).map((ext) => ext.extnID);

View File

@ -2,7 +2,7 @@
<LinkedBlock class="list-item-row" @params={{array "issuers.issuer.details" pkiIssuer.id}} @linkPrefix={{@mountPoint}}>
<div class="level is-mobile">
<div class="level-left">
<div>
<div data-test-issuer-list={{pkiIssuer.id}}>
<Icon @name="certificate" class="has-text-grey-light" />
<span class="has-text-weight-semibold is-underline">
{{pkiIssuer.issuerRef}}

View File

@ -6,7 +6,7 @@
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { click, currentURL, fillIn, visit, isSettled, waitUntil } from '@ember/test-helpers';
import { click, currentURL, fillIn, visit, isSettled, waitUntil, find } from '@ember/test-helpers';
import { v4 as uuidv4 } from 'uuid';
import authPage from 'vault/tests/pages/auth';
@ -193,5 +193,30 @@ module('Acceptance | pki configuration test', function (hooks) {
"This PKI mount hasn't yet been configured with a certificate issuer. There are existing certificates. Use the CLI to perform any operations with them until an issuer is configured."
);
});
// test coverage for ed25519 certs not displaying because the verify() function errors
test('it generates and displays a root issuer of key type = ed25519', async function (assert) {
assert.expect(4);
await authPage.login(this.pkiAdminToken);
await visit(`/vault/secrets/${this.mountPath}/pki/overview`);
await click(SELECTORS.issuersTab);
await click(SELECTORS.generateIssuerDropdown);
await click(SELECTORS.generateIssuerRoot);
await fillIn(SELECTORS.configuration.inputByName('type'), 'internal');
await fillIn(SELECTORS.configuration.inputByName('commonName'), 'my-certificate');
await click(SELECTORS.configuration.keyParamsGroupToggle);
await fillIn(SELECTORS.configuration.inputByName('keyType'), 'ed25519');
await click(SELECTORS.configuration.generateRootSave);
const issuerId = find(SELECTORS.configuration.saved.issuerLink).innerHTML;
await visit(`/vault/secrets/${this.mountPath}/pki/issuers`);
assert.dom(SELECTORS.issuerListItem(issuerId)).exists();
assert
.dom('[data-test-common-name="0"]')
.hasText('my-certificate', 'parses certificate metadata in the list view');
await click(SELECTORS.issuerListItem(issuerId));
assert.strictEqual(currentURL(), `/vault/secrets/${this.mountPath}/pki/issuers/${issuerId}/details`);
assert.dom(SELECTORS.configuration.saved.commonName).exists('renders issuer details');
});
});
});

View File

@ -180,3 +180,6 @@ export const oldParentIssuerCert = `-----BEGIN CERTIFICATE-----\nMIIDKzCCAhOgAwI
export const parentIssuerCert = `-----BEGIN CERTIFICATE-----\nMIIDKzCCAhOgAwIBAgIUBxLeuD3K0hF5dGpaEgZqytTN3lswDQYJKoZIhvcNAQEL\nBQAwHTEbMBkGA1UEAxMSTG9uZy1MaXZlZCBSb290IFgyMB4XDTIzMDEyNTAwMjQz\nM1oXDTIzMDIyNjAwMjUwM1owHTEbMBkGA1UEAxMSTG9uZy1MaXZlZCBSb290IFgy\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuqkwN4m5dFLwFi0iYs4r\nTO4HWzloF4yCOaNfVksh1cOafVu9vjFwOWgHZFe6h8BOn6biKdFtvGTyIzlMHe5t\nyFmec9pfjX243bH9Ev4n2RTMKs818g9LdoZT6SI7DxHEu3yuHBg9TM87+GB+dA1V\nkRsK5hgtNCSMdgFSljM169sYbNilpk8M7O2hr+AmgRi0c1nUEPCe4JAr0Zv8iweJ\ntFRVHiQJXD9WIVxaWVxqWFsHoXseZS7H76RSdf4jNfENmBguHZMAPhtqlc/pMan8\nu0IJETWjWENn+WYC7DnnfQtNqyebU2LdT3oKO8tELqITygjT2tCS1Zavmsy69VY0\nYwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\nHQ4EFgQUxgchIBo+1F++IFW0F586I5QDFGYwHwYDVR0jBBgwFoAUxgchIBo+1F++\nIFW0F586I5QDFGYwDQYJKoZIhvcNAQELBQADggEBAI6DdnW8q/FqGqk/Y0k7iUrZ\nYkfMRlss6uBTzLev53eXqFIJ3+EFVfV+ohDEedlYYm2QCELzQcJSR7Q2I22PQj8X\nTO0yqk6LOCMv/4yiDhF4D+haiDU4joq5GX1dpFdlNSQ5fJmnLKu8HYbOhbwUo4ns\n4yGzIMulZR1Zqf/HGEOCYPDQ0ZHucmHn7uGhmV+kgYGoKVEZ8XxfmyNPKuwTAUHL\nfInPJZtbxXTVmiWWy3iraeI4XcUvaD0JtVnsVphYrqrSZ60DjgFsjiyenxePGHXf\nYXV9HIS6OXlvWhJKlSINOTv9fAa+e+JtK7frdvxJNHoTG34PiGXfOV2swTvLJQo=\n-----END CERTIFICATE-----\n`;
export const intIssuerCert = `-----BEGIN CERTIFICATE-----\nMIIDKzCCAhOgAwIBAgIUPt5VyO6gyA4hVaMkdpNyBlP+I64wDQYJKoZIhvcNAQEL\nBQAwHTEbMBkGA1UEAxMSTG9uZy1MaXZlZCBSb290IFgxMB4XDTIzMDEyNTAwMjQz\nM1oXDTIzMDIyNjAwMjUwM1owHTEbMBkGA1UEAxMSU2hvcnQtTGl2ZWQgSW50IFIx\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqsvFU7lzt06n1w6BL+Wa\nf9zd+Z3G90Kv0HAksoLaWYinhkxNIUTU8ar9HLa2WV4EoJbNq91Hn+jFc2SYEXtR\nV+jm0kEhz4C4AoQ4D0s83JcYNssiNbVA04wa5ovD0iA/pzwVz8TnJSfAAuZ3vXFl\nyIbQ3ESozt9hGjo/JOpoBh67E7xkuzw4lnC2rXGHdh9pk1Di+wqREnKU4nuhDLnT\nC/LL+Mkm07F1aMAW3Z/PWFmmsDJHMhZnaYo2LGCwU4A0U1ED0XpwflobVbkzZDmC\nXOPEI8UXG6VcL36zWnzEQnlZKN91MAa+s0E4z40KHKVSblSkjYD1K6n0y787ic2m\nDwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\nHQ4EFgQUkBK+oGpo5DNj2pCKoUE08WFOxQUwHwYDVR0jBBgwFoAUQsdYFMtsNMYN\nDIhZHMd77kcLLi8wDQYJKoZIhvcNAQELBQADggEBAIf4Bp/NYftiN8LmQrVzPWAe\nc4Bxm/NFFtkwQEvFhndMN68MUyXa5yxAdnYAHN+fRpYPxbjoZNXjW/jx3Kjft44r\ntyNGrrkjR80TI9FbL53nN7hLtZQdizsQD0Wype4Q1JOIxYw2Wd5Hr/PVPrJZ3PGg\nwNeI5IRu/cVbVT/vkRaHqYSwpa+V2cZTaEk6h62KPaKu3ui+omoeitU6qXHOysXQ\nrdGkJl/x831sIKmN0dMiGeoJdHGAr/E2f3ijKbVPsjIxZbm2SSumldOFYWn9cNYD\nI6sizFH976Wpde/GRIvBIzJnlK3xgfy0D9AUvwKyt75PVEnshc9tlhxoSVlKaUE=\n-----END CERTIFICATE-----\n`;
export const newlySignedCert = `-----BEGIN CERTIFICATE-----\nMIIDKzCCAhOgAwIBAgIUKapKK5Coau2sfIJgqA9jcC6BkWIwDQYJKoZIhvcNAQEL\nBQAwHTEbMBkGA1UEAxMSTG9uZy1MaXZlZCBSb290IFgyMB4XDTIzMDEyNTIyMjky\nNVoXDTIzMDIyNjIyMjk1NVowHTEbMBkGA1UEAxMSU2hvcnQtTGl2ZWQgSW50IFIx\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqsvFU7lzt06n1w6BL+Wa\nf9zd+Z3G90Kv0HAksoLaWYinhkxNIUTU8ar9HLa2WV4EoJbNq91Hn+jFc2SYEXtR\nV+jm0kEhz4C4AoQ4D0s83JcYNssiNbVA04wa5ovD0iA/pzwVz8TnJSfAAuZ3vXFl\nyIbQ3ESozt9hGjo/JOpoBh67E7xkuzw4lnC2rXGHdh9pk1Di+wqREnKU4nuhDLnT\nC/LL+Mkm07F1aMAW3Z/PWFmmsDJHMhZnaYo2LGCwU4A0U1ED0XpwflobVbkzZDmC\nXOPEI8UXG6VcL36zWnzEQnlZKN91MAa+s0E4z40KHKVSblSkjYD1K6n0y787ic2m\nDwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV\nHQ4EFgQUkBK+oGpo5DNj2pCKoUE08WFOxQUwHwYDVR0jBBgwFoAUxgchIBo+1F++\nIFW0F586I5QDFGYwDQYJKoZIhvcNAQELBQADggEBAJaems1vgEjxgb3d1y9PYxzN\nLZbuf/+0+BCVa9k4bEsbuhXhEecFdIi2OKS6fabeoEOF97Gvqrgc+LEpNsU6lIRA\nkJ/nHe0CD2hf0aBQsGsOllYy/4QnrPlbowb4KizPknEMWdGcvfnlzzOJzo4/UuMk\nMZ9vn2GrINzfml/sLocOzP/MsPd8bBhXI2Emh2O9tJ4+zeHLhEzcM1gdNk8pp+wP\nEOks0EcN4UBkpEnDZcDTJVgp9XpWy19EEGqsxjBq6rlpIvPW8XHoH1jZSGY1KWBJ\nRGtDcGugwTxO9jYHz/a1qu4BVt5FFcb0L3IOvcr+3QCCeiJQHcVY8QRbO9M4AQk=\n-----END CERTIFICATE-----\n`;
// both certs generated with key type ed25519
export const unsupportedSignatureRoot = `-----BEGIN CERTIFICATE-----\nMIIBXTCCAQ+gAwIBAgIUcp9CkzsU5Pkv2ZJO8Gp+tJrzuJYwBQYDK2VwMBIxEDAO\nBgNVBAMTB215LXJvb3QwHhcNMjMwNzE4MTYyNzQ3WhcNMjMwODE5MTYyODE3WjAS\nMRAwDgYDVQQDEwdteS1yb290MCowBQYDK2VwAyEAmZ+By07QvgAEX1HRjhltJlgK\nA8il2LYUpH0uw7f2lXCjdzB1MA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD\nAQH/MB0GA1UdDgQWBBTAcYaOaiKhDmYqSe6vg/lAtYspkDAfBgNVHSMEGDAWgBTA\ncYaOaiKhDmYqSe6vg/lAtYspkDASBgNVHREECzAJggdteS1yb290MAUGAytlcANB\nAG9xXZnKNEXRyfa91hm9S80PwlwIMh4MkWetwfPBn3M74cHzDK1okANmweca4RRq\nQHDPT7shx3CuosvL2Ori/ws=\n-----END CERTIFICATE-----`;
export const unsupportedSignatureInt = `-----BEGIN CERTIFICATE-----\nMIICfTCCAWWgAwIBAgIUei2XIhhsP1/ytDciEGfA1C7t/sMwDQYJKoZIhvcNAQEL\nBQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMjMwNzE4MTg1NDA3WhcNMjMw\nODE5MTg1NDM3WjASMRAwDgYDVQQDEwdpbnQtY3NyMCowBQYDK2VwAyEAa9vHnJA3\nnzA/fYiTUg8EhomjMtVp5O2c01nQRXEv72OjgcAwgb0wDgYDVR0PAQH/BAQDAgEG\nMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFGtjjUwrRGmFmYBHrUE38tSxvVM3\nMB8GA1UdIwQYMBaAFNng9+uArFyIUcD23XdvCSIfYiDPMEYGCCsGAQUFBwEBBDow\nODAZBggrBgEFBQcwAoYNaGFzaGljb3JwLmNvbTAbBggrBgEFBQcwAoYPdmF1bHRw\ncm9qZWN0LmlvMBIGA1UdEQQLMAmCB2ludC1jc3IwDQYJKoZIhvcNAQELBQADggEB\nAAOSNgZjesJG4BgLU8jQmOO7n6W8WcR+dT+ELDC1nLlEZ2BJCDSXXUX8AihIHKxn\nA9W4slABUacyJlAZo/o/wcxyfbA6PUXmHnoqEPZ3zXMwuLN/iRW7/uQvI6TIwnpH\nXETFARLmK8cfGgbhi24STkHTF4ljczkOab7sTUQTHELlo+F2gNtmgnyaBFCGUYor\nX1pkMBcBa9BWRsfhy8E+tBVVUrNNUddwzC/5nMLqT8XqENMndDoG7eeT9Ex6otZy\nzURkcq09FtcmyY2RBYkV4UzyHN7cESMIk/J33ZCNAfHaDGuOqTy5nYU5fTtjJcit\nwEcWiSesrKPCletBpuMpgiU=\n-----END CERTIFICATE-----\n`;

View File

@ -12,6 +12,7 @@ import { SELECTORS as CONFIGURATION } from './pki-configure-create';
import { SELECTORS as DELETE } from './pki-delete-all-issuers';
import { SELECTORS as TIDY } from './page/pki-tidy-form';
import { SELECTORS as CONFIGEDIT } from './page/pki-configuration-edit';
import { SELECTORS as GENROOT } from './pki-generate-root';
export const SELECTORS = {
breadcrumbContainer: '[data-test-breadcrumbs]',
@ -51,6 +52,7 @@ export const SELECTORS = {
...KEYPAGES,
},
// ISSUERS
issuerListItem: (id) => `[data-test-issuer-list="${id}"]`,
importIssuerLink: '[data-test-generate-issuer="import"]',
generateIssuerDropdown: '[data-test-issuer-generate-dropdown]',
generateIssuerRoot: '[data-test-generate-issuer="root"]',
@ -70,6 +72,7 @@ export const SELECTORS = {
...CONFIGURATION,
...DELETE,
...TIDY,
...GENROOT,
},
// EDIT CONFIGURATION
configEdit: {

View File

@ -11,14 +11,19 @@ import { fromBase64, stringToArrayBuffer } from 'pvutils';
import { Certificate } from 'pkijs';
import { addHours, fromUnixTime, isSameDay } from 'date-fns';
import errorMessage from 'vault/utils/error-message';
import { SAN_TYPES } from 'vault/utils/parse-pki-cert-oids';
import { OTHER_OIDs, SAN_TYPES } from 'vault/utils/parse-pki-cert-oids';
import {
certWithoutCN,
loadedCert,
pssTrueCert,
skeletonCert,
unsupportedOids,
unsupportedSignatureRoot,
unsupportedSignatureInt,
} from 'vault/tests/helpers/pki/values';
import { verifyCertificates } from 'vault/utils/parse-pki-cert';
import { jsonToCertObject } from 'vault/utils/parse-pki-cert';
import { verifySignature } from 'vault/utils/parse-pki-cert';
module('Integration | Util | parse pki certificate', function (hooks) {
setupTest(hooks);
@ -227,6 +232,35 @@ module('Integration | Util | parse pki certificate', function (hooks) {
);
});
test('the helper verifyCertificates catches errors', async function (assert) {
assert.expect(5);
const verifiedRoot = await verifyCertificates(unsupportedSignatureRoot, unsupportedSignatureRoot);
assert.true(verifiedRoot, 'returns true for root certificate');
const verifiedInt = await verifyCertificates(unsupportedSignatureInt, unsupportedSignatureInt);
assert.false(verifiedInt, 'returns false for intermediate cert');
const filterExtensions = (list, oid) => list.filter((ext) => ext.extnID !== oid);
const { subject_key_identifier, authority_key_identifier } = OTHER_OIDs;
const testCert = jsonToCertObject(unsupportedSignatureRoot);
const certWithoutSKID = testCert;
certWithoutSKID.extensions = filterExtensions(testCert.extensions, subject_key_identifier);
assert.false(
await verifySignature(certWithoutSKID, certWithoutSKID),
'returns false if no subject key ID'
);
const certWithoutAKID = testCert;
certWithoutAKID.extensions = filterExtensions(testCert.extensions, authority_key_identifier);
assert.false(await verifySignature(certWithoutAKID, certWithoutAKID), 'returns false if no AKID');
const certWithoutKeyID = testCert;
certWithoutAKID.extensions = [];
assert.false(
await verifySignature(certWithoutKeyID, certWithoutKeyID),
'returns false if neither SKID or AKID'
);
});
test('it fails silently when passed null', async function (assert) {
assert.expect(3);
const parsedCert = parseCertificate(certWithoutCN);