diff --git a/changelog/13238.txt b/changelog/13238.txt new file mode 100644 index 000000000..7b11758d7 --- /dev/null +++ b/changelog/13238.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: Fixes node-forge error when parsing EC (elliptical curve) certs +``` \ No newline at end of file diff --git a/ui/app/helpers/parse-pki-cert.js b/ui/app/helpers/parse-pki-cert.js index 6da3c745d..56a897fe6 100644 --- a/ui/app/helpers/parse-pki-cert.js +++ b/ui/app/helpers/parse-pki-cert.js @@ -6,14 +6,24 @@ export function parsePkiCert([model]) { if (!model.certificate) { return; } - const cert = pki.certificateFromPem(model.certificate); - const commonName = cert.subject.getField('CN') ? cert.subject.getField('CN').value : null; - const issueDate = cert.validity.notBefore; - const expiryDate = cert.validity.notAfter; + let cert; + // node-forge cannot parse EC (elliptical curve) certs + // set canParse to false if unable to convert a Forge cert from PEM + try { + cert = pki.certificateFromPem(model.certificate); + } catch (error) { + return { + can_parse: false, + }; + } + const commonName = cert?.subject.getField('CN') ? cert.subject.getField('CN').value : null; + const expiryDate = cert?.validity.notAfter; + const issueDate = cert?.validity.notBefore; return { + can_parse: true, common_name: commonName, - issue_date: issueDate, expiry_date: expiryDate, + issue_date: issueDate, }; } diff --git a/ui/app/models/pki-ca-certificate.js b/ui/app/models/pki-ca-certificate.js index 821716563..c81a10f89 100644 --- a/ui/app/models/pki-ca-certificate.js +++ b/ui/app/models/pki-ca-certificate.js @@ -4,7 +4,6 @@ import { computed } from '@ember/object'; import Certificate from './pki-certificate'; import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; -// TODO: alphabetize attrs export default Certificate.extend({ DISPLAY_FIELDS: computed(function() { return [ @@ -28,6 +27,7 @@ export default Certificate.extend({ backend: attr('string', { readOnly: true, }), + canParse: attr('boolean'), caType: attr('string', { possibleValues: ['root', 'intermediate'], defaultValue: 'root', @@ -35,18 +35,71 @@ export default Certificate.extend({ readOnly: true, }), commonName: attr('string'), + csr: attr('string', { + editType: 'textarea', + label: 'CSR', + masked: true, + }), expiryDate: attr('string', { label: 'Expiration date', }), issueDate: attr('string'), + keyBits: attr('number', { + defaultValue: 2048, + }), + keyType: attr('string', { + possibleValues: ['rsa', 'ec', 'ed25519'], + defaultValue: 'rsa', + }), + maxPathLength: attr('number', { + defaultValue: -1, + }), + organization: attr({ + editType: 'stringArray', + }), + ou: attr({ + label: 'OU (OrganizationalUnit)', + editType: 'stringArray', + }), pemBundle: attr('string', { label: 'PEM bundle', editType: 'file', }), + permittedDnsNames: attr('string', { + label: 'Permitted DNS domains', + }), + privateKeyFormat: attr('string', { + possibleValues: ['', 'der', 'pem', 'pkcs8'], + defaultValue: '', + }), + type: attr('string', { + possibleValues: ['internal', 'exported'], + defaultValue: 'internal', + }), uploadPemBundle: attr('boolean', { label: 'Upload PEM bundle', readOnly: true, }), + + // address attrs + country: attr({ + editType: 'stringArray', + }), + locality: attr({ + editType: 'stringArray', + label: 'Locality/City', + }), + streetAddress: attr({ + editType: 'stringArray', + }), + postalCode: attr({ + editType: 'stringArray', + }), + province: attr({ + editType: 'stringArray', + label: 'Province/State', + }), + fieldDefinition: computed('caType', 'uploadPemBundle', function() { const type = this.caType; const isUpload = this.uploadPemBundle; @@ -98,58 +151,6 @@ export default Certificate.extend({ return groups; }), - type: attr('string', { - possibleValues: ['internal', 'exported'], - defaultValue: 'internal', - }), - ou: attr({ - label: 'OU (OrganizationalUnit)', - editType: 'stringArray', - }), - organization: attr({ - editType: 'stringArray', - }), - country: attr({ - editType: 'stringArray', - }), - locality: attr({ - editType: 'stringArray', - label: 'Locality/City', - }), - province: attr({ - editType: 'stringArray', - label: 'Province/State', - }), - streetAddress: attr({ - editType: 'stringArray', - }), - postalCode: attr({ - editType: 'stringArray', - }), - - keyType: attr('string', { - possibleValues: ['rsa', 'ec','ed25519'], - defaultValue: 'rsa', - }), - keyBits: attr('number', { - defaultValue: 2048, - }), - privateKeyFormat: attr('string', { - possibleValues: ['', 'der', 'pem', 'pkcs8'], - defaultValue: '', - }), - maxPathLength: attr('number', { - defaultValue: -1, - }), - permittedDnsNames: attr('string', { - label: 'Permitted DNS domains', - }), - - csr: attr('string', { - editType: 'textarea', - label: 'CSR', - masked: true, - }), deletePath: lazyCapabilities(apiPath`${'backend'}/root`, 'backend'), canDeleteRoot: and('deletePath.canDelete', 'deletePath.canSudo'), diff --git a/ui/app/models/pki-certificate.js b/ui/app/models/pki-certificate.js index ae57039ec..6b657f17a 100644 --- a/ui/app/models/pki-certificate.js +++ b/ui/app/models/pki-certificate.js @@ -6,10 +6,6 @@ import fieldToAttrs, { expandAttributeMeta } from 'vault/utils/field-to-attrs'; export default Model.extend({ idPrefix: 'cert/', - - backend: attr('string', { - readOnly: true, - }), //the id prefixed with `cert/` so we can use it as the *secret param for the secret show route idForNav: attr('string', { readOnly: true, @@ -29,55 +25,59 @@ export default Model.extend({ ]; }), - commonName: attr('string'), - expiryDate: attr('string', { - label: 'Expiration date', - }), - issueDate: attr('string'), - role: attr('object', { - readOnly: true, - }), - revocationTime: attr('number'), altNames: attr('string', { label: 'DNS/Email Subject Alternative Names (SANs)', }), + backend: attr('string', { + readOnly: true, + }), + caChain: attr('string', { + label: 'CA chain', + masked: true, + }), + canParse: attr('boolean'), + certificate: attr('string', { + masked: true, + }), + commonName: attr('string'), + excludeCnFromSans: attr('boolean', { + label: 'Exclude Common Name from Subject Alternative Names (SANs)', + defaultValue: false, + }), + expiryDate: attr('string', { + label: 'Expiration date', + }), + format: attr('string', { + defaultValue: 'pem', + possibleValues: ['pem', 'der', 'pem_bundle'], + }), ipSans: attr('string', { label: 'IP Subject Alternative Names (SANs)', }), + issueDate: attr('string'), + issuingCa: attr('string', { + label: 'Issuing CA', + masked: true, + }), otherSans: attr({ editType: 'stringArray', label: 'Other SANs', helpText: 'The format is the same as OpenSSL: ;: where the only current valid type is UTF8', }), - ttl: attr({ - label: 'TTL', - editType: 'ttl', - }), - format: attr('string', { - defaultValue: 'pem', - possibleValues: ['pem', 'der', 'pem_bundle'], - }), - excludeCnFromSans: attr('boolean', { - label: 'Exclude Common Name from Subject Alternative Names (SANs)', - defaultValue: false, - }), - certificate: attr('string', { - masked: true, - }), - issuingCa: attr('string', { - label: 'Issuing CA', - masked: true, - }), - caChain: attr('string', { - label: 'CA chain', - masked: true, - }), privateKey: attr('string', { masked: true, }), privateKeyType: attr('string'), + revocationTime: attr('number'), + role: attr('object', { + readOnly: true, + }), serialNumber: attr('string'), + ttl: attr({ + label: 'TTL', + editType: 'ttl', + }), fieldsToAttrs(fieldGroups) { return fieldToAttrs(this, fieldGroups); diff --git a/ui/app/templates/components/config-pki-ca.hbs b/ui/app/templates/components/config-pki-ca.hbs index 3da44ecde..9fe0b04b8 100644 --- a/ui/app/templates/components/config-pki-ca.hbs +++ b/ui/app/templates/components/config-pki-ca.hbs @@ -8,6 +8,13 @@ {{/if}} {{#if (or model.certificate model.csr)}} + {{#if (not (eq model.canParse true))}} + + {{/if}} {{#each model.attrs as |attr|}} {{#if attr.options.masked}} - {{else if (and (get model attr.name) (or (eq attr.name "issueDate") (eq attr.name "expiryDate")))}} + {{else if (and (get model attr.name) (or (eq attr.name "issueDate") (eq attr.name "expiryDate")))}} diff --git a/ui/app/templates/components/pki-cert-show.hbs b/ui/app/templates/components/pki-cert-show.hbs index cfade36dc..00080cef4 100644 --- a/ui/app/templates/components/pki-cert-show.hbs +++ b/ui/app/templates/components/pki-cert-show.hbs @@ -8,7 +8,13 @@ - +{{#if (not (eq model.canParse true))}} + +{{/if}}
{{#each model.attrs as |attr|}} diff --git a/ui/tests/acceptance/settings/configure-secret-backends/pki/section-cert-test.js b/ui/tests/acceptance/settings/configure-secret-backends/pki/section-cert-test.js index b680e9964..70ca4f5bf 100644 --- a/ui/tests/acceptance/settings/configure-secret-backends/pki/section-cert-test.js +++ b/ui/tests/acceptance/settings/configure-secret-backends/pki/section-cert-test.js @@ -94,6 +94,16 @@ BXUV2Uwtxf+QCphnlht9muX2fsLIzDJea0JipWj1uf2H8OZsjE8= ); }); + test('EC cert config: generate', async function(assert) { + await mountAndNav(assert); + await settled(); + assert.equal(currentRouteName(), 'vault.cluster.settings.configure-secret-backend.section'); + + await page.form.generateCAKeyTypeEC(); + + assert.dom('[data-test-warning]').exists('Info banner renders when unable to parse certificate metadata'); + }); + test('cert config: upload', async function(assert) { await mountAndNav(assert); await settled(); diff --git a/ui/tests/pages/components/config-pki-ca.js b/ui/tests/pages/components/config-pki-ca.js index 8f7bc4ae3..7e77dfb9e 100644 --- a/ui/tests/pages/components/config-pki-ca.js +++ b/ui/tests/pages/components/config-pki-ca.js @@ -32,6 +32,9 @@ export default { enterCertAsText: clickable('[data-test-text-toggle]'), pemBundle: fillable('[data-test-text-file-textarea="true"]'), commonName: fillable('[data-test-input="commonName"]'), + toggleOptions: clickable('[data-test-toggle-group="Options"]'), + keyType: fillable('[data-test-input="keyType"]'), + keyBits: fillable('[data-test-input="keyBits"]'), issueDateIsPresent: text('[data-test-row-value="Issue date"]'), expiryDateIsPresent: text('[data-test-row-value="Expiration date"]'), @@ -48,6 +51,15 @@ export default { .submit(); }, + async generateCAKeyTypeEC(commonName = 'PKI CA EC') { + return await this.replaceCA() + .commonName(commonName) + .toggleOptions() + .keyType('ec') + .keyBits(256) + .submit(); + }, + async uploadCA(pem) { return await this.replaceCA() .uploadCert()