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 }} + + +
+ + +
+
\ 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 this.alertBanner}} + + {{/if}} + {{#let (find-by "name" "commonName" @newRootModel.allFields) as |attr|}} + + {{/let}} + {{#let (find-by "name" "issuerName" @newRootModel.allFields) as |attr|}} + + {{/let}} +
+ + {{#if this.showOldSettings}} + + {{/if}} +
+ +
+
+ + +
+ {{#if this.invalidFormAlert}} +
+ +
+ {{/if}} +
+ + {{/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 @@