ui: add params to pki parser (#18760)

* refactor parser to pull serial number from subject

* refactor pki parser

* uninstall pvtutils

* remove hideFormSection as attr

* remove hideFormSection as attr

* add string-list

* test removing issueDate

* update tests

* final answer - make number types

* change to unix time - since valueOf() is typically used internally

* add algo mapping

* add comment to complete in followon

* add attrs to pki parser

* add conditional operands so parser continues when values dont exist

* add error handling WIP

* finish tests, add error handling

* revert to helper

* move helper to util

* add parseSubject test

* finish tests

* move certs to pki helper file

* wrap parsing functions in try...catch
This commit is contained in:
claire bontempo 2023-01-23 16:49:16 -08:00 committed by GitHub
parent 3ded1388cb
commit 92cc175eb6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 600 additions and 128 deletions

View File

@ -1,4 +1,4 @@
import { parsePkiCert } from '../helpers/parse-pki-cert';
import { parsePkiCert } from 'vault/utils/parse-pki-cert';
import ApplicationAdapter from './application';
export default ApplicationAdapter.extend({
@ -49,7 +49,7 @@ export default ApplicationAdapter.extend({
response.modelName = type.modelName;
// only parse if certificate is attached to response
if (response.data && response.data.certificate) {
const caCertMetadata = parsePkiCert([response.data]);
const caCertMetadata = parsePkiCert(response.data);
const transformedResponse = { ...response, ...caCertMetadata };
store.pushPayload(type.modelName, transformedResponse);
} else {

View File

@ -1,70 +0,0 @@
import { helper } from '@ember/component/helper';
import * as asn1js from 'asn1js';
import { fromBase64, stringToArrayBuffer } from 'pvutils';
import { Convert } from 'pvtsutils';
import { Certificate } from 'pkijs';
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 });
} catch (error) {
console.debug('DEBUG: Parsing Certificate', error); // eslint-disable-line
return {
can_parse: false,
};
}
// We wish to get the CN element out of this certificate's subject. A
// subject is a list of RDNs, where each RDN is a (type, value) tuple
// and where a type is an OID. The OID for CN can be found here:
//
// http://oid-info.com/get/2.5.4.3
// https://datatracker.ietf.org/doc/html/rfc5280#page-112
//
// Each value is then encoded as another ASN.1 object; in the case of a
// CommonName field, this is usually a PrintableString, BMPString, or a
// UTF8String. Regardless of encoding, it should be present in the
// valueBlock's value field if it is renderable.
const commonNameOID = '2.5.4.3';
const commonNames = cert?.subject?.typesAndValues
.filter((rdn) => rdn?.type === commonNameOID)
.map((rdn) => rdn?.value?.valueBlock?.value);
// Theoretically, there might be multiple (or no) CommonNames -- but Vault
// presently refuses to issue certificates without CommonNames in most
// cases. For now, return the first CommonName we find. Alternatively, we
// might update our callers to handle multiple, or join them using some
// separator like ','.
const commonName = commonNames ? (commonNames.length ? commonNames[0] : null) : null;
// Date instances are stored in the value field as the notAfter/notBefore
// field themselves are Time values.
const expiryDate = cert?.notAfter?.value;
const issueDate = cert?.notBefore?.value;
const serialNumber = Convert.ToHex(cert.serialNumber.valueBlock.valueHex)
.match(/.{1,2}/g)
.join(':');
return {
can_parse: true,
common_name: commonName,
serial_number: serialNumber,
expiry_date: expiryDate,
issue_date: issueDate,
not_valid_after: expiryDate.valueOf(),
not_valid_before: issueDate.valueOf(),
};
}
export function parsePkiCert([model]) {
// model has to be the responseJSON from PKI serializer
// return if no certificate or if the "certificate" is actually a CRL
if (!model.certificate || model.certificate.includes('BEGIN X509 CRL')) {
return;
}
return parseCertificate(model.certificate);
}
export default helper(parsePkiCert);

View File

@ -1,10 +0,0 @@
import Model, { attr } from '@ember-data/model';
export default class PkiCertificateModel extends Model {
@attr('string', { readOnly: true }) backend;
@attr('string') commonName;
@attr('string') issueDate;
@attr('string') serialNumber;
@attr('string') notAfter;
@attr('string') notBeforeDuration;
}

View File

@ -15,7 +15,6 @@ const certDisplayFields = [
'certificate',
'commonName',
'revocationTime',
'issueDate',
'serialNumber',
'notValidBefore',
'notValidAfter',
@ -48,9 +47,11 @@ export default class PkiCertificateBaseModel extends Model {
@attr('string') serialNumber;
// Parsed from cert in serializer
@attr('number', { formatDate: true }) issueDate;
@attr('number', { formatDate: true }) notValidAfter;
@attr('number', { formatDate: true }) notValidBefore;
@attr('string') uriSans;
@attr('string') altNames;
@attr('string') signatureBits;
// For importing
@attr('string') pemBundle;

View File

@ -13,10 +13,10 @@ const issuerUrls = ['issuingCertificates', 'crlDistributionPoints', 'ocspServers
'caChain',
'commonName',
'issuerName',
'notValidBefore',
'serialNumber',
'keyId',
'uriSans',
'notValidBefore',
'notValidAfter',
],
},
@ -34,10 +34,6 @@ export default class PkiIssuerModel extends PkiCertificateBaseModel {
@attr('string') issuerId;
@attr('string', { displayType: 'masked' }) certificate;
@attr('string', { displayType: 'masked', label: 'CA Chain' }) caChain;
@attr('date', {
label: 'Issue date',
})
notValidBefore;
@attr('string', {
label: 'Default key ID',

View File

@ -156,7 +156,6 @@ export default class PkiRoleModel extends Model {
label: 'Allowed domains',
subText: 'Specifies the domains this role is allowed to issue certificates for. Add one item per row.',
editType: 'stringArray',
hideFormSection: true,
})
allowedDomains;
@ -194,7 +193,6 @@ export default class PkiRoleModel extends Model {
label: 'Policy identifiers',
subText: 'A comma-separated string or list of policy object identifiers (OIDs). Add one per row. ',
editType: 'stringArray',
hideFormSection: true,
})
policyIdentifiers;
/* End of overriding Policy identifier options */
@ -213,7 +211,6 @@ export default class PkiRoleModel extends Model {
subText: 'Defines allowed URI Subject Alternative Names. Add one item per row',
editType: 'stringArray',
docLink: '/docs/concepts/policies',
hideFormSection: true,
})
allowedUriSans;
@ -229,7 +226,6 @@ export default class PkiRoleModel extends Model {
label: 'Other SANs',
subText: 'Defines allowed custom OID/UTF8-string SANs. Add one item per row.',
editType: 'stringArray',
hideFormSection: true,
})
allowedOtherSans;
/* End of overriding SAN options */
@ -240,7 +236,6 @@ export default class PkiRoleModel extends Model {
subText:
'A list of allowed serial numbers to be requested during certificate issuance. Shell-style globbing is supported. If empty, custom-specified serial numbers will be forbidden.',
editType: 'stringArray',
hideFormSection: true,
})
allowedSerialNumbers;
@ -271,7 +266,6 @@ export default class PkiRoleModel extends Model {
label: 'Organization Units (OU)',
subText:
'A list of allowed serial numbers to be requested during certificate issuance. Shell-style globbing is supported. If empty, custom-specified serial numbers will be forbidden.',
hideFormSection: true,
})
ou;
@ -293,12 +287,12 @@ export default class PkiRoleModel extends Model {
})
extKeyUsageOids;
@attr({ hideFormSection: true }) organization;
@attr({ hideFormSection: true }) country;
@attr({ hideFormSection: true }) locality;
@attr({ hideFormSection: true }) province;
@attr({ hideFormSection: true }) streetAddress;
@attr({ hideFormSection: true }) postalCode;
@attr('string') organization;
@attr('string') country;
@attr('string') locality;
@attr('string') province;
@attr('string') streetAddress;
@attr('string') postalCode;
/* End of overriding Additional subject field options */
/* CAPABILITIES

View File

@ -2,7 +2,7 @@ import RESTSerializer from '@ember-data/serializer/rest';
import { isNone, isBlank } from '@ember/utils';
import { assign } from '@ember/polyfills';
import { decamelize } from '@ember/string';
import { parsePkiCert } from '../../helpers/parse-pki-cert';
import { parsePkiCert } from 'vault/utils/parse-pki-cert';
export default RESTSerializer.extend({
keyForAttribute: function (attr) {
@ -45,7 +45,7 @@ export default RESTSerializer.extend({
let transformedPayload, certMetadata;
// hits cert/list endpoint first which returns an array of keys, only want to parse if response contains certificates
if (!Array.isArray(responseJSON)) {
certMetadata = parsePkiCert([responseJSON]);
certMetadata = parsePkiCert(responseJSON);
transformedPayload = { [modelName]: { ...certMetadata, ...responseJSON } };
} else {
transformedPayload = { [modelName]: responseJSON };

View File

@ -1,4 +1,4 @@
import { parseCertificate } from 'vault/helpers/parse-pki-cert';
import { parseCertificate } from 'vault/utils/parse-pki-cert';
import ApplicationSerializer from '../../application';
export default class PkiCertificateBaseSerializer extends ApplicationSerializer {
@ -12,11 +12,6 @@ export default class PkiCertificateBaseSerializer extends ApplicationSerializer
if (payload.data.certificate) {
// Parse certificate back from the API and add to payload
const parsedCert = parseCertificate(payload.data.certificate);
// convert issueDate to same format as other date values
// this can be moved into the parseCertificate helper once the old pki implementation is removed
if (parsedCert.issue_date) {
parsedCert.issue_date = parsedCert.issue_date.valueOf();
}
const json = super.normalizeResponse(
store,
primaryModelClass,

View File

@ -1,4 +1,4 @@
import { parseCertificate } from 'vault/helpers/parse-pki-cert';
import { parseCertificate } from 'vault/utils/parse-pki-cert';
import ApplicationSerializer from '../application';
export default class PkiIssuerSerializer extends ApplicationSerializer {

View File

@ -7,7 +7,8 @@
}
}
.field:first-child .form-section {
.field:first-child .form-section,
.box > .field > .field.form-section.string-list {
padding: 0;
box-shadow: none;
}

View File

@ -0,0 +1,55 @@
//* 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',
ou: '2.5.4.11',
organization: '2.5.4.10',
country: '2.5.4.6',
locality: '2.5.4.7',
province: '2.5.4.8',
street_address: '2.5.4.9',
postal_code: '2.5.4.17',
};
export const EXTENSION_OIDs = {
key_usage: '2.5.29.15',
subject_alt_name: '2.5.29.17', // contains SAN_TYPES below
basic_constraints: '2.5.29.19', // contains max_path_length
name_constraints: '2.5.29.30', // contains permitted_dns_domains
};
// these are allowed ext oids, but not parsed and passed to cross-signed certs
export const IGNORED_OIDs = {
subject_key_identifier: '2.5.29.14',
authority_key_identifier: '2.5.29.35',
};
// SubjectAltName/GeneralName types (scroll up to page 38 -> https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.7 )
export const SAN_TYPES = {
alt_names: 2, // dNSName
uri_sans: 6, // uniformResourceIdentifier
ip_sans: 7, // iPAddress - OCTET STRING
};
export const SIGNATURE_ALGORITHM_OIDs = {
'1.2.840.113549.1.1.2': '0', // MD2-RSA
'1.2.840.113549.1.1.4': '0', // MD5-RSA
'1.2.840.113549.1.1.5': '0', // SHA1-RSA
'1.2.840.113549.1.1.11': '256', // SHA256-RSA
'1.2.840.113549.1.1.12': '384', // SHA384-RSA
'1.2.840.113549.1.1.13': '512', // SHA512-RSA
'1.2.840.113549.1.1.10': {
// RSA-PSS have additional OIDs that need to be mapped
'2.16.840.1.101.3.4.2.1': '256', // SHA-256
'2.16.840.1.101.3.4.2.2': '384', // SHA-384
'2.16.840.1.101.3.4.2.3': '512', // SHA-512
},
'1.2.840.10040.4.3': '0', // DSA-SHA1
'2.16.840.1.101.3.4.3.2': '256', // DSA-SHA256
'1.2.840.10045.4.1': '0', // ECDSA-SHA1
'1.2.840.10045.4.3.2': '256', // ECDSA-SHA256
'1.2.840.10045.4.3.3': '384', // ECDSA-SHA384
'1.2.840.10045.4.3.4': '512', // ECDSA-SHA512
'1.3.101.112': '0', // Ed25519
};

View File

@ -0,0 +1,241 @@
import * as asn1js from 'asn1js';
import { fromBase64, stringToArrayBuffer } from 'pvutils';
import { Certificate } from 'pkijs';
import { differenceInHours, getUnixTime } from 'date-fns';
import {
EXTENSION_OIDs,
SUBJECT_OIDs,
IGNORED_OIDs,
SAN_TYPES,
SIGNATURE_ALGORITHM_OIDs,
} from './parse-pki-cert-oids';
/*
It may be helpful to visualize a certificate's SEQUENCE structure alongside this parsing file.
You can do so by decoding a certificate here: https://lapo.it/asn1js/#
A certificate is encoded in ASN.1 data - a SEQUENCE is how you define structures in ASN.1.
GeneralNames, Extension, AlgorithmIdentifier are all examples of SEQUENCEs
* Error handling:
{ can_parse: false } -> returned if the external library cannot convert the certificate
{ parsing_errors: [] } -> returned if the certificate was converted, but there's ANY problem parsing certificate details.
This means we cannot cross-sign in the UI and prompt the user to do so manually using the CLI.
*/
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 });
} catch (error) {
console.debug('DEBUG: Converting Certificate', error); // eslint-disable-line
return { can_parse: false };
}
let parsedCertificateValues;
try {
const subjectValues = parseSubject(cert?.subject?.typesAndValues);
const extensionValues = parseExtensions(cert?.extensions);
const [signature_bits, use_pss] = mapSignatureBits(cert?.signatureAlgorithm);
const formattedValues = formatValues(subjectValues, extensionValues);
parsedCertificateValues = { ...formattedValues, signature_bits, use_pss };
} catch (error) {
console.debug('DEBUG: Parsing Certificate', error); // eslint-disable-line
parsedCertificateValues = { parsing_errors: [new Error('error parsing certificate values')] };
}
const expiryDate = cert?.notAfter?.value;
const issueDate = cert?.notBefore?.value;
const ttl = `${differenceInHours(expiryDate, issueDate)}h`;
return {
...parsedCertificateValues,
can_parse: true,
expiry_date: expiryDate, // remove along with old PKI work
issue_date: issueDate, // remove along with old PKI work
not_valid_after: getUnixTime(expiryDate),
not_valid_before: getUnixTime(issueDate),
ttl,
};
}
export function parsePkiCert(model) {
// model has to be the responseJSON from PKI serializer
// return if no certificate or if the "certificate" is actually a CRL
if (!model.certificate || model.certificate.includes('BEGIN X509 CRL')) {
return;
}
return parseCertificate(model.certificate);
}
export function formatValues(subject, extension) {
if (!subject || !extension) {
return { parsing_errors: [new Error('error formatting certificate values')] };
}
const { subjValues, subjErrors } = subject;
const { extValues, extErrors } = extension;
const parsing_errors = [...subjErrors, ...extErrors];
const exclude_cn_from_sans =
extValues.alt_names?.length > 0 && !extValues.alt_names?.includes(subjValues?.common_name) ? true : false;
// now that we've finished parsing data, join all extension arrays
for (const ext in extValues) {
if (Array.isArray(extValues[ext])) {
extValues[ext] = extValues[ext].length !== 0 ? extValues[ext].join(', ') : null;
}
}
// TODO remove this deletion when key_usage is parsed, update test
delete extValues.key_usage;
return {
...subjValues,
...extValues,
parsing_errors,
exclude_cn_from_sans,
};
}
//* PARSING HELPERS
/*
We wish to get each SUBJECT_OIDs (see utils/parse-pki-cert-oids.js) out of this certificate's subject.
A subject is a list of RDNs, where each RDN is a (type, value) tuple
and where a type is an OID. The OID for CN can be found here:
https://datatracker.ietf.org/doc/html/rfc5280#page-112
Each value is then encoded as another ASN.1 object; in the case of a
CommonName field, this is usually a PrintableString, BMPString, or a
UTF8String. Regardless of encoding, it should be present in the
valueBlock's value field if it is renderable.
*/
export function parseSubject(subject) {
if (!subject) return null;
const values = {};
const errors = [];
if (subject.any((rdn) => !Object.values(SUBJECT_OIDs).includes(rdn.type))) {
errors.push(new Error('certificate contains unsupported subject OIDs'));
}
const returnValues = (OID) => {
const values = subject.filter((rdn) => rdn?.type === OID).map((rdn) => rdn?.value?.valueBlock?.value);
// Theoretically, there might be multiple (or no) CommonNames -- but Vault
// presently refuses to issue certificates without CommonNames in most
// cases. For now, return the first CommonName we find. Alternatively, we
// might update our callers to handle multiple and return a string array
return values ? (values?.length ? values[0] : null) : null;
};
Object.keys(SUBJECT_OIDs).forEach((key) => (values[key] = returnValues(SUBJECT_OIDs[key])));
return { subjValues: values, subjErrors: errors };
}
export function parseExtensions(extensions) {
if (!extensions) return null;
const values = {};
const errors = [];
const allowedOids = Object.values({ ...EXTENSION_OIDs, ...IGNORED_OIDs });
if (extensions.any((ext) => !allowedOids.includes(ext.extnID))) {
errors.push(new Error('certificate contains unsupported extension OIDs'));
}
// make each extension its own key/value pair
for (const attrName in EXTENSION_OIDs) {
values[attrName] = extensions.find((ext) => ext.extnID === EXTENSION_OIDs[attrName])?.parsedValue;
}
if (values.subject_alt_name) {
// we only support SANs of type 2 (altNames), 6 (uri) and 7 (ipAddress)
const supportedTypes = Object.values(SAN_TYPES);
const supportedNames = Object.keys(SAN_TYPES);
const sans = values.subject_alt_name?.altNames;
if (!sans) {
errors.push(new Error('certificate contains unsupported subjectAltName values'));
} else if (sans.any((san) => !supportedTypes.includes(san.type))) {
// pass along error that unsupported values exist
errors.push(new Error('subjectAltName contains unsupported types'));
// still check and parse any supported values
if (sans.any((san) => supportedTypes.includes(san.type))) {
supportedNames.forEach((attrName) => {
values[attrName] = sans
.filter((gn) => gn.type === Number(SAN_TYPES[attrName]))
.map((gn) => gn.value);
});
}
} else if (sans.every((san) => supportedTypes.includes(san.type))) {
supportedNames.forEach((attrName) => {
values[attrName] = sans.filter((gn) => gn.type === Number(SAN_TYPES[attrName])).map((gn) => gn.value);
});
} else {
errors.push(new Error('unsupported subjectAltName values'));
}
}
// permitted_dns_domains
if (values.name_constraints) {
// we only support Name Constraints of dnsName (type 2), this value lives in the permittedSubtree of the Name Constraints sequence
// permittedSubtrees contain an array of subtree objects, each object has a 'base' key and EITHER a 'minimum' or 'maximum' key
// GeneralSubtree { "base": { "type": 2, "value": "dnsname1.com" }, minimum: 0 }
const nameConstraints = values.name_constraints;
if (Object.keys(nameConstraints).includes('excludedSubtrees')) {
errors.push(new Error('nameConstraints contains excludedSubtrees'));
} else if (nameConstraints.permittedSubtrees.any((subtree) => subtree.minimum !== 0)) {
errors.push(new Error('nameConstraints permittedSubtree contains non-zero minimums'));
} else if (nameConstraints.permittedSubtrees.any((subtree) => subtree.maximum)) {
errors.push(new Error('nameConstraints permittedSubtree contains maximum'));
} else if (nameConstraints.permittedSubtrees.any((subtree) => subtree.base.type !== 2)) {
errors.push(new Error('nameConstraints permittedSubtree can only contain dnsName (type 2)'));
// still check and parse any supported values
if (nameConstraints.permittedSubtrees.any((subtree) => subtree.base.type === 2)) {
values.permitted_dns_domains = nameConstraints.permittedSubtrees
.filter((gn) => gn.base.type === 2)
.map((gn) => gn.base.value);
}
} else if (nameConstraints.permittedSubtrees.every((subtree) => subtree.base.type === 2)) {
values.permitted_dns_domains = nameConstraints.permittedSubtrees.map((gn) => gn.base.value);
} else {
errors.push(new Error('unsupported nameConstraints values'));
}
}
if (values.basic_constraints) {
values.max_path_length = values.basic_constraints?.pathLenConstraint;
}
if (values.ip_sans) {
// TODO parse octet string for IP addresses
}
if (values.key_usage) {
// TODO parse key_usage
}
delete values.subject_alt_name;
delete values.basic_constraints;
delete values.name_constraints;
return { extValues: values, extErrors: errors };
/*
values is an object with keys from EXTENSION_OIDs and SAN_TYPES
values = {
"alt_names": string[],
"uri_sans": string[],
"permitted_dns_domains": string[],
"max_path_length": int,
"key_usage": BitString, <- to-be-parsed
"ip_sans": OctetString[], <- currently array of OctetStrings to-be-parsed
}
*/
}
function mapSignatureBits(sigAlgo) {
const { algorithmId } = sigAlgo;
// use_pss is true, additional OIDs need to be mapped
if (algorithmId === '1.2.840.113549.1.1.10') {
// object identifier for PSS is very nested
const objId = sigAlgo.algorithmParams?.valueBlock?.value[0]?.valueBlock?.value[0]?.valueBlock?.value[0]
.toString()
.split(' : ')[1];
return [SIGNATURE_ALGORITHM_OIDs[algorithmId][objId], true];
}
return [SIGNATURE_ALGORITHM_OIDs[algorithmId], false];
}

View File

@ -215,7 +215,6 @@
@onChange={{this.setAndBroadcast}}
@attrName={{@attr.name}}
@subText={{@attr.options.subText}}
@hideFormSection={{@attr.options.hideFormSection}}
/>
{{else if (eq @attr.options.sensitive true)}}
{{! Masked Input }}

View File

@ -1,5 +1,5 @@
<div
class={{concat "field string-list" (if @hideFormSection "" " form-section")}}
class="field string-list form-section"
data-test-component="string-list"
{{did-insert this.autoSize}}
{{did-update this.autoSizeUpdate}}

View File

@ -20,7 +20,6 @@ import { next } from '@ember/runloop';
* @param {string} type=array - Optional type for inputValue.
* @param {string} attrName - We use this to check the type so we can modify the tooltip content.
* @param {string} subText - Text below the label.
* @param {boolean} hideFormSection - If true do not add form-section class on surrounding div.
*/
export default class StringList extends Component {

View File

@ -79,7 +79,7 @@
<InfoTableRow
@label={{or attr.options.label (humanize (dasherize attr.name))}}
@value={{get @issuer attr.name}}
@formatDate={{if (eq attr.type "date") "MMM d yyyy HH:mm:ss a zzzz"}}
@formatDate={{if attr.options.formatDate "MMM d yyyy HH:mm:ss a zzzz"}}
@alwaysRender={{true}}
/>
{{/if}}

View File

@ -19,6 +19,7 @@
/>
<div class="has-top-margin-xxl">
<StringList
class="is-box-shadowless"
data-test-input="extKeyUsageOids"
@label="Extended key usage oids"
@inputValue={{get @model "extKeyUsageOids"}}
@ -26,6 +27,5 @@
@attrName="extKeyUsageOids"
@subText="A list of extended key usage oids. Add one item per row."
@showHelpText={{false}}
@hideFormSection={{true}}
/>
</div>

View File

@ -256,7 +256,6 @@
"highlight.js": "^10.4.1",
"js-yaml": "^3.13.1",
"lodash": "^4.17.13",
"node-notifier": "^8.0.1",
"pvtsutils": "^1.3.2"
"node-notifier": "^8.0.1"
}
}

View File

@ -124,3 +124,14 @@ UmYDODRN4qh9xYruKJ8i89iMGQfbdcq78dCC4JwBIx3bysC8oF4lqbTYoYNVTnAi
LVqvLdHycEOMlqV0ecq8uMLhPVBalCmIlKdWNQFpXB0TQCsn95rCCdi7ZTsYk5zv
Q4raFvQrZth3Cz/X5yPTtQL78oBYrmHzoQKDFJ2z
-----END CERTIFICATE-----`;
// for parse-pki-cert tests:
// certificate contains all allowable params
export const loadedCert = `-----BEGIN CERTIFICATE-----\nMIIFFTCCA/2gAwIBAgIULIZoZjgoLLQeYd/I0EQgdUegragwDQYJKoZIhvcNAQEN\nBQAwgdoxDzANBgNVBAYTBkZyYW5jZTESMBAGA1UECBMJQ2hhbXBhZ25lMQ4wDAYD\nVQQHEwVQYXJpczETMBEGA1UECRMKMjM0IHNlc2FtZTEPMA0GA1UEERMGMTIzNDU2\nMSQwDQYDVQQKEwZXaWRnZXQwEwYDVQQKEwxJbmNvcnBvcmF0ZWQxKDAOBgNVBAsT\nB0ZpbmFuY2UwFgYDVQQLEw9IdW1hbiBSZXNvdXJjZXMxGDAWBgNVBAMTD2NvbW1v\nbi1uYW1lLmNvbTETMBEGA1UEBRMKY2VyZWFsMTI5MjAeFw0yMzAxMjEwMDUyMzBa\nFw0zMzAxMTgwMDUzMDBaMIHaMQ8wDQYDVQQGEwZGcmFuY2UxEjAQBgNVBAgTCUNo\nYW1wYWduZTEOMAwGA1UEBxMFUGFyaXMxEzARBgNVBAkTCjIzNCBzZXNhbWUxDzAN\nBgNVBBETBjEyMzQ1NjEkMA0GA1UEChMGV2lkZ2V0MBMGA1UEChMMSW5jb3Jwb3Jh\ndGVkMSgwDgYDVQQLEwdGaW5hbmNlMBYGA1UECxMPSHVtYW4gUmVzb3VyY2VzMRgw\nFgYDVQQDEw9jb21tb24tbmFtZS5jb20xEzARBgNVBAUTCmNlcmVhbDEyOTIwggEi\nMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZRug7meAek7/LvKyPqVL0L9hO\n3RQrvotWAGxCUp7gEPVxVBuVH97hwfABazikQQGhXQVeISrwaX7zI945fd3dGx3R\n3iDPrGp3A8KXsaS70luMg6WyIQJ5GM21GIGchACXiIKv+Ln0++0wivFyMw8sA4V2\nbQyZHOsN5puoYqhEFyypw0E3yiyvBW7KuDrkOLzuVSCa1WdYCnpg7O1v/ViM6dIk\no83CH1p1MtQ6ZPgBfB4V6JPAm4R3zhoG0Geg3FziCXm+F2qyfbICyTQLoXXB0YD9\nE5D4jnsGwRvSLIdadxfqZCN740JOHIIZopQLhJDHNjQjTcuqtW8EhC1UJzIjAgMB\nAAGjgdAwgc0wDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAREwHQYD\nVR0OBBYEFAsrMoFu6tt1pybxx9ln6w5QK/2tMB8GA1UdIwQYMBaAFAsrMoFu6tt1\npybxx9ln6w5QK/2tMDcGA1UdEQQwMC6CCGFsdG5hbWUxgghhbHRuYW1lMocEwJ4B\nJoYIdGVzdHVyaTGGCHRlc3R1cmkyMC4GA1UdHgEB/wQkMCKgIDAOggxkbnNuYW1l\nMS5jb20wDoIMZHNubmFtZTIuY29tMA0GCSqGSIb3DQEBDQUAA4IBAQCLIQ/AEVME\n5F9N5kqT0PdJ7PgjCHraWnEa25TH7RxH5mh6BakuUkJr5TFnytDU6TwkVfixgT9j\nT6O+BdB6ILv1u3ECGBQNObq1HtO0NM/Q1IZewEUNIjDVfdXFIxHLLlyxoGiCV/PS\nm/QHHX6K7EezAIdw4OvvO5lfjOzPZ6vaWEab1BCCPgxaWOqQ4U6MX3NzLiP5VqTs\npMFoLJ0yG1yMkW0pr8d1NkqDoZI1JW/DGrQEdYg182ckHogjmjydVE0B00yCzGHh\nOYqj7AHqjkpa9DMZMH22reuiSGNun7o2jEQ9iRt79UEpqkIap3aohsypeqgYCMGf\n6V/JEhjKPzap\n-----END CERTIFICATE-----`;
// use_pss = true
export const pssTrueCert = `-----BEGIN CERTIFICATE-----\nMIIDqTCCAl2gAwIBAgIUVY2PTRZl1t/fjfyEwrG4HvGjYekwQQYJKoZIhvcNAQEK\nMDSgDzANBglghkgBZQMEAgEFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgEF\nAKIDAgEgMBoxGDAWBgNVBAMTD2NvbW1vbi1uYW1lLmNvbTAeFw0yMzAxMjEwMTA3\nNDBaFw0yMzAyMjIwMTA4MTBaMBoxGDAWBgNVBAMTD2NvbW1vbi1uYW1lLmNvbTCC\nASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANlG6DuZ4B6Tv8u8rI+pUvQv\n2E7dFCu+i1YAbEJSnuAQ9XFUG5Uf3uHB8AFrOKRBAaFdBV4hKvBpfvMj3jl93d0b\nHdHeIM+sancDwpexpLvSW4yDpbIhAnkYzbUYgZyEAJeIgq/4ufT77TCK8XIzDywD\nhXZtDJkc6w3mm6hiqEQXLKnDQTfKLK8Fbsq4OuQ4vO5VIJrVZ1gKemDs7W/9WIzp\n0iSjzcIfWnUy1Dpk+AF8HhXok8CbhHfOGgbQZ6DcXOIJeb4XarJ9sgLJNAuhdcHR\ngP0TkPiOewbBG9Ish1p3F+pkI3vjQk4cghmilAuEkMc2NCNNy6q1bwSELVQnMiMC\nAwEAAaN/MH0wDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O\nBBYEFAsrMoFu6tt1pybxx9ln6w5QK/2tMB8GA1UdIwQYMBaAFAsrMoFu6tt1pybx\nx9ln6w5QK/2tMBoGA1UdEQQTMBGCD2NvbW1vbi1uYW1lLmNvbTBBBgkqhkiG9w0B\nAQowNKAPMA0GCWCGSAFlAwQCAQUAoRwwGgYJKoZIhvcNAQEIMA0GCWCGSAFlAwQC\nAQUAogMCASADggEBAFh+PMwEmxaZR6OtfB0Uvw2vA7Oodmm3W0bYjQlEz8U+Q+JZ\ncIPa4VnRy1QALmKbPCbRApA/gcWzIwtzo1JhLtcDINg2Tl0nj4WvgpIvj0/lQNMq\nmwP7G/K4PyJTv3+y5XwVfepZAZITB0w5Sg5dLC6HP8AGVIaeb3hGNHYvPlE+pbT+\njL0xxzFjOorWoy5fxbWoVyVv9iZ4j0zRnbkYHIi3d8g56VV6Rbyw4WJt6p87lmQ8\n0wbiJTtuew/0Rpuc3PEcR9XfB5ct8bvaGGTSTwh6JQ33ohKKAKjbBNmhBDSP1thQ\n2mTkms/mbDRaTiQKHZx25TmOlLN5Ea1TSS0K6yw=\n-----END CERTIFICATE-----`;
// only has common name
export const skeletonCert = `-----BEGIN CERTIFICATE-----\nMIIDQTCCAimgAwIBAgIUVQy58VgdVpAK9c8SfS31idSv6FUwDQYJKoZIhvcNAQEL\nBQAwGjEYMBYGA1UEAxMPY29tbW9uLW5hbWUuY29tMB4XDTIzMDEyMTAxMjAyOVoX\nDTIzMDIyMjAxMjA1OVowGjEYMBYGA1UEAxMPY29tbW9uLW5hbWUuY29tMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2UboO5ngHpO/y7ysj6lS9C/YTt0U\nK76LVgBsQlKe4BD1cVQblR/e4cHwAWs4pEEBoV0FXiEq8Gl+8yPeOX3d3Rsd0d4g\nz6xqdwPCl7Gku9JbjIOlsiECeRjNtRiBnIQAl4iCr/i59PvtMIrxcjMPLAOFdm0M\nmRzrDeabqGKoRBcsqcNBN8osrwVuyrg65Di87lUgmtVnWAp6YOztb/1YjOnSJKPN\nwh9adTLUOmT4AXweFeiTwJuEd84aBtBnoNxc4gl5vhdqsn2yAsk0C6F1wdGA/ROQ\n+I57BsEb0iyHWncX6mQje+NCThyCGaKUC4SQxzY0I03LqrVvBIQtVCcyIwIDAQAB\no38wfTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU\nCysygW7q23WnJvHH2WfrDlAr/a0wHwYDVR0jBBgwFoAUCysygW7q23WnJvHH2Wfr\nDlAr/a0wGgYDVR0RBBMwEYIPY29tbW9uLW5hbWUuY29tMA0GCSqGSIb3DQEBCwUA\nA4IBAQDPco+FIHXczf0HTwFAmIVu4HKaeIwDsVPxoUqqWEix8AyCsB5uqpKZasby\nedlrdBohM4dnoV+VmV0de04y95sdo3Ot60hm/czLog3tHg4o7AmfA7saS+5hCL1M\nCJWqoJHRFo0hOWJHpLJRWz5DqRZWspASoVozLOYyjRD+tNBjO5hK4FtaG6eri38t\nOpTt7sdInVODlntpNuuCVprPpHGj4kPOcViQULoFQq5fwyadpdjqSXmEGlt0to5Y\nMbTb4Jhj0HywgO53BUUmMzzY9idXh/8A7ThrM5LtqhxaYHLVhyeo+5e0mgiXKp+n\nQ8Uh4TNNTCvOUlAHycZNaxYTlEPn\n-----END CERTIFICATE-----`;
// contains unsupported subject and extension OIDs
export const unsupportedOids = `-----BEGIN CERTIFICATE-----\nMIIEjDCCA3SgAwIBAgIUD4EeORgh/i+ZZFOk8KsGKQPWsoIwDQYJKoZIhvcNAQEL\nBQAwgZIxMTAvBgNVBAMMKGZhbmN5LWNlcnQtdW5zdXBwb3J0ZWQtc3Viai1hbmQt\nZXh0LW9pZHMxCzAJBgNVBAYTAlVTMQ8wDQYDVQQIDAZLYW5zYXMxDzANBgNVBAcM\nBlRvcGVrYTESMBAGA1UECgwJQWNtZSwgSW5jMRowGAYJKoZIhvcNAQkBFgtmb29A\nYmFyLmNvbTAeFw0yMzAxMjMxODQ3MjNaFw0zMzAxMjAxODQ3MjNaMIGSMTEwLwYD\nVQQDDChmYW5jeS1jZXJ0LXVuc3VwcG9ydGVkLXN1YmotYW5kLWV4dC1vaWRzMQsw\nCQYDVQQGEwJVUzEPMA0GA1UECAwGS2Fuc2FzMQ8wDQYDVQQHDAZUb3Bla2ExEjAQ\nBgNVBAoMCUFjbWUsIEluYzEaMBgGCSqGSIb3DQEJARYLZm9vQGJhci5jb20wggEi\nMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDyYH5qS7krfZ2tA5uZsY2qXbTb\ntGNG1BsyDhZ/qqVlQybjDsHJZwNUbpfhBcCLaKyAwH1R9n54NOOOn6bYgfKWTgy3\nL7224YDAqYe7Y/GPjgI2MRvRfn6t2xzQxtJ0l0k8LeyNcwhiqYLQyOOfDdc127fm\nW40r2nmhLpH0i9e2I/YP1HQ+ldVgVBqeUTntgVSBfrQF56v9mAcvvHEa5sdHqmX4\nJ2lhWTnx9jqb7NZxCem76BlX1Gt5TpP3Ym2ZFVQI9fuPK4O8JVhk1KBCmIgR3Ft+\nPpFUs/c41EMunKJNzveYrInSDScaC6voIJpK23nMAiM1HckLfUUc/4UojD+VAgMB\nAAGjgdcwgdQwHQYDVR0OBBYEFH7tt4enejKTZtYjUKUUx6PXyzlgMB8GA1UdIwQY\nMBaAFH7tt4enejKTZtYjUKUUx6PXyzlgMA4GA1UdDwEB/wQEAwIFoDAgBgNVHSUB\nAf8EFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBCjBM\nBgNVHREERTBDhwTAngEmhgx1cmlTdXBwb3J0ZWSCEWRucy1OYW1lU3VwcG9ydGVk\noBoGAyoDBKATDBFleGFtcGxlIG90aGVybmFtZTANBgkqhkiG9w0BAQsFAAOCAQEA\nP6ckVJgbcJue+MK3RVDuG+Mh7dl89ynC7NwpQFRjLVZQuoMHZT/dcLlVeFejVXu5\nR+IPLmQU6NV7JAmy4zGap8awf12QTy3g410ecrSF94WWlu8bPoekfUnnP+kfzLPH\nCUAkRKxWDSRKX5C8cMMxacVBBaBIayuusLcHkHmxLLDw34PFzyz61gtZOJq7JYnD\nhU9YsNh6bCDmnBDBsDMOI7h8lBRQwTiWVoSD9YNVvFiY29YvFbJQGdh+pmBtf7E+\n1B/0t5NbvqlQSbhMM0QgYFhuCxr3BGNob7kRjgW4i+oh+Nc5ptA5q70QMaYudqRS\nd8SYWhRdxmH3qcHNPcR1iw==\n-----END CERTIFICATE-----`;
export const certWithoutCN = `-----BEGIN CERTIFICATE-----\nMIIDUDCCAjigAwIBAgIUEUpM5i7XMd/imZkR9XvonMaqPyYwDQYJKoZIhvcNAQEL\nBQAwHDEaMBgGCSqGSIb3DQEJARYLZm9vQGJhci5jb20wHhcNMjMwMTIzMjMyODEw\nWhcNMzMwMTIwMjMyODEwWjAcMRowGAYJKoZIhvcNAQkBFgtmb29AYmFyLmNvbTCC\nASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPGSdeqLICZcoUzxk88F8Tp+\nVNI+mS74L8pHyb9ZNZfeXPo0E9L5pi+KKI7rkxAtBGUecG1ENSxDDK9p6XZhWHSU\nZ6bdjOsjcIlfiM+1hhtDclIVxIDnz2Jt1/Vmnm8DXwdwVATWiFLTnfm288deNwsT\npl0ehAR3BadkZvteC6t+giEw/4qm1/FP53GEBOQeUWJDZRvtL37rdx4joFv3cR4w\nV0dukOjc5AGXtIOorO145OSZj8s7RsW3pfGcFUcOg7/flDxfK1UqFflQa7veLvKa\nWE/fOMyB/711QjSkTuQ5Rw3Rf9Fr2pqVJQgElTIW1SKaX5EJTB9mtGB34UqUXtsC\nAwEAAaOBiTCBhjAdBgNVHQ4EFgQUyhFP/fm+798mErPD5VQvEaAZQrswHwYDVR0j\nBBgwFoAUyhFP/fm+798mErPD5VQvEaAZQrswDgYDVR0PAQH/BAQDAgWgMCAGA1Ud\nJQEB/wQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEK\nMA0GCSqGSIb3DQEBCwUAA4IBAQCishzVkhuSAtqxgsZdYzBs3GpakGIio5zReW27\n6dk96hYCbbe4K3DtcFbRD1B8t6aTJlHxkFRaOWErSXu9WP3fUhIDNRE64Qsrg1zk\n3Km430qBlorXmTp6xhYHQfY5bn5rT2YY7AmaYIlIFxRhod43i5GDbBP+e+d/vTqR\nv1AJflYofeR4LeATP64B6a4R+QQVoxI43+pyH3ka+nRHwJBR9h8SMtJoqBy7x9pl\nYlBDa8lSn05doA3+e03VIzitvBBWI4oX1XB0tShSLk6YJXayIwe0ZNVvfYLIRKCp\nb4DUwChYzG/FwFSssUAqzVFhu3i+uU3Z47bsLVm0R5m7hLiZ\n-----END CERTIFICATE-----`;

View File

@ -65,7 +65,7 @@ module('Integration | Component | pki | Page::PkiCertificateDetails', function (
assert
.dom('[data-test-component="info-table-row"]')
.exists({ count: 6 }, 'Correct number of fields render when certificate has not been revoked');
.exists({ count: 5 }, 'Correct number of fields render when certificate has not been revoked');
assert
.dom('[data-test-value-div="Certificate"] [data-test-masked-input]')
.exists('Masked input renders for certificate');

View File

@ -107,7 +107,7 @@ module('Integration | Component | pki-role-form', function (hooks) {
const groupBoxHeight = find('[data-test-toggle-div="Key usage"]').clientHeight;
assert.strictEqual(
groupBoxHeight,
518,
567,
'renders the correct height of the box element if the component is rending as a flexbox'
);
await click(SELECTORS.roleCreateButton);

View File

@ -0,0 +1,268 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { parseCertificate, parseExtensions, parseSubject, formatValues } from 'vault/utils/parse-pki-cert';
import * as asn1js from 'asn1js';
import { fromBase64, stringToArrayBuffer } from 'pvutils';
import { Certificate } from 'pkijs';
import { addHours, fromUnixTime, isSameDay } from 'date-fns';
import errorMessage from 'vault/utils/error-message';
import { SAN_TYPES } from 'vault/utils/parse-pki-cert-oids';
import {
certWithoutCN,
loadedCert,
pssTrueCert,
skeletonCert,
unsupportedOids,
} from 'vault/tests/helpers/pki/values';
module('Integration | Util | parse pki certificate', function (hooks) {
setupTest(hooks);
hooks.beforeEach(function () {
this.getErrorMessages = (certErrors) => certErrors.map((error) => errorMessage(error));
this.certSchema = (cert) => {
const cert_base64 = cert.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 });
};
this.parsableLoadedCert = this.certSchema(loadedCert);
this.parsableUnsupportedCert = this.certSchema(unsupportedOids);
});
test('it parses a certificate with supported values', async function (assert) {
assert.expect(2);
// certificate contains all allowable params
const parsedCert = parseCertificate(loadedCert);
assert.propEqual(
parsedCert,
{
alt_names: 'altname1, altname2',
can_parse: true,
common_name: 'common-name.com',
country: 'France',
exclude_cn_from_sans: true,
expiry_date: {},
ip_sans: 'OCTET STRING : C09E0126', // when parsed, should be 192.158.1.38
issue_date: {},
locality: 'Paris',
max_path_length: 17,
not_valid_after: 1989622380,
not_valid_before: 1674262350,
organization: 'Widget',
ou: 'Finance',
parsing_errors: [],
permitted_dns_domains: 'dnsname1.com, dsnname2.com',
postal_code: '123456',
province: 'Champagne',
serial_number: 'cereal1292',
signature_bits: '512',
street_address: '234 sesame',
ttl: '87600h',
uri_sans: 'testuri1, testuri2',
use_pss: false,
},
'it contains expected attrs, cn is excluded from alt_names (exclude_cn_from_sans: true)'
);
assert.ok(
isSameDay(
addHours(fromUnixTime(parsedCert.not_valid_before), Number(parsedCert.ttl.split('h')[0])),
fromUnixTime(parsedCert.not_valid_after),
'ttl value is correct'
)
);
});
test('it parses a certificate with use_pass=true and exclude_cn_from_sans=false', async function (assert) {
assert.expect(2);
const parsedPssCert = parseCertificate(pssTrueCert);
assert.propContains(
parsedPssCert,
{ signature_bits: '256', ttl: '768h', use_pss: true },
'returns signature_bits value and use_pss is true'
);
assert.propContains(
parsedPssCert,
{
alt_names: 'common-name.com',
can_parse: true,
common_name: 'common-name.com',
exclude_cn_from_sans: false,
},
'common name is included in alt_names'
);
});
test('it returns parsing_errors when certificate has unsupported values', async function (assert) {
assert.expect(2);
const parsedCert = parseCertificate(unsupportedOids); // contains unsupported subject and extension OIDs
const parsingErrors = this.getErrorMessages(parsedCert.parsing_errors);
assert.propContains(
parsedCert,
{
alt_names: 'dns-NameSupported',
common_name: 'fancy-cert-unsupported-subj-and-ext-oids',
ip_sans: 'OCTET STRING : C09E0126', // when parsed, should be 192.158.1.38
parsing_errors: [{}, {}, {}],
uri_sans: 'uriSupported',
},
'supported values are present when unsupported values exist'
);
assert.propEqual(
parsingErrors,
[
'certificate contains unsupported subject OIDs',
'certificate contains unsupported extension OIDs',
'subjectAltName contains unsupported types',
],
'it contains expected error messages'
);
});
test('it returns attr with a null value if nonexistent', async function (assert) {
assert.expect(1);
const onlyHasCommonName = parseCertificate(skeletonCert);
assert.propContains(
onlyHasCommonName,
{
alt_names: 'common-name.com',
common_name: 'common-name.com',
country: null,
ip_sans: null,
locality: null,
max_path_length: undefined,
organization: null,
ou: null,
postal_code: null,
province: null,
serial_number: null,
street_address: null,
uri_sans: null,
},
'it contains expected attrs'
);
});
test('the helper parseSubject returns object with correct key/value pairs', async function (assert) {
assert.expect(3);
const supportedSubj = parseSubject(this.parsableLoadedCert.subject.typesAndValues);
assert.propEqual(
supportedSubj,
{
subjErrors: [],
subjValues: {
common_name: 'common-name.com',
country: 'France',
locality: 'Paris',
organization: 'Widget',
ou: 'Finance',
postal_code: '123456',
province: 'Champagne',
serial_number: 'cereal1292',
street_address: '234 sesame',
},
},
'it returns supported subject values'
);
const unsupportedSubj = parseSubject(this.parsableUnsupportedCert.subject.typesAndValues);
assert.propEqual(
this.getErrorMessages(unsupportedSubj.subjErrors),
['certificate contains unsupported subject OIDs'],
'it returns subject errors'
);
assert.ok(
unsupportedSubj.subjErrors.every((e) => e instanceof Error),
'subjErrors contain error objects'
);
});
test('the helper parseExtensions returns object with correct key/value pairs', async function (assert) {
assert.expect(9);
// assert supported extensions return correct type
const supportedExtensions = parseExtensions(this.parsableLoadedCert.extensions);
let { extValues, extErrors } = supportedExtensions;
for (const keyName in SAN_TYPES) {
assert.ok(Array.isArray(extValues[keyName]), `${keyName} is an array`);
}
assert.ok(Array.isArray(extValues.permitted_dns_domains), 'permitted_dns_domains is an array');
assert.ok(Number.isInteger(extValues.max_path_length), 'max_path_length is an integer');
// TODO add assertion for key_usage
assert.strictEqual(extErrors.length, 0, 'no extension errors');
// assert unsupported extensions return errors
const unsupportedExt = parseExtensions(this.parsableUnsupportedCert.extensions);
({ extValues, extErrors } = unsupportedExt);
assert.propEqual(
this.getErrorMessages(extErrors),
['certificate contains unsupported extension OIDs', 'subjectAltName contains unsupported types'],
'it returns extension errors'
);
assert.ok(
extErrors.every((e) => e instanceof Error),
'subjErrors contain error objects'
);
assert.ok(Number.isInteger(extValues.max_path_length), 'max_path_length is an integer');
});
test('the helper formatValues returns object with correct types', async function (assert) {
assert.expect(1);
const supportedSubj = parseSubject(this.parsableLoadedCert.subject.typesAndValues);
const supportedExtensions = parseExtensions(this.parsableLoadedCert.extensions);
assert.propContains(
formatValues(supportedSubj, supportedExtensions),
{
alt_names: 'altname1, altname2',
ip_sans: 'OCTET STRING : C09E0126', // when parsed, should be 192.158.1.38
permitted_dns_domains: 'dnsname1.com, dsnname2.com',
uri_sans: 'testuri1, testuri2',
parsing_errors: [],
exclude_cn_from_sans: true,
},
`values for ${Object.keys(SAN_TYPES).join(', ')} are comma separated strings (and no longer arrays)`
);
});
test('it fails silently when passed null', async function (assert) {
assert.expect(3);
const parsedCert = parseCertificate(certWithoutCN);
assert.propEqual(
parsedCert,
{
can_parse: true,
common_name: null,
country: null,
exclude_cn_from_sans: false,
expiry_date: {},
issue_date: {},
locality: null,
max_path_length: 10,
not_valid_after: 1989876490,
not_valid_before: 1674516490,
organization: null,
ou: null,
parsing_errors: [{}, {}],
postal_code: null,
province: null,
serial_number: null,
signature_bits: '256',
street_address: null,
ttl: '87600h',
use_pss: false,
},
'it parses a cert without CN'
);
const parsingErrors = this.getErrorMessages(parsedCert.parsing_errors);
assert.propEqual(
parsingErrors,
['certificate contains unsupported subject OIDs', 'certificate contains unsupported extension OIDs'],
'it returns correct errors'
);
assert.propEqual(
formatValues(null, null),
{ parsing_errors: [Error('error parsing certificate')] },
'it returns error if unable to format values'
);
});
});

View File

@ -15994,13 +15994,6 @@ punycode@^2.1.0, punycode@^2.1.1:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
pvtsutils@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.2.tgz#9f8570d132cdd3c27ab7d51a2799239bf8d8d5de"
integrity sha512-+Ipe2iNUyrZz+8K/2IOo+kKikdtfhRKzNpQbruF2URmqPtoqAs8g3xS7TJvFF2GcPXjh7DkqMnpVveRFq4PgEQ==
dependencies:
tslib "^2.4.0"
pvutils@^1.0.17, pvutils@latest:
version "1.0.17"
resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.0.17.tgz#ade3c74dfe7178944fe44806626bd2e249d996bf"
@ -18225,7 +18218,7 @@ tslib@^2.0.3:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==
tslib@^2.1.0, tslib@^2.4.0:
tslib@^2.1.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"
integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==