UI/Fix node-forge EC error (#13238)

* add catch for node-forge error handling

* update comment

* adds changelog

* alphabetize attrs and add canParse attr

* show alert banner if unable to parse metadata

* add test to check info banner renders
This commit is contained in:
claire bontempo 2021-11-23 13:51:02 -05:00 committed by GitHub
parent d7c54b50e7
commit e8c9affee1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 145 additions and 96 deletions

3
changelog/13238.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
ui: Fixes node-forge error when parsing EC (elliptical curve) certs
```

View File

@ -6,14 +6,24 @@ export function parsePkiCert([model]) {
if (!model.certificate) { if (!model.certificate) {
return; return;
} }
const cert = pki.certificateFromPem(model.certificate); let cert;
const commonName = cert.subject.getField('CN') ? cert.subject.getField('CN').value : null; // node-forge cannot parse EC (elliptical curve) certs
const issueDate = cert.validity.notBefore; // set canParse to false if unable to convert a Forge cert from PEM
const expiryDate = cert.validity.notAfter; try {
cert = pki.certificateFromPem(model.certificate);
} catch (error) {
return { 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, common_name: commonName,
issue_date: issueDate,
expiry_date: expiryDate, expiry_date: expiryDate,
issue_date: issueDate,
}; };
} }

View File

@ -4,7 +4,6 @@ import { computed } from '@ember/object';
import Certificate from './pki-certificate'; import Certificate from './pki-certificate';
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
// TODO: alphabetize attrs
export default Certificate.extend({ export default Certificate.extend({
DISPLAY_FIELDS: computed(function() { DISPLAY_FIELDS: computed(function() {
return [ return [
@ -28,6 +27,7 @@ export default Certificate.extend({
backend: attr('string', { backend: attr('string', {
readOnly: true, readOnly: true,
}), }),
canParse: attr('boolean'),
caType: attr('string', { caType: attr('string', {
possibleValues: ['root', 'intermediate'], possibleValues: ['root', 'intermediate'],
defaultValue: 'root', defaultValue: 'root',
@ -35,18 +35,71 @@ export default Certificate.extend({
readOnly: true, readOnly: true,
}), }),
commonName: attr('string'), commonName: attr('string'),
csr: attr('string', {
editType: 'textarea',
label: 'CSR',
masked: true,
}),
expiryDate: attr('string', { expiryDate: attr('string', {
label: 'Expiration date', label: 'Expiration date',
}), }),
issueDate: attr('string'), 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', { pemBundle: attr('string', {
label: 'PEM bundle', label: 'PEM bundle',
editType: 'file', 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', { uploadPemBundle: attr('boolean', {
label: 'Upload PEM bundle', label: 'Upload PEM bundle',
readOnly: true, 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() { fieldDefinition: computed('caType', 'uploadPemBundle', function() {
const type = this.caType; const type = this.caType;
const isUpload = this.uploadPemBundle; const isUpload = this.uploadPemBundle;
@ -98,58 +151,6 @@ export default Certificate.extend({
return groups; 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'), deletePath: lazyCapabilities(apiPath`${'backend'}/root`, 'backend'),
canDeleteRoot: and('deletePath.canDelete', 'deletePath.canSudo'), canDeleteRoot: and('deletePath.canDelete', 'deletePath.canSudo'),

View File

@ -6,10 +6,6 @@ import fieldToAttrs, { expandAttributeMeta } from 'vault/utils/field-to-attrs';
export default Model.extend({ export default Model.extend({
idPrefix: 'cert/', 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 //the id prefixed with `cert/` so we can use it as the *secret param for the secret show route
idForNav: attr('string', { idForNav: attr('string', {
readOnly: true, 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', { altNames: attr('string', {
label: 'DNS/Email Subject Alternative Names (SANs)', 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', { ipSans: attr('string', {
label: 'IP Subject Alternative Names (SANs)', label: 'IP Subject Alternative Names (SANs)',
}), }),
issueDate: attr('string'),
issuingCa: attr('string', {
label: 'Issuing CA',
masked: true,
}),
otherSans: attr({ otherSans: attr({
editType: 'stringArray', editType: 'stringArray',
label: 'Other SANs', label: 'Other SANs',
helpText: helpText:
'The format is the same as OpenSSL: <oid>;<type>:<value> where the only current valid type is UTF8', 'The format is the same as OpenSSL: <oid>;<type>:<value> 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', { privateKey: attr('string', {
masked: true, masked: true,
}), }),
privateKeyType: attr('string'), privateKeyType: attr('string'),
revocationTime: attr('number'),
role: attr('object', {
readOnly: true,
}),
serialNumber: attr('string'), serialNumber: attr('string'),
ttl: attr({
label: 'TTL',
editType: 'ttl',
}),
fieldsToAttrs(fieldGroups) { fieldsToAttrs(fieldGroups) {
return fieldToAttrs(this, fieldGroups); return fieldToAttrs(this, fieldGroups);

View File

@ -8,6 +8,13 @@
{{/if}} {{/if}}
</h2> </h2>
{{#if (or model.certificate model.csr)}} {{#if (or model.certificate model.csr)}}
{{#if (not (eq model.canParse true))}}
<AlertBanner
@type="info"
@message="There was an error parsing the certificate's metadata. As a result, Vault cannot display the issue and expiration dates. This will not interfere with the certificate's functionality."
data-test-warning
/>
{{/if}}
{{#each model.attrs as |attr|}} {{#each model.attrs as |attr|}}
{{#if attr.options.masked}} {{#if attr.options.masked}}
<InfoTableRow data-test-table-row <InfoTableRow data-test-table-row

View File

@ -8,7 +8,13 @@
</h1> </h1>
</p.levelLeft> </p.levelLeft>
</PageHeader> </PageHeader>
{{#if (not (eq model.canParse true))}}
<AlertBanner
@type="info"
@message="There was an error parsing the certificate's metadata. As a result, Vault cannot display the common name or the issue and expiration dates. This will not interfere with the certificate's functionality."
data-test-warning
/>
{{/if}}
<div class="box is-fullwidth is-sideless is-paddingless is-marginless"> <div class="box is-fullwidth is-sideless is-paddingless is-marginless">
<MessageError @model={{model}} /> <MessageError @model={{model}} />
{{#each model.attrs as |attr|}} {{#each model.attrs as |attr|}}

View File

@ -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) { test('cert config: upload', async function(assert) {
await mountAndNav(assert); await mountAndNav(assert);
await settled(); await settled();

View File

@ -32,6 +32,9 @@ export default {
enterCertAsText: clickable('[data-test-text-toggle]'), enterCertAsText: clickable('[data-test-text-toggle]'),
pemBundle: fillable('[data-test-text-file-textarea="true"]'), pemBundle: fillable('[data-test-text-file-textarea="true"]'),
commonName: fillable('[data-test-input="commonName"]'), 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"]'), issueDateIsPresent: text('[data-test-row-value="Issue date"]'),
expiryDateIsPresent: text('[data-test-row-value="Expiration date"]'), expiryDateIsPresent: text('[data-test-row-value="Expiration date"]'),
@ -48,6 +51,15 @@ export default {
.submit(); .submit();
}, },
async generateCAKeyTypeEC(commonName = 'PKI CA EC') {
return await this.replaceCA()
.commonName(commonName)
.toggleOptions()
.keyType('ec')
.keyBits(256)
.submit();
},
async uploadCA(pem) { async uploadCA(pem) {
return await this.replaceCA() return await this.replaceCA()
.uploadCert() .uploadCert()