diff --git a/ui/app/models/pki/certificate/base.js b/ui/app/models/pki/certificate/base.js index 4bd80f7487..143c420768 100644 --- a/ui/app/models/pki/certificate/base.js +++ b/ui/app/models/pki/certificate/base.js @@ -49,8 +49,9 @@ export default class PkiCertificateBaseModel extends Model { // Parsed from cert in serializer @attr('number', { formatDate: true }) notValidAfter; @attr('number', { formatDate: true }) notValidBefore; - @attr('string') uriSans; - @attr('string') altNames; + @attr('string', { label: 'URI Subject Alternative Names (URI SANs)' }) uriSans; + @attr('string', { label: 'IP Subject Alternative Names (IP SANs)' }) ipSans; + @attr('string', { label: 'Subject Alternative Names (SANs)' }) altNames; @attr('string') signatureBits; // For importing diff --git a/ui/app/models/pki/issuer.js b/ui/app/models/pki/issuer.js index 5df3b3466e..9fe49c19d7 100644 --- a/ui/app/models/pki/issuer.js +++ b/ui/app/models/pki/issuer.js @@ -16,6 +16,7 @@ const issuerUrls = ['issuingCertificates', 'crlDistributionPoints', 'ocspServers 'serialNumber', 'keyId', 'uriSans', + 'ipSans', 'notValidBefore', 'notValidAfter', ], @@ -38,11 +39,6 @@ export default class PkiIssuerModel extends PkiCertificateBaseModel { }) keyId; - @attr({ - label: 'URI Subject Alternative Names (URI SANs)', - }) - uriSans; - @attr('string') issuerName; @attr({ diff --git a/ui/app/utils/parse-pki-cert-oids.js b/ui/app/utils/parse-pki-cert-oids.js index e7bc91c2cd..715bc5c48d 100644 --- a/ui/app/utils/parse-pki-cert-oids.js +++ b/ui/app/utils/parse-pki-cert-oids.js @@ -13,7 +13,7 @@ export const SUBJECT_OIDs = { }; export const EXTENSION_OIDs = { - key_usage: '2.5.29.15', + key_usage: '2.5.29.15', // contains keyUsage values (KEY_USAGE_BITS below) 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 @@ -21,12 +21,29 @@ export const EXTENSION_OIDs = { // 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', + // These two extensions are controlled by the parent authority. authority_key_identifier: '2.5.29.35', + authority_access_info: '1.3.6.1.5.5.7.1.1', + // This extension is based off the key material of the new issuer, which + // will automatically match the existing issuer's key material. + subject_key_identifier: '2.5.29.14', }; +export const KEY_USAGE_BITS = [ + 'DigitalSignature', + 'ContentCommitment', + 'KeyEncipherment', + 'DataEncipherment', + 'KeyAgreement', + 'CertSign', + 'CRLSign', + 'EncipherOnly', + 'DecipherOnly', +]; + // SubjectAltName/GeneralName types (scroll up to page 38 -> https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.7 ) export const SAN_TYPES = { + other_sans: 0, // ;UTF8: alt_names: 2, // dNSName uri_sans: 6, // uniformResourceIdentifier ip_sans: 7, // iPAddress - OCTET STRING diff --git a/ui/app/utils/parse-pki-cert.js b/ui/app/utils/parse-pki-cert.js index 51f6aac50d..b95976f85d 100644 --- a/ui/app/utils/parse-pki-cert.js +++ b/ui/app/utils/parse-pki-cert.js @@ -4,10 +4,11 @@ import { Certificate } from 'pkijs'; import { differenceInHours, getUnixTime } from 'date-fns'; import { EXTENSION_OIDs, - SUBJECT_OIDs, IGNORED_OIDs, + KEY_USAGE_BITS, SAN_TYPES, SIGNATURE_ALGORITHM_OIDs, + SUBJECT_OIDs, } from './parse-pki-cert-oids'; /* @@ -87,8 +88,6 @@ export function formatValues(subject, extension) { } } - // TODO remove this deletion when key_usage is parsed, update test - delete extValues.key_usage; return { ...subjValues, ...extValues, @@ -114,9 +113,13 @@ 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 isUnexpectedSubjectOid = (rdn) => !Object.values(SUBJECT_OIDs).includes(rdn.type); + if (subject.any(isUnexpectedSubjectOid)) { + const unknown = subject.filter(isUnexpectedSubjectOid).map((rdn) => rdn.type); + errors.push(new Error('certificate contains unsupported subject OIDs: ' + unknown.join(', '))); } + 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 @@ -134,8 +137,10 @@ export function parseExtensions(extensions) { 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')); + const isUnknownExtension = (ext) => !allowedOids.includes(ext.extnID); + if (extensions.any(isUnknownExtension)) { + const unknown = extensions.filter(isUnknownExtension).map((ext) => ext.extnID); + errors.push(new Error('certificate contains unsupported extension OIDs: ' + unknown.join(', '))); } // make each extension its own key/value pair @@ -149,7 +154,7 @@ export function parseExtensions(extensions) { const supportedNames = Object.keys(SAN_TYPES); const sans = values.subject_alt_name?.altNames; if (!sans) { - errors.push(new Error('certificate contains unsupported subjectAltName values')); + errors.push(new Error('certificate contains an unsupported subjectAltName construction')); } else if (sans.any((san) => !supportedTypes.includes(san.type))) { // pass along error that unsupported values exist errors.push(new Error('subjectAltName contains unsupported types')); @@ -202,11 +207,108 @@ export function parseExtensions(extensions) { } if (values.ip_sans) { - // TODO parse octet string for IP addresses + const parsed_ips = []; + for (const ip_san of values.ip_sans) { + const unused = ip_san.valueBlock.unusedBits; + if (unused !== undefined && unused !== null && unused !== 0) { + errors.push(new Error('unsupported ip_san value: non-zero unused bits in encoding')); + continue; + } + + const ip = new Uint8Array(ip_san.valueBlock.valueHex); + + // Length of the IP determines the type: 4 bytes for IPv4, 16 bytes for + // IPv6. + if (ip.length === 4) { + const ip_addr = ip.join('.'); + parsed_ips.push(ip_addr); + } else if (ip.length === 16) { + const src = new Array(...ip); + const hex = src.map((value) => '0' + new Number(value).toString(16)); + const trimmed = hex.map((value) => value.slice(value.length - 2, 3)); + // add a colon after every other number (those with an odd index) + let ip_addr = trimmed.map((value, index) => (index % 2 === 0 ? value : value + ':')).join(''); + // Remove trailing :, if any. + ip_addr = ip_addr.slice(-1) === ':' ? ip_addr.slice(0, -1) : ip_addr; + parsed_ips.push(ip_addr); + } else { + errors.push( + new Error( + 'unsupported ip_san value: unknown IP address size (should be 4 or 16 bytes, was ' + + parseInt(ip.length / 2) + + ')' + ) + ); + } + } + values.ip_sans = parsed_ips; } if (values.key_usage) { - // TODO parse key_usage + // KeyUsage is a big-endian bit-packed enum. Unused right-most bits are + // truncated. So, a KeyUsage with CertSign+CRLSign would be "000001100", + // with the right two bits truncated, and packed into an 8-bit, one-byte + // string ("00000011"), introducing a leading zero. unused indicates that + // this bit can be discard, shifting our result over by one, to go back + // to its original form (minus trailing zeros). + // + // We can thus take our enumeration (KEY_USAGE_BITS), check whether the + // bits are asserted, and push in our pretty names as appropriate. + const unused = values.key_usage.valueBlock.unusedBits; + const keyUsage = new Uint8Array(values.key_usage.valueBlock.valueHex); + + const computedKeyUsages = []; + for (const enumIndex in KEY_USAGE_BITS) { + // May span two bytes. + const byteIndex = parseInt(enumIndex / 8); + const bitIndex = parseInt(enumIndex % 8); + const enumName = KEY_USAGE_BITS[enumIndex]; + const mask = 1 << (8 - bitIndex); // Big endian. + if (byteIndex >= keyUsage.length) { + // DecipherOnly is rare and would push into a second byte, but we + // don't have one so exit. + break; + } + + let enumByte = keyUsage[byteIndex]; + const needsAdjust = byteIndex + 1 === keyUsage.length && unused > 0; + if (needsAdjust) { + enumByte = parseInt(enumByte << unused); + } + + const isSet = (mask & enumByte) === mask; + if (isSet) { + computedKeyUsages.push(enumName); + } + } + + // Vault currently doesn't allow setting key_usage during issuer + // generation, but will allow it if it comes in via an externally + // generated CSR. Validate that key_usage matches expectations and + // prune accordingly. + const expectedUsages = ['CertSign', 'CRLSign']; + const isUnexpectedKeyUsage = (ext) => !expectedUsages.includes(ext); + + if (computedKeyUsages.any(isUnexpectedKeyUsage)) { + const unknown = computedKeyUsages.filter(isUnexpectedKeyUsage); + errors.push(new Error('unsupported key usage value on issuer certificate: ' + unknown.join(', '))); + } + + values.key_usage = computedKeyUsages; + } + + if (values.other_sans) { + // We need to parse these into their server-side values. + const parsed_sans = []; + for (const san of values.other_sans) { + let [objectId, constructed] = san.valueBlock.value; + objectId = objectId.toJSON().valueBlock.value; + constructed = constructed.valueBlock.value[0].toJSON(); // can I just grab the first element here? + const { blockName } = constructed; + const value = constructed.valueBlock.value; + parsed_sans.push(`${objectId};${blockName.replace('String', '')}:${value}`); + } + values.other_sans = parsed_sans; } delete values.subject_alt_name; @@ -220,8 +322,8 @@ export function parseExtensions(extensions) { "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 + "key_usage": ['CertSign', 'CRLSign'], + "ip_sans": ['192.158.1.38', '1234:fd2:5621:1:89::4500'], } */ } diff --git a/ui/lib/pki/addon/components/pki-issuer-cross-sign.hbs b/ui/lib/pki/addon/components/pki-issuer-cross-sign.hbs index 5b87c5ed71..7bf19bd97f 100644 --- a/ui/lib/pki/addon/components/pki-issuer-cross-sign.hbs +++ b/ui/lib/pki/addon/components/pki-issuer-cross-sign.hbs @@ -11,7 +11,7 @@ update the manual_chain - for each of your new certificates listed below,w and the existing issuer. + for each of your new certificates listed below, and the existing issuer.

{{#each this.inputFields as |column|}} diff --git a/ui/lib/pki/addon/components/pki-issuer-cross-sign.js b/ui/lib/pki/addon/components/pki-issuer-cross-sign.js index 4b4510b83d..43f0ef984c 100644 --- a/ui/lib/pki/addon/components/pki-issuer-cross-sign.js +++ b/ui/lib/pki/addon/components/pki-issuer-cross-sign.js @@ -99,6 +99,8 @@ export default class PkiIssuerCrossSign extends Component { @action async crossSignIntermediate(intMount, intName, newCrossSignedIssuer) { // 1. Fetch issuer we want to sign + // + // What/Recovery: any failure is early enough that you can bail safely/normally. const existingIssuer = await this.store.queryRecord('pki/issuer', { backend: intMount, id: intName, @@ -115,6 +117,8 @@ export default class PkiIssuerCrossSign extends Component { } // 2. Create the new CSR + // + // What/Recovery: any failure is early enough that you can bail safely/normally. const newCsr = await this.store .createRecord('pki/action', { keyRef: existingIssuer.keyId, @@ -127,7 +131,10 @@ export default class PkiIssuerCrossSign extends Component { }) .then(({ csr }) => csr); - // 3. Sign newCSR with correct parent to create cross-signed cert + // 3. Sign newCSR with correct parent to create cross-signed cert, "issuing" + // an intermediate certificate. + // + // What/Recovery: any failure is early enough that you can bail safely/normally. const signedCaChain = await this.store .createRecord('pki/action', { csr: newCsr, @@ -144,6 +151,35 @@ export default class PkiIssuerCrossSign extends Component { .then(({ caChain }) => caChain.join('\n')); // 4. Import the newly cross-signed cert to become an issuer + // + // What/Recovery: + // + // 1. Permission issue -> give the cert (`signedCaChain`) to the user, + // let them import & name. (Issue you have is that you already issued + // it (step 3) and so "undo" would mean revoking the cert, which + // you might not have permissions to do either). + // + // 2. CRL rebuilding fails ("the CRL" in error message). Server returns + // an error, we wanted the CRL rebuilt -- but the issuer was still + // imported anyways. Only way to detect would be to do a list issuers + // before and after. Recovery would be on the operator in this case; + // reproduce the error and let them deal with it. + // + // End result: user should solve this issue, but we shouldn't undo anything + // either. + // + // -> For 1 though, make sure to give the `signedCaChain` in the + // error message for them. + // -> For 2, you could list before and after to find the id of the + // new issuer(s) so they can name them and fix any issues with + // them. + // + // If its not a permissions error _and_ you did two lists, not finding + // a new issuer... + // + // -> Unknown error. Could give them `signedCaChain` and serial of + // the newly issued intermediate CA, so that they can do recovery + // as they'd like. const issuerId = await this.store .createRecord('pki/issuer', { pemBundle: signedCaChain }) .save({ adapterOptions: { import: true, mount: intMount } }) @@ -155,9 +191,15 @@ export default class PkiIssuerCrossSign extends Component { }); // 5. Fetch issuer imported above by issuer_id, name and save + // Recovery: cosmetic issue; can let the user deal with it. Usually + // fails because the name is in use. + // Pre-fix: list all issuers, check the desired name isn't either + // an existing issuer_id or an issuer_name. const crossSignedIssuer = await this.store.queryRecord('pki/issuer', { backend: intMount, id: issuerId }); crossSignedIssuer.issuerName = newCrossSignedIssuer; - crossSignedIssuer.save({ adapterOptions: { mount: intMount } }); + await crossSignedIssuer.save({ adapterOptions: { mount: intMount } }); + + // 6. Return the data to our caller. return { intermediateIssuer: existingIssuer, newCrossSignedIssuer: crossSignedIssuer, diff --git a/ui/tests/acceptance/pki/pki-engine-workflow-test.js b/ui/tests/acceptance/pki/pki-engine-workflow-test.js index b90eae5ee0..4d44bd095c 100644 --- a/ui/tests/acceptance/pki/pki-engine-workflow-test.js +++ b/ui/tests/acceptance/pki/pki-engine-workflow-test.js @@ -427,7 +427,7 @@ module('Acceptance | pki workflow', function (hooks) { assert.dom(SELECTORS.issuerDetails.title).hasText('View issuer certificate'); assert .dom(`${SELECTORS.issuerDetails.defaultGroup} ${SELECTORS.issuerDetails.row}`) - .exists({ count: 9 }, 'Renders 9 info table items under default group'); + .exists({ count: 10 }, 'Renders 10 info table items under default group'); assert .dom(`${SELECTORS.issuerDetails.urlsGroup} ${SELECTORS.issuerDetails.row}`) .exists({ count: 3 }, 'Renders 4 info table items under URLs group'); diff --git a/ui/tests/helpers/pki/values.js b/ui/tests/helpers/pki/values.js index c7cf3fb9f8..bc4c19fb94 100644 --- a/ui/tests/helpers/pki/values.js +++ b/ui/tests/helpers/pki/values.js @@ -127,7 +127,7 @@ Q4raFvQrZth3Cz/X5yPTtQL78oBYrmHzoQKDFJ2z // 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-----`; +export const loadedCert = `-----BEGIN CERTIFICATE-----\nMIIE7TCCA9WgAwIBAgIULcrWXSz3/kG81EgBo0A4Zt+ZgkYwDQYJKoZIhvcNAQEL\nBQAwga0xDzANBgNVBAYTBkZyYW5jZTESMBAGA1UECBMJQ2hhbXBhZ25lMQ4wDAYD\nVQQHEwVQYXJpczETMBEGA1UECRMKMjM0IHNlc2FtZTEPMA0GA1UEERMGMTIzNDU2\nMQ8wDQYDVQQKEwZXaWRnZXQxEDAOBgNVBAsTB0ZpbmFuY2UxGDAWBgNVBAMTD2Nv\nbW1vbi1uYW1lLmNvbTETMBEGA1UEBRMKY2VyZWFsMTI5MjAeFw0yMzAyMDMxNzI3\nMzNaFw0yMzAzMDcxNzI4MDNaMIGtMQ8wDQYDVQQGEwZGcmFuY2UxEjAQBgNVBAgT\nCUNoYW1wYWduZTEOMAwGA1UEBxMFUGFyaXMxEzARBgNVBAkTCjIzNCBzZXNhbWUx\nDzANBgNVBBETBjEyMzQ1NjEPMA0GA1UEChMGV2lkZ2V0MRAwDgYDVQQLEwdGaW5h\nbmNlMRgwFgYDVQQDEw9jb21tb24tbmFtZS5jb20xEzARBgNVBAUTCmNlcmVhbDEy\nOTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC8NO7LXHp28SzOqmQv\nns4fGogKydEklWG4JEN3pM+k9nTyEgA8DFhtSLvcqF0cqhEydw4FVU+LEUGySUer\nmM4VNl9qglFBgmYE8TNgWkUw9ZP6MNgx13I8zXTXOIDj0iwXks02x8451oPbqqdq\nXsCc4vSP7BPwQOjc0C56c54zyRC1zFm9jlh+As0QinuYcjFjVabCku6JSYc4kunh\nz7derU9cURUxB5/ja9zC7jGS8tg4XUWdUkbj1O/krEWfjQx9Kj8aEU1gFfAvW/Bd\nIqgAlHATYN6i8HDmAmdGty9zLht9wUgnAtVh3lK3939h/rI0qCLV6N/RjCC7csnz\n9I67AgMBAAGjggEBMIH+MA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/\nAgERMB0GA1UdDgQWBBSSdKle0wMGy0jvPmcoDanGduhLqzAfBgNVHSMEGDAWgBSS\ndKle0wMGy0jvPmcoDanGduhLqzAuBgNVHR4BAf8EJDAioCAwDoIMZG5zbmFtZTEu\nY29tMA6CDGRzbm5hbWUyLmNvbTBoBgNVHREEYTBfoB0GCCsBBAEFCQIGoBEMD3Nv\nbWUtdXRmLXN0cmluZ4IIYWx0bmFtZTGCCGFsdG5hbWUyhwTAngEmhxASNA/SViEA\nAQCJAAAAAEUAhgh0ZXN0dXJpMYYIdGVzdHVyaTIwDQYJKoZIhvcNAQELBQADggEB\nAAQukDwIg01QLQK2MQqjePNZlJleKLMK9LiabyGqc7u4bgmX3gYIrH7uopvO5cIv\nvqxcVBATQ6ez29t5MagzDu1+vnwE8fQhRoe0sp5TRLiGSlBJf53+0Wb3vbaOT0Fx\n/FFK0f2wHqYv3h/CTxu8YxDY4DwCRTPJ2KfTvT85BXtTUlzKIp1ytALSKcz0Owoe\neQPtQUdi8UHef8uHuWbk7DftMXojXbCqtHQdS3Rrl9zyc+Ds67flb5hKEseQZRgw\ntPtAIxhjSfZPTjl/3aasCBikESdeS8IOxIXL1bGun0xWnIBBc9uRe8hpdPjZj7Eh\nIt7ucIzFep0DLWCeQrAHeqo=\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 diff --git a/ui/tests/integration/components/pki/pki-issuer-cross-sign-test.js b/ui/tests/integration/components/pki/pki-issuer-cross-sign-test.js index 2cdfc611ee..06dabdd4df 100644 --- a/ui/tests/integration/components/pki/pki-issuer-cross-sign-test.js +++ b/ui/tests/integration/components/pki/pki-issuer-cross-sign-test.js @@ -337,7 +337,7 @@ module('Integration | Component | pki issuer cross sign', function (hooks) { assert .dom('[data-test-alert-banner="alert"] .alert-banner-message-body') .hasText( - 'certificate contains unsupported subject OIDs, certificate contains unsupported extension OIDs, subjectAltName contains unsupported types ' + 'certificate contains unsupported subject OIDs: 1.2.840.113549.1.9.1, certificate contains unsupported extension OIDs: 2.5.29.37' ); for (const field of FIELDS) { diff --git a/ui/tests/integration/utils/parse-pki-cert-test.js b/ui/tests/integration/utils/parse-pki-cert-test.js index 7b14de51c2..82856e2bee 100644 --- a/ui/tests/integration/utils/parse-pki-cert-test.js +++ b/ui/tests/integration/utils/parse-pki-cert-test.js @@ -41,14 +41,16 @@ module('Integration | Util | parse pki certificate', function (hooks) { can_parse: true, common_name: 'common-name.com', country: 'France', + other_sans: '1.3.1.4.1.5.9.2.6;UTF8:some-utf-string', exclude_cn_from_sans: true, expiry_date: {}, - ip_sans: 'OCTET STRING : C09E0126', // when parsed, should be 192.158.1.38 + ip_sans: '192.158.1.38, 1234:0fd2:5621:0001:0089:0000:0000:4500', + key_usage: 'CertSign, CRLSign', issue_date: {}, locality: 'Paris', max_path_length: 17, - not_valid_after: 1989622380, - not_valid_before: 1674262350, + not_valid_after: 1678210083, + not_valid_before: 1675445253, organization: 'Widget', ou: 'Finance', parsing_errors: [], @@ -56,13 +58,13 @@ module('Integration | Util | parse pki certificate', function (hooks) { postal_code: '123456', province: 'Champagne', serial_number: 'cereal1292', - signature_bits: '512', + signature_bits: '256', street_address: '234 sesame', - ttl: '87600h', + ttl: '768h', uri_sans: 'testuri1, testuri2', use_pss: false, }, - 'it contains expected attrs, cn is excluded from alt_names (exclude_cn_from_sans: true)' + 'it contains expected attrs, cn is excluded from alt_names (exclude_cn_from_sans: true) and ipV6 is compressed correctly' ); assert.ok( isSameDay( @@ -97,14 +99,13 @@ module('Integration | Util | parse pki certificate', function (hooks) { 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: [{}, {}, {}], + ip_sans: '192.158.1.38', + parsing_errors: [{}, {}], uri_sans: 'uriSupported', }, 'supported values are present when unsupported values exist' @@ -112,9 +113,8 @@ module('Integration | Util | parse pki certificate', function (hooks) { assert.propEqual( parsingErrors, [ - 'certificate contains unsupported subject OIDs', - 'certificate contains unsupported extension OIDs', - 'subjectAltName contains unsupported types', + 'certificate contains unsupported subject OIDs: 1.2.840.113549.1.9.1', + 'certificate contains unsupported extension OIDs: 2.5.29.37', ], 'it contains expected error messages' ); @@ -169,7 +169,7 @@ module('Integration | Util | parse pki certificate', function (hooks) { const unsupportedSubj = parseSubject(this.parsableUnsupportedCert.subject.typesAndValues); assert.propEqual( this.getErrorMessages(unsupportedSubj.subjErrors), - ['certificate contains unsupported subject OIDs'], + ['certificate contains unsupported subject OIDs: 1.2.840.113549.1.9.1'], 'it returns subject errors' ); assert.ok( @@ -179,7 +179,7 @@ module('Integration | Util | parse pki certificate', function (hooks) { }); test('the helper parseExtensions returns object with correct key/value pairs', async function (assert) { - assert.expect(9); + assert.expect(11); // assert supported extensions return correct type const supportedExtensions = parseExtensions(this.parsableLoadedCert.extensions); let { extValues, extErrors } = supportedExtensions; @@ -188,7 +188,7 @@ module('Integration | Util | parse pki certificate', function (hooks) { } 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.propEqual(extValues.key_usage, ['CertSign', 'CRLSign'], 'parses key_usage'); assert.strictEqual(extErrors.length, 0, 'no extension errors'); // assert unsupported extensions return errors @@ -196,7 +196,7 @@ module('Integration | Util | parse pki certificate', function (hooks) { ({ extValues, extErrors } = unsupportedExt); assert.propEqual( this.getErrorMessages(extErrors), - ['certificate contains unsupported extension OIDs', 'subjectAltName contains unsupported types'], + ['certificate contains unsupported extension OIDs: 2.5.29.37'], 'it returns extension errors' ); assert.ok( @@ -214,7 +214,7 @@ module('Integration | Util | parse pki certificate', function (hooks) { formatValues(supportedSubj, supportedExtensions), { alt_names: 'altname1, altname2', - ip_sans: 'OCTET STRING : C09E0126', // when parsed, should be 192.158.1.38 + ip_sans: '192.158.1.38, 1234:0fd2:5621:0001:0089:0000:0000:4500', permitted_dns_domains: 'dnsname1.com, dsnname2.com', uri_sans: 'testuri1, testuri2', parsing_errors: [], @@ -236,6 +236,7 @@ module('Integration | Util | parse pki certificate', function (hooks) { exclude_cn_from_sans: false, expiry_date: {}, issue_date: {}, + key_usage: null, locality: null, max_path_length: 10, not_valid_after: 1989876490, @@ -256,7 +257,10 @@ module('Integration | Util | parse pki certificate', function (hooks) { const parsingErrors = this.getErrorMessages(parsedCert.parsing_errors); assert.propEqual( parsingErrors, - ['certificate contains unsupported subject OIDs', 'certificate contains unsupported extension OIDs'], + [ + 'certificate contains unsupported subject OIDs: 1.2.840.113549.1.9.1', + 'certificate contains unsupported extension OIDs: 2.5.29.37', + ], 'it returns correct errors' ); assert.propEqual(