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:
parent
d7c54b50e7
commit
e8c9affee1
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:bug
|
||||||
|
ui: Fixes node-forge error when parsing EC (elliptical curve) certs
|
||||||
|
```
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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'),
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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|}}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue