diff --git a/ui/app/adapters/pki-ca-certificate.js b/ui/app/adapters/pki-ca-certificate.js index b07fab16ec..db681f644d 100644 --- a/ui/app/adapters/pki-ca-certificate.js +++ b/ui/app/adapters/pki-ca-certificate.js @@ -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 { diff --git a/ui/app/helpers/parse-pki-cert.js b/ui/app/helpers/parse-pki-cert.js deleted file mode 100644 index b049e95b6a..0000000000 --- a/ui/app/helpers/parse-pki-cert.js +++ /dev/null @@ -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); diff --git a/ui/app/models/pki/certificate.js b/ui/app/models/pki/certificate.js deleted file mode 100644 index 33546aa893..0000000000 --- a/ui/app/models/pki/certificate.js +++ /dev/null @@ -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; -} diff --git a/ui/app/models/pki/certificate/base.js b/ui/app/models/pki/certificate/base.js index 3453167e8c..395f5b8078 100644 --- a/ui/app/models/pki/certificate/base.js +++ b/ui/app/models/pki/certificate/base.js @@ -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; diff --git a/ui/app/models/pki/issuer.js b/ui/app/models/pki/issuer.js index 814e3a043c..d7902ea761 100644 --- a/ui/app/models/pki/issuer.js +++ b/ui/app/models/pki/issuer.js @@ -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', diff --git a/ui/app/models/pki/role.js b/ui/app/models/pki/role.js index 8030faefc0..29a40d7b59 100644 --- a/ui/app/models/pki/role.js +++ b/ui/app/models/pki/role.js @@ -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 diff --git a/ui/app/serializers/pki/cert.js b/ui/app/serializers/pki/cert.js index 451e511ce0..4e2f4e9346 100644 --- a/ui/app/serializers/pki/cert.js +++ b/ui/app/serializers/pki/cert.js @@ -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 }; diff --git a/ui/app/serializers/pki/certificate/base.js b/ui/app/serializers/pki/certificate/base.js index 30e904ae7d..2da71e6d23 100644 --- a/ui/app/serializers/pki/certificate/base.js +++ b/ui/app/serializers/pki/certificate/base.js @@ -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, diff --git a/ui/app/serializers/pki/issuer.js b/ui/app/serializers/pki/issuer.js index b8e0bff99d..286e19add3 100644 --- a/ui/app/serializers/pki/issuer.js +++ b/ui/app/serializers/pki/issuer.js @@ -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 { diff --git a/ui/app/styles/components/form-section.scss b/ui/app/styles/components/form-section.scss index a6bbe524c2..3d6a94a403 100644 --- a/ui/app/styles/components/form-section.scss +++ b/ui/app/styles/components/form-section.scss @@ -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; } diff --git a/ui/app/utils/parse-pki-cert-oids.js b/ui/app/utils/parse-pki-cert-oids.js new file mode 100644 index 0000000000..e7bc91c2cd --- /dev/null +++ b/ui/app/utils/parse-pki-cert-oids.js @@ -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 +}; diff --git a/ui/app/utils/parse-pki-cert.js b/ui/app/utils/parse-pki-cert.js new file mode 100644 index 0000000000..51f6aac50d --- /dev/null +++ b/ui/app/utils/parse-pki-cert.js @@ -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]; +} diff --git a/ui/lib/core/addon/components/form-field.hbs b/ui/lib/core/addon/components/form-field.hbs index 72cfcd5d9b..3bdfe14949 100644 --- a/ui/lib/core/addon/components/form-field.hbs +++ b/ui/lib/core/addon/components/form-field.hbs @@ -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 }} diff --git a/ui/lib/core/addon/components/string-list.hbs b/ui/lib/core/addon/components/string-list.hbs index 095bb047bf..b27d8386ba 100644 --- a/ui/lib/core/addon/components/string-list.hbs +++ b/ui/lib/core/addon/components/string-list.hbs @@ -1,5 +1,5 @@
{{/if}} diff --git a/ui/lib/pki/addon/components/pki-key-usage.hbs b/ui/lib/pki/addon/components/pki-key-usage.hbs index c17de8cfde..77bd9a1a24 100644 --- a/ui/lib/pki/addon/components/pki-key-usage.hbs +++ b/ui/lib/pki/addon/components/pki-key-usage.hbs @@ -19,6 +19,7 @@ />
\ No newline at end of file diff --git a/ui/package.json b/ui/package.json index a647e4426d..3523fd67d3 100644 --- a/ui/package.json +++ b/ui/package.json @@ -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" } } diff --git a/ui/tests/helpers/pki/values.js b/ui/tests/helpers/pki/values.js index c4059c334d..aab827beba 100644 --- a/ui/tests/helpers/pki/values.js +++ b/ui/tests/helpers/pki/values.js @@ -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-----`; diff --git a/ui/tests/integration/components/pki/page/pki-certificate-details-test.js b/ui/tests/integration/components/pki/page/pki-certificate-details-test.js index b8dbdfc3d6..4bde1237ba 100644 --- a/ui/tests/integration/components/pki/page/pki-certificate-details-test.js +++ b/ui/tests/integration/components/pki/page/pki-certificate-details-test.js @@ -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'); diff --git a/ui/tests/integration/components/pki/pki-role-form-test.js b/ui/tests/integration/components/pki/pki-role-form-test.js index 2065b3f0f1..5a1fcc73d0 100644 --- a/ui/tests/integration/components/pki/pki-role-form-test.js +++ b/ui/tests/integration/components/pki/pki-role-form-test.js @@ -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); diff --git a/ui/tests/integration/utils/parse-pki-cert-test.js b/ui/tests/integration/utils/parse-pki-cert-test.js new file mode 100644 index 0000000000..7b14de51c2 --- /dev/null +++ b/ui/tests/integration/utils/parse-pki-cert-test.js @@ -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' + ); + }); +}); diff --git a/ui/yarn.lock b/ui/yarn.lock index 108ec9a346..57861f9ddd 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -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==