mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-01 19:17:58 +00:00
UI: pki cross-signing acceptance tests (#20495)
* move selectors to helper file * wip cross sign acceptance tests * finish tests
This commit is contained in:
@@ -104,20 +104,27 @@ export function formatValues(subject, extension) {
|
||||
}
|
||||
|
||||
/*
|
||||
How to use the verify function for cross-signing:
|
||||
Explanation of cross-signing and how to use the verify function:
|
||||
(See setup script here: https://github.com/hashicorp/vault-tools/blob/main/vault-ui/pki/pki-cross-sign-config.sh)
|
||||
1. A trust chain exists between "old-parent-issuer-name" -> "old-intermediate"
|
||||
2. Cross-sign "old-intermediate" against "my-parent-issuer-name" creating a new certificate: "newly-cross-signed-int-name"
|
||||
3. Generate a leaf certificate from "newly-cross-signed-int-name", let's call it "baby-leaf"
|
||||
4. Verify that "baby-leaf" validates against both chains:
|
||||
2. We create a new root, "my-parent-issuer-name" to phase out the old one
|
||||
|
||||
* cross-signing step performed in the UI *
|
||||
3. Cross-sign "old-intermediate" against new root "my-parent-issuer-name" which generates a new intermediate issuer,
|
||||
"newly-cross-signed-int-name", to phase out the old intermediate
|
||||
|
||||
* validate cross-signing accurately copied the old intermediate issuer *
|
||||
4. Generate a leaf certificate from "newly-cross-signed-int-name", let's call it "baby-leaf"
|
||||
5. Verify that "baby-leaf" validates against both chains:
|
||||
"old-parent-issuer-name" -> "old-intermediate" -> "baby-leaf"
|
||||
"my-parent-issuer-name" -> "newly-cross-signed-int-name" -> "baby-leaf"
|
||||
|
||||
A valid cross-signing would mean BOTH of the following return true:
|
||||
verifyCertificates(oldParentCert, oldIntCert, leaf)
|
||||
verifyCertificates(newParentCert, crossSignedCert, leaf)
|
||||
We're just concerned with the link between the leaf and both intermediates
|
||||
to confirm the UI performed the cross-sign correctly
|
||||
(which already assumes the link between each parent and intermediate is valid)
|
||||
|
||||
verifyCertificates(oldIntermediate, crossSignedCert, leaf)
|
||||
|
||||
each arg is the JSON string certificate value
|
||||
*/
|
||||
export async function verifyCertificates(certA, certB, leaf) {
|
||||
const parsedCertA = jsonToCertObject(certA);
|
||||
@@ -131,7 +138,7 @@ export async function verifyCertificates(certA, certB, leaf) {
|
||||
const isEqualB = parsedLeaf.issuer.isEqual(parsedCertB.subject);
|
||||
return chainA && chainB && isEqualA && isEqualB;
|
||||
}
|
||||
// can be used to validate if a certificate is self-signed, by passing it as both certA and B (i.e. a root cert)
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
111
ui/tests/acceptance/pki/pki-cross-sign-test.js
Normal file
111
ui/tests/acceptance/pki/pki-cross-sign-test.js
Normal file
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { visit, click, fillIn, find } from '@ember/test-helpers';
|
||||
import { setupApplicationTest } from 'vault/tests/helpers';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import authPage from 'vault/tests/pages/auth';
|
||||
import logout from 'vault/tests/pages/logout';
|
||||
import enablePage from 'vault/tests/pages/settings/mount-secret-backend';
|
||||
import { runCommands } from 'vault/tests/helpers/pki/pki-run-commands';
|
||||
import { SELECTORS } from 'vault/tests/helpers/pki/pki-issuer-cross-sign';
|
||||
import { verifyCertificates } from 'vault/utils/parse-pki-cert';
|
||||
module('Acceptance | pki/pki cross sign', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
|
||||
hooks.beforeEach(async function () {
|
||||
await authPage.login();
|
||||
this.parentMountPath = `parent-mount-${uuidv4()}`;
|
||||
this.oldParentIssuerName = 'old-parent-issuer'; // old parent issuer we're transferring from
|
||||
this.parentIssuerName = 'new-parent-issuer'; // issuer where cross-signing action will begin
|
||||
this.intMountPath = `intermediate-mount-${uuidv4()}`; // first input box in cross-signing page
|
||||
this.intIssuerName = 'my-intermediate-issuer'; // second input box in cross-signing page
|
||||
this.newlySignedIssuer = 'my-newly-signed-int'; // third input
|
||||
await enablePage.enable('pki', this.parentMountPath);
|
||||
await enablePage.enable('pki', this.intMountPath);
|
||||
|
||||
await runCommands([
|
||||
`write "${this.parentMountPath}/root/generate/internal" common_name="Long-Lived Root X1" ttl=8960h issuer_name="${this.oldParentIssuerName}"`,
|
||||
`write "${this.parentMountPath}/root/generate/internal" common_name="Long-Lived Root X2" ttl=8960h issuer_name="${this.parentIssuerName}"`,
|
||||
`write "${this.parentMountPath}/config/issuers" default="${this.parentIssuerName}"`,
|
||||
]);
|
||||
});
|
||||
|
||||
hooks.afterEach(async function () {
|
||||
// Cleanup engine
|
||||
await runCommands([`delete sys/mounts/${this.intMountPath}`]);
|
||||
await runCommands([`delete sys/mounts/${this.parentMountPath}`]);
|
||||
await logout.visit();
|
||||
});
|
||||
|
||||
test('it cross-signs an issuer', async function (assert) {
|
||||
// configure parent and intermediate mounts to make them cross-signable
|
||||
await visit(`/vault/secrets/${this.intMountPath}/pki/configuration/create`);
|
||||
await click(SELECTORS.configure.optionByKey('generate-csr'));
|
||||
await fillIn(SELECTORS.inputByName('type'), 'internal');
|
||||
await fillIn(SELECTORS.inputByName('commonName'), 'Short-Lived Int R1');
|
||||
await click('[data-test-save]');
|
||||
const csr = find(SELECTORS.copyButton('CSR')).getAttribute('data-clipboard-text');
|
||||
await visit(`vault/secrets/${this.parentMountPath}/pki/issuers/${this.oldParentIssuerName}/sign`);
|
||||
await fillIn(SELECTORS.inputByName('csr'), csr);
|
||||
await fillIn(SELECTORS.inputByName('format'), 'pem_bundle');
|
||||
await click('[data-test-pki-sign-intermediate-save]');
|
||||
const pemBundle = find(SELECTORS.copyButton('CA Chain'))
|
||||
.getAttribute('data-clipboard-text')
|
||||
.replace(/,/, '\n');
|
||||
await visit(`vault/secrets/${this.intMountPath}/pki/configuration/create`);
|
||||
await click(SELECTORS.configure.optionByKey('import'));
|
||||
await click('[data-test-text-toggle]');
|
||||
await fillIn('[data-test-text-file-textarea]', pemBundle);
|
||||
await click(SELECTORS.configure.importSubmit);
|
||||
await visit(`vault/secrets/${this.intMountPath}/pki/issuers`);
|
||||
await click('[data-test-is-default]');
|
||||
// name default issuer of intermediate
|
||||
const oldIntIssuerId = find(SELECTORS.rowValue('Issuer ID')).innerText;
|
||||
const oldIntCert = find(SELECTORS.copyButton('Certificate')).getAttribute('data-clipboard-text');
|
||||
await click(SELECTORS.details.configure);
|
||||
await fillIn(SELECTORS.inputByName('issuerName'), this.intIssuerName);
|
||||
await click('[data-test-save]');
|
||||
|
||||
// perform cross-sign
|
||||
await visit(`vault/secrets/${this.parentMountPath}/pki/issuers/${this.parentIssuerName}/cross-sign`);
|
||||
await fillIn(SELECTORS.objectListInput('intermediateMount'), this.intMountPath);
|
||||
await fillIn(SELECTORS.objectListInput('intermediateIssuer'), this.intIssuerName);
|
||||
await fillIn(SELECTORS.objectListInput('newCrossSignedIssuer'), this.newlySignedIssuer);
|
||||
await click(SELECTORS.submitButton);
|
||||
assert
|
||||
.dom(`${SELECTORS.signedIssuerCol('intermediateMount')} a`)
|
||||
.hasAttribute('href', `/ui/vault/secrets/${this.intMountPath}/pki/overview`);
|
||||
assert
|
||||
.dom(`${SELECTORS.signedIssuerCol('intermediateIssuer')} a`)
|
||||
.hasAttribute('href', `/ui/vault/secrets/${this.intMountPath}/pki/issuers/${oldIntIssuerId}/details`);
|
||||
|
||||
// get certificate data of newly signed issuer
|
||||
await click(`${SELECTORS.signedIssuerCol('newCrossSignedIssuer')} a`);
|
||||
const newIntCert = find(SELECTORS.copyButton('Certificate')).getAttribute('data-clipboard-text');
|
||||
|
||||
// verify cross-sign was accurate by creating a role to issue a leaf certificate
|
||||
const myRole = 'some-role';
|
||||
await runCommands([
|
||||
`write ${this.intMountPath}/roles/${myRole} \
|
||||
issuer_ref=${this.newlySignedIssuer}\
|
||||
allow_any_name=true \
|
||||
max_ttl="720h"`,
|
||||
]);
|
||||
await visit(`vault/secrets/${this.intMountPath}/pki/roles/${myRole}/generate`);
|
||||
await fillIn(SELECTORS.inputByName('commonName'), 'my-leaf');
|
||||
await fillIn('[data-test-ttl-value="TTL"]', '3600');
|
||||
await click('[data-test-pki-generate-button]');
|
||||
const myLeafCert = find(SELECTORS.copyButton('Certificate')).getAttribute('data-clipboard-text');
|
||||
|
||||
// see comments in utils/parse-pki-cert.js for step-by-step explanation of of verifyCertificates method
|
||||
assert.true(
|
||||
await verifyCertificates(oldIntCert, newIntCert, myLeafCert),
|
||||
'the leaf certificate validates against both intermediate certificates'
|
||||
);
|
||||
});
|
||||
});
|
||||
23
ui/tests/helpers/pki/pki-issuer-cross-sign.js
Normal file
23
ui/tests/helpers/pki/pki-issuer-cross-sign.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import { SELECTORS as CONFIGURE } from './pki-configure-create';
|
||||
import { SELECTORS as DETAILS } from './pki-issuer-details';
|
||||
|
||||
export const SELECTORS = {
|
||||
objectListInput: (key, row = 0) => `[data-test-object-list-input="${key}-${row}"]`,
|
||||
inputByName: (name) => `[data-test-input="${name}"]`,
|
||||
addRow: '[data-test-object-list-add-button',
|
||||
submitButton: '[data-test-cross-sign-submit]',
|
||||
cancelButton: '[data-test-cross-sign-cancel]',
|
||||
statusCount: '[data-test-cross-sign-status-count]',
|
||||
signedIssuerRow: (row = 0) => `[data-test-info-table-row="${row}"]`,
|
||||
signedIssuerCol: (attr) => `[data-test-info-table-column="${attr}"]`,
|
||||
// for cross signing acceptance tests
|
||||
configure: { ...CONFIGURE },
|
||||
details: { ...DETAILS },
|
||||
rowValue: (attr) => `[data-test-row-value="${attr}"]`,
|
||||
copyButton: (attr) => `[data-test-value-div="${attr}"] [data-test-copy-button]`,
|
||||
};
|
||||
@@ -18,16 +18,8 @@ import {
|
||||
parentIssuerCert,
|
||||
unsupportedOids,
|
||||
} from 'vault/tests/helpers/pki/values';
|
||||
import { SELECTORS } from 'vault/tests/helpers/pki/pki-issuer-cross-sign';
|
||||
|
||||
const SELECTORS = {
|
||||
input: (key, row = 0) => `[data-test-object-list-input="${key}-${row}"]`,
|
||||
addRow: '[data-test-object-list-add-button',
|
||||
submitButton: '[data-test-cross-sign-submit]',
|
||||
cancelButton: '[data-test-cross-sign-cancel]',
|
||||
statusCount: '[data-test-cross-sign-status-count]',
|
||||
signedIssuerRow: (row = 0) => `[data-test-info-table-row="${row}"]`,
|
||||
signedIssuerCol: (attr) => `[data-test-info-table-column="${attr}"]`,
|
||||
};
|
||||
const FIELDS = [
|
||||
{
|
||||
label: 'Mount path',
|
||||
@@ -190,7 +182,7 @@ module('Integration | Component | pki issuer cross sign', function (hooks) {
|
||||
});
|
||||
// fill out form and submit
|
||||
for (const field of FIELDS) {
|
||||
await fillIn(SELECTORS.input(field.key), this.testInputs[field.key]);
|
||||
await fillIn(SELECTORS.objectListInput(field.key), this.testInputs[field.key]);
|
||||
}
|
||||
await click(SELECTORS.submitButton);
|
||||
|
||||
@@ -248,15 +240,15 @@ module('Integration | Component | pki issuer cross sign', function (hooks) {
|
||||
|
||||
// fill out form and submit
|
||||
for (const field of FIELDS) {
|
||||
await fillIn(SELECTORS.input(field.key), this.testInputs[field.key]);
|
||||
await fillIn(SELECTORS.objectListInput(field.key), this.testInputs[field.key]);
|
||||
}
|
||||
await click(SELECTORS.addRow);
|
||||
for (const field of FIELDS) {
|
||||
await fillIn(SELECTORS.input(field.key, 1), nonexistentIssuer[field.key]);
|
||||
await fillIn(SELECTORS.objectListInput(field.key, 1), nonexistentIssuer[field.key]);
|
||||
}
|
||||
await click(SELECTORS.addRow);
|
||||
for (const field of FIELDS) {
|
||||
await fillIn(SELECTORS.input(field.key, 2), unsupportedCert[field.key]);
|
||||
await fillIn(SELECTORS.objectListInput(field.key, 2), unsupportedCert[field.key]);
|
||||
}
|
||||
await click(SELECTORS.submitButton);
|
||||
|
||||
@@ -296,7 +288,7 @@ module('Integration | Component | pki issuer cross sign', function (hooks) {
|
||||
|
||||
// fill out form and submit
|
||||
for (const field of FIELDS) {
|
||||
await fillIn(SELECTORS.input(field.key), this.testInputs[field.key]);
|
||||
await fillIn(SELECTORS.objectListInput(field.key), this.testInputs[field.key]);
|
||||
}
|
||||
await click(SELECTORS.submitButton);
|
||||
|
||||
@@ -329,7 +321,7 @@ module('Integration | Component | pki issuer cross sign', function (hooks) {
|
||||
});
|
||||
// fill out form and submit
|
||||
for (const field of FIELDS) {
|
||||
await fillIn(SELECTORS.input(field.key), this.testInputs[field.key]);
|
||||
await fillIn(SELECTORS.objectListInput(field.key), this.testInputs[field.key]);
|
||||
}
|
||||
await click(SELECTORS.submitButton);
|
||||
assert.dom(SELECTORS.statusCount).hasText('Cross-signing complete (0 successful, 1 error)');
|
||||
|
||||
Reference in New Issue
Block a user