mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-22 23:21:08 +02:00
* Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License. Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUS-1.1 * Fix test that expected exact offset on hcl file --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com> Co-authored-by: Sarah Thompson <sthompson@hashicorp.com> Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
254 lines
10 KiB
JavaScript
254 lines
10 KiB
JavaScript
/**
|
||
* Copyright (c) HashiCorp, Inc.
|
||
* SPDX-License-Identifier: BUSL-1.1
|
||
*/
|
||
|
||
import Component from '@glimmer/component';
|
||
import { action } from '@ember/object';
|
||
import { task } from 'ember-concurrency';
|
||
import { inject as service } from '@ember/service';
|
||
import { tracked } from '@glimmer/tracking';
|
||
import errorMessage from 'vault/utils/error-message';
|
||
import { waitFor } from '@ember/test-waiters';
|
||
import { parseCertificate } from 'vault/utils/parse-pki-cert';
|
||
/**
|
||
* @module PkiIssuerCrossSign
|
||
* PkiIssuerCrossSign components render from a parent issuer's details page to cross-sign an intermediate issuer (from a different mount).
|
||
* The component reads an existing intermediate issuer, cross-signs it with a parent issuer and imports the new
|
||
* issuer into the existing intermediate mount using three inputs from the user:
|
||
* intermediateMount (the mount path where the issuer to be cross signed lives)
|
||
* intermediateIssuer (the name of the intermediate issuer, located in the above mount)
|
||
* newCrossSignedIssuer (the name of the to-be-cross-signed, new issuer)
|
||
*
|
||
* The requests involved and how those inputs are used:
|
||
* 1. Read an existing intermediate issuer
|
||
* -> GET /:intermediateMount/issuer/:intermediateIssuer
|
||
* 2. Create a new CSR based on this existing issuer ID
|
||
* -> POST /:intermediateMount/intermediate/generate/existing
|
||
* 3. Sign it with the new parent issuer, minting a new certificate.
|
||
* -> POST /this.args.parentIssuer.backend/issuer/this.args.parentIssuer.issuerRef/sign-intermediate
|
||
* 4. Import it back into the existing mount
|
||
* -> POST /:intermediateMount/issuers/import/bundle
|
||
* 5. Read the imported issuer
|
||
* -> GET /:intermediateMount/issuer/:issuer_id
|
||
* 6. Update this issuer with the newCrossSignedIssuer
|
||
* -> POST /:intermediateMount/issuer/:issuer_id
|
||
*
|
||
* @example
|
||
* ```js
|
||
* <PkiIssuerCrossSign @parentIssuer={{this.model}} />
|
||
* ```
|
||
* @param {object} parentIssuer - the model of the issuing certificate that will sign the issuer to-be cross-signed
|
||
*/
|
||
|
||
export default class PkiIssuerCrossSign extends Component {
|
||
@service store;
|
||
@tracked formData = [];
|
||
@tracked signedIssuers = [];
|
||
@tracked intermediateIssuers = {};
|
||
@tracked validationErrors = [];
|
||
|
||
inputFields = [
|
||
{
|
||
label: 'Mount path',
|
||
key: 'intermediateMount',
|
||
placeholder: 'Mount path',
|
||
helpText: 'The mount in which your new certificate can be found.',
|
||
},
|
||
{
|
||
label: "Issuer's current name",
|
||
key: 'intermediateIssuer',
|
||
placeholder: 'Current issuer name',
|
||
helpText: 'The API name of the previous intermediate which was cross-signed.',
|
||
},
|
||
{
|
||
label: 'New issuer name',
|
||
key: 'newCrossSignedIssuer',
|
||
placeholder: 'Enter a new issuer name',
|
||
helpText: `This is your new issuer’s name in the API.`,
|
||
},
|
||
];
|
||
|
||
get statusCount() {
|
||
const error = this.signedIssuers.filter((issuer) => issuer.hasError).length;
|
||
const success = this.signedIssuers.length - error;
|
||
return `${success} successful, ${error} ${error === 1 ? 'error' : 'errors'}`;
|
||
}
|
||
|
||
@task
|
||
@waitFor
|
||
*submit(e) {
|
||
e.preventDefault();
|
||
this.signedIssuers = [];
|
||
this.validationErrors = [];
|
||
|
||
// Validate name input for new issuer does not already exist in mount
|
||
for (let row = 0; row < this.formData.length; row++) {
|
||
const { intermediateMount, newCrossSignedIssuer } = this.formData[row];
|
||
const issuers = yield this.store
|
||
.query('pki/issuer', { backend: intermediateMount })
|
||
.then((resp) => resp.map(({ issuerName, issuerId }) => ({ issuerName, issuerId })))
|
||
.catch(() => []);
|
||
|
||
// for cross-signing error handling we want to record the list of issuers before the process starts
|
||
this.intermediateIssuers[intermediateMount] = issuers;
|
||
this.validationErrors.addObject({
|
||
newCrossSignedIssuer: this.nameValidation(newCrossSignedIssuer, issuers),
|
||
});
|
||
}
|
||
if (this.validationErrors.any((row) => !row.newCrossSignedIssuer.isValid)) return;
|
||
|
||
// iterate through submitted data and cross-sign each certificate
|
||
for (let row = 0; row < this.formData.length; row++) {
|
||
const { intermediateMount, intermediateIssuer, newCrossSignedIssuer } = this.formData[row];
|
||
try {
|
||
// returns data from existing and newly cross-signed issuers
|
||
// { intermediateIssuer: existingIssuer, newCrossSignedIssuer: crossSignedIssuer, intermediateMount: intMount }
|
||
const data = yield this.crossSignIntermediate(
|
||
intermediateMount,
|
||
intermediateIssuer,
|
||
newCrossSignedIssuer
|
||
);
|
||
this.signedIssuers.addObject({ ...data, hasError: false });
|
||
} catch (error) {
|
||
this.signedIssuers.addObject({
|
||
...this.formData[row],
|
||
hasError: errorMessage(error),
|
||
hasUnsupportedParams: error.cause ? error.cause.map((e) => e.message).join(', ') : null,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
@action
|
||
async crossSignIntermediate(intMount, intName, newCrossSignedIssuer) {
|
||
// 1. Fetch issuer we want to sign
|
||
// What/Recovery: any failure is early enough that you can bail safely/normally.
|
||
const existingIssuer = await this.store.queryRecord('pki/issuer', {
|
||
backend: intMount,
|
||
id: intName,
|
||
});
|
||
|
||
// Return if user is attempting to self-sign issuer
|
||
if (existingIssuer.issuerId === this.args.parentIssuer.issuerId) {
|
||
throw new Error('Cross-signing a root issuer with itself must be performed manually using the CLI.');
|
||
}
|
||
|
||
// Translate certificate values to API parameters to pass along: CSR -> Signed CSR -> Cross-Signed issuer
|
||
// some of these values do not apply to a CSR, but pass anyway. If there is any issue parsing the certificate,
|
||
// (ex. the certificate contains unsupported values) direct user to manually cross-sign via CLI
|
||
const certData = parseCertificate(existingIssuer.certificate);
|
||
if (certData.parsing_errors.length > 0) {
|
||
throw new Error('Certificate must be manually cross-signed using the CLI.', {
|
||
cause: certData.parsing_errors,
|
||
});
|
||
}
|
||
|
||
// 2. Create the new CSR
|
||
// What/Recovery: any failure is early enough that you can bail safely/normally.
|
||
const newCsr = await this.store
|
||
.createRecord('pki/action', {
|
||
keyRef: existingIssuer.keyId,
|
||
commonName: existingIssuer.commonName,
|
||
type: 'existing',
|
||
...certData,
|
||
})
|
||
.save({
|
||
adapterOptions: { actionType: 'generate-csr', mount: intMount, useIssuer: false },
|
||
})
|
||
.then(({ csr }) => csr);
|
||
|
||
// 3. Sign newCSR with correct parent to create cross-signed cert, "issuing"
|
||
// an intermediate certificate.
|
||
// What/Recovery: any failure is early enough that you can bail safely/normally.
|
||
const signedCaChain = await this.store
|
||
.createRecord('pki/action', {
|
||
csr: newCsr,
|
||
commonName: existingIssuer.commonName,
|
||
...certData,
|
||
})
|
||
.save({
|
||
adapterOptions: {
|
||
actionType: 'sign-intermediate',
|
||
mount: this.args.parentIssuer.backend,
|
||
issuerRef: this.args.parentIssuer.issuerRef,
|
||
},
|
||
})
|
||
.then(({ caChain }) => caChain.join('\n'));
|
||
|
||
// 4. Import the newly cross-signed cert to become an issuer
|
||
// What/Recovery:
|
||
// 1. Permission issue -> give the cert (`signedCaChain`) to the user,
|
||
// let them import & name. (Issue you have is that you already issued
|
||
// it (step 3) and so "undo" would mean revoking the cert, which
|
||
// you might not have permissions to do either).
|
||
//
|
||
// 2. CRL rebuilding fails ("the CRL" in error message). Server returns
|
||
// an error, we wanted the CRL rebuilt -- but the issuer was still
|
||
// imported anyways. Only way to detect would be to do a list issuers
|
||
// before and after. Recovery would be on the operator in this case;
|
||
// reproduce the error and let them deal with it.
|
||
//
|
||
// End result: user should solve this issue, but we shouldn't undo anything
|
||
// either.
|
||
//
|
||
// -> For 1 though, make sure to give the `signedCaChain` in the
|
||
// error message for them.
|
||
// -> For 2, you could list before and after to find the id of the
|
||
// new issuer(s) so they can name them and fix any issues with
|
||
// them.
|
||
//
|
||
// If its not a permissions error _and_ you did two lists, not finding
|
||
// a new issuer...
|
||
//
|
||
// -> Unknown error. Could give them `signedCaChain` and serial of
|
||
// the newly issued intermediate CA, so that they can do recovery
|
||
// as they'd like.
|
||
const issuerId = await this.store
|
||
.createRecord('pki/action', { pemBundle: signedCaChain })
|
||
.save({ adapterOptions: { actionType: 'import', mount: intMount, useIssuer: true } })
|
||
.then((importedIssuer) => {
|
||
return Object.keys(importedIssuer.mapping).find(
|
||
// matching key is the issuer_id
|
||
(key) => importedIssuer.mapping[key] === existingIssuer.keyId
|
||
);
|
||
})
|
||
.catch((e) => {
|
||
console.debug('CA_CHAIN \n', signedCaChain); // eslint-disable-line
|
||
throw new Error(`${errorMessage(e)} See console for signed ca_chain data.`);
|
||
});
|
||
|
||
// 5. Fetch issuer imported above by issuer_id, name and save
|
||
// Recovery: cosmetic issue; can let the user deal with it. Usually
|
||
// fails because the name is in use.
|
||
// Pre-fix: list all issuers, check the desired name isn't either
|
||
// an existing issuer_id or an issuer_name.
|
||
const crossSignedIssuer = await this.store.queryRecord('pki/issuer', { backend: intMount, id: issuerId });
|
||
crossSignedIssuer.issuerName = newCrossSignedIssuer;
|
||
await crossSignedIssuer.save({ adapterOptions: { mount: intMount } });
|
||
|
||
// 6. Return the data to our caller.
|
||
return {
|
||
intermediateIssuer: existingIssuer,
|
||
newCrossSignedIssuer: crossSignedIssuer,
|
||
intermediateMount: intMount,
|
||
};
|
||
}
|
||
|
||
@action
|
||
reset() {
|
||
this.signedIssuers = [];
|
||
this.validationErrors = [];
|
||
this.formData = [];
|
||
}
|
||
|
||
nameValidation(nameInput, existing) {
|
||
if (existing.any((i) => i.issuerName === nameInput || i.issuerId === nameInput))
|
||
return {
|
||
errors: [`Issuer reference '${nameInput}' already exists in this mount.`],
|
||
isValid: false,
|
||
};
|
||
return { errors: [], isValid: true };
|
||
}
|
||
}
|