mirror of
https://github.com/hashicorp/vault.git
synced 2026-02-03 20:40:45 -05:00
* fix parsing of key_usage VAULT-40303 * VAULT-40220 add extended key usage flags and test coverage * add test coverage * add changelog * fix mislabeled role parameter * update tests * VAULT-40219 link jira * update changelog text Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>
This commit is contained in:
parent
e67da36b58
commit
0e6dc73772
11 changed files with 139 additions and 26 deletions
9
changelog/_10416.txt
Normal file
9
changelog/_10416.txt
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
```release-note:bug
|
||||
ui/pki: Fixes certificate parsing of the `key_usage` extension so details accurately reflect certificate values.
|
||||
```
|
||||
```release-note:bug
|
||||
ui/pki: Fixes creating and updating a role so `basic_constraints_valid_for_non_ca` is correctly set.
|
||||
```
|
||||
```release-note:improvement
|
||||
ui/pki: Adds support to configure `server_flag`, `client_flag`, `code_signing_flag`, and `email_protection_flag` parameters for creating/updating a role.
|
||||
```
|
||||
|
|
@ -30,7 +30,7 @@ export default class PkiRoleModel extends Model {
|
|||
'generateLease',
|
||||
'noStore',
|
||||
'noStoreMetadata',
|
||||
'addBasicConstraints',
|
||||
'basicConstraintsValidForNonCa',
|
||||
];
|
||||
if (this.version.isCommunity) {
|
||||
const entFields = ['noStoreMetadata'];
|
||||
|
|
@ -163,7 +163,7 @@ export default class PkiRoleModel extends Model {
|
|||
detailsLabel: 'Add basic constraints',
|
||||
subText: 'Mark Basic Constraints valid when issuing non-CA certificates.',
|
||||
})
|
||||
addBasicConstraints;
|
||||
basicConstraintsValidForNonCa;
|
||||
/* End of overriding default options */
|
||||
|
||||
/* Overriding OpenApi Domain handling options */
|
||||
|
|
|
|||
|
|
@ -33,6 +33,15 @@ export const OTHER_OIDs = {
|
|||
subject_key_identifier: '2.5.29.14',
|
||||
};
|
||||
|
||||
// The order of KEY_USAGE_BITS is critical because the index corresponds
|
||||
// to the bit position in a certificate.
|
||||
// The byte value "10101000" contains
|
||||
// Bit position: 0 1 2 3 4 5 6 7
|
||||
// Byte: 1 0 1 0 1 0 0 0
|
||||
// | | |
|
||||
// | | KeyAgreement (index 4)
|
||||
// | KeyEncipherment (index 2)
|
||||
// DigitalSignature (index 0)
|
||||
export const KEY_USAGE_BITS = [
|
||||
'DigitalSignature',
|
||||
'ContentCommitment',
|
||||
|
|
|
|||
|
|
@ -310,17 +310,19 @@ export function parseExtensions(extensions) {
|
|||
}
|
||||
|
||||
if (values.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;
|
||||
// KeyUsage is a big-endian bit-packed enum encoded as an ASN.1 BIT STRING.
|
||||
// The rightmost bits may be unused padding. For example, a KeyUsage with
|
||||
// CertSign (bit 5) and CRLSign (bit 6) would be encoded as:
|
||||
// Bits: 0 0 0 0 0 1 1 0 = 0x06
|
||||
// Unused bits: 1 (the rightmost bit, bit 8, is padding)
|
||||
// This means there is no data stored beyond bit 7.
|
||||
// The byte value has the data bits in their correct positions.
|
||||
// We can take our enumeration (KEY_USAGE_BITS), check whether each
|
||||
// bit is asserted using bitwise masks, and push the pretty names as appropriate.
|
||||
|
||||
const keyUsage = new Uint8Array(values.key_usage.valueBlock.valueHex);
|
||||
// DEBUG tip! To inspect key_usage in binary:
|
||||
// console.log(keyUsage[0].toString(2).padStart(8, '0'))
|
||||
|
||||
const computedKeyUsages = [];
|
||||
for (const enumIndex in KEY_USAGE_BITS) {
|
||||
|
|
@ -328,19 +330,23 @@ export function parseExtensions(extensions) {
|
|||
const byteIndex = parseInt(enumIndex / 8);
|
||||
const bitIndex = parseInt(enumIndex % 8);
|
||||
const enumName = KEY_USAGE_BITS[enumIndex];
|
||||
const mask = 1 << (8 - bitIndex); // Big endian.
|
||||
// start with 1 (binary 00000001) and << is a "left shift" which moves the 1 bit left by N (7 - bitIndex) positions
|
||||
// so each `mask` sets the bit position as 1 for the corresponding KEY_USAGE_BITS index
|
||||
// For example the `mask` for KeyEncipherment is: 00100000
|
||||
const mask = 1 << (7 - 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 enumByte = keyUsage[byteIndex];
|
||||
// bitwise AND (&) operator compares the bit positions between two numbers.
|
||||
// the resulting number has 1 if BOTH numbers have 1 in that bit position.
|
||||
// 00001000 (mask)
|
||||
// & 10101000 (enumByte)
|
||||
// ----------
|
||||
// 00001000 (result) => If this equals `mask` isSet is true
|
||||
const isSet = (mask & enumByte) === mask;
|
||||
if (isSet) {
|
||||
computedKeyUsages.push(enumName);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,20 @@
|
|||
data-test-key-usage-ext-key-usage-checkboxes
|
||||
/>
|
||||
<div class="has-top-margin-xxl">
|
||||
<div class="has-bottom-margin-s">
|
||||
<FormFieldLabel
|
||||
for="Extended key usage flags"
|
||||
@label="Extended key usage flags"
|
||||
@subText="These flags enable common extended key usages. They are separate from the explicit extended key usage constraints above."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{{#each this.keyUsageFlags as |field|}}
|
||||
{{#let (get @model.allByKey field) as |attr|}}
|
||||
<FormField data-test-field={{true}} @attr={{attr}} @model={{@model}} />
|
||||
{{/let}}
|
||||
{{/each}}
|
||||
|
||||
<StringList
|
||||
class="is-shadowless"
|
||||
data-test-input="extKeyUsageOids"
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ interface PkiKeyUsageArgs {
|
|||
}
|
||||
|
||||
export default class PkiKeyUsage extends Component<PkiKeyUsageArgs> {
|
||||
keyUsageFlags = ['clientFlag', 'serverFlag', 'codeSigningFlag', 'emailProtectionFlag'];
|
||||
keyUsageFields = KEY_USAGE_FIELDS;
|
||||
extKeyUsageFields = EXT_KEY_USAGE_FIELDS;
|
||||
|
||||
|
|
|
|||
|
|
@ -213,6 +213,7 @@ hU9YsNh6bCDmnBDBsDMOI7h8lBRQwTiWVoSD9YNVvFiY29YvFbJQGdh+pmBtf7E+
|
|||
d8SYWhRdxmH3qcHNPcR1iw==
|
||||
-----END CERTIFICATE-----`;
|
||||
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-----`;
|
||||
const certWithAllKeyUsage = `-----BEGIN CERTIFICATE-----\nMIIDjjCCAnagAwIBAgIUYykdVqVIR0kyYMzGIPeqijtsypowDQYJKoZIhvcNAQEL\nBQAwEjEQMA4GA1UEAxMHbXktcm9vdDAeFw0yNTEwMjcyMDUxMjNaFw0yNTEwMjcy\nMjUxNTNaMBYxFDASBgNVBAMTC2FsbGtleXVzYWdlMIIBIjANBgkqhkiG9w0BAQEF\nAAOCAQ8AMIIBCgKCAQEAxmTxzVAon27u1utDb6QCVftjJaMhop/XcIMea8kus0Kj\n1ASE6vITOq6OV0piofdJ1wL7PsZNMJFYHZQVKwXi+UlSxChJQdeqFfSJ6n/V6B7g\nO9cADMJNJOgrujyYQKDWDE9YxQJV4RoikthS0jMDuOSTYcX9s9KywGm5l9p+He0I\nHWZHhBpS3QIrn0InUjANtV5CD8aTqLIu5A3maX1Sba7hvH2h9gzXRhWxmjSAtUOp\ngyYXBbyL4bx7wELKCnIudUDBE4G7imzGbAR0nIRidZJ8vn3Cx9JyK4WMvmEC99wo\n+rDNzaVMsH1WPW9OAlaVX8y6b8Z1kQtCTpYk+fQlFwIDAQABo4HXMIHUMA8GA1Ud\nDwEB/wQFAwMH/4AwaQYDVR0lBGIwYAYEVR0lAAYIKwYBBQUHAwEGCCsGAQUFBwMC\nBggrBgEFBQcDAwYIKwYBBQUHAwQGCCsGAQUFBwMFBggrBgEFBQcDBgYIKwYBBQUH\nAwcGCCsGAQUFBwMIBggrBgEFBQcDCTAdBgNVHQ4EFgQUnakdzwExzVyymSbc6GPP\nyfNHPvEwHwYDVR0jBBgwFoAUBq3YXv2yL2unSF36GmuqeaW/SQkwFgYDVR0RBA8w\nDYILYWxsa2V5dXNhZ2UwDQYJKoZIhvcNAQELBQADggEBAL5f52isY1Oqn20Cez3R\ncgOuMvVBTKw3UAzjJhoSmZEVAhn1TcDjIXrb0dUhleacRdkV7whzL8BAsPV3lB2R\nw9fnWQct4CAlXSgli2w6ONcsfX8ehQ0tiEhjrfJEBu//6Zgj9K5FwMlZ/R0qok5s\nh2hGNU+j8AmE+MWlpdY2+hnvV800ENcHZLbokcRDu+WDGOGdbZcxBZE4Iyg2Ec+7\nSBIBbsq5T1IbQfMmS+udtxEIbb/n+XAGhRdo1hO0A8y4AYcurv+5uLXNSyVh61Xt\nBRjcpUQ6wtgJvGP2xNPSllPQ2UelueKfLqZi6dZWWd/+T6DG6gBbfZzSbFk+W/jt\nbS8=\n-----END CERTIFICATE-----`;
|
||||
|
||||
// CROSS-SIGNING:
|
||||
const newCSR = {
|
||||
|
|
@ -241,6 +242,7 @@ export const CERTIFICATES = {
|
|||
unsupportedOids,
|
||||
unsupportedPem,
|
||||
certWithoutCN,
|
||||
certWithAllKeyUsage,
|
||||
newCSR,
|
||||
oldParentIssuerCert,
|
||||
parentIssuerCert,
|
||||
|
|
|
|||
|
|
@ -337,7 +337,7 @@ module('Integration | Component | pki issuer cross sign', function (hooks) {
|
|||
assert
|
||||
.dom('[data-test-cross-sign-alert-message]')
|
||||
.hasText(
|
||||
'certificate contains unsupported subject OIDs: 1.2.840.113549.1.9.1, certificate contains unsupported extension OIDs: 2.5.29.37'
|
||||
'certificate contains unsupported subject OIDs: 1.2.840.113549.1.9.1, certificate contains unsupported extension OIDs: 2.5.29.37, unsupported key usage value on issuer certificate: DigitalSignature, KeyEncipherment'
|
||||
);
|
||||
|
||||
for (const field of FIELDS) {
|
||||
|
|
|
|||
|
|
@ -18,11 +18,61 @@ module('Integration | Component | pki-key-usage', function (hooks) {
|
|||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.model = this.store.createRecord('pki/role');
|
||||
// add fields that openapi normally hydrates
|
||||
// ideally we pull this from the openapi schema in the future
|
||||
const openapifields = [
|
||||
{
|
||||
name: 'clientFlag',
|
||||
type: 'boolean',
|
||||
options: {
|
||||
editType: 'boolean',
|
||||
helpText:
|
||||
'If set, certificates are flagged for client auth use. Defaults to true. See also RFC 5280 Section 4.2.1.12.',
|
||||
fieldGroup: 'default',
|
||||
defaultValue: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'serverFlag',
|
||||
type: 'boolean',
|
||||
options: {
|
||||
editType: 'boolean',
|
||||
helpText:
|
||||
'If set, certificates are flagged for server auth use. Defaults to true. See also, RFC 5280 Section 4.2.1.12.',
|
||||
fieldGroup: 'default',
|
||||
defaultValue: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'codeSigningFlag',
|
||||
type: 'boolean',
|
||||
options: {
|
||||
editType: 'boolean',
|
||||
helpText:
|
||||
'If set, certificates are flagged for code signing use. Defaults to false. See also RFC 5280 Section 4.2.1.12.',
|
||||
fieldGroup: 'default',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'emailProtectionFlag',
|
||||
type: 'boolean',
|
||||
options: {
|
||||
editType: 'boolean',
|
||||
helpText:
|
||||
'If set, certificates are flagged for email protection use. Defaults to false. See also RFC 5280 Section 4.2.1.12.',
|
||||
fieldGroup: 'default',
|
||||
},
|
||||
},
|
||||
];
|
||||
this.model._allByKey = {};
|
||||
openapifields.forEach((f) => {
|
||||
this.model._allByKey[f.name] = f;
|
||||
this.model[f.name] = f.options.defaultValue;
|
||||
});
|
||||
this.model.backend = 'pki';
|
||||
});
|
||||
|
||||
test('it should render the component', async function (assert) {
|
||||
assert.expect(6);
|
||||
await render(
|
||||
hbs`
|
||||
<div class="has-top-margin-xxl">
|
||||
|
|
@ -38,6 +88,10 @@ module('Integration | Component | pki-key-usage', function (hooks) {
|
|||
assert.dom(PKI_ROLE_FORM.keyAgreement).isChecked('Key Agreement is true by default');
|
||||
assert.dom(PKI_ROLE_FORM.keyEncipherment).isChecked('Key Encipherment is true by default');
|
||||
assert.dom(PKI_ROLE_FORM.any).isNotChecked('Any is false by default');
|
||||
assert.dom(GENERAL.inputByAttr('clientFlag')).isChecked();
|
||||
assert.dom(GENERAL.inputByAttr('serverFlag')).isChecked();
|
||||
assert.dom(GENERAL.inputByAttr('codeSigningFlag')).isNotChecked();
|
||||
assert.dom(GENERAL.inputByAttr('emailProtectionFlag')).isNotChecked();
|
||||
assert.dom(GENERAL.inputByAttr('extKeyUsageOids')).exists('Extended Key usage oids renders');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ module('Integration | Component | pki-role-form', function (hooks) {
|
|||
assert
|
||||
.dom(GENERAL.fieldByAttr('noStoreMetadata'))
|
||||
.doesNotExist('noStoreMetadata is not shown b/c not enterprise');
|
||||
assert.dom(GENERAL.inputByAttr('addBasicConstraints')).exists();
|
||||
assert.dom(GENERAL.inputByAttr('basicConstraintsValidForNonCa')).exists();
|
||||
assert.dom(GENERAL.button('Domain handling')).exists('shows form-field group add domain handling');
|
||||
assert.dom(GENERAL.button('Key parameters')).exists('shows form-field group key params');
|
||||
assert.dom(GENERAL.button('Key usage')).exists('shows form-field group key usage');
|
||||
|
|
@ -127,7 +127,7 @@ module('Integration | Component | pki-role-form', function (hooks) {
|
|||
.includesText('Name is required.', 'show correct error message');
|
||||
|
||||
await fillIn(GENERAL.inputByAttr('name'), 'test-role');
|
||||
await click('[data-test-input="addBasicConstraints"]');
|
||||
await click('[data-test-input="basicConstraintsValidForNonCa"]');
|
||||
await click(GENERAL.button('Domain handling'));
|
||||
await click('[data-test-input="allowedDomainsTemplate"]');
|
||||
await click(GENERAL.button('Policy identifiers'));
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { CERTIFICATES } from 'vault/tests/helpers/pki/pki-helpers';
|
|||
|
||||
const {
|
||||
certWithoutCN,
|
||||
certWithAllKeyUsage,
|
||||
loadedCert,
|
||||
pssTrueCert,
|
||||
skeletonCert,
|
||||
|
|
@ -83,6 +84,18 @@ module('Integration | Util | parse pki certificate', function (hooks) {
|
|||
);
|
||||
});
|
||||
|
||||
test('it parses a certificate with every key usage bit set', async function (assert) {
|
||||
// certificate contains every key usage constraint
|
||||
// Key Usage is manually parsed and relies on the order of KEY_USAGE_BITS
|
||||
// so this test ensures that order is preserved.
|
||||
const { key_usage } = parseCertificate(certWithAllKeyUsage);
|
||||
assert.strictEqual(
|
||||
key_usage,
|
||||
'DigitalSignature, ContentCommitment, KeyEncipherment, DataEncipherment, KeyAgreement, CertSign, CRLSign, EncipherOnly, DecipherOnly',
|
||||
'it contains expected key usage values'
|
||||
);
|
||||
});
|
||||
|
||||
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);
|
||||
|
|
@ -123,6 +136,7 @@ module('Integration | Util | parse pki certificate', function (hooks) {
|
|||
[
|
||||
'certificate contains unsupported subject OIDs: 1.2.840.113549.1.9.1',
|
||||
'certificate contains unsupported extension OIDs: 2.5.29.37',
|
||||
'unsupported key usage value on issuer certificate: DigitalSignature, KeyEncipherment',
|
||||
],
|
||||
'it contains expected error messages'
|
||||
);
|
||||
|
|
@ -204,7 +218,10 @@ module('Integration | Util | parse pki certificate', function (hooks) {
|
|||
({ extValues, extErrors } = unsupportedExt);
|
||||
assert.propEqual(
|
||||
this.getErrorMessages(extErrors),
|
||||
['certificate contains unsupported extension OIDs: 2.5.29.37'],
|
||||
[
|
||||
'certificate contains unsupported extension OIDs: 2.5.29.37',
|
||||
'unsupported key usage value on issuer certificate: DigitalSignature, KeyEncipherment',
|
||||
],
|
||||
'it returns extension errors'
|
||||
);
|
||||
assert.ok(
|
||||
|
|
@ -271,14 +288,14 @@ module('Integration | Util | parse pki certificate', function (hooks) {
|
|||
common_name: null,
|
||||
country: null,
|
||||
exclude_cn_from_sans: false,
|
||||
key_usage: null,
|
||||
key_usage: 'DigitalSignature, KeyEncipherment',
|
||||
locality: null,
|
||||
max_path_length: 10,
|
||||
not_valid_after: 1989876490,
|
||||
not_valid_before: 1674516490,
|
||||
organization: null,
|
||||
ou: null,
|
||||
parsing_errors: [{}, {}],
|
||||
parsing_errors: [{}, {}, {}],
|
||||
postal_code: null,
|
||||
province: null,
|
||||
subject_serial_number: null,
|
||||
|
|
@ -295,6 +312,7 @@ module('Integration | Util | parse pki certificate', function (hooks) {
|
|||
[
|
||||
'certificate contains unsupported subject OIDs: 1.2.840.113549.1.9.1',
|
||||
'certificate contains unsupported extension OIDs: 2.5.29.37',
|
||||
'unsupported key usage value on issuer certificate: DigitalSignature, KeyEncipherment',
|
||||
],
|
||||
'it returns correct errors'
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue