mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 10:37:56 +00:00 
			
		
		
		
	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:
		| @@ -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 { | ||||
|   | ||||
| @@ -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); | ||||
| @@ -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; | ||||
| } | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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', | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 }; | ||||
|   | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
|   | ||||
							
								
								
									
										55
									
								
								ui/app/utils/parse-pki-cert-oids.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								ui/app/utils/parse-pki-cert-oids.js
									
									
									
									
									
										Normal 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 | ||||
| }; | ||||
							
								
								
									
										241
									
								
								ui/app/utils/parse-pki-cert.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										241
									
								
								ui/app/utils/parse-pki-cert.js
									
									
									
									
									
										Normal 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]; | ||||
| } | ||||
| @@ -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 }} | ||||
|   | ||||
| @@ -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}} | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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}} | ||||
|   | ||||
| @@ -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> | ||||
| @@ -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" | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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-----`; | ||||
|   | ||||
| @@ -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'); | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
							
								
								
									
										268
									
								
								ui/tests/integration/utils/parse-pki-cert-test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										268
									
								
								ui/tests/integration/utils/parse-pki-cert-test.js
									
									
									
									
									
										Normal 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' | ||||
|     ); | ||||
|   }); | ||||
| }); | ||||
| @@ -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== | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 claire bontempo
					claire bontempo