diff --git a/ui/app/adapters/pki/action.js b/ui/app/adapters/pki/action.js
index 5c99c7a95..3cc3359ee 100644
--- a/ui/app/adapters/pki/action.js
+++ b/ui/app/adapters/pki/action.js
@@ -30,6 +30,8 @@ export default class PkiActionAdapter extends ApplicationAdapter {
: `${baseUrl}/intermediate/generate/${type}`;
case 'sign-intermediate':
return `${baseUrl}/issuer/${encodePath(issuerRef)}/sign-intermediate`;
+ case 'rotate-root':
+ return `${baseUrl}/root/rotate/${type}`;
default:
assert('actionType must be one of import, generate-root, generate-csr or sign-intermediate');
}
diff --git a/ui/app/models/pki/action.js b/ui/app/models/pki/action.js
index 701db9922..c21d9dc76 100644
--- a/ui/app/models/pki/action.js
+++ b/ui/app/models/pki/action.js
@@ -16,10 +16,14 @@ const validations = {
issuerName: [
{
validator(model) {
- if (model.actionType === 'generate-root' && model.issuerName === 'default') return false;
+ if (
+ (model.actionType === 'generate-root' || model.actionType === 'rotate-root') &&
+ model.issuerName === 'default'
+ )
+ return false;
return true;
},
- message: 'Issuer name must be unique across all issuers and not be the reserved value default.',
+ message: `Issuer name must be unique across all issuers and not be the reserved value 'default'.`,
},
],
};
@@ -47,6 +51,13 @@ export default class PkiActionModel extends Model {
@attr('string', { readOnly: true, masked: true }) certificate;
/* actionType generate-root */
+
+ // readonly attrs returned right after root generation
+ @attr serialNumber;
+ @attr('string', { label: 'Issuing CA', readOnly: true, masked: true }) issuingCa;
+ @attr keyName;
+ // end of readonly
+
@attr('string', {
possibleValues: ['exported', 'internal', 'existing', 'kms'],
noDefault: true,
@@ -149,7 +160,9 @@ export default class PkiActionModel extends Model {
subText:
"Specifies the requested Subject's named Serial Number value. This has no impact on the Certificate's serial number randomly generated by Vault.",
})
- serialNumber;
+ subjectSerialNumber;
+ // this is different from the number (16:5e:a0...) randomly generated by Vault
+ // https://developer.hashicorp.com/vault/api-docs/secret/pki#serial_number
@attr('boolean', {
subText: 'Whether to add a Basic Constraints extension with CA: true.',
diff --git a/ui/app/models/pki/issuer.js b/ui/app/models/pki/issuer.js
index 0d2748bfc..f455b94ce 100644
--- a/ui/app/models/pki/issuer.js
+++ b/ui/app/models/pki/issuer.js
@@ -25,7 +25,7 @@ const displayFields = [
'commonName',
'issuerName',
'issuerId',
- 'serialNumber',
+ 'subjectSerialNumber',
'keyId',
'altNames',
'uriSans',
@@ -53,8 +53,8 @@ export default class PkiIssuerModel extends Model {
// READ ONLY
@attr isDefault;
- @attr('string', { label: 'Issuer ID' }) issuerId;
- @attr('string', { label: 'Default key ID' }) keyId;
+ @attr('string', { label: 'Issuer ID', detailLinkTo: 'issuers.issuer.details' }) issuerId;
+ @attr('string', { label: 'Default key ID', detailLinkTo: 'keys.key.details' }) keyId;
@attr({ label: 'CA Chain', masked: true }) caChain;
@attr({ masked: true }) certificate;
@@ -62,7 +62,7 @@ export default class PkiIssuerModel extends Model {
@attr commonName;
@attr('number', { formatDate: true }) notValidAfter;
@attr('number', { formatDate: true }) notValidBefore;
- @attr serialNumber; // this is not the UUID serial number field randomly generated by Vault for leaf certificates
+ @attr subjectSerialNumber; // this is not the UUID serial number field randomly generated by Vault for leaf certificates
@attr({ label: 'Subject Alternative Names (SANs)' }) altNames;
@attr({ label: 'IP SANs' }) ipSans;
@attr({ label: 'URI SANs' }) uriSans;
diff --git a/ui/app/models/pki/sign-intermediate.js b/ui/app/models/pki/sign-intermediate.js
index a36f662c9..6fe884528 100644
--- a/ui/app/models/pki/sign-intermediate.js
+++ b/ui/app/models/pki/sign-intermediate.js
@@ -94,5 +94,5 @@ export default class PkiSignIntermediateModel extends PkiCertificateBaseModel {
subText:
"Specifies the requested Subject's named Serial Number value. This has no impact on the Certificate's serial number randomly generated by Vault.",
})
- serialNumber;
+ subjectSerialNumber;
}
diff --git a/ui/app/serializers/pki/action.js b/ui/app/serializers/pki/action.js
index bdc0fd7e3..5f370d95c 100644
--- a/ui/app/serializers/pki/action.js
+++ b/ui/app/serializers/pki/action.js
@@ -11,6 +11,7 @@ export default class PkiActionSerializer extends ApplicationSerializer {
attrs = {
customTtl: { serialize: false },
type: { serialize: false },
+ subjectSerialNumber: { serialize: false },
};
serialize(snapshot, requestType) {
@@ -18,6 +19,9 @@ export default class PkiActionSerializer extends ApplicationSerializer {
// requestType is a custom value specified from the pki/action adapter
const allowedPayloadAttributes = this._allowedParamsByType(requestType, snapshot.record.type);
if (!allowedPayloadAttributes) return data;
+ // the backend expects the subject's serial number param to be 'serial_number'
+ // we label it as subject_serial_number to differentiate from the vault generated UUID
+ data.serial_number = data.subject_serial_number;
const payload = {};
allowedPayloadAttributes.forEach((key) => {
@@ -63,6 +67,17 @@ export default class PkiActionSerializer extends ApplicationSerializer {
'private_key_format',
'ttl',
];
+ case 'rotate-root':
+ return [
+ ...commonProps,
+ 'issuer_name',
+ 'max_path_length',
+ 'not_after',
+ 'not_before_duration',
+ 'permitted_dns_domains',
+ 'private_key_format',
+ 'ttl',
+ ];
case 'generate-csr':
return [...commonProps, 'add_basic_constraints'];
case 'sign-intermediate':
diff --git a/ui/app/serializers/pki/issuer.js b/ui/app/serializers/pki/issuer.js
index c0ab2dd6b..ad62963bb 100644
--- a/ui/app/serializers/pki/issuer.js
+++ b/ui/app/serializers/pki/issuer.js
@@ -4,6 +4,7 @@
*/
import { parseCertificate } from 'vault/utils/parse-pki-cert';
+import { parsedParameters } from 'vault/utils/parse-pki-cert-oids';
import ApplicationSerializer from '../application';
export default class PkiIssuerSerializer extends ApplicationSerializer {
@@ -12,21 +13,7 @@ export default class PkiIssuerSerializer extends ApplicationSerializer {
constructor() {
super(...arguments);
// remove following attrs from serialization
- const attrs = [
- 'altNames',
- 'caChain',
- 'certificate',
- 'commonName',
- 'ipSans',
- 'issuerId',
- 'keyId',
- 'otherSans',
- 'notValidAfter',
- 'notValidBefore',
- 'serialNumber',
- 'signatureBits',
- 'uriSans',
- ];
+ const attrs = ['caChain', 'certificate', 'issuerId', 'keyId', ...parsedParameters];
this.attrs = attrs.reduce((attrObj, attr) => {
attrObj[attr] = { serialize: false };
return attrObj;
diff --git a/ui/app/utils/camelize-object-keys.js b/ui/app/utils/camelize-object-keys.js
new file mode 100644
index 000000000..bb988700d
--- /dev/null
+++ b/ui/app/utils/camelize-object-keys.js
@@ -0,0 +1,13 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+import { camelize } from '@ember/string';
+
+export default function camelizeKeys(object) {
+ const newObject = {};
+ Object.entries(object).forEach(([key, value]) => {
+ newObject[camelize(key)] = value;
+ });
+ return newObject;
+}
diff --git a/ui/app/utils/parse-pki-cert-oids.js b/ui/app/utils/parse-pki-cert-oids.js
index e60928a81..d0f7120c0 100644
--- a/ui/app/utils/parse-pki-cert-oids.js
+++ b/ui/app/utils/parse-pki-cert-oids.js
@@ -2,12 +2,13 @@
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
+import camelizeKeys from 'vault/utils/camelize-object-keys';
//* lookup OIDs: http://oid-info.com/basic-search.htm
export const SUBJECT_OIDs = {
common_name: '2.5.4.3',
- serial_number: '2.5.4.5',
+ subject_serial_number: '2.5.4.5',
ou: '2.5.4.11',
organization: '2.5.4.10',
country: '2.5.4.6',
@@ -75,3 +76,14 @@ export const SIGNATURE_ALGORITHM_OIDs = {
'1.2.840.10045.4.3.4': '512', // ECDSA-SHA512
'1.3.101.112': '0', // Ed25519
};
+
+// returns array of strings that correspond to model attributes
+// can be passed to display views in details pages containing certificates
+export const parsedParameters = [
+ ...Object.keys(camelizeKeys(SUBJECT_OIDs)),
+ ...Object.keys(camelizeKeys(EXTENSION_OIDs)),
+ ...Object.keys(camelizeKeys(SAN_TYPES)),
+ 'usePss',
+ 'notValidBefore',
+ 'notValidAfter',
+];
diff --git a/ui/app/utils/parse-pki-cert.js b/ui/app/utils/parse-pki-cert.js
index 84bbe143d..072f3911a 100644
--- a/ui/app/utils/parse-pki-cert.js
+++ b/ui/app/utils/parse-pki-cert.js
@@ -29,13 +29,17 @@ import {
This means we cannot cross-sign in the UI and prompt the user to do so manually using the CLI.
*/
+export function jsonToCertObject(jsonString) {
+ const cert_base64 = jsonString.replace(/(-----(BEGIN|END) CERTIFICATE-----|\n)/g, '');
+ const cert_der = fromBase64(cert_base64);
+ const cert_asn1 = asn1js.fromBER(stringToArrayBuffer(cert_der));
+ return new Certificate({ schema: cert_asn1.result });
+}
+
export function parseCertificate(certificateContent) {
let cert;
try {
- const cert_base64 = certificateContent.replace(/(-----(BEGIN|END) CERTIFICATE-----|\n)/g, '');
- const cert_der = fromBase64(cert_base64);
- const cert_asn1 = asn1js.fromBER(stringToArrayBuffer(cert_der));
- cert = new Certificate({ schema: cert_asn1.result });
+ cert = jsonToCertObject(certificateContent);
} catch (error) {
console.debug('DEBUG: Converting Certificate', error); // eslint-disable-line
return { can_parse: false };
@@ -101,6 +105,38 @@ export function formatValues(subject, extension) {
};
}
+/*
+How to use the verify function for cross-signing:
+(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:
+"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)
+
+each arg is the JSON string certificate value
+*/
+export async function verifyCertificates(certA, certB, leaf) {
+ const parsedCertA = jsonToCertObject(certA);
+ const parsedCertB = jsonToCertObject(certB);
+ if (leaf) {
+ const parsedLeaf = jsonToCertObject(leaf);
+ const chainA = await parsedLeaf.verify(parsedCertA);
+ const chainB = await parsedLeaf.verify(parsedCertB);
+ // 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, by passing it as both certA and B (i.e. a root cert)
+ return (await parsedCertA.verify(parsedCertB)) && parsedCertA.issuer.isEqual(parsedCertB.subject);
+}
+
//* PARSING HELPERS
/*
We wish to get each SUBJECT_OIDs (see utils/parse-pki-cert-oids.js) out of this certificate's subject.
diff --git a/ui/lib/core/addon/components/download-button.js b/ui/lib/core/addon/components/download-button.js
index ba462d85d..49f17f8a5 100644
--- a/ui/lib/core/addon/components/download-button.js
+++ b/ui/lib/core/addon/components/download-button.js
@@ -8,6 +8,8 @@ import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import errorMessage from 'vault/utils/error-message';
import timestamp from 'vault/utils/timestamp';
+import { tracked } from '@glimmer/tracking';
+import { assert } from '@ember/debug';
/**
* @module DownloadButton
* DownloadButton components are an action button used to download data. Both the action text and icon are yielded.
@@ -29,6 +31,7 @@ import timestamp from 'vault/utils/timestamp';
* ```
* @param {string} [filename] - name of file that prefixes the ISO timestamp generated at download
* @param {string} [data] - data to download
+ * @param {function} [fetchData] - function that fetches data and returns download content
* @param {string} [extension='txt'] - file extension, the download service uses this to determine the mimetype
* @param {boolean} [stringify=false] - argument to stringify the data before passing to the File constructor
*/
@@ -36,7 +39,16 @@ import timestamp from 'vault/utils/timestamp';
export default class DownloadButton extends Component {
@service download;
@service flashMessages;
+ @tracked fetchedData;
+ constructor() {
+ super(...arguments);
+ const hasConflictingArgs = this.args.data && this.args.fetchData;
+ assert(
+ 'Only pass either @data or @fetchData, passing both means @data will be overwritten by the return value of @fetchData',
+ !hasConflictingArgs
+ );
+ }
get filename() {
const ts = timestamp.now().toISOString();
return this.args.filename ? this.args.filename + '-' + ts : ts;
@@ -46,7 +58,7 @@ export default class DownloadButton extends Component {
if (this.args.stringify) {
return JSON.stringify(this.args.data, null, 2);
}
- return this.args.data;
+ return this.fetchedData || this.args.data;
}
get extension() {
@@ -54,7 +66,10 @@ export default class DownloadButton extends Component {
}
@action
- handleDownload() {
+ async handleDownload() {
+ if (this.args.fetchData) {
+ this.fetchedData = await this.args.fetchData();
+ }
try {
this.download.miscExtension(this.filename, this.content, this.extension);
this.flashMessages.info(`Downloading ${this.filename}`);
diff --git a/ui/lib/core/addon/helpers/message-types.js b/ui/lib/core/addon/helpers/message-types.js
index cf8a13612..b80ea2019 100644
--- a/ui/lib/core/addon/helpers/message-types.js
+++ b/ui/lib/core/addon/helpers/message-types.js
@@ -37,6 +37,11 @@ export const MESSAGE_TYPES = {
glyph: 'loading',
text: 'Loading',
},
+ rotation: {
+ class: 'is-info',
+ glyphClass: 'has-text-grey',
+ glyph: 'rotate-cw',
+ },
};
export function messageTypes([type]) {
diff --git a/ui/lib/pki/addon/components/page/pki-configure-create.hbs b/ui/lib/pki/addon/components/page/pki-configure-create.hbs
index 1e72ed56b..72b946e9b 100644
--- a/ui/lib/pki/addon/components/page/pki-configure-create.hbs
+++ b/ui/lib/pki/addon/components/page/pki-configure-create.hbs
@@ -9,7 +9,9 @@
-{{#unless @config.id}}
+{{#if @config.id}}
+
+{{else}}
{{#each this.configTypes as |option|}}
@@ -40,7 +42,7 @@
{{/each}}
-{{/unless}}
+{{/if}}
{{#if (eq @config.actionType "import")}}
{{else if (eq @config.actionType "generate-root")}}
+ {{#if @config.privateKey}}
+
+ {{/if}}
- {{!-- {{#if @canRotate}}
-
- Rotate this root
-
- {{/if}} --}}
- {{#if @canCrossSign}}
-
- Cross-sign Issuer
+ Rotate this root
+
+
+ {{/if}}
+ {{#if @canCrossSign}}
+
+ Cross-sign issuers
{{/if}}
{{#if @canSignIntermediate}}
@@ -67,7 +68,7 @@
{{#if @canConfigure}}
-
+
Configure
{{/if}}
@@ -83,7 +84,6 @@
You may also want to configure its usage and other behaviors.
{{/if}}
-
{{#each @issuer.formFieldGroups as |fieldGroup|}}
{{#each-in fieldGroup as |group fields|}}
@@ -124,5 +124,48 @@
{{/each-in}}
{{/each}}
+
-
\ No newline at end of file
+{{! ROOT ROTATION MODAL }}
+
+
+ Root rotation
+
+ Root rotation is an impactful process. Please be ready to ensure that the new root is properly distributed to
+ end-users’ trust stores. You can also do this manually by
+
+ following our documentation.
+
+
+ How root rotation will work
+
+
+ - The new root will be generated using defaults from the old one that you can customize.
+ - You will identify intermediates, which Vault will then cross-sign.
+
+ Then, you can begin re-issuing leaf certs and phase out the old root.
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/lib/pki/addon/components/page/pki-issuer-details.js b/ui/lib/pki/addon/components/page/pki-issuer-details.js
new file mode 100644
index 000000000..09815dee0
--- /dev/null
+++ b/ui/lib/pki/addon/components/page/pki-issuer-details.js
@@ -0,0 +1,11 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+
+export default class PkiIssuerDetailsComponent extends Component {
+ @tracked showRotationModal = false;
+}
diff --git a/ui/lib/pki/addon/components/page/pki-issuer-generate-intermediate.hbs b/ui/lib/pki/addon/components/page/pki-issuer-generate-intermediate.hbs
index ace909dcf..bdb968253 100644
--- a/ui/lib/pki/addon/components/page/pki-issuer-generate-intermediate.hbs
+++ b/ui/lib/pki/addon/components/page/pki-issuer-generate-intermediate.hbs
@@ -9,6 +9,9 @@
+{{#if @model.id}}
+
+{{/if}}
+{{#if @model.id}}
+
+{{/if}}
+{{#if @model.privateKey}}
+
+{{/if}}
+{{#if @model.id}}
+
+{{/if}}
+
+
+
+
+
+ {{if @newRootModel.id "View issuer certificate" "Generate new root"}}
+
+
+
+
+{{#if @newRootModel.id}}
+
+
+
+ Cross-sign issuers
+
+
+ Sign Intermediate
+
+
+
+ Download
+
+
+
+
+
+
+
+ Configure
+
+
+
+{{/if}}
+
+{{#if @newRootModel.id}}
+
+
+ Your new root has been generated.
+ {{#if @newRootModel.privateKey}}
+ Make sure to copy and save the
+ private_key
+ as it is only available once.
+ {{/if}}
+ If you’re ready, you can begin cross-signing issuers now. If not, the option to cross-sign is available when you use
+ this certificate.
+
+
+ Cross-sign issuers
+
+
+
+{{else}}
+
+ {{#each this.generateOptions as |option|}}
+
+ {{/each}}
+
+{{/if}}
+
+{{#if (eq this.displayedForm "use-old-settings")}}
+ {{#if @newRootModel.id}}
+
+
+ {{else}}
+ {{! USE OLD SETTINGS FORM INPUTS }}
+
+
+ {{/if}}
+{{else}}
+
+{{/if}}
\ No newline at end of file
diff --git a/ui/lib/pki/addon/components/page/pki-issuer-rotate-root.ts b/ui/lib/pki/addon/components/page/pki-issuer-rotate-root.ts
new file mode 100644
index 000000000..9080da63b
--- /dev/null
+++ b/ui/lib/pki/addon/components/page/pki-issuer-rotate-root.ts
@@ -0,0 +1,131 @@
+import Component from '@glimmer/component';
+import { inject as service } from '@ember/service';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+import { waitFor } from '@ember/test-waiters';
+import { task } from 'ember-concurrency';
+import errorMessage from 'vault/utils/error-message';
+// TYPES
+import Store from '@ember-data/store';
+import Router from '@ember/routing/router';
+import FlashMessageService from 'vault/services/flash-messages';
+import SecretMountPath from 'vault/services/secret-mount-path';
+import PkiIssuerModel from 'vault/models/pki/issuer';
+import PkiActionModel from 'vault/vault/models/pki/action';
+import { Breadcrumb } from 'vault/vault/app-types';
+import { parsedParameters } from 'vault/utils/parse-pki-cert-oids';
+
+interface Args {
+ oldRoot: PkiIssuerModel;
+ newRootModel: PkiActionModel;
+ breadcrumbs: Breadcrumb;
+ parsingErrors: string;
+}
+
+const RADIO_BUTTON_KEY = {
+ oldSettings: 'use-old-settings',
+ customizeNew: 'customize',
+};
+
+export default class PagePkiIssuerRotateRootComponent extends Component {
+ @service declare readonly store: Store;
+ @service declare readonly router: Router;
+ @service declare readonly flashMessages: FlashMessageService;
+ @service declare readonly secretMountPath: SecretMountPath;
+
+ @tracked displayedForm = RADIO_BUTTON_KEY.oldSettings;
+ @tracked showOldSettings = false;
+ // form alerts below are only for "use old settings" option
+ // validations/errors for "customize new root" are handled by component
+ @tracked alertBanner = '';
+ @tracked invalidFormAlert = '';
+ @tracked modelValidations = null;
+
+ get bannerType() {
+ if (this.args.parsingErrors && !this.invalidFormAlert) {
+ return {
+ title: 'Not all of the certificate values could be parsed and transfered to new root',
+ type: 'warning',
+ };
+ }
+ return { type: 'danger' };
+ }
+
+ get generateOptions() {
+ return [
+ {
+ key: RADIO_BUTTON_KEY.oldSettings,
+ icon: 'certificate',
+ label: 'Use old root settings',
+ description: `Provide only a new common name and issuer name, using the old root’s settings. Selecting this option generates a root with Vault-internal key material.`,
+ },
+ {
+ key: RADIO_BUTTON_KEY.customizeNew,
+ icon: 'award',
+ label: 'Customize new root certificate',
+ description:
+ 'Generates a new self-signed CA certificate and private key. This generated root will sign its own CRL.',
+ },
+ ];
+ }
+
+ // for displaying old root details, and generated root details
+ get displayFields() {
+ const addKeyFields = ['privateKey', 'privateKeyType'];
+ const defaultFields = [
+ 'certificate',
+ 'caChain',
+ 'issuerId',
+ 'issuerName',
+ 'issuingCa',
+ 'keyName',
+ 'keyId',
+ 'serialNumber',
+ ...parsedParameters,
+ ];
+ return this.args.newRootModel.id ? [...defaultFields, ...addKeyFields] : defaultFields;
+ }
+
+ checkFormValidity() {
+ if (this.args.newRootModel.validate) {
+ const { isValid, state, invalidFormMessage } = this.args.newRootModel.validate();
+ this.modelValidations = state;
+ this.invalidFormAlert = invalidFormMessage;
+ return isValid;
+ }
+ return true;
+ }
+
+ @task
+ @waitFor
+ *save(event: Event) {
+ event.preventDefault();
+ const continueSave = this.checkFormValidity();
+ if (!continueSave) return;
+ try {
+ yield this.args.newRootModel.save({ adapterOptions: { actionType: 'rotate-root' } });
+ this.flashMessages.success('Successfully generated root.');
+ } catch (e) {
+ this.alertBanner = errorMessage(e);
+ this.invalidFormAlert = 'There was a problem generating root.';
+ }
+ }
+
+ @action
+ async fetchDataForDownload(format: string) {
+ const endpoint = `/v1/${this.secretMountPath.currentPath}/issuer/${this.args.newRootModel.issuerId}/${format}`;
+ const adapter = this.store.adapterFor('application');
+ try {
+ return adapter
+ .rawRequest(endpoint, 'GET', { unauthenticated: true })
+ .then(function (response: Response) {
+ if (format === 'der') {
+ return response.blob();
+ }
+ return response.text();
+ });
+ } catch (e) {
+ return null;
+ }
+ }
+}
diff --git a/ui/lib/pki/addon/components/pki-generate-csr.hbs b/ui/lib/pki/addon/components/pki-generate-csr.hbs
index fbb5bfe63..c52781e2d 100644
--- a/ui/lib/pki/addon/components/pki-generate-csr.hbs
+++ b/ui/lib/pki/addon/components/pki-generate-csr.hbs
@@ -1,6 +1,5 @@
{{#if @model.id}}
{{! Model only has ID once form has been submitted and saved }}
-
diff --git a/ui/lib/pki/addon/components/pki-generate-csr.ts b/ui/lib/pki/addon/components/pki-generate-csr.ts
index 5efc9300b..e288f45a5 100644
--- a/ui/lib/pki/addon/components/pki-generate-csr.ts
+++ b/ui/lib/pki/addon/components/pki-generate-csr.ts
@@ -59,7 +59,7 @@ export default class PkiGenerateCsrComponent extends Component {
'commonName',
'excludeCnFromSans',
'format',
- 'serialNumber',
+ 'subjectSerialNumber',
'addBasicConstraints',
]);
}
diff --git a/ui/lib/pki/addon/components/pki-generate-root.hbs b/ui/lib/pki/addon/components/pki-generate-root.hbs
index be875f0d9..b7a6b7f4b 100644
--- a/ui/lib/pki/addon/components/pki-generate-root.hbs
+++ b/ui/lib/pki/addon/components/pki-generate-root.hbs
@@ -1,21 +1,13 @@
{{! Show results if model has an ID, which is only generated after save }}
{{#if @model.id}}
-
- {{#if @model.privateKey}}
-
- {{/if}}
{{#each this.returnedFields as |field|}}
{{#let (find-by "name" field @model.allFields) as |attr|}}
{{#if attr.options.detailLinkTo}}
{{get @model attr.name}}
diff --git a/ui/lib/pki/addon/components/pki-generate-root.ts b/ui/lib/pki/addon/components/pki-generate-root.ts
index 9b9695b29..df43adf31 100644
--- a/ui/lib/pki/addon/components/pki-generate-root.ts
+++ b/ui/lib/pki/addon/components/pki-generate-root.ts
@@ -14,6 +14,7 @@ import PkiActionModel from 'vault/models/pki/action';
import PkiUrlsModel from 'vault/models/pki/urls';
import FlashMessageService from 'ember-cli-flash/services/flash-messages';
import errorMessage from 'vault/utils/error-message';
+import { parsedParameters } from 'vault/utils/parse-pki-cert-oids';
interface AdapterOptions {
actionType: string;
@@ -26,6 +27,7 @@ interface Args {
onComplete: CallableFunction;
onSave?: CallableFunction;
adapterOptions: AdapterOptions;
+ hideAlertBanner: boolean;
}
/**
@@ -71,13 +73,13 @@ export default class PkiGenerateRootComponent extends Component {
get returnedFields() {
return [
'certificate',
- 'expiration',
'issuerId',
'issuerName',
'issuingCa',
- 'keyId',
'keyName',
+ 'keyId',
'serialNumber',
+ ...parsedParameters,
];
}
diff --git a/ui/lib/pki/addon/components/pki-generate-toggle-groups.ts b/ui/lib/pki/addon/components/pki-generate-toggle-groups.ts
index 09fe46286..dce984aa6 100644
--- a/ui/lib/pki/addon/components/pki-generate-toggle-groups.ts
+++ b/ui/lib/pki/addon/components/pki-generate-toggle-groups.ts
@@ -43,7 +43,7 @@ export default class PkiGenerateToggleGroupsComponent extends Component {
};
// excludeCnFromSans and serialNumber are present in default fields for generate-csr -- only include for other types
if (this.args.model.actionType !== 'generate-csr') {
- groups['Subject Alternative Name (SAN) Options'].unshift('excludeCnFromSans', 'serialNumber');
+ groups['Subject Alternative Name (SAN) Options'].unshift('excludeCnFromSans', 'subjectSerialNumber');
}
return groups;
}
diff --git a/ui/lib/pki/addon/components/pki-import-pem-bundle.hbs b/ui/lib/pki/addon/components/pki-import-pem-bundle.hbs
index 0b799d97b..6479eec58 100644
--- a/ui/lib/pki/addon/components/pki-import-pem-bundle.hbs
+++ b/ui/lib/pki/addon/components/pki-import-pem-bundle.hbs
@@ -1,5 +1,4 @@
{{#if this.importedResponse}}
-
diff --git a/ui/lib/pki/addon/components/pki-info-table-rows.hbs b/ui/lib/pki/addon/components/pki-info-table-rows.hbs
new file mode 100644
index 000000000..5799ff3c7
--- /dev/null
+++ b/ui/lib/pki/addon/components/pki-info-table-rows.hbs
@@ -0,0 +1,26 @@
+{{#each @displayFields as |field|}}
+ {{#let (find-by "name" field @model.allFields) as |attr|}}
+ {{#let (get @model attr.name) as |value|}}
+ {{! only render if there's a value, unless it's the commonName or privateKey/Type }}
+ {{#if (or value (or (eq attr.name "commonName") (eq attr.name "privateKey") (eq attr.name "privateKeyType")))}}
+
+ {{#if (and attr.options.masked value)}}
+
+ {{else if attr.options.detailLinkTo}}
+ {{value}}
+ {{else if (or (eq attr.name "privateKey") (eq attr.name "privateKeyType"))}}
+ {{or value "internal"}}
+ {{else if attr.options.formatDate}}
+ {{date-format value "MMM d yyyy HH:mm:ss a zzzz"}}
+ {{else}}
+ {{value}}
+ {{/if}}
+
+ {{/if}}
+ {{/let}}
+ {{/let}}
+{{/each}}
\ No newline at end of file
diff --git a/ui/lib/pki/addon/components/pki-sign-intermediate-form.ts b/ui/lib/pki/addon/components/pki-sign-intermediate-form.ts
index c54ccbbfa..1feb298df 100644
--- a/ui/lib/pki/addon/components/pki-sign-intermediate-form.ts
+++ b/ui/lib/pki/addon/components/pki-sign-intermediate-form.ts
@@ -57,7 +57,7 @@ export default class PkiSignIntermediateFormComponent extends Component {
'province',
'streetAddress',
'postalCode',
- 'serialNumber', // this is different from the UUID serial number generated by vault (in show fields below)
+ 'subjectSerialNumber', // this is different from the UUID serial number generated by vault (in show fields below)
],
};
}
diff --git a/ui/lib/pki/addon/routes.js b/ui/lib/pki/addon/routes.js
index f3498f0da..1768327c0 100644
--- a/ui/lib/pki/addon/routes.js
+++ b/ui/lib/pki/addon/routes.js
@@ -33,6 +33,7 @@ export default buildRoutes(function () {
this.route('edit');
this.route('sign');
this.route('cross-sign');
+ this.route('rotate-root');
});
});
this.route('certificates', function () {
diff --git a/ui/lib/pki/addon/routes/issuers/issuer/index.js b/ui/lib/pki/addon/routes/issuers/issuer.js
similarity index 94%
rename from ui/lib/pki/addon/routes/issuers/issuer/index.js
rename to ui/lib/pki/addon/routes/issuers/issuer.js
index 50474ac65..3d27356bb 100644
--- a/ui/lib/pki/addon/routes/issuers/issuer/index.js
+++ b/ui/lib/pki/addon/routes/issuers/issuer.js
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: MPL-2.0
*/
-import PkiIssuersListRoute from '../index';
+import PkiIssuersListRoute from '.';
// Single issuer index route extends issuers list route
export default class PkiIssuerIndexRoute extends PkiIssuersListRoute {
diff --git a/ui/lib/pki/addon/routes/issuers/issuer/cross-sign.js b/ui/lib/pki/addon/routes/issuers/issuer/cross-sign.js
index c49d104f0..09a9536e1 100644
--- a/ui/lib/pki/addon/routes/issuers/issuer/cross-sign.js
+++ b/ui/lib/pki/addon/routes/issuers/issuer/cross-sign.js
@@ -3,11 +3,11 @@
* SPDX-License-Identifier: MPL-2.0
*/
-import PkiIssuerIndexRoute from './index';
+import PkiIssuerRoute from '../issuer';
import { withConfirmLeave } from 'core/decorators/confirm-leave';
@withConfirmLeave()
-export default class PkiIssuerCrossSignRoute extends PkiIssuerIndexRoute {
+export default class PkiIssuerCrossSignRoute extends PkiIssuerRoute {
setupController(controller, resolvedModel) {
super.setupController(controller, resolvedModel);
controller.breadcrumbs.push(
diff --git a/ui/lib/pki/addon/routes/issuers/issuer/details.js b/ui/lib/pki/addon/routes/issuers/issuer/details.js
index 542801bfa..a6d25ac9b 100644
--- a/ui/lib/pki/addon/routes/issuers/issuer/details.js
+++ b/ui/lib/pki/addon/routes/issuers/issuer/details.js
@@ -3,17 +3,23 @@
* SPDX-License-Identifier: MPL-2.0
*/
-import PkiIssuerIndexRoute from './index';
+import PkiIssuerRoute from '../issuer';
+import { verifyCertificates } from 'vault/utils/parse-pki-cert';
+import { hash } from 'rsvp';
+export default class PkiIssuerDetailsRoute extends PkiIssuerRoute {
+ model() {
+ const issuer = this.modelFor('issuers.issuer');
+ return hash({
+ issuer,
+ pem: this.fetchCertByFormat(issuer.id, 'pem'),
+ der: this.fetchCertByFormat(issuer.id, 'der'),
+ isRotatable: this.isRoot(issuer),
+ });
+ }
-export default class PkiIssuerDetailsRoute extends PkiIssuerIndexRoute {
- // Details route gets issuer data from PkiIssuerIndexRoute
- async setupController(controller, resolvedModel) {
+ setupController(controller, resolvedModel) {
super.setupController(controller, resolvedModel);
- controller.breadcrumbs.push({ label: resolvedModel.id });
- const pem = await this.fetchCertByFormat(resolvedModel.id, 'pem');
- const der = await this.fetchCertByFormat(resolvedModel.id, 'der');
- controller.pem = pem;
- controller.der = der;
+ controller.breadcrumbs.push({ label: resolvedModel.issuer.id });
}
/**
@@ -33,4 +39,9 @@ export default class PkiIssuerDetailsRoute extends PkiIssuerIndexRoute {
return null;
}
}
+
+ async isRoot({ certificate, keyId }) {
+ const isSelfSigned = await verifyCertificates(certificate, certificate);
+ return isSelfSigned && !!keyId;
+ }
}
diff --git a/ui/lib/pki/addon/routes/issuers/issuer/edit.js b/ui/lib/pki/addon/routes/issuers/issuer/edit.js
index 83a269162..788524409 100644
--- a/ui/lib/pki/addon/routes/issuers/issuer/edit.js
+++ b/ui/lib/pki/addon/routes/issuers/issuer/edit.js
@@ -8,7 +8,7 @@ import { inject as service } from '@ember/service';
import { withConfirmLeave } from 'core/decorators/confirm-leave';
@withConfirmLeave()
-export default class PkiIssuerDetailRoute extends Route {
+export default class PkiIssuerEditRoute extends Route {
@service store;
@service secretMountPath;
@service pathHelp;
diff --git a/ui/lib/pki/addon/routes/issuers/issuer/rotate-root.js b/ui/lib/pki/addon/routes/issuers/issuer/rotate-root.js
new file mode 100644
index 000000000..cd07617f7
--- /dev/null
+++ b/ui/lib/pki/addon/routes/issuers/issuer/rotate-root.js
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+import PkiIssuerRoute from '../issuer';
+import { inject as service } from '@ember/service';
+import { hash } from 'rsvp';
+import { parseCertificate } from 'vault/utils/parse-pki-cert';
+import camelizeKeys from 'vault/utils/camelize-object-keys';
+import { withConfirmLeave } from 'core/decorators/confirm-leave';
+
+@withConfirmLeave('model.newRootModel')
+export default class PkiIssuerRotateRootRoute extends PkiIssuerRoute {
+ @service secretMountPath;
+ @service store;
+
+ model() {
+ const oldRoot = this.modelFor('issuers.issuer');
+ const certData = parseCertificate(oldRoot.certificate);
+ let parsingErrors;
+ if (certData.parsing_errors && certData.parsing_errors.length > 0) {
+ const errorMessage = certData.parsing_errors.map((e) => e.message).join(', ');
+ parsingErrors = errorMessage;
+ }
+ const newRootModel = this.store.createRecord('pki/action', {
+ actionType: 'rotate-root',
+ type: 'internal',
+ ...camelizeKeys(certData), // copy old root settings over to new one
+ });
+ return hash({
+ oldRoot,
+ newRootModel,
+ parsingErrors,
+ });
+ }
+
+ setupController(controller, resolvedModel) {
+ super.setupController(controller, resolvedModel);
+ controller.breadcrumbs = [
+ { label: 'secrets', route: 'secrets', linkExternal: true },
+ { label: this.secretMountPath.currentPath, route: 'overview' },
+ { label: 'issuers', route: 'issuers.index' },
+ { label: resolvedModel.oldRoot.id, route: 'issuers.issuer.details' },
+ { label: 'rotate root' },
+ ];
+ }
+}
diff --git a/ui/lib/pki/addon/templates/issuers/index.hbs b/ui/lib/pki/addon/templates/issuers/index.hbs
index 5a689ac7b..3c9745c50 100644
--- a/ui/lib/pki/addon/templates/issuers/index.hbs
+++ b/ui/lib/pki/addon/templates/issuers/index.hbs
@@ -74,7 +74,7 @@