mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-30 02:02:43 +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
	 claire bontempo
					claire bontempo