mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-18 04:27:02 +02:00
254 lines
10 KiB
JavaScript
254 lines
10 KiB
JavaScript
/**
|
||
* Copyright (c) HashiCorp, Inc.
|
||
* SPDX-License-Identifier: MPL-2.0
|
||
*/
|
||
|
||
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 };
|
||
}
|
||
}
|