open-vault/ui/tests/integration/utils/parse-pki-cert-test.js
Alexander Scheel fcb24ad8bc
Add support for missing attributes in PKI UI (#18953)
* Add additional OIDs for extKeyUsage

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Allow ignoring AIA info on issuers

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Tell users which extension OIDs are not allowed

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add commentary on cross-signing failure modes

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add parsing of keyUsage

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Remove ext_key_usage parsing - doesn't exist on API

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add support for parsing ip_sans attribute

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Use Uint8Array directly for key_usage parsing

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add error on unknown key usage values

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Fix typing of IPv6 SANs, verficiation of keyUsages

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Correctly format ip addresses

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* add ip_sans to details page

* fix typo

* update tests

* alphabetize attrs

* hold off on ip compression

* rename model attrs

* parse other_names

* is that illegal

* add parenthesis to labels

* update tests to account for other_sans

---------

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
Co-authored-by: clairebontempo@gmail.com <clairebontempo@gmail.com>
Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>
2023-02-03 11:36:02 -08:00

273 lines
9.4 KiB
JavaScript

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',
other_sans: '1.3.1.4.1.5.9.2.6;UTF8:some-utf-string',
exclude_cn_from_sans: true,
expiry_date: {},
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: 1678210083,
not_valid_before: 1675445253,
organization: 'Widget',
ou: 'Finance',
parsing_errors: [],
permitted_dns_domains: 'dnsname1.com, dsnname2.com',
postal_code: '123456',
province: 'Champagne',
serial_number: 'cereal1292',
signature_bits: '256',
street_address: '234 sesame',
ttl: '768h',
uri_sans: 'testuri1, testuri2',
use_pss: false,
},
'it contains expected attrs, cn is excluded from alt_names (exclude_cn_from_sans: true) and ipV6 is compressed correctly'
);
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: '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: 1.2.840.113549.1.9.1',
'certificate contains unsupported extension OIDs: 2.5.29.37',
],
'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: 1.2.840.113549.1.9.1'],
'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(11);
// 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');
assert.propEqual(extValues.key_usage, ['CertSign', 'CRLSign'], 'parses 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: 2.5.29.37'],
'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: '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: [],
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: {},
key_usage: null,
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: 1.2.840.113549.1.9.1',
'certificate contains unsupported extension OIDs: 2.5.29.37',
],
'it returns correct errors'
);
assert.propEqual(
formatValues(null, null),
{ parsing_errors: [Error('error parsing certificate')] },
'it returns error if unable to format values'
);
});
});