From c5eb8c8732a811e46d3192ab606068315d9e8fa1 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Sat, 5 Aug 2023 07:47:35 -0400 Subject: [PATCH 01/39] perf: remove unnecessary steps from validateNewEntry() --- apps/meerkat/src/app/x500/validateNewEntry.ts | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/apps/meerkat/src/app/x500/validateNewEntry.ts b/apps/meerkat/src/app/x500/validateNewEntry.ts index 4679fe620..1122a95d0 100644 --- a/apps/meerkat/src/app/x500/validateNewEntry.ts +++ b/apps/meerkat/src/app/x500/validateNewEntry.ts @@ -838,36 +838,31 @@ async function validateEntry ( ); } - const attrsFromDN: Value[] = rdn - .map((atav): Value => ({ - type: atav.type_, - value: atav.value, - })); const rdnAttributes: Set = new Set(); const duplicatedAFDNs: AttributeType[] = []; const unrecognizedAFDNs: AttributeType[] = []; const cannotBeUsedInNameAFDNs: AttributeType[] = []; const unmatchedAFDNs: AttributeType[] = []; - for (const afdn of attrsFromDN) { - const oid: string = afdn.type.toString(); + for (const atav of rdn) { + const oid: string = atav.type_.toString(); if (rdnAttributes.has(oid)) { - duplicatedAFDNs.push(afdn.type); + duplicatedAFDNs.push(atav.type_); continue; } else { rdnAttributes.add(oid); } - const spec = ctx.attributeTypes.get(afdn.type.toString()); + const spec = ctx.attributeTypes.get(atav.type_.toString()); if (!spec) { - unrecognizedAFDNs.push(afdn.type); + unrecognizedAFDNs.push(atav.type_); continue; } if (spec.validator) { try { - spec.validator(afdn.value); + spec.validator(atav.value); } catch { throw new errors.NameError( ctx.i18n.t("err:invalid_attribute_syntax", { - type: afdn.type.toString(), + type: atav.type_.toString(), }), new NameErrorData( NameProblem_invalidAttributeSyntax, @@ -890,9 +885,9 @@ async function validateEntry ( ); } } - const matcher = getNamingMatcherGetter(ctx)(afdn.type); + const matcher = getNamingMatcherGetter(ctx)(atav.type_); if (!matcher) { - cannotBeUsedInNameAFDNs.push(afdn.type); + cannotBeUsedInNameAFDNs.push(atav.type_); continue; } const someAttributeMatched = values.some((attr) => ( @@ -906,11 +901,11 @@ async function validateEntry ( * can see that distinguished values MAY have contexts. */ // (!attr.contexts || (attr.contexts.length === 0)) - attr.type.isEqualTo(afdn.type) - && matcher(attr.value, afdn.value) + attr.type.isEqualTo(atav.type_) + && matcher(attr.value, atav.value) )); if (!someAttributeMatched) { - unmatchedAFDNs.push(afdn.type); + unmatchedAFDNs.push(atav.type_); continue; } } From 4de04fe7dc6396bcd22e43260da120460e1b4a47 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Sat, 5 Aug 2023 07:57:00 -0400 Subject: [PATCH 02/39] feat: rbac, part 1 --- .../authz/accessControlSchemesThatUseRBAC.ts | 25 ++ apps/meerkat/src/app/authz/acdf.ts | 76 ++++++ .../src/app/authz/permittedToFindDSE.ts | 46 ++-- .../app/authz/permittedToFindDseViaRbac.ts | 110 ++++++++ apps/meerkat/src/app/authz/rbacACDF.ts | 244 ++++++++++++++++++ apps/meerkat/src/app/ctx.ts | 3 + apps/meerkat/src/app/distributed/findDSE.ts | 69 +++-- apps/meerkat/src/app/distributed/modifyDN.ts | 1 + apps/meerkat/src/app/pki/verifyCertPath.ts | 30 ++- libs/meerkat-types/package.json | 2 +- libs/meerkat-types/src/lib/types.ts | 43 +++ 11 files changed, 594 insertions(+), 55 deletions(-) create mode 100644 apps/meerkat/src/app/authz/accessControlSchemesThatUseRBAC.ts create mode 100644 apps/meerkat/src/app/authz/acdf.ts create mode 100644 apps/meerkat/src/app/authz/permittedToFindDseViaRbac.ts create mode 100644 apps/meerkat/src/app/authz/rbacACDF.ts diff --git a/apps/meerkat/src/app/authz/accessControlSchemesThatUseRBAC.ts b/apps/meerkat/src/app/authz/accessControlSchemesThatUseRBAC.ts new file mode 100644 index 000000000..8b1c561be --- /dev/null +++ b/apps/meerkat/src/app/authz/accessControlSchemesThatUseRBAC.ts @@ -0,0 +1,25 @@ +import { + rule_and_basic_access_control, +} from "@wildboar/x500/src/lib/modules/BasicAccessControl/rule-and-basic-access-control.va"; +import { + rule_and_simple_access_control, +} from "@wildboar/x500/src/lib/modules/BasicAccessControl/rule-and-simple-access-control.va"; +import { IndexableOID } from "@wildboar/meerkat-types"; + +/** + * @summary The access control schemes that use rule-based access control + * @description + * + * This is a set of stringified object identifiers in dot-delimited notation, + * each of which is an access control scheme that uses Rule-Based Access + * Control (RBAC). + * + * @constant + */ +export +const accessControlSchemesThatUseRBAC: Set = new Set([ + rule_and_basic_access_control.toString(), + rule_and_simple_access_control.toString(), +]); + +export default accessControlSchemesThatUseRBAC; diff --git a/apps/meerkat/src/app/authz/acdf.ts b/apps/meerkat/src/app/authz/acdf.ts new file mode 100644 index 000000000..7f17578d5 --- /dev/null +++ b/apps/meerkat/src/app/authz/acdf.ts @@ -0,0 +1,76 @@ +import { Context, Vertex, ClientAssociation } from "@wildboar/meerkat-types"; +import { bacACDF } from "@wildboar/x500"; +import { + _decode_SignedSecurityLabel, +} from "@wildboar/x500/src/lib/modules/EnhancedSecurity/SignedSecurityLabel.ta"; +import { OBJECT_IDENTIFIER } from "asn1-ts"; +import { EvaluateFilterSettings } from "@wildboar/x500/src/lib/utils/evaluateFilter"; +import { ProtectedItem, ACDFTupleExtended } from "@wildboar/x500"; +import type { + NameAndOptionalUID, +} from "@wildboar/x500/src/lib/modules/SelectedAttributeTypes/NameAndOptionalUID.ta"; +import accessControlSchemesThatUseACIItems from "./accessControlSchemesThatUseACIItems"; +import accessControlSchemesThatUseRBAC from "./accessControlSchemesThatUseRBAC"; +import { rbacACDF } from "./rbacACDF"; +import { attributeValueSecurityLabelContext } from "@wildboar/x500/src/lib/collections/contexts"; +// import { PERMISSION_CATEGORY_DISCLOSE_ON_ERROR } from "@wildboar/x500/src/lib/bac/bacACDF"; + +// attributeValueSecurityLabelContext +const avslc = attributeValueSecurityLabelContext["&id"]; + +export +function acdf ( + ctx: Context, + accessControlScheme: OBJECT_IDENTIFIER, + assn: ClientAssociation | undefined, // This has a clearance field. + target: Vertex, + permissions: number[], + tuples: ACDFTupleExtended[], + requester: NameAndOptionalUID | undefined | null, + request: ProtectedItem, + settings: EvaluateFilterSettings, + tuplesAlreadySplit?: boolean +): boolean { + const acs = accessControlScheme.toString(); + if (accessControlSchemesThatUseACIItems.has(acs)) { + const { authorized } = bacACDF( + tuples, + requester, + request, + permissions, + settings, + tuplesAlreadySplit, + ); + if (!authorized) { + return false; + } + } + if ( + accessControlSchemesThatUseRBAC.has(acs) + && ("value" in request) + && assn + // TODO: Find DSE basically runs this same code twice. I want to find some optimization to avoid that. + // && (permissions.length !== 1 || permissions[0] !== PERMISSION_CATEGORY_DISCLOSE_ON_ERROR) + ) { + const labelContext = request.contexts + ?.find((c) => c.contextType.isEqualTo(avslc)); + if (labelContext?.contextValues.length) { + // return true; // If there is no label, access is allowed. + const label = _decode_SignedSecurityLabel(labelContext.contextValues[0]); + const authorized: boolean = rbacACDF( + ctx, + assn, + target, + label, + request.value.type_, + request.value.value, + request.contexts ?? [], + permissions, + ); + if (!authorized) { + return false; + } + } + } + return true; +} diff --git a/apps/meerkat/src/app/authz/permittedToFindDSE.ts b/apps/meerkat/src/app/authz/permittedToFindDSE.ts index f8ebdeff9..4e458299e 100644 --- a/apps/meerkat/src/app/authz/permittedToFindDSE.ts +++ b/apps/meerkat/src/app/authz/permittedToFindDSE.ts @@ -1,4 +1,4 @@ -import type { Context, Vertex } from "@wildboar/meerkat-types"; +import type { ClientAssociation, Context, Vertex } from "@wildboar/meerkat-types"; import { OBJECT_IDENTIFIER, ObjectIdentifier } from "asn1-ts"; import type ACDFTuple from "@wildboar/x500/src/lib/types/ACDFTuple"; import type ACDFTupleExtended from "@wildboar/x500/src/lib/types/ACDFTupleExtended"; @@ -25,8 +25,9 @@ import type { AuthenticationLevel, } from "@wildboar/x500/src/lib/modules/BasicAccessControl/AuthenticationLevel.ta"; import preprocessTuples from "./preprocessTuples"; -import rdnToID from "../dit/rdnToID"; -import getVertexById from "../database/getVertexById"; +import accessControlSchemesThatUseRBAC from "./accessControlSchemesThatUseRBAC"; +import dnToVertex from "../dit/dnToVertex"; +import permittedToFindDseViaRbac from "./permittedToFindDseViaRbac"; export interface PermittedToFindDSEReturn { @@ -45,13 +46,18 @@ const ENTRY_NOT_FOUND: PermittedToFindDSEReturn = { * @summary Whether a user is permitted to find a given DSE. * @description * - * Resolves a `boolean` indicating whether the user can discover a given DSE. + * Determines whether the user can discover a given DSE. * This function checks that every vertex from the top-level DSE down to the * target DSE can be discovered by the user. * * @param ctx The context object * @param assn The client association - * @returns A `boolean` indicating whether the bound client may add a top-level DSE. + * @param root The root of the DIT to begin evaluating access + * @param needleDN The distinguished name whose discoverability is to be determined. + * @param user The name and UID of the user requesting access + * @param authLevel The authentication level of the user. + * @returns Information about the discoverability of the entry, including + * whether it really exists or not. * * @function * @async @@ -59,6 +65,7 @@ const ENTRY_NOT_FOUND: PermittedToFindDSEReturn = { export async function permittedToFindDSE ( ctx: Context, + assn: ClientAssociation, root: Vertex, needleDN: DistinguishedName, user: NameAndOptionalUID | undefined | null, @@ -72,8 +79,9 @@ async function permittedToFindDSE ( let authorizedToDiscloseOnError: boolean = false; for (let i = 0; i < needleDN.length; i++) { - const id = await rdnToID(ctx, dse_i.dse.id, needleDN[i]); - if (!id) { + const rdn = needleDN[i]; + const vertex = await dnToVertex(ctx, dse_i, [ rdn ]); + if (!vertex) { return ENTRY_NOT_FOUND; } accessControlScheme = (dse_i.dse.admPoint @@ -81,18 +89,15 @@ async function permittedToFindDSE ( : [ ...admPoints ]) .reverse() .find((ap) => ap.dse.admPoint!.accessControlScheme)?.dse.admPoint!.accessControlScheme; - const vertex = await getVertexById(ctx, dse_i, id); - if (!vertex) { - return ENTRY_NOT_FOUND; - } dse_i = vertex; if (dse_i.dse.admPoint?.accessControlScheme) { accessControlScheme = dse_i.dse.admPoint.accessControlScheme; } - if ( // Check if the user can actually access it. - accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { + if (!accessControlScheme) { + continue; + } + const acs = accessControlScheme.toString(); + if (accessControlSchemesThatUseACIItems.has(acs)) { const childDN = getDistinguishedName(dse_i); const objectClasses = Array.from(dse_i.dse.objectClass).map(ObjectIdentifier.fromString); // Without this, all first-level DSEs are discoverable. @@ -152,6 +157,17 @@ async function permittedToFindDSE ( }; } } + if (accessControlSchemesThatUseRBAC.has(acs)) { + const authorized: boolean = await permittedToFindDseViaRbac(ctx, assn, dse_i); + if (!authorized) { + return { + // We can't say with certainty that it does NOT exist unless we're on the last iteration. + exists: (i === (needleDN.length - 1)) || undefined, + permittedToFind: false, + discloseOnError: false, + }; + } + } } return { exists: true, diff --git a/apps/meerkat/src/app/authz/permittedToFindDseViaRbac.ts b/apps/meerkat/src/app/authz/permittedToFindDseViaRbac.ts new file mode 100644 index 000000000..003baff34 --- /dev/null +++ b/apps/meerkat/src/app/authz/permittedToFindDseViaRbac.ts @@ -0,0 +1,110 @@ +import type { ClientAssociation, Context, Vertex } from "@wildboar/meerkat-types"; +import { ASN1Construction, BERElement, ObjectIdentifier } from "asn1-ts"; +import { + PERMISSION_CATEGORY_BROWSE, + PERMISSION_CATEGORY_RETURN_DN, +} from "@wildboar/x500/src/lib/bac/bacACDF"; +import { attributeValueSecurityLabelContext } from "@wildboar/x500/src/lib/collections/contexts"; +import getEqualityNormalizer from "../x500/getEqualityNormalizer"; +import { rbacACDF } from "./rbacACDF"; +import { _decode_SignedSecurityLabel } from "@wildboar/x500/src/lib/modules/EnhancedSecurity/SignedSecurityLabel.ta"; + +/** + * @summary Whether a user is permitted to find a given DSE under RBAC. + * @description + * + * Resolves a `boolean` indicating whether the user can discover a given DSE + * based on Rule-Based Access Control (RBAC). + * + * This deviates from the technically correct X.500 procedures for rule based + * access control, which require every value of the DSE to be checked to + * determine whether a user can discover it. This could result in a virtually + * unlimited number of ACDF calls to determine access, and it would require + * reading every single value from the database for every entry. In addition + * to this, the requirements around operational attributes are murky, since they + * are not supposed to have context values. + * + * Meerkat DSA deviates by only checking access to the values of the RDN. If the + * user is not permitted to any distinguished value of the RDN under RBAC, + * access to the entry is denied. + * + * @param ctx The context object + * @param assn The client association + * @param vertex The DIT vertex whose discoverability is to be determined. + * @returns A `boolean` indicating whether the bound client may discover the DSE. + * + * @function + * @async + */ +export +async function permittedToFindDseViaRbac ( + ctx: Context, + assn: ClientAssociation, + vertex: Vertex, +): Promise { + const getNormalizer = getEqualityNormalizer(ctx); + const dvs = await ctx.db.attributeValue.findMany({ + where: { + /** + * This selection will technically over-match. For instance, + * if the RDN is gn=Jonathan+sn=Wilbur, it will also return + * values sn=Jonathan and gn=Wilbur. But this is a silly + * edge case and it is more strict, so it is fine. + */ + type_oid: { + in: vertex.dse.rdn.map((atav) => atav.type_.toBytes()), + }, + normalized_str: { + in: vertex.dse.rdn.map((atav) => getNormalizer(atav.type_)?.(ctx, atav.value) ?? ""), + }, + }, + select: { + type_oid: true, + tag_class: true, + tag_number: true, + constructed: true, + content_octets: true, + ContextValue: { + where: { + type: attributeValueSecurityLabelContext["&id"].toString(), + }, + select: { + ber: true, + }, + }, + }, + }); + for (const dv of dvs) { + if (dv.ContextValue.length === 0) { + continue; + } + const value_el = new BERElement(); + value_el.tagClass = dv.tag_class; + value_el.tagNumber = dv.tag_number; + value_el.construction = dv.constructed + ? ASN1Construction.constructed + : ASN1Construction.primitive; + value_el.value = dv.content_octets; + + const cval_el = new BERElement(); + cval_el.fromBytes(dv.ContextValue[0].ber); + const label = _decode_SignedSecurityLabel(cval_el); + const type = ObjectIdentifier.fromBytes(dv.type_oid); + const authorized = rbacACDF( + ctx, + assn, + vertex, + label, + type, + value_el, + [], // TODO: Document this discrepancy. + [ PERMISSION_CATEGORY_BROWSE, PERMISSION_CATEGORY_RETURN_DN ], + ); + if (!authorized) { + return false; + } + } + return true; +} + +export default permittedToFindDseViaRbac; diff --git a/apps/meerkat/src/app/authz/rbacACDF.ts b/apps/meerkat/src/app/authz/rbacACDF.ts new file mode 100644 index 000000000..24ec256c7 --- /dev/null +++ b/apps/meerkat/src/app/authz/rbacACDF.ts @@ -0,0 +1,244 @@ +import { Context, Vertex, ClientAssociation, RBAC_ACDF } from "@wildboar/meerkat-types"; +import { compareDistinguishedName } from "@wildboar/x500"; +import { + SignedSecurityLabel, +} from "@wildboar/x500/src/lib/modules/EnhancedSecurity/SignedSecurityLabel.ta"; +import { + _encode_SignedSecurityLabelContent, +} from "@wildboar/x500/src/lib/modules/EnhancedSecurity/SignedSecurityLabelContent.ta"; +import { + Context as X500Context, +} from "@wildboar/x500/src/lib/modules/InformationFramework/Context.ta"; +import { ASN1Element, DERElement, ObjectIdentifier, TRUE_BIT, packBits } from "asn1-ts"; +import getNamingMatcherGetter from "../x500/getNamingMatcherGetter"; +import { KeyObject } from "node:crypto"; +import { digestOIDToNodeHash } from "../pki/digestOIDToNodeHash"; +import * as crypto from "node:crypto"; +import { AttributeType } from "@wildboar/x500/src/lib/modules/InformationFramework/AttributeType.ta"; +import { + AttributeTypeAndValue, + _encode_AttributeTypeAndValue, +} from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/AttributeTypeAndValue.ta"; +import { DER } from "asn1-ts/dist/node/functional"; +import { verifySignature } from "../pki/verifyCertPath"; +import { + ClassList_confidential, + ClassList_restricted, + ClassList_secret, + ClassList_topSecret, + ClassList_unmarked, +} from "@wildboar/x500/src/lib/modules/EnhancedSecurity/ClassList.ta"; +import { + SecurityClassification, + SecurityClassification_confidential, + SecurityClassification_restricted, + SecurityClassification_secret, + SecurityClassification_top_secret, + SecurityClassification_unclassified, + SecurityClassification_unmarked, +} from "@wildboar/x500/src/lib/modules/EnhancedSecurity/SecurityClassification.ta"; +import { + id_wildboar, +} from "@wildboar/parity-schema/src/lib/modules/Wildboar/id-wildboar.va"; + +// TODO: Add this to the registry. +export const id_basicSecurityPolicy = new ObjectIdentifier([ 403, 1 ], id_wildboar); + +export +const simple_rbac_acdf: RBAC_ACDF = ( + ctx: Context, + assn: ClientAssociation, // This has a clearance field. + target: Vertex, + signedLabel: SignedSecurityLabel, + // _value: ASN1Element, + // _contexts: X500Context[], + // _permissions: number[], +): boolean => { + const label = signedLabel.toBeSigned.securityLabel; + const classification = Number(label.security_classification ?? SecurityClassification_unmarked); + if (classification === SecurityClassification_unclassified) { + return true; // If unclassified, the user may always see it. + } + const policyId = label.security_policy_identifier ?? id_basicSecurityPolicy; // TODO: Needs documentation. + let highestClearanceLevel: number = 0; + for (const clearance of assn.clearances) { + if (!clearance.policyId.isEqualTo(policyId)) { + continue; + } + const clearanceLevel: SecurityClassification = (() => { + if (!clearance.classList) { + return SecurityClassification_unclassified; + } + else if (clearance.classList[ClassList_topSecret] === TRUE_BIT) { + return SecurityClassification_top_secret; + } + else if (clearance.classList[ClassList_secret] === TRUE_BIT) { + return SecurityClassification_secret; + } + else if (clearance.classList[ClassList_confidential] === TRUE_BIT) { + return SecurityClassification_confidential; + } + else if (clearance.classList[ClassList_restricted] === TRUE_BIT) { + return SecurityClassification_restricted; + } + // TODO: Document treating unmarked as higher than unclassified. + else if (clearance.classList[ClassList_unmarked] === TRUE_BIT) { + return SecurityClassification_unmarked; + } + else { + return SecurityClassification_unclassified; + } + })(); + if (clearanceLevel > highestClearanceLevel) { + highestClearanceLevel = Number(clearanceLevel); + } + } + // Just to make sure that classification cannot be given a large, + // illegitimate value to make a protected value universally inaccessible. + if (highestClearanceLevel == SecurityClassification_top_secret) { + return true; + } + return (highestClearanceLevel >= classification); +}; + +// TODO: Log invalid hashes and such so admins can know if they are locked out of values. +export function rbacACDF ( + ctx: Context, + assn: ClientAssociation, // This has a clearance field. + target: Vertex, + label: SignedSecurityLabel, + attributeType: AttributeType, + value: ASN1Element, + contexts: X500Context[], + permissions: number[], +): boolean { + if (!assn.clearances.length) { + // If the user has no clearance, only allow access for things unmarked. + return (label.toBeSigned.securityLabel.security_classification == SecurityClassification_unclassified); + } + // const applicable_clearances = assn.clearances.filter((c) => c.policyId.isEqualTo(label.)) + const policyId = label.toBeSigned.securityLabel.security_policy_identifier + ?? id_basicSecurityPolicy; // TODO: Needs documentation. + const acdf = ctx.rbacPolicies.get(policyId.toString()); + if (!acdf) { + return false; // If the policy ID is not understood, deny access. + } + + const atav_hash_alg = digestOIDToNodeHash.get(label.toBeSigned.attHash.algorithmIdentifier.algorithm.toString()); + if (!atav_hash_alg) { + return false; // Hash algorithm not understood. + } + const atav = new AttributeTypeAndValue(attributeType, value); + const atav_bytes = _encode_AttributeTypeAndValue(atav, DER).toBytes(); + const hasher = crypto.createHash(atav_hash_alg); + hasher.update(atav_bytes); + const calculated_digest = hasher.digest(); + const provided_digest_bytes = packBits(label.toBeSigned.attHash.hashValue); + const provided_digest = Buffer.from( + provided_digest_bytes.buffer, + provided_digest_bytes.byteOffset, + provided_digest_bytes.byteLength, + ); + + if (Buffer.compare(calculated_digest, provided_digest)) { + return false; // The hashes don't match up. + } + + const namingMatcher = getNamingMatcherGetter(ctx); + let publicKey: KeyObject | undefined; + if ( // If no keyIdentifier, and no issuer or the issuer is this DSA... + !label.toBeSigned.keyIdentifier + && ( + !label.toBeSigned.issuer + || compareDistinguishedName( + ctx.dsa.accessPoint.ae_title.rdnSequence, + label.toBeSigned.issuer.rdnSequence, + namingMatcher, + ) + ) + ) { // Then we validate the signature against this DSA's signing key. + publicKey = ctx.config.signing.key; + } else if (label.toBeSigned.keyIdentifier) { + const key_id = Buffer.from(label.toBeSigned.keyIdentifier).toString("base64"); + const authority = ctx.labellingAuthorities.get(key_id); + if (authority === null) { + return false; + } + const issuer = label.toBeSigned.issuer; + const authority_is_valid: boolean = !!( + authority + && authority.authorized + && ( + !issuer + || authority.issuerNames.some((iss_name) => compareDistinguishedName( + iss_name.rdnSequence, + issuer.rdnSequence, + namingMatcher, + )) + ) + ); + if (!authority_is_valid) { + return false; + } + publicKey = authority?.publicKey; + } else if (label.toBeSigned.issuer) { + const issuer = label.toBeSigned.issuer; + for (const authority of ctx.labellingAuthorities.values()) { + if (!authority) { + continue; + } + if (!authority.authorized) { + continue; + } + const issuer_names_match: boolean = authority.issuerNames + .some((iss_name) => compareDistinguishedName( + iss_name.rdnSequence, + issuer.rdnSequence, + namingMatcher, + )); + if (!issuer_names_match) { + continue; + } + publicKey = authority.publicKey; + } + } else { // This should actually be unreachable. + return false; + } + if (!publicKey) { + return false; + } + const tbs_bytes = label.originalDER + ? (() => { + const el = new DERElement(); + el.fromBytes(label.originalDER); + const tbs = el.sequence[0]; + return tbs.toBytes(); + })() + : _encode_SignedSecurityLabelContent(label.toBeSigned, DER).toBytes(); + const sig_value = packBits(label.signature); + const sig_valid = verifySignature( + tbs_bytes, + label.algorithmIdentifier, + sig_value, + publicKey, + ); + if (!sig_valid) { + if (label.altAlgorithmIdentifier && label.altSignature) { + const alt_sig_value = packBits(label.signature); + const alt_sig_valid = verifySignature( + tbs_bytes, + label.altAlgorithmIdentifier, + alt_sig_value, + publicKey, + ); + if (!alt_sig_valid) { + return false; + } + } else { + return false; + } + } + // At this point, we know that the label is correctly bound to the value, + // so we can use the policy-specific RBAC ACDF. + return acdf(ctx, assn, target, label, value, contexts, permissions); +} diff --git a/apps/meerkat/src/app/ctx.ts b/apps/meerkat/src/app/ctx.ts index 786959787..aef886c2e 100644 --- a/apps/meerkat/src/app/ctx.ts +++ b/apps/meerkat/src/app/ctx.ts @@ -108,6 +108,7 @@ import type { } from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/PkiPath.ta"; import { rootCertificates } from "tls"; import { strict as assert } from "assert"; +import { id_basicSecurityPolicy, simple_rbac_acdf } from "./authz/rbacACDF"; export interface MeerkatTelemetryClient { @@ -1133,6 +1134,8 @@ const ctx: MeerkatContext = { pendingShadowingUpdateCycles: new Map(), shadowUpdateCycles: new Map(), updatingShadow: new Set(), + labellingAuthorities: new Map(), + rbacPolicies: new Map([ [id_basicSecurityPolicy.toString(), simple_rbac_acdf] ]), }; export default ctx; diff --git a/apps/meerkat/src/app/distributed/findDSE.ts b/apps/meerkat/src/app/distributed/findDSE.ts index 523535b77..879f6f72e 100644 --- a/apps/meerkat/src/app/distributed/findDSE.ts +++ b/apps/meerkat/src/app/distributed/findDSE.ts @@ -70,7 +70,7 @@ import { import getRelevantSubentries from "../dit/getRelevantSubentries"; import type ACDFTuple from "@wildboar/x500/src/lib/types/ACDFTuple"; import type ACDFTupleExtended from "@wildboar/x500/src/lib/types/ACDFTupleExtended"; -import bacACDF, { +import { PERMISSION_CATEGORY_BROWSE, PERMISSION_CATEGORY_RETURN_DN, PERMISSION_CATEGORY_DISCLOSE_ON_ERROR, @@ -141,6 +141,7 @@ import getEqualityNormalizer from "../x500/getEqualityNormalizer"; import { isModificationOperation } from "@wildboar/x500"; import { EXT_BIT_USE_ALIAS_ON_UPDATE } from "@wildboar/x500/src/lib/dap/extensions"; import stringifyDN from "../x500/stringifyDN"; +import { acdf } from "../authz/acdf"; const autonomousArea: string = id_ar_autonomousArea.toString(); @@ -986,11 +987,7 @@ export if (matchedVertex.dse.admPoint?.accessControlScheme) { accessControlScheme = matchedVertex.dse.admPoint.accessControlScheme; } - if ( - !ctx.config.bulkInsertMode - && accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { + if (!ctx.config.bulkInsertMode && accessControlScheme) { const childDN = getDistinguishedName(matchedVertex); // Without this, all first-level DSEs are discoverable. const relevantAdmPoints: Vertex[] = matchedVertex.dse.admPoint @@ -1023,23 +1020,32 @@ export const objectClasses = Array .from(matchedVertex.dse.objectClass) .map(ObjectIdentifier.fromString); - const { authorized: authorizedToDiscover } = bacACDF( - relevantTuples, - user, - { entry: objectClasses }, + + const authorizedToDiscover: boolean = acdf( + ctx, + accessControlScheme, + assn, + matchedVertex, [ PERMISSION_CATEGORY_BROWSE, PERMISSION_CATEGORY_RETURN_DN, ], + relevantTuples, + user, + { entry: objectClasses }, bacSettings, true, ); if (!authorizedToDiscover) { - const { authorized: authorizedToDiscoverOnError } = bacACDF( + const authorizedToDiscoverOnError: boolean = acdf( + ctx, + accessControlScheme, + assn, + matchedVertex, + [PERMISSION_CATEGORY_DISCLOSE_ON_ERROR], relevantTuples, user, { entry: objectClasses }, - [PERMISSION_CATEGORY_DISCLOSE_ON_ERROR], bacSettings, true, ); @@ -1198,14 +1204,18 @@ export * procedure, but it is important for preventing information * disclosure vulnerabilities. */ - const { authorized: authorizedToDiscover } = bacACDF( - relevantTuples, - user, - { entry: objectClasses }, + const authorizedToDiscover: boolean = acdf( + ctx, + accessControlScheme, + assn, + child, [ PERMISSION_CATEGORY_BROWSE, PERMISSION_CATEGORY_RETURN_DN, ], + relevantTuples, + user, + { entry: objectClasses }, bacSettings, true, ); @@ -1215,11 +1225,15 @@ export aid: assn?.id ?? "INTERNAL", dn: stringifyDN(ctx, needleDN).slice(0, 1024), })); - const { authorized: authorizedToDiscoverOnError } = bacACDF( + const authorizedToDiscoverOnError: boolean = acdf( + ctx, + accessControlScheme, + assn, + child, + [PERMISSION_CATEGORY_DISCLOSE_ON_ERROR], relevantTuples, user, { entry: objectClasses }, - [PERMISSION_CATEGORY_DISCLOSE_ON_ERROR], bacSettings, true, ); @@ -1346,17 +1360,18 @@ export isMemberOfGroup, NAMING_MATCHER, ); - const { authorized: authorizedToDiscloseOnError } = bacACDF( + const objectClasses = Array + .from(dse_i.dse.objectClass) + .map(ObjectIdentifier.fromString); + const authorizedToDiscloseOnError: boolean = acdf( + ctx, + accessControlScheme, + assn, + dse_i, + [PERMISSION_CATEGORY_DISCLOSE_ON_ERROR], relevantTuples, user, - { - entry: Array - .from(dse_i.dse.objectClass) - .map(ObjectIdentifier.fromString), - }, - [ - PERMISSION_CATEGORY_DISCLOSE_ON_ERROR, - ], + { entry: objectClasses }, bacSettings, true, ); diff --git a/apps/meerkat/src/app/distributed/modifyDN.ts b/apps/meerkat/src/app/distributed/modifyDN.ts index 635af1c6f..e93967632 100644 --- a/apps/meerkat/src/app/distributed/modifyDN.ts +++ b/apps/meerkat/src/app/distributed/modifyDN.ts @@ -833,6 +833,7 @@ async function modifyDN ( const superiorDN = getDistinguishedName(superior); const permittedToFindResult = await permittedToFindDSE( ctx, + assn, ctx.dit.root, [ ...superiorDN, newRDN ], user, diff --git a/apps/meerkat/src/app/pki/verifyCertPath.ts b/apps/meerkat/src/app/pki/verifyCertPath.ts index 9743355a2..df587290f 100644 --- a/apps/meerkat/src/app/pki/verifyCertPath.ts +++ b/apps/meerkat/src/app/pki/verifyCertPath.ts @@ -29,7 +29,7 @@ import compareDistinguishedName from "@wildboar/x500/src/lib/comparators/compare import getNamingMatcherGetter from "../x500/getNamingMatcherGetter"; import { DER } from "asn1-ts/dist/node/functional"; import getDateFromTime from "@wildboar/x500/src/lib/utils/getDateFromTime"; -import { createVerify, createPublicKey } from "crypto"; +import { createVerify, createPublicKey, KeyObject } from "crypto"; import { AlgorithmIdentifier, _encode_AlgorithmIdentifier, } from "@wildboar/x500/src/lib/modules/AuthenticationFramework/AlgorithmIdentifier.ta" @@ -786,7 +786,7 @@ async function checkRemoteCRLs ( if (ctx.config.signing.disableAllSignatureVerification) { return VCP_RETURN_CRL_REVOKED; } - const bytes = crl.originalDER + const bytes = crl.originalDER // FIXME: This is incorrect. ?? _encode_CertificateList(crl, DER).toBytes(); const sigValue = packBits(crl.signature); const signatureIsValid: boolean | undefined = verifySignature( @@ -826,23 +826,29 @@ function verifySignature ( bytes: Uint8Array, alg: AlgorithmIdentifier, sigValue: Uint8Array, - issuerSPKI: SubjectPublicKeyInfo | SubjectAltPublicKeyInfo, + keyOrSPKI: SubjectPublicKeyInfo | SubjectAltPublicKeyInfo | KeyObject, ): boolean | undefined { - const spkiBytes: Uint8Array = ((issuerSPKI instanceof SubjectPublicKeyInfo) - ? _encode_SubjectPublicKeyInfo(issuerSPKI, DER) - : _encode_SubjectAltPublicKeyInfo(issuerSPKI, DER)).toBytes(); - const issuerPublicKey = createPublicKey({ - key: Buffer.from(spkiBytes.buffer), - format: "der", - type: "spki", - }); + const pubKey: KeyObject = keyOrSPKI instanceof KeyObject + ? keyOrSPKI + : (() => { + const spkiBytes: Uint8Array = ((keyOrSPKI instanceof SubjectPublicKeyInfo) + ? _encode_SubjectPublicKeyInfo(keyOrSPKI, DER) + : _encode_SubjectAltPublicKeyInfo(keyOrSPKI, DER)).toBytes(); + const issuerPublicKey = createPublicKey({ + key: Buffer.from(spkiBytes.buffer), + format: "der", + type: "spki", + }); + return issuerPublicKey; + })(); + const nodejsDigestName = sigAlgOidToNodeJSDigest.get(alg.algorithm.toString()); if (!nodejsDigestName) { return undefined; // Unknown algorithm. } const verifier = createVerify(nodejsDigestName); verifier.update(bytes); - const signatureIsValid = verifier.verify(issuerPublicKey, sigValue); + const signatureIsValid = verifier.verify(pubKey, sigValue); return signatureIsValid; } diff --git a/libs/meerkat-types/package.json b/libs/meerkat-types/package.json index 324bf3f2a..0f3d57b22 100644 --- a/libs/meerkat-types/package.json +++ b/libs/meerkat-types/package.json @@ -1,5 +1,5 @@ { "name": "@wildboar/meerkat-types", - "version": "1.9.0", + "version": "1.10.0", "license": "MIT" } diff --git a/libs/meerkat-types/src/lib/types.ts b/libs/meerkat-types/src/lib/types.ts index d226eb38a..93120dba2 100644 --- a/libs/meerkat-types/src/lib/types.ts +++ b/libs/meerkat-types/src/lib/types.ts @@ -103,6 +103,10 @@ import type { PwdResponseValue, } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/PwdResponseValue.ta"; import { Timeout } from "safe-timers"; +import { Clearance } from "@wildboar/x500/src/lib/modules/EnhancedSecurity/Clearance.ta"; +import { + SignedSecurityLabel, +} from "@wildboar/x500/src/lib/modules/EnhancedSecurity/SignedSecurityLabel.ta"; type EventReceiver = (params: T) => void; @@ -2928,6 +2932,30 @@ interface DSARelationships { byStringDN: Map; } +/** + * An Access Control Decision Function (ACDF) to determine authorization to a + * given attribute value, according to a specific Rule-Based Access Control + * (RBAC) policy. + */ +export type RBAC_ACDF = ( + ctx: Context, + assn: ClientAssociation, // This has a clearance field. + target: Vertex, + label: SignedSecurityLabel, + value: ASN1Element, + contexts: X500Context[], + permissions: number[], +) => boolean; + +/** + * Information about a labelling authority for the purposes of + * Rule-Based Access Control (RBAC). + */ +export interface LabellingAuthorityInfo { + authorized: boolean; // This exists so you can have negative caching. + issuerNames: Name[]; + publicKey: KeyObject; +} /** * @summary Type definition for the context object @@ -3150,6 +3178,19 @@ interface Context { * shadow updates for the same operational binding. */ updatingShadow: Set; + + /** + * A mapping of Rule-Based Access Control (RBAC) policy identifiers to their + * corresponding Access Control Decision Functions (ACDFs). + */ + rbacPolicies: Map; + + /** + * A mapping of the base64-encoded key identifier to labelling authority + * information. A returned `null` means that labelling authority could not + * be found. + */ + labellingAuthorities: Map; } /** @@ -3318,6 +3359,8 @@ abstract class ClientAssociation implements WithIntegerProtocolVersion { public authorizedForSignedResults: boolean = false; public authorizedForSignedErrors: boolean = false; + public clearances: Clearance[] = []; + /** * An index of the outstanding paged results requests by base64-encoded * `queryReference`. From 27889037f7dd2c257448ef36d64ce13142863b2a Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Sun, 6 Aug 2023 14:58:15 -0400 Subject: [PATCH 03/39] feat: use RBAC in readPermittedEntryInformation() --- .../entry/readPermittedEntryInformation.ts | 306 +++++++++++------- .../src/app/distributed/modifyEntry.ts | 2 + apps/meerkat/src/app/distributed/read.ts | 2 + apps/meerkat/src/app/distributed/search_i.ts | 2 + 4 files changed, 203 insertions(+), 109 deletions(-) diff --git a/apps/meerkat/src/app/database/entry/readPermittedEntryInformation.ts b/apps/meerkat/src/app/database/entry/readPermittedEntryInformation.ts index dc658364c..5eeba57e1 100644 --- a/apps/meerkat/src/app/database/entry/readPermittedEntryInformation.ts +++ b/apps/meerkat/src/app/database/entry/readPermittedEntryInformation.ts @@ -1,9 +1,10 @@ -import type { Context, Vertex } from "@wildboar/meerkat-types"; -import type { - EntryInformation_information_Item, +import type { ClientAssociation, Context, Vertex } from "@wildboar/meerkat-types"; +import { + Attribute, + type EntryInformation_information_Item, } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/EntryInformation-information-Item.ta"; import readEntryInformation from "./readEntryInformation"; -import type { OBJECT_IDENTIFIER } from "asn1-ts"; +import { TRUE, type OBJECT_IDENTIFIER } from "asn1-ts"; import type ACDFTupleExtended from "@wildboar/x500/src/lib/types/ACDFTupleExtended"; import bacACDF, { PERMISSION_CATEGORY_READ, @@ -21,6 +22,12 @@ import type { } from "@wildboar/x500/src/lib/modules/SelectedAttributeTypes/NameAndOptionalUID.ta"; import bacSettings from "../../authz/bacSettings"; import isOperationalAttributeType from "../../x500/isOperationalAttributeType"; +import accessControlSchemesThatUseRBAC from "../../authz/accessControlSchemesThatUseRBAC"; +import { EntryInformationSelection } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/EntryInformationSelection.ta"; +import { attributeValueSecurityLabelContext } from "@wildboar/x500/src/lib/collections/contexts"; +import { rbacACDF } from "../../authz/rbacACDF"; +import { Attribute_valuesWithContext_Item } from "@wildboar/pki-stub/src/lib/modules/InformationFramework/Attribute-valuesWithContext-Item.ta"; +import { attributeTypesOnly } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/EntryInformationSelection-infoTypes.ta"; export interface ReadPermittedEntryInformationReturn { @@ -73,6 +80,7 @@ interface ReadPermittedEntryInformationReturn { * - `readValues()` * * @param ctx The context object + * @param assn The client association * @param target The DSE whose entry information is to be read * @param user The distinguished name and unique identifier of the user * attempting to read the entry @@ -90,142 +98,222 @@ interface ReadPermittedEntryInformationReturn { export async function readPermittedEntryInformation ( ctx: Context, + assn: ClientAssociation | undefined, target: Vertex, user: NameAndOptionalUID | undefined | null, relevantTuples: ACDFTupleExtended[], accessControlScheme?: OBJECT_IDENTIFIER, options?: ReadAttributesOptions, ): Promise { + const acs = accessControlScheme?.toString(); const einfo: EntryInformation_information_Item[] = await readEntryInformation( ctx, target, - options, + { + ...options, + // If we are using RBAC, we need types, values, and contexts. + selection: (acs && accessControlSchemesThatUseRBAC.has(acs) && assn) + ? new EntryInformationSelection( + options?.selection?.attributes, + undefined, // We want full attributes, not just types. + options?.selection?.extraAttributes, + options?.selection?.contextSelection, + TRUE, // We also need all contexts. + options?.selection?.familyReturn, + ) + : options?.selection, + }, ); - if ( - !accessControlScheme - || !accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { + if (!acs) { return { incompleteEntry: false, information: einfo, discloseIncompleteEntry: true, }; } - const permittedEinfo: EntryInformation_information_Item[] = accessControlScheme - ? [] - : einfo; let incompleteEntry: boolean = true; let discloseIncompleteEntry: boolean = false; - for (const info of einfo) { - if ("attribute" in info) { - const { authorized: authorizedToAddAttributeType } = bacACDF( - relevantTuples, - user, - { - attributeType: info.attribute.type_, - operational: isOperationalAttributeType(ctx, info.attribute.type_), - }, - [ - PERMISSION_CATEGORY_READ, - ], - bacSettings, - true, - ); - if (!authorizedToAddAttributeType) { - incompleteEntry = true; - // Optimization: we only need to check this if we haven't - // already established that we can disclose incompleteness. - if (!discloseIncompleteEntry) { - const { authorized: authorizedToKnowAboutExcludedAttribute } = bacACDF( - relevantTuples, - user, - { - attributeType: info.attribute.type_, - operational: isOperationalAttributeType(ctx, info.attribute.type_), - }, - [ - PERMISSION_CATEGORY_DISCLOSE_ON_ERROR, - ], - bacSettings, - true, - ); - if (authorizedToKnowAboutExcludedAttribute) { - discloseIncompleteEntry = true; + const permittedEinfo: EntryInformation_information_Item[] = []; + const permittedEinfoViaRbac: EntryInformation_information_Item[] = []; + if (accessControlSchemesThatUseACIItems.has(acs)) { + for (const info of einfo) { + if ("attribute" in info) { + const { authorized: authorizedToAddAttributeType } = bacACDF( + relevantTuples, + user, + { + attributeType: info.attribute.type_, + operational: isOperationalAttributeType(ctx, info.attribute.type_), + }, + [ + PERMISSION_CATEGORY_READ, + ], + bacSettings, + true, + ); + if (!authorizedToAddAttributeType) { + incompleteEntry = true; + // Optimization: we only need to check this if we haven't + // already established that we can disclose incompleteness. + if (!discloseIncompleteEntry) { + const { authorized: authorizedToKnowAboutExcludedAttribute } = bacACDF( + relevantTuples, + user, + { + attributeType: info.attribute.type_, + operational: isOperationalAttributeType(ctx, info.attribute.type_), + }, + [ + PERMISSION_CATEGORY_DISCLOSE_ON_ERROR, + ], + bacSettings, + true, + ); + if (authorizedToKnowAboutExcludedAttribute) { + discloseIncompleteEntry = true; + } } + continue; } - continue; - } - const permittedValues = valuesFromAttribute(info.attribute) - .filter((value) => { - const atav = new AttributeTypeAndValue( - value.type, - value.value, - ); - const acdfResult = bacACDF( - relevantTuples, - user, - { - value: atav, - operational: isOperationalAttributeType(ctx, atav.type_), - }, - [ PERMISSION_CATEGORY_READ ], - bacSettings, - true, - ); - if (!acdfResult.authorized) { - // Optimization: we only need to check this if we haven't - // already established that we can disclose incompleteness. - if (!discloseIncompleteEntry) { - const { authorized: authorizedToKnowAboutExcludedAttributeValue } = bacACDF( - relevantTuples, - user, - { - value: atav, - operational: isOperationalAttributeType(ctx, atav.type_), - }, - [ PERMISSION_CATEGORY_DISCLOSE_ON_ERROR ], - bacSettings, - true, - ); - if (authorizedToKnowAboutExcludedAttributeValue) { - discloseIncompleteEntry = true; + const permittedValues = valuesFromAttribute(info.attribute) + .filter((value) => { + const atav = new AttributeTypeAndValue( + value.type, + value.value, + ); + const acdfResult = bacACDF( + relevantTuples, + user, + { + value: atav, + operational: isOperationalAttributeType(ctx, atav.type_), + }, + [ PERMISSION_CATEGORY_READ ], + bacSettings, + true, + ); + if (!acdfResult.authorized) { + // Optimization: we only need to check this if we haven't + // already established that we can disclose incompleteness. + if (!discloseIncompleteEntry) { + const { authorized: authorizedToKnowAboutExcludedAttributeValue } = bacACDF( + relevantTuples, + user, + { + value: atav, + operational: isOperationalAttributeType(ctx, atav.type_), + }, + [ PERMISSION_CATEGORY_DISCLOSE_ON_ERROR ], + bacSettings, + true, + ); + if (authorizedToKnowAboutExcludedAttributeValue) { + discloseIncompleteEntry = true; + } } + incompleteEntry = true; } - incompleteEntry = true; - } - return acdfResult.authorized; - }); - const attribute = attributesFromValues(permittedValues)[0]; - if (attribute) { - permittedEinfo.push({ attribute }); + return acdfResult.authorized; + }); + const attribute = attributesFromValues(permittedValues)[0]; + if (attribute) { + permittedEinfo.push({ attribute }); + } else { + permittedEinfo.push({ + attributeType: info.attribute.type_, + }); + } + } else if ("attributeType" in info) { + const { authorized: authorizedToAddAttributeType } = bacACDF( + relevantTuples, + user, + { + attributeType: info.attributeType, + operational: isOperationalAttributeType(ctx, info.attributeType), + }, + [ PERMISSION_CATEGORY_READ ], + bacSettings, + true, + ); + if (authorizedToAddAttributeType) { + permittedEinfo.push(info); + } } else { - permittedEinfo.push({ - attributeType: info.attribute.type_, - }); + continue; } - } else if ("attributeType" in info) { - const { authorized: authorizedToAddAttributeType } = bacACDF( - relevantTuples, - user, - { - attributeType: info.attributeType, - operational: isOperationalAttributeType(ctx, info.attributeType), - }, - [ PERMISSION_CATEGORY_READ ], - bacSettings, - true, - ); - if (authorizedToAddAttributeType) { - permittedEinfo.push(info); + } + } + + if (!accessControlSchemesThatUseRBAC.has(acs) || !assn) { + return { + incompleteEntry, + discloseIncompleteEntry, + information: permittedEinfo, + }; + } + + const typesOnly: boolean = (options?.selection?.infoTypes === attributeTypesOnly); + for (const info of permittedEinfo) { + if (!("attribute" in info)) { + // This should actually never happen. + continue; + } + const attr = info.attribute; + const newVwc: Attribute_valuesWithContext_Item[] = []; + for (const vwc of attr.valuesWithContext ?? []) { + const labelContext = vwc.contextList + .find((c) => c.contextType.isEqualTo(attributeValueSecurityLabelContext["&id"]) && (c.contextValues.length > 0)); + if (labelContext) { + const label = attributeValueSecurityLabelContext.decoderFor["&Type"]!(labelContext.contextValues[0]); + const authorized: boolean = rbacACDF( + ctx, + assn, + target, + label, + attr.type_, + vwc.value, + vwc.contextList, + [PERMISSION_CATEGORY_READ], + ); + if (authorized) { + newVwc.push(vwc); + } + // If selecting typesonly, flag the attribute type as blacklisted. + // Otherwise, just remove the value. } - } else { + } + + if ((newVwc.length === 0) && (attr.values.length === 0)) { + // All values were filtered out. Permission to the whole attribute type is denied. continue; } + if (typesOnly) { + permittedEinfoViaRbac.push({ attributeType: attr.type_ }); + } else { + if (options?.selection?.returnContexts) { + permittedEinfoViaRbac.push({ + attribute: new Attribute( + attr.type_, + attr.values, + newVwc.length > 0 ? newVwc : undefined, + ), + }); + } else { + permittedEinfoViaRbac.push({ + attribute: new Attribute( + attr.type_, + [ ...attr.values, ...newVwc.map((v) => v.value) ], + undefined, + ), + }); + } + } } + return { incompleteEntry, discloseIncompleteEntry, - information: permittedEinfo, + information: permittedEinfoViaRbac, }; } diff --git a/apps/meerkat/src/app/distributed/modifyEntry.ts b/apps/meerkat/src/app/distributed/modifyEntry.ts index a93d3f07a..db6c79742 100644 --- a/apps/meerkat/src/app/distributed/modifyEntry.ts +++ b/apps/meerkat/src/app/distributed/modifyEntry.ts @@ -3797,6 +3797,7 @@ async function modifyEntry ( ) { const permittedEntryInfo = await readPermittedEntryInformation( ctx, + assn, target, user, relevantTuples, @@ -3828,6 +3829,7 @@ async function modifyEntry ( .slice(1) // Skip the first member, which is the read entry. .map((member) => readPermittedEntryInformation( ctx, + assn, member, user, relevantTuples, diff --git a/apps/meerkat/src/app/distributed/read.ts b/apps/meerkat/src/app/distributed/read.ts index f9d0fc6ed..d330b3494 100644 --- a/apps/meerkat/src/app/distributed/read.ts +++ b/apps/meerkat/src/app/distributed/read.ts @@ -495,6 +495,7 @@ async function read ( const permittedEntryInfo = await readPermittedEntryInformation( ctx, + assn, target, user, relevantTuples, @@ -527,6 +528,7 @@ async function read ( .slice(1) // Skip the first member, which is the read entry. .map((member) => readPermittedEntryInformation( ctx, + assn, member, user, relevantTuples, diff --git a/apps/meerkat/src/app/distributed/search_i.ts b/apps/meerkat/src/app/distributed/search_i.ts index ac53b1d48..7b8ed8b3e 100644 --- a/apps/meerkat/src/app/distributed/search_i.ts +++ b/apps/meerkat/src/app/distributed/search_i.ts @@ -2493,6 +2493,7 @@ async function search_i_ex ( .map(async (member): Promise<[ number, [ Vertex, BOOLEAN, EntryInformation_information_Item[], boolean ] ]> => { const permittedEntryReturn = await readPermittedEntryInformation( ctx, + assn, member, user, relevantTuples, @@ -2818,6 +2819,7 @@ async function search_i_ex ( .map(async (member) => { const permittedEntryReturn = await readPermittedEntryInformation( ctx, + assn, member, user, relevantTuples, From 4f5912cb02ae330fbc3af505e0dbb927ee2dc1b5 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Sun, 6 Aug 2023 22:32:45 -0400 Subject: [PATCH 04/39] feat: RBAC for all directory operations --- apps/meerkat/src/app/authz/acdf.ts | 4 +- apps/meerkat/src/app/authz/rbacACDF.ts | 6 +- apps/meerkat/src/app/distributed/addEntry.ts | 93 ++++--- .../src/app/distributed/administerPassword.ts | 61 +++-- .../src/app/distributed/changePassword.ts | 47 ++-- apps/meerkat/src/app/distributed/compare.ts | 62 +++-- apps/meerkat/src/app/distributed/findDSE.ts | 13 +- apps/meerkat/src/app/distributed/list_i.ts | 45 ++-- apps/meerkat/src/app/distributed/list_ii.ts | 46 ++-- apps/meerkat/src/app/distributed/modifyDN.ts | 117 +++++---- .../src/app/distributed/modifyEntry.ts | 243 ++++++++++-------- apps/meerkat/src/app/distributed/read.ts | 59 +++-- .../src/app/distributed/removeEntry.ts | 17 +- .../distributed/searchRuleCheckProcedure_i.ts | 22 +- apps/meerkat/src/app/distributed/search_i.ts | 101 ++++---- 15 files changed, 527 insertions(+), 409 deletions(-) diff --git a/apps/meerkat/src/app/authz/acdf.ts b/apps/meerkat/src/app/authz/acdf.ts index 7f17578d5..d9afaea11 100644 --- a/apps/meerkat/src/app/authz/acdf.ts +++ b/apps/meerkat/src/app/authz/acdf.ts @@ -29,7 +29,8 @@ function acdf ( requester: NameAndOptionalUID | undefined | null, request: ProtectedItem, settings: EvaluateFilterSettings, - tuplesAlreadySplit?: boolean + tuplesAlreadySplit?: boolean, + addingEntry: boolean = false, ): boolean { const acs = accessControlScheme.toString(); if (accessControlSchemesThatUseACIItems.has(acs)) { @@ -49,6 +50,7 @@ function acdf ( accessControlSchemesThatUseRBAC.has(acs) && ("value" in request) && assn + && !addingEntry // RBAC basically has no effect on adding entries unless the superior is hidden. // TODO: Find DSE basically runs this same code twice. I want to find some optimization to avoid that. // && (permissions.length !== 1 || permissions[0] !== PERMISSION_CATEGORY_DISCLOSE_ON_ERROR) ) { diff --git a/apps/meerkat/src/app/authz/rbacACDF.ts b/apps/meerkat/src/app/authz/rbacACDF.ts index 24ec256c7..504644b40 100644 --- a/apps/meerkat/src/app/authz/rbacACDF.ts +++ b/apps/meerkat/src/app/authz/rbacACDF.ts @@ -50,9 +50,9 @@ const simple_rbac_acdf: RBAC_ACDF = ( assn: ClientAssociation, // This has a clearance field. target: Vertex, signedLabel: SignedSecurityLabel, - // _value: ASN1Element, - // _contexts: X500Context[], - // _permissions: number[], + _value: ASN1Element, + _contexts: X500Context[], + _permissions: number[], ): boolean => { const label = signedLabel.toBeSigned.securityLabel; const classification = Number(label.security_classification ?? SecurityClassification_unmarked); diff --git a/apps/meerkat/src/app/distributed/addEntry.ts b/apps/meerkat/src/app/distributed/addEntry.ts index 8597d6804..2467fbcb2 100644 --- a/apps/meerkat/src/app/distributed/addEntry.ts +++ b/apps/meerkat/src/app/distributed/addEntry.ts @@ -116,7 +116,6 @@ import { abandoned, } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/abandoned.oa"; import getACIItems from "../authz/getACIItems"; -import accessControlSchemesThatUseACIItems from "../authz/accessControlSchemesThatUseACIItems"; import updateAffectedSubordinateDSAs from "../dop/updateAffectedSubordinateDSAs"; import type { DistinguishedName } from "@wildboar/x500/src/lib/modules/InformationFramework/DistinguishedName.ta"; import updateSuperiorDSA from "../dop/updateSuperiorDSA"; @@ -207,6 +206,7 @@ import { SubordinateChanges, } from "@wildboar/x500/src/lib/modules/DirectoryShadowAbstractService/SubordinateChanges.ta"; import { saveIncrementalRefresh } from "../disp/saveIncrementalRefresh"; +import { acdf } from "../authz/acdf"; const ID_AUTONOMOUS: string = id_ar_autonomousArea.toString(); const ID_AC_SPECIFIC: string = id_ar_accessControlSpecificArea.toString(); @@ -421,12 +421,13 @@ async function addEntry ( isMemberOfGroup, NAMING_MATCHER, ); - if ( - !ctx.config.bulkInsertMode - && accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { - const { authorized } = bacACDF( + if (!ctx.config.bulkInsertMode && accessControlScheme) { + const authorized = acdf( + ctx, + accessControlScheme, + assn, + immediateSuperior, // This basically doesn't matter, because addingEntry is true. + [PERMISSION_CATEGORY_ADD], relevantTuples, user, { @@ -440,11 +441,9 @@ async function addEntry ( }) : undefined, }, - [ - PERMISSION_CATEGORY_ADD, - ], bacSettings, true, + true, ); if (!authorized) { throw new errors.SecurityError( @@ -487,11 +486,7 @@ async function addEntry ( || immediateSuperior.dse.sa || immediateSuperior.dse.dsSubentry ) { - if ( - !ctx.config.bulkInsertMode - && accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { + if (!ctx.config.bulkInsertMode && accessControlScheme) { const relevantACIItemsForSuperior = await getACIItems( ctx, accessControlScheme, @@ -515,11 +510,15 @@ async function addEntry ( const superiorObjectClasses = Array .from(immediateSuperior.dse.objectClass) .map(ObjectIdentifier.fromString); - const { authorized: authorizedToReadSuperior } = bacACDF( + const authorizedToReadSuperior = acdf( + ctx, + accessControlScheme, + assn, + immediateSuperior, + [ PERMISSION_CATEGORY_READ ], relevantTuplesForSuperior, user, { entry: superiorObjectClasses }, - [ PERMISSION_CATEGORY_READ ], bacSettings, true, ); @@ -551,32 +550,46 @@ async function addEntry ( ); } - const { authorized: authorizedToReadSuperiorDSEType } = bacACDF( + const authorizedToReadSuperiorDSEType = acdf( + ctx, + accessControlScheme, + assn, + immediateSuperior, + [ PERMISSION_CATEGORY_READ ], relevantTuplesForSuperior, user, { attributeType: dseType["&id"], operational: true, }, - [ PERMISSION_CATEGORY_READ ], bacSettings, true, ); + // TODO: We do not check that the user has permission to the DSEType value! // const superiorDSEType = await readValuesOfType(ctx, immediateSuperior, dseType["&id"])[0]; - const { authorized: authorizedToReadSuperiorObjectClasses } = bacACDF( + const authorizedToReadSuperiorObjectClasses = acdf( + ctx, + accessControlScheme, + assn, + immediateSuperior, + [ PERMISSION_CATEGORY_READ ], relevantTuplesForSuperior, user, { attributeType: objectClass["&id"], operational: false, }, - [ PERMISSION_CATEGORY_READ ], bacSettings, true, ); const superiorObjectClassesAuthorized = superiorObjectClasses - .filter((oc) => bacACDF( + .filter((oc) => acdf( + ctx, + accessControlScheme, + assn, + immediateSuperior, + [ PERMISSION_CATEGORY_READ ], relevantTuplesForSuperior, user, { @@ -586,10 +599,9 @@ async function addEntry ( ), operational: false, }, - [ PERMISSION_CATEGORY_READ ], bacSettings, true, - ).authorized); + )); if ( (immediateSuperior.dse.alias || immediateSuperior.dse.sa) // superior is some kind of alias, and... @@ -760,7 +772,6 @@ async function addEntry ( ?? accessControlScheme; if ( // If access control does not apply to the existing entry,... !effectiveAccessControlScheme - || !accessControlSchemesThatUseACIItems.has(effectiveAccessControlScheme.toString()) ) { // We can inform the user that it does not exist; no need to do any more work. throw new errors.UpdateError( ctx.i18n.t("err:entry_already_exists", { @@ -813,13 +824,17 @@ async function addEntry ( isMemberOfGroup, NAMING_MATCHER, ); - const { authorized: authorizedToKnowAboutExistingEntry } = bacACDF( + const authorizedToKnowAboutExistingEntry: boolean = acdf( + ctx, + effectiveAccessControlScheme, + assn, + existing, + [ PERMISSION_CATEGORY_DISCLOSE_ON_ERROR ], relevantSubordinateTuples, user, { entry: Array.from(existing.dse.objectClass).map(ObjectIdentifier.fromString), }, - [ PERMISSION_CATEGORY_DISCLOSE_ON_ERROR ], bacSettings, true, ); @@ -917,11 +932,7 @@ async function addEntry ( ); } - if ( - !ctx.config.bulkInsertMode - && accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { + if (!ctx.config.bulkInsertMode && accessControlScheme) { const maxValueCountInUse: boolean = relevantTuples .some((tuple) => (tuple[2].maxValueCount !== undefined)); const valueCountByAttribute: Map = maxValueCountInUse @@ -939,7 +950,12 @@ async function addEntry ( ) : new Map(); for (const attr of data.entry) { - const { authorized: authorizedToAddAttributeType } = bacACDF( + const authorizedToAddAttributeType = acdf( + ctx, + accessControlScheme, + assn, + immediateSuperior, + [ PERMISSION_CATEGORY_ADD ], relevantTuples, user, { @@ -947,9 +963,9 @@ async function addEntry ( valuesCount: valueCountByAttribute.get(attr.type_.toString()), operational: isOperationalAttributeType(ctx, attr.type_), }, - [ PERMISSION_CATEGORY_ADD ], bacSettings, true, + true, ); if (!authorizedToAddAttributeType) { throw new errors.SecurityError( @@ -977,7 +993,12 @@ async function addEntry ( } } for (const value of values) { - const { authorized: authorizedToAddAttributeValue } = bacACDF( + const authorizedToAddAttributeValue = acdf( + ctx, + accessControlScheme, + assn, + immediateSuperior, + [ PERMISSION_CATEGORY_ADD ], relevantTuples, user, { @@ -992,9 +1013,9 @@ async function addEntry ( )), operational: isOperationalAttributeType(ctx, value.type), }, - [PERMISSION_CATEGORY_ADD], bacSettings, true, + true, ); if (!authorizedToAddAttributeValue) { throw new errors.SecurityError( diff --git a/apps/meerkat/src/app/distributed/administerPassword.ts b/apps/meerkat/src/app/distributed/administerPassword.ts index 6f7d5a78f..69f2b74c9 100644 --- a/apps/meerkat/src/app/distributed/administerPassword.ts +++ b/apps/meerkat/src/app/distributed/administerPassword.ts @@ -31,7 +31,7 @@ import getRelevantSubentries from "../dit/getRelevantSubentries"; import getDistinguishedName from "../x500/getDistinguishedName"; import type ACDFTuple from "@wildboar/x500/src/lib/types/ACDFTuple"; import type ACDFTupleExtended from "@wildboar/x500/src/lib/types/ACDFTupleExtended"; -import bacACDF, { +import { PERMISSION_CATEGORY_ADD, PERMISSION_CATEGORY_REMOVE, PERMISSION_CATEGORY_MODIFY, @@ -53,7 +53,6 @@ import { } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/securityError.oa"; import type { OperationDispatcherState } from "./OperationDispatcher"; import getACIItems from "../authz/getACIItems"; -import accessControlSchemesThatUseACIItems from "../authz/accessControlSchemesThatUseACIItems"; import getNamingMatcherGetter from "../x500/getNamingMatcherGetter"; import bacSettings from "../authz/bacSettings"; import { @@ -90,6 +89,7 @@ import { pwdReset } from "@wildboar/parity-schema/src/lib/modules/LDAPPasswordPo import { pwdHistorySlots } from "@wildboar/x500/src/lib/collections/attributes"; import { id_scrypt } from "@wildboar/scrypt-0"; import { validateAlgorithmParameters } from "../authn/validateAlgorithmParameters"; +import { acdf } from "../authz/acdf"; const USER_PASSWORD_OID: string = userPassword["&id"].toString(); const USER_PWD_OID: string = userPwd["&id"].toString(); @@ -222,10 +222,7 @@ async function administerPassword ( const accessControlScheme = [ ...state.admPoints ] // Array.reverse() works in-place, so we create a new array. .reverse() .find((ap) => ap.dse.admPoint!.accessControlScheme)?.dse.admPoint!.accessControlScheme; - if ( - accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { + if (accessControlScheme) { const relevantACIItems = await getACIItems( ctx, accessControlScheme, @@ -247,61 +244,75 @@ async function administerPassword ( isMemberOfGroup, NAMING_MATCHER, ); - const { authorized: authorizedToModifyEntry } = bacACDF( + const authorizedToModifyEntry = acdf( + ctx, + accessControlScheme, + assn, + target, + [PERMISSION_CATEGORY_MODIFY], relevantTuples, user, { entry: Array.from(target.dse.objectClass).map(ObjectIdentifier.fromString), }, - [ - PERMISSION_CATEGORY_MODIFY, - ], bacSettings, true, ); - const { authorized: authorizedToModifyUserPassword } = bacACDF( + const authorizedToModifyUserPassword = acdf( + ctx, + accessControlScheme, + assn, + target, + [ + PERMISSION_CATEGORY_ADD, + PERMISSION_CATEGORY_REMOVE, + PERMISSION_CATEGORY_MODIFY, + ], relevantTuples, user, { attributeType: userPassword["&id"], operational: false, }, + bacSettings, + true, + ); + const authorizedToModifyUserPwd = acdf( + ctx, + accessControlScheme, + assn, + target, [ PERMISSION_CATEGORY_ADD, PERMISSION_CATEGORY_REMOVE, PERMISSION_CATEGORY_MODIFY, ], - bacSettings, - true, - ); - const { authorized: authorizedToModifyUserPwd } = bacACDF( relevantTuples, user, { attributeType: userPwd["&id"], operational: false, }, + bacSettings, + true, + ); + // Permission to modify the user password history is required for administerPassword (in Meerkat DSA). + const authorizedToModifyPwdHistory = acdf( + ctx, + accessControlScheme, + assn, + target, [ PERMISSION_CATEGORY_ADD, PERMISSION_CATEGORY_REMOVE, PERMISSION_CATEGORY_MODIFY, ], - bacSettings, - true, - ); - // Permission to modify the user password history is required for administerPassword (in Meerkat DSA). - const { authorized: authorizedToModifyPwdHistory } = bacACDF( relevantTuples, user, { attributeType: userPwdHistory["&id"], operational: true, }, - [ - PERMISSION_CATEGORY_ADD, - PERMISSION_CATEGORY_REMOVE, - PERMISSION_CATEGORY_MODIFY, - ], bacSettings, true, ); diff --git a/apps/meerkat/src/app/distributed/changePassword.ts b/apps/meerkat/src/app/distributed/changePassword.ts index 1faa64a80..fd30988ef 100644 --- a/apps/meerkat/src/app/distributed/changePassword.ts +++ b/apps/meerkat/src/app/distributed/changePassword.ts @@ -34,7 +34,7 @@ import getRelevantSubentries from "../dit/getRelevantSubentries"; import getDistinguishedName from "../x500/getDistinguishedName"; import type ACDFTuple from "@wildboar/x500/src/lib/types/ACDFTuple"; import type ACDFTupleExtended from "@wildboar/x500/src/lib/types/ACDFTupleExtended"; -import bacACDF, { +import { PERMISSION_CATEGORY_ADD, PERMISSION_CATEGORY_REMOVE, PERMISSION_CATEGORY_MODIFY, @@ -56,7 +56,6 @@ import { } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/securityError.oa"; import type { OperationDispatcherState } from "./OperationDispatcher"; import getACIItems from "../authz/getACIItems"; -import accessControlSchemesThatUseACIItems from "../authz/accessControlSchemesThatUseACIItems"; import getNamingMatcherGetter from "../x500/getNamingMatcherGetter"; import bacSettings from "../authz/bacSettings"; import { @@ -97,6 +96,7 @@ import { pwdChangeAllowed } from "@wildboar/x500/src/lib/collections/attributes" import { id_scrypt } from "@wildboar/scrypt-0"; import { SimpleCredentials } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/SimpleCredentials.ta"; import { validateAlgorithmParameters } from "../authn/validateAlgorithmParameters"; +import { acdf } from "../authz/acdf"; const USER_PASSWORD_OID: string = userPassword["&id"].toString(); const USER_PWD_OID: string = userPwd["&id"].toString(); @@ -228,10 +228,7 @@ async function changePassword ( const accessControlScheme = [ ...state.admPoints ] // Array.reverse() works in-place, so we create a new array. .reverse() .find((ap) => ap.dse.admPoint!.accessControlScheme)?.dse.admPoint!.accessControlScheme; - if ( - accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { + if (accessControlScheme) { const relevantACIItems = await getACIItems( ctx, accessControlScheme, @@ -253,45 +250,55 @@ async function changePassword ( isMemberOfGroup, NAMING_MATCHER, ); - const { authorized: authorizedToModifyEntry } = bacACDF( + const authorizedToModifyEntry = acdf( + ctx, + accessControlScheme, + assn, + target, + [PERMISSION_CATEGORY_MODIFY], relevantTuples, user, { entry: Array.from(target.dse.objectClass).map(ObjectIdentifier.fromString), }, - [ - PERMISSION_CATEGORY_MODIFY, - ], bacSettings, true, ); - const { authorized: authorizedToModifyUserPassword } = bacACDF( + const authorizedToModifyUserPassword = acdf( + ctx, + accessControlScheme, + assn, + target, + [ + PERMISSION_CATEGORY_ADD, + PERMISSION_CATEGORY_REMOVE, + PERMISSION_CATEGORY_MODIFY, + ], relevantTuples, user, { attributeType: userPassword["&id"], operational: false, }, + bacSettings, + true, + ); + const authorizedToModifyUserPwd = acdf( + ctx, + accessControlScheme, + assn, + target, [ PERMISSION_CATEGORY_ADD, PERMISSION_CATEGORY_REMOVE, PERMISSION_CATEGORY_MODIFY, ], - bacSettings, - true, - ); - const { authorized: authorizedToModifyUserPwd } = bacACDF( relevantTuples, user, { attributeType: userPwd["&id"], operational: false, }, - [ - PERMISSION_CATEGORY_ADD, - PERMISSION_CATEGORY_REMOVE, - PERMISSION_CATEGORY_MODIFY, - ], bacSettings, true, ); diff --git a/apps/meerkat/src/app/distributed/compare.ts b/apps/meerkat/src/app/distributed/compare.ts index 6d4b4b39c..04d6a9309 100644 --- a/apps/meerkat/src/app/distributed/compare.ts +++ b/apps/meerkat/src/app/distributed/compare.ts @@ -51,7 +51,7 @@ import { import getRelevantSubentries from "../dit/getRelevantSubentries"; import type ACDFTuple from "@wildboar/x500/src/lib/types/ACDFTuple"; import type ACDFTupleExtended from "@wildboar/x500/src/lib/types/ACDFTupleExtended"; -import bacACDF, { +import { PERMISSION_CATEGORY_READ, PERMISSION_CATEGORY_COMPARE, PERMISSION_CATEGORY_DISCLOSE_ON_ERROR, @@ -85,7 +85,6 @@ import { abandoned, } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/abandoned.oa"; import getACIItems from "../authz/getACIItems"; -import accessControlSchemesThatUseACIItems from "../authz/accessControlSchemesThatUseACIItems"; import getAttributeSubtypes from "../x500/getAttributeSubtypes"; import { ServiceControlOptions_noSubtypeMatch, @@ -147,6 +146,7 @@ import { import { SimpleCredentials, } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/SimpleCredentials.ta"; +import { acdf } from "../authz/acdf"; /** * @summary The compare operation, as specified in ITU Recommendation X.511. @@ -287,19 +287,18 @@ async function compare ( isMemberOfGroup, NAMING_MATCHER, ); - if ( - accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { - const { authorized: authorizedToEntry } = bacACDF( + if (accessControlScheme) { + const authorizedToEntry = acdf( + ctx, + accessControlScheme, + assn, + target, + [PERMISSION_CATEGORY_READ], relevantTuples, user, { entry: Array.from(target.dse.objectClass).map(ObjectIdentifier.fromString), }, - [ - PERMISSION_CATEGORY_READ, - ], bacSettings, true, ); @@ -347,29 +346,37 @@ async function compare ( signErrors, ); } - const { authorized: authorizedToCompareAttributeType } = bacACDF( + const authorizedToCompareAttributeType = acdf( + ctx, + accessControlScheme, + assn, + target, + [ + PERMISSION_CATEGORY_COMPARE, + PERMISSION_CATEGORY_READ, // Not mandated by the spec, but required by Meerkat. + ], relevantTuples, user, { attributeType: type_, operational: isOperationalAttributeType(ctx, type_), }, - [ - PERMISSION_CATEGORY_COMPARE, - PERMISSION_CATEGORY_READ, // Not mandated by the spec, but required by Meerkat. - ], bacSettings, true, ); if (!authorizedToCompareAttributeType) { - const { authorized: authorizedToDiscloseAttribute } = bacACDF( + const authorizedToDiscloseAttribute = acdf( + ctx, + accessControlScheme, + assn, + target, + [PERMISSION_CATEGORY_DISCLOSE_ON_ERROR], relevantTuples, user, { attributeType: type_, operational: isOperationalAttributeType(ctx, type_), }, - [ PERMISSION_CATEGORY_DISCLOSE_ON_ERROR ], bacSettings, true, ); @@ -624,13 +631,16 @@ async function compare ( continue; } - if ( - accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { - const { - authorized, - } = bacACDF( + if (accessControlScheme) { + const authorized = acdf( + ctx, + accessControlScheme, + assn, + target, + [ + PERMISSION_CATEGORY_COMPARE, + PERMISSION_CATEGORY_READ, // Not mandated by the spec, but required by Meerkat. + ], relevantTuples, user, { @@ -640,10 +650,6 @@ async function compare ( ), operational: isOperationalAttributeType(ctx, value.type), }, - [ - PERMISSION_CATEGORY_COMPARE, - PERMISSION_CATEGORY_READ, // Not mandated by the spec, but required by Meerkat. - ], bacSettings, true, ); diff --git a/apps/meerkat/src/app/distributed/findDSE.ts b/apps/meerkat/src/app/distributed/findDSE.ts index 879f6f72e..b3cc90c9f 100644 --- a/apps/meerkat/src/app/distributed/findDSE.ts +++ b/apps/meerkat/src/app/distributed/findDSE.ts @@ -96,7 +96,6 @@ import { } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/abandoned.oa"; import vertexFromDatabaseEntry from "../database/vertexFromDatabaseEntry"; import getACIItems from "../authz/getACIItems"; -import accessControlSchemesThatUseACIItems from "../authz/accessControlSchemesThatUseACIItems"; import cloneChainingArgs from "../x500/cloneChainingArguments"; import bacSettings from "../authz/bacSettings"; import { @@ -1163,11 +1162,7 @@ export if (child.dse.admPoint?.accessControlScheme) { accessControlScheme = child.dse.admPoint.accessControlScheme; } - if ( // Check if the user can actually access it. - !ctx.config.bulkInsertMode - && accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { + if (!ctx.config.bulkInsertMode && accessControlScheme) { const childDN = getDistinguishedName(child); // Without this, all first-level DSEs are discoverable. const relevantAdmPoints: Vertex[] = child.dse.admPoint @@ -1330,11 +1325,7 @@ export } let discloseOnError: boolean = true; - if ( - !ctx.config.bulkInsertMode - && accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { + if (!ctx.config.bulkInsertMode && accessControlScheme) { const currentDN = getDistinguishedName(dse_i); const relevantSubentries: Vertex[] = (await Promise.all( state.admPoints.map((ap) => getRelevantSubentries(ctx, dse_i, currentDN, ap)), diff --git a/apps/meerkat/src/app/distributed/list_i.ts b/apps/meerkat/src/app/distributed/list_i.ts index 79506e957..5da7fc0af 100644 --- a/apps/meerkat/src/app/distributed/list_i.ts +++ b/apps/meerkat/src/app/distributed/list_i.ts @@ -50,7 +50,7 @@ import { import getRelevantSubentries from "../dit/getRelevantSubentries"; import type ACDFTuple from "@wildboar/x500/src/lib/types/ACDFTuple"; import type ACDFTupleExtended from "@wildboar/x500/src/lib/types/ACDFTupleExtended"; -import bacACDF, { +import { PERMISSION_CATEGORY_BROWSE, PERMISSION_CATEGORY_RETURN_DN, PERMISSION_CATEGORY_READ, @@ -95,7 +95,6 @@ import { import getDateFromTime from "@wildboar/x500/src/lib/utils/getDateFromTime"; import type { OperationDispatcherState } from "./OperationDispatcher"; import getACIItems from "../authz/getACIItems"; -import accessControlSchemesThatUseACIItems from "../authz/accessControlSchemesThatUseACIItems"; import { child, } from "@wildboar/x500/src/lib/modules/InformationFramework/child.oa"; @@ -150,6 +149,7 @@ import { } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/securityError.oa"; import DSPAssociation from "../dsp/DSPConnection"; import { entryACI, prescriptiveACI, subentryACI } from "@wildboar/x500/src/lib/collections/attributes"; +import { acdf } from "../authz/acdf"; const BYTES_IN_A_UUID: number = 16; const PARENT: string = parent["&id"].toString(); @@ -572,10 +572,7 @@ async function list_i ( let authorizedToKnowSubordinateIsAlias: boolean = true; const effectiveAccessControlScheme = subordinate.dse.admPoint?.accessControlScheme ?? targetAccessControlScheme; - if ( - effectiveAccessControlScheme - && accessControlSchemesThatUseACIItems.has(effectiveAccessControlScheme.toString()) - ) { + if (effectiveAccessControlScheme) { const subordinateDN = [ ...targetDN, subordinate.dse.rdn ]; const effectiveRelevantSubentries = subordinate.dse.admPoint?.administrativeRole.has(ID_AUTONOMOUS) ? [] @@ -606,14 +603,18 @@ async function list_i ( NAMING_MATCHER, ); const objectClasses = Array.from(subordinate.dse.objectClass).map(ObjectIdentifier.fromString); - const { authorized: authorizedToList } = bacACDF( - relevantSubordinateTuples, - user, - { entry: objectClasses }, + const authorizedToList = acdf( + ctx, + effectiveAccessControlScheme, + assn, + subordinate, [ PERMISSION_CATEGORY_BROWSE, PERMISSION_CATEGORY_RETURN_DN, ], + relevantSubordinateTuples, + user, + { entry: objectClasses }, bacSettings, true, ); @@ -621,18 +622,27 @@ async function list_i ( continue; } if (subordinate.dse.alias) { - const { authorized: authorizedToReadObjectClasses } = bacACDF( + const authorizedToReadObjectClasses = acdf( + ctx, + effectiveAccessControlScheme, + assn, + target, + [PERMISSION_CATEGORY_READ], relevantSubordinateTuples, user, { attributeType: objectClass["&id"], operational: false, }, - [ PERMISSION_CATEGORY_READ ], bacSettings, true, ); - const { authorized: authorizedToReadAliasObjectClasses } = bacACDF( + const authorizedToReadAliasObjectClasses = acdf( + ctx, + effectiveAccessControlScheme, + assn, + subordinate, + [PERMISSION_CATEGORY_READ], relevantSubordinateTuples, user, { @@ -642,18 +652,21 @@ async function list_i ( ), operational: false, }, - [ PERMISSION_CATEGORY_READ ], bacSettings, true, ); - const { authorized: authorizedToReadAliasedEntryName } = bacACDF( + const authorizedToReadAliasedEntryName = acdf( + ctx, + effectiveAccessControlScheme, + assn, + subordinate, + [PERMISSION_CATEGORY_READ], relevantSubordinateTuples, user, { attributeType: aliasedEntryName["&id"], operational: false, }, - [ PERMISSION_CATEGORY_READ ], bacSettings, true, ); diff --git a/apps/meerkat/src/app/distributed/list_ii.ts b/apps/meerkat/src/app/distributed/list_ii.ts index e53e187b1..da7a0830a 100644 --- a/apps/meerkat/src/app/distributed/list_ii.ts +++ b/apps/meerkat/src/app/distributed/list_ii.ts @@ -45,7 +45,7 @@ import { import getRelevantSubentries from "../dit/getRelevantSubentries"; import type ACDFTuple from "@wildboar/x500/src/lib/types/ACDFTuple"; import type ACDFTupleExtended from "@wildboar/x500/src/lib/types/ACDFTupleExtended"; -import bacACDF, { +import { PERMISSION_CATEGORY_BROWSE, PERMISSION_CATEGORY_RETURN_DN, PERMISSION_CATEGORY_READ, @@ -93,7 +93,6 @@ import getListResultStatistics from "../telemetry/getListResultStatistics"; import getPartialOutcomeQualifierStatistics from "../telemetry/getPartialOutcomeQualifierStatistics"; import failover from "../utils/failover"; import getACIItems from "../authz/getACIItems"; -import accessControlSchemesThatUseACIItems from "../authz/accessControlSchemesThatUseACIItems"; import { MAX_RESULTS, UNTRUSTED_REQ_AUTH_LEVEL } from "../constants"; import type { Prisma } from "@prisma/client"; import { @@ -152,6 +151,7 @@ import { import DSPAssociation from "../dsp/DSPConnection"; import { generateSignature } from "../pki/generateSignature"; import { SIGNED } from "@wildboar/x500/src/lib/modules/AuthenticationFramework/SIGNED.ta"; +import { acdf } from "../authz/acdf"; const BYTES_IN_A_UUID: number = 16; const PARENT: string = parent["&id"].toString(); @@ -544,10 +544,7 @@ async function list_ii ( let authorizedToKnowSubordinateIsAlias: boolean = true; const effectiveAccessControlScheme = subordinate.dse.admPoint?.accessControlScheme ?? targetAccessControlScheme; - if ( - effectiveAccessControlScheme - && accessControlSchemesThatUseACIItems.has(effectiveAccessControlScheme.toString()) - ) { + if (effectiveAccessControlScheme) { const subordinateDN = [ ...targetDN, subordinate.dse.rdn ]; const effectiveRelevantSubentries = subordinate.dse.admPoint?.administrativeRole.has(ID_AUTONOMOUS) ? [] @@ -578,14 +575,18 @@ async function list_ii ( NAMING_MATCHER, ); const objectClasses = Array.from(subordinate.dse.objectClass).map(ObjectIdentifier.fromString); - const { authorized: authorizedToList } = bacACDF( - relevantSubordinateTuples, - user, - { entry: objectClasses }, + const authorizedToList = acdf( + ctx, + effectiveAccessControlScheme, + assn, + subordinate, [ PERMISSION_CATEGORY_BROWSE, PERMISSION_CATEGORY_RETURN_DN, ], + relevantSubordinateTuples, + user, + { entry: objectClasses }, bacSettings, true, ); @@ -593,18 +594,27 @@ async function list_ii ( continue; } if (subordinate.dse.alias) { - const { authorized: authorizedToReadObjectClasses } = bacACDF( + const authorizedToReadObjectClasses = acdf( + ctx, + effectiveAccessControlScheme, + assn, + target, + [PERMISSION_CATEGORY_READ], relevantSubordinateTuples, user, { attributeType: objectClass["&id"], operational: false, }, - [ PERMISSION_CATEGORY_READ ], bacSettings, true, ); - const { authorized: authorizedToReadAliasObjectClasses } = bacACDF( + const authorizedToReadAliasObjectClasses = acdf( + ctx, + effectiveAccessControlScheme, + assn, + subordinate, + [PERMISSION_CATEGORY_READ], relevantSubordinateTuples, user, { @@ -612,19 +622,23 @@ async function list_ii ( objectClass["&id"], _encodeObjectIdentifier(alias["&id"], DER), ), + operational: false, }, - [ PERMISSION_CATEGORY_READ ], bacSettings, true, ); - const { authorized: authorizedToReadAliasedEntryName } = bacACDF( + const authorizedToReadAliasedEntryName = acdf( + ctx, + effectiveAccessControlScheme, + assn, + subordinate, + [PERMISSION_CATEGORY_READ], relevantSubordinateTuples, user, { attributeType: aliasedEntryName["&id"], operational: false, }, - [ PERMISSION_CATEGORY_READ ], bacSettings, true, ); diff --git a/apps/meerkat/src/app/distributed/modifyDN.ts b/apps/meerkat/src/app/distributed/modifyDN.ts index e93967632..75b0fd066 100644 --- a/apps/meerkat/src/app/distributed/modifyDN.ts +++ b/apps/meerkat/src/app/distributed/modifyDN.ts @@ -129,7 +129,6 @@ import { } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/NameProblem.ta"; import getACIItems from "../authz/getACIItems"; import { differenceInMilliseconds } from "date-fns"; -import accessControlSchemesThatUseACIItems from "../authz/accessControlSchemesThatUseACIItems"; import updateAffectedSubordinateDSAs from "../dop/updateAffectedSubordinateDSAs"; import updateSuperiorDSA from "../dop/updateSuperiorDSA"; import bacSettings from "../authz/bacSettings"; @@ -186,6 +185,7 @@ import getEqualityNormalizer from "../x500/getEqualityNormalizer"; import { getShadowIncrementalSteps } from "../dop/getRelevantSOBs"; import { SubordinateChanges } from "@wildboar/x500/src/lib/modules/DirectoryShadowAbstractService/SubordinateChanges.ta"; import { saveIncrementalRefresh } from "../disp/saveIncrementalRefresh"; +import { acdf } from "../authz/acdf"; /** * @summary Determine whether a DSE is local to this DSA @@ -499,15 +499,13 @@ async function modifyDN ( isMemberOfGroup, NAMING_MATCHER, ); - if ( - accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { + if (accessControlScheme) { if (data.newRDN) { - const { authorized: authorizedToEntry } = bacACDF( - relevantTuples, - user, - { entry: objectClasses }, + const authorizedToEntry = acdf( + ctx, + accessControlScheme, + assn, + target, [ /** * Read permission is not required by the specification, but @@ -526,6 +524,9 @@ async function modifyDN ( PERMISSION_CATEGORY_READ, PERMISSION_CATEGORY_RENAME, ], + relevantTuples, + user, + { entry: objectClasses }, bacSettings, true, ); @@ -566,18 +567,27 @@ async function modifyDN ( * the entry itself: 4E7AC6BB-CD58-47C8-B4DC-1B101A608C0E */ for (const atav of data.newRDN) { - const { authorized: authorizedToReadAttributeType } = bacACDF( + const authorizedToReadAttributeType = acdf( + ctx, + accessControlScheme, + assn, + target, + [ PERMISSION_CATEGORY_READ ], relevantTuples, user, { attributeType: atav.type_, operational: isOperationalAttributeType(ctx, atav.type_), }, - [ PERMISSION_CATEGORY_READ ], bacSettings, true, ); - const { authorized: authorizedToReadValue } = bacACDF( + const authorizedToReadValue = acdf( + ctx, + accessControlScheme, + assn, + target, + [ PERMISSION_CATEGORY_READ ], relevantTuples, user, { @@ -587,7 +597,6 @@ async function modifyDN ( ), operational: isOperationalAttributeType(ctx, atav.type_), }, - [ PERMISSION_CATEGORY_READ ], bacSettings, true, ); @@ -616,11 +625,15 @@ async function modifyDN ( } } if (data.newSuperior) { - const { authorized: authorizedToEntry } = bacACDF( + const authorizedToEntry = acdf( + ctx, + accessControlScheme, + assn, + target, + [ PERMISSION_CATEGORY_EXPORT ], relevantTuples, user, { entry: objectClasses }, - [ PERMISSION_CATEGORY_EXPORT ], bacSettings, true, ); @@ -715,10 +728,7 @@ async function modifyDN ( const newAccessControlScheme = [ ...newAdmPoints ] // Array.reverse() works in-place, so we create a new array. .reverse() .find((ap) => ap.dse.admPoint!.accessControlScheme)?.dse.admPoint!.accessControlScheme; - if ( - newAccessControlScheme - && accessControlSchemesThatUseACIItems.has(newAccessControlScheme.toString()) - ) { + if (newAccessControlScheme) { const relevantACIItems = await getACIItems( ctx, accessControlScheme, @@ -738,9 +748,12 @@ async function modifyDN ( isMemberOfGroup, NAMING_MATCHER, ); - const { - authorized: authorizedToEntry, - } = bacACDF( + const authorizedToEntry = acdf( + ctx, + newAccessControlScheme, + assn, + target, + [ PERMISSION_CATEGORY_IMPORT ], relevantTuples, user, { @@ -752,13 +765,9 @@ async function modifyDN ( }, }), }, - [ - PERMISSION_CATEGORY_IMPORT, - ], bacSettings, true, ); - if (!authorizedToEntry && !topLevelException) { throw new errors.SecurityError( ctx.i18n.t("err:not_authz_import"), @@ -901,11 +910,7 @@ async function modifyDN ( || superior.dse.sa || superior.dse.dsSubentry ) { - if ( - !ctx.config.bulkInsertMode - && accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { + if (!ctx.config.bulkInsertMode && accessControlScheme) { const relevantACIItemsForSuperior = await getACIItems( ctx, accessControlScheme, @@ -929,11 +934,16 @@ async function modifyDN ( const superiorObjectClasses = Array .from(superior.dse.objectClass) .map(ObjectIdentifier.fromString); - const { authorized: authorizedToReadSuperior } = bacACDF( + + const authorizedToReadSuperior = acdf( + ctx, + accessControlScheme, + assn, + superior, + [ PERMISSION_CATEGORY_READ ], relevantTuplesForSuperior, user, { entry: superiorObjectClasses }, - [ PERMISSION_CATEGORY_READ ], bacSettings, true, ); @@ -964,33 +974,45 @@ async function modifyDN ( signErrors, ); } - - const { authorized: authorizedToReadSuperiorDSEType } = bacACDF( + const authorizedToReadSuperiorDSEType = acdf( + ctx, + accessControlScheme, + assn, + superior, + [ PERMISSION_CATEGORY_READ ], relevantTuplesForSuperior, user, { attributeType: dseType["&id"], operational: true, }, - [ PERMISSION_CATEGORY_READ ], bacSettings, true, ); // TODO: We do not check that the user has permission to the DSEType value! // const superiorDSEType = await readValuesOfType(ctx, immediateSuperior, dseType["&id"])[0]; - const { authorized: authorizedToReadSuperiorObjectClasses } = bacACDF( + const authorizedToReadSuperiorObjectClasses = acdf( + ctx, + accessControlScheme, + assn, + superior, + [ PERMISSION_CATEGORY_READ ], relevantTuplesForSuperior, user, { attributeType: objectClass["&id"], operational: false, }, - [ PERMISSION_CATEGORY_READ ], bacSettings, true, ); const superiorObjectClassesAuthorized = superiorObjectClasses - .filter((oc) => bacACDF( + .filter((oc) => acdf( + ctx, + accessControlScheme, + assn, + superior, + [ PERMISSION_CATEGORY_READ ], relevantTuplesForSuperior, user, { @@ -1000,10 +1022,9 @@ async function modifyDN ( ), operational: false, }, - [ PERMISSION_CATEGORY_READ ], bacSettings, true, - ).authorized); + )); if ( (superior.dse.alias || superior.dse.sa) // superior is some kind of alias, and... @@ -1788,19 +1809,19 @@ async function modifyDN ( // Make sure that mandatory attributes are not deleted and that user has permission to delete. if (data.deleteOldRDN) { for (const atav of oldRDN) { - if ( - !ctx.config.bulkInsertMode - && accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { - const { authorized: authorizedToRemoveOldRDNValue } = bacACDF( + if (!ctx.config.bulkInsertMode && accessControlScheme) { + const authorizedToRemoveOldRDNValue = acdf( + ctx, + accessControlScheme, + assn, + target, + [ PERMISSION_CATEGORY_REMOVE ], relevantTuples, user, { value: atav, operational: isOperationalAttributeType(ctx, atav.type_), }, - [ PERMISSION_CATEGORY_REMOVE ], bacSettings, true, ); diff --git a/apps/meerkat/src/app/distributed/modifyEntry.ts b/apps/meerkat/src/app/distributed/modifyEntry.ts index db6c79742..7dfc23ca4 100644 --- a/apps/meerkat/src/app/distributed/modifyEntry.ts +++ b/apps/meerkat/src/app/distributed/modifyEntry.ts @@ -102,7 +102,7 @@ import { SecurityErrorData } from "@wildboar/x500/src/lib/modules/DirectoryAbstr import getRelevantSubentries from "../dit/getRelevantSubentries"; import type ACDFTuple from "@wildboar/x500/src/lib/types/ACDFTuple"; import type ACDFTupleExtended from "@wildboar/x500/src/lib/types/ACDFTupleExtended"; -import bacACDF, { +import { PERMISSION_CATEGORY_ADD, PERMISSION_CATEGORY_REMOVE, PERMISSION_CATEGORY_MODIFY, @@ -206,7 +206,6 @@ import { id_oc_child, } from "@wildboar/x500/src/lib/modules/InformationFramework/id-oc-child.va"; import getACIItems from "../authz/getACIItems"; -import accessControlSchemesThatUseACIItems from "../authz/accessControlSchemesThatUseACIItems"; import updateAffectedSubordinateDSAs from "../dop/updateAffectedSubordinateDSAs"; import { MINIMUM_MAX_ATTR_SIZE } from "../constants"; import updateSuperiorDSA from "../dop/updateSuperiorDSA"; @@ -278,6 +277,7 @@ import { import { getShadowIncrementalSteps } from "../dop/getRelevantSOBs"; import { saveIncrementalRefresh } from "../disp/saveIncrementalRefresh"; import { governingStructureRule } from "@wildboar/x500/src/lib/collections/attributes"; +import { acdf } from "../authz/acdf"; type ValuesIndex = Map; type ContextRulesIndex = Map; @@ -552,6 +552,7 @@ async function checkAttributePresence ( * @function */ function checkPermissionToAddValues ( + target: Vertex, modificationType: "addAttribute" | "addValues", attribute: Attribute, ctx: Context, @@ -562,21 +563,22 @@ function checkPermissionToAddValues ( aliasDereferenced?: boolean, signErrors: boolean = false, ): void { - if ( - !accessControlScheme - || !accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { + if (!accessControlScheme) { return; } const values = valuesFromAttribute(attribute); - const { authorized: authorizedForAttributeType } = bacACDF( + const authorizedForAttributeType = acdf( + ctx, + accessControlScheme, + assn, + target, + [ PERMISSION_CATEGORY_ADD ], relevantACDFTuples, user, { attributeType: attribute.type_, operational: isOperationalAttributeType(ctx, attribute.type_), }, - [ PERMISSION_CATEGORY_ADD ], bacSettings, true, ); @@ -591,7 +593,12 @@ function checkPermissionToAddValues ( ); } for (const value of values) { - const { authorized: authorizedForValue } = bacACDF( + const authorizedForValue = acdf( + ctx, + accessControlScheme, + assn, + target, + [ PERMISSION_CATEGORY_ADD ], relevantACDFTuples, user, { @@ -606,7 +613,6 @@ function checkPermissionToAddValues ( )), operational: isOperationalAttributeType(ctx, value.type), }, - [ PERMISSION_CATEGORY_ADD ], bacSettings, true, ); @@ -977,6 +983,7 @@ function removeValuesFromPatch ( * @async */ async function executeAddAttribute ( + target: Vertex, mod: Attribute, ctx: Context, assn: ClientAssociation, @@ -995,6 +1002,7 @@ async function executeAddAttribute ( * already known, so this is not a problem beyond this point. */ checkPermissionToAddValues( + target, "addAttribute", mod, ctx, @@ -1075,6 +1083,7 @@ async function executeAddAttribute ( * @async */ async function executeRemoveAttribute ( + target: Vertex, mod: AttributeType, ctx: Context, assn: ClientAssociation, @@ -1115,18 +1124,19 @@ async function executeRemoveAttribute ( * This is intentionally checked before even checking if the attribute is * present so that we avoid revealing whether it exists already or not. */ - if ( - accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { - const { authorized: authorizedForAttributeType } = bacACDF( + if (accessControlScheme) { + const authorizedForAttributeType = acdf( + ctx, + accessControlScheme, + assn, + target, + [ PERMISSION_CATEGORY_REMOVE ], relevantACDFTuples, user, { attributeType: mod, operational: isOperationalAttributeType(ctx, mod), }, - [ PERMISSION_CATEGORY_REMOVE ], bacSettings, true, ); @@ -1213,6 +1223,7 @@ async function executeRemoveAttribute ( * @async */ async function executeAddValues ( + target: Vertex, mod: Attribute, ctx: Context, assn: ClientAssociation, @@ -1258,6 +1269,7 @@ async function executeAddValues ( } checkAttributeArity(ctx, assn, entry, mod, aliasDereferenced, signErrors); checkPermissionToAddValues( + target, "addValues", mod, ctx, @@ -1306,6 +1318,7 @@ async function executeAddValues ( * @async */ async function executeRemoveValues ( + target: Vertex, mod: Attribute, ctx: Context, assn: ClientAssociation, @@ -1385,20 +1398,19 @@ async function executeRemoveValues ( signErrors, ) } - if ( - accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { - const { - authorized: authorizedForAttributeType, - } = bacACDF( + if (accessControlScheme) { + const authorizedForAttributeType = acdf( + ctx, + accessControlScheme, + assn, + target, + [ PERMISSION_CATEGORY_REMOVE ], relevantACDFTuples, user, { attributeType: mod.type_, operational: isOperationalAttributeType(ctx, mod.type_), }, - [ PERMISSION_CATEGORY_REMOVE ], bacSettings, true, ); @@ -1413,9 +1425,12 @@ async function executeRemoveValues ( ); } for (const value of values) { - const { - authorized: authorizedForValue, - } = bacACDF( + const authorizedForValue = acdf( + ctx, + accessControlScheme, + assn, + target, + [ PERMISSION_CATEGORY_REMOVE ], relevantACDFTuples, user, { @@ -1425,7 +1440,6 @@ async function executeRemoveValues ( ), operational: isOperationalAttributeType(ctx, value.type), }, - [ PERMISSION_CATEGORY_REMOVE ], bacSettings, true, ); @@ -1481,6 +1495,7 @@ async function executeRemoveValues ( * @async */ async function executeAlterValues ( + target: Vertex, mod: AttributeTypeAndValue, ctx: Context, assn: ClientAssociation, @@ -1521,21 +1536,22 @@ async function executeAlterValues ( signErrors, ); } - if ( - accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { - const { authorized: authorizedForAttributeType } = bacACDF( + if (accessControlScheme) { + const authorizedForAttributeType = acdf( + ctx, + accessControlScheme, + assn, + target, + [ + PERMISSION_CATEGORY_MODIFY, // DEVIATION: More strict than spec. + PERMISSION_CATEGORY_REMOVE, + ], relevantACDFTuples, user, { attributeType: mod.type_, operational: isOperationalAttributeType(ctx, mod.type_), }, - [ - PERMISSION_CATEGORY_MODIFY, // DEVIATION: More strict than spec. - PERMISSION_CATEGORY_REMOVE, - ], bacSettings, true, ); @@ -1559,12 +1575,17 @@ async function executeAlterValues ( signErrors, ); const values = await readValuesOfType(ctx, entry, mod.type_); - if ( - accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { + if (accessControlScheme) { for (const value of values) { - const { authorized: authorizedForValue } = bacACDF( + const authorizedForValue = acdf( + ctx, + accessControlScheme, + assn, + target, + [ + PERMISSION_CATEGORY_MODIFY, // DEVIATION: More strict than spec. + PERMISSION_CATEGORY_REMOVE, + ], relevantACDFTuples, user, { @@ -1574,14 +1595,6 @@ async function executeAlterValues ( ), operational: isOperationalAttributeType(ctx, value.type), }, - // DEVIATON: X.511 requires Add permissions, but I think this is - // a mistake. Why would you need permission to add values that - // you are changing? Instead, Meerkat DSA checks for modify and - // remove permissions. - [ - PERMISSION_CATEGORY_MODIFY, - PERMISSION_CATEGORY_REMOVE, - ], bacSettings, true, ); @@ -1599,12 +1612,16 @@ async function executeAlterValues ( } const TYPE_OID: IndexableOID = mod.type_.toString(); const newValues = values.map(alterer); - if ( - accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { + if (accessControlScheme) { for (const value of newValues) { - const { authorized: authorizedForValue } = bacACDF( + const authorizedForValue = acdf( + ctx, + accessControlScheme, + assn, + target, + // DEVIATON: X.511 does not explicitly require add permissions + // for the newly-produced values, but I think this is an error. + [ PERMISSION_CATEGORY_ADD ], relevantACDFTuples, user, { @@ -1619,9 +1636,6 @@ async function executeAlterValues ( )), operational: isOperationalAttributeType(ctx, value.type), }, - // DEVIATON: X.511 does not explicitly require add permissions - // for the newly-produced values, but I think this is an error. - [ PERMISSION_CATEGORY_ADD ], bacSettings, true, ); @@ -1680,6 +1694,7 @@ async function executeAlterValues ( * @async */ async function executeResetValue ( + target: Vertex, mod: AttributeType, ctx: Context, assn: ClientAssociation, @@ -1700,10 +1715,7 @@ async function executeResetValue ( }, }, }; - if ( - accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { + if (accessControlScheme) { // This is not incorrect. Contexts are only maintained for userApplications // attribute types, so this mod can just read right from this table. const rows = await ctx.db.attributeValue.findMany({ @@ -1717,9 +1729,14 @@ async function executeResetValue ( }); for (const row of rows) { const el = attributeValueFromDB(row); - const { - authorized: authorizedForAttributeType, - } = bacACDF( + const authorizedForAttributeType = acdf( + ctx, + accessControlScheme, + assn, + target, + // DEVIATON: X.511 does not explicitly require add permissions + // for the newly-produced values, but I think this is an error. + [ PERMISSION_CATEGORY_REMOVE ], relevantACDFTuples, user, { @@ -1729,9 +1746,6 @@ async function executeResetValue ( ), operational: isOperationalAttributeType(ctx, mod), }, - [ - PERMISSION_CATEGORY_REMOVE, - ], bacSettings, true, ); @@ -1802,6 +1816,7 @@ async function executeResetValue ( * @async */ async function executeReplaceValues ( + target: Vertex, mod: Attribute, ctx: Context, assn: ClientAssociation, @@ -1816,18 +1831,21 @@ async function executeReplaceValues ( checkAttributeArity(ctx, assn, entry, mod, aliasDereferenced, signErrors); const TYPE_OID: string = mod.type_.toString(); const values = valuesFromAttribute(mod); - if ( - accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { - const { authorized: authorizedForAttributeType } = bacACDF( + if (accessControlScheme) { + const authorizedForAttributeType = acdf( + ctx, + accessControlScheme, + assn, + target, + // DEVIATON: X.511 does not explicitly require add permissions + // for the newly-produced values, but I think this is an error. + [ PERMISSION_CATEGORY_ADD ], relevantACDFTuples, user, { attributeType: mod.type_, operational: isOperationalAttributeType(ctx, mod.type_), }, - [ PERMISSION_CATEGORY_ADD ], bacSettings, true, ); @@ -1843,7 +1861,12 @@ async function executeReplaceValues ( } const existingValues = await readValuesOfType(ctx, entry, mod.type_); for (const xv of existingValues) { - const { authorized: authorizedForValue } = bacACDF( + const authorizedForValue = acdf( + ctx, + accessControlScheme, + assn, + target, + [ PERMISSION_CATEGORY_REMOVE ], relevantACDFTuples, user, { @@ -1853,7 +1876,6 @@ async function executeReplaceValues ( ), operational: isOperationalAttributeType(ctx, mod.type_), }, - [ PERMISSION_CATEGORY_REMOVE ], bacSettings, true, ); @@ -2210,6 +2232,7 @@ async function executeEntryModification ( aliasDereferenced?: boolean, signErrors: boolean = false, ): Promise[]> { + const target = state.foundDSE; const commonArguments = [ ctx, @@ -2393,26 +2416,26 @@ async function executeEntryModification ( await checkPassword(mod.addAttribute); checkPreclusion(mod.addAttribute.type_); const attrWithDefaultContexts = checkContextRule(mod.addAttribute); - return executeAddAttribute(attrWithDefaultContexts, ...commonArguments); + return executeAddAttribute(target, attrWithDefaultContexts, ...commonArguments); } else if ("removeAttribute" in mod) { check(mod.removeAttribute, true); - return executeRemoveAttribute(mod.removeAttribute, ...commonArguments); + return executeRemoveAttribute(target, mod.removeAttribute, ...commonArguments); } else if ("addValues" in mod) { check(mod.addValues.type_, false); await checkPassword(mod.addValues); checkPreclusion(mod.addValues.type_); const attrWithDefaultContexts = checkContextRule(mod.addValues); - return executeAddValues(attrWithDefaultContexts, ...commonArguments); + return executeAddValues(target, attrWithDefaultContexts, ...commonArguments); } else if ("removeValues" in mod) { check(mod.removeValues.type_, true); - return executeRemoveValues(mod.removeValues, ...commonArguments); + return executeRemoveValues(target, mod.removeValues, ...commonArguments); } else if ("alterValues" in mod) { check(mod.alterValues.type_, false); - return executeAlterValues(mod.alterValues, ...commonArguments); + return executeAlterValues(target, mod.alterValues, ...commonArguments); } else if ("resetValue" in mod) { check(mod.resetValue, true); @@ -2448,14 +2471,14 @@ async function executeEntryModification ( signErrors, ); } - return executeResetValue(mod.resetValue, ...commonArguments); + return executeResetValue(target, mod.resetValue, ...commonArguments); } else if ("replaceValues" in mod) { check(mod.replaceValues.type_, false); await checkPassword(mod.replaceValues); checkPreclusion(mod.replaceValues.type_); const attrWithDefaultContexts = checkContextRule(mod.replaceValues); - return executeReplaceValues(attrWithDefaultContexts, ...commonArguments); + return executeReplaceValues(target, attrWithDefaultContexts, ...commonArguments); } else { // TODO: Log not-understood alternative. @@ -2651,27 +2674,22 @@ async function modifyEntry ( NAMING_MATCHER, ); - const authorizedToEntry = (permissions: number[]): boolean => { - const { - authorized, - } = bacACDF( - relevantTuples, - user, - { - entry: Array.from(target.dse.objectClass).map(ObjectIdentifier.fromString), - }, - permissions, - bacSettings, - true, - ); - return authorized; - }; + const authorizedToEntry = (permissions: number[]): boolean => !accessControlScheme || acdf( + ctx, + accessControlScheme, + assn, + target, + permissions, + relevantTuples, + user, + { + entry: Array.from(target.dse.objectClass).map(ObjectIdentifier.fromString), + }, + bacSettings, + true, + ); - if ( - !ctx.config.bulkInsertMode - && accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { + if (!ctx.config.bulkInsertMode && accessControlScheme) { const authorizedToModifyEntry: boolean = authorizedToEntry([ PERMISSION_CATEGORY_MODIFY ]); if (!authorizedToModifyEntry) { throw new errors.SecurityError( @@ -2809,14 +2827,15 @@ async function modifyEntry ( signErrors, ); } - if ( - !ctx.config.bulkInsertMode - && accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { + if (!ctx.config.bulkInsertMode && accessControlScheme) { const deletedValues = patch.removedValues.get(type_)?.length ?? 0; const typeOid = ObjectIdentifier.fromString(type_); - const { authorized: authorizedForAttributeType } = bacACDF( + const authorizedForAttributeType = acdf( + ctx, + accessControlScheme, + assn, + target, + [ PERMISSION_CATEGORY_ADD ], relevantTuples, user, { @@ -2824,7 +2843,6 @@ async function modifyEntry ( valuesCount: (values.length - deletedValues), operational: isOperationalAttributeType(ctx, typeOid), }, - [ PERMISSION_CATEGORY_ADD ], bacSettings, true, ); @@ -3789,10 +3807,7 @@ async function modifyEntry ( data.selection && ( !accessControlScheme - || ( - accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - && !authorizedToEntry([ PERMISSION_CATEGORY_READ ]) - ) + || authorizedToEntry([ PERMISSION_CATEGORY_READ ]) ) ) { const permittedEntryInfo = await readPermittedEntryInformation( diff --git a/apps/meerkat/src/app/distributed/read.ts b/apps/meerkat/src/app/distributed/read.ts index d330b3494..a1e1d3242 100644 --- a/apps/meerkat/src/app/distributed/read.ts +++ b/apps/meerkat/src/app/distributed/read.ts @@ -47,7 +47,7 @@ import getRelevantSubentries from "../dit/getRelevantSubentries"; import type ACDFTuple from "@wildboar/x500/src/lib/types/ACDFTuple"; import type ACDFTupleExtended from "@wildboar/x500/src/lib/types/ACDFTupleExtended"; import type ProtectedItem from "@wildboar/x500/src/lib/types/ProtectedItem"; -import bacACDF, { +import { PERMISSION_CATEGORY_ADD, PERMISSION_CATEGORY_REMOVE, PERMISSION_CATEGORY_READ, @@ -108,7 +108,6 @@ import { AttributeProblem_noSuchAttributeOrValue, } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/AttributeProblem.ta"; import getACIItems from "../authz/getACIItems"; -import accessControlSchemesThatUseACIItems from "../authz/accessControlSchemesThatUseACIItems"; import { MINIMUM_MAX_ATTR_SIZE, UNTRUSTED_REQ_AUTH_LEVEL } from "../constants"; import { ServiceControlOptions_noSubtypeSelection, @@ -151,6 +150,7 @@ import { Extension } from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/Extension import { singleUse } from "@wildboar/x500/src/lib/modules/AttributeCertificateDefinitions/singleUse.oa"; import { noAssertion } from "@wildboar/x500/src/lib/modules/AttributeCertificateDefinitions/noAssertion.oa"; import { _encode_AlgorithmIdentifier } from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/AlgorithmIdentifier.ta"; +import { acdf } from "../authz/acdf"; function createAttributeCertificate ( ctx: MeerkatContext, @@ -383,15 +383,22 @@ async function read ( ); const objectClasses: OBJECT_IDENTIFIER[] = Array.from(target.dse.objectClass).map(ObjectIdentifier.fromString); const getPermittedToDoXToY = (y: ProtectedItem): (permissions: number[]) => boolean => - (permissions: number[]) => - bacACDF(relevantTuples, user, y, permissions, bacSettings, true).authorized; + (permissions: number[]) => !accessControlScheme || acdf( + ctx, + accessControlScheme, + assn, + target, + permissions, + relevantTuples, + user, + y, + bacSettings, + true, + ); const permittedToDoXToThisEntry = getPermittedToDoXToY({ entry: objectClasses, }); - if ( - accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { + if (accessControlScheme) { if (!permittedToDoXToThisEntry([ PERMISSION_CATEGORY_READ ])) { throw new errors.SecurityError( ctx.i18n.t("err:not_authz_read"), @@ -571,20 +578,21 @@ async function read ( ]; if (permittedEntryInfo.information.length === 0) { const discloseOnErrorOnAnyOfTheSelectedAttributes: boolean = selectedAttributes - .some((attr) => { - const { authorized: authorizedToKnowAboutExcludedAttribute } = bacACDF( - relevantTuples, - user, - { - attributeType: attr, - operational: isOperationalAttributeType(ctx, attr), - }, - [ PERMISSION_CATEGORY_DISCLOSE_ON_ERROR ], - bacSettings, - true, - ); - return authorizedToKnowAboutExcludedAttribute; - }); + .some((attr) => !accessControlScheme || acdf( + ctx, + accessControlScheme, + assn, + target, + [ PERMISSION_CATEGORY_DISCLOSE_ON_ERROR ], + relevantTuples, + user, + { + attributeType: attr, + operational: isOperationalAttributeType(ctx, attr), + }, + bacSettings, + true, + )); // See ITU Recommendation X.511 (2016), Section 10.1.5.1.b for this part: if ( permittedEntryInfo.incompleteEntry // An attribute value was not permitted to be read... @@ -656,7 +664,6 @@ async function read ( // TODO: Make this behavior configurable. && (("basicLevels" in assn.authLevel) && (assn.authLevel.basicLevels.level > 0)) && accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) ) { const authorizedToAddEntry: boolean = permittedToDoXToThisEntry([ PERMISSION_CATEGORY_ADD ]); const authorizedToRemoveEntry: boolean = permittedToDoXToThisEntry([ PERMISSION_CATEGORY_REMOVE ]); @@ -746,11 +753,7 @@ async function read ( state.partialName, false, ), - ( - data.modifyRightsRequest - && accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) + (data.modifyRightsRequest && accessControlScheme) ? modifyRights : undefined, extensions, diff --git a/apps/meerkat/src/app/distributed/removeEntry.ts b/apps/meerkat/src/app/distributed/removeEntry.ts index 38181aeb9..a5af33663 100644 --- a/apps/meerkat/src/app/distributed/removeEntry.ts +++ b/apps/meerkat/src/app/distributed/removeEntry.ts @@ -51,7 +51,7 @@ import { import getRelevantSubentries from "../dit/getRelevantSubentries"; import type ACDFTuple from "@wildboar/x500/src/lib/types/ACDFTuple"; import type ACDFTupleExtended from "@wildboar/x500/src/lib/types/ACDFTupleExtended"; -import bacACDF, { +import { PERMISSION_CATEGORY_REMOVE, } from "@wildboar/x500/src/lib/bac/bacACDF"; import getACDFTuplesFromACIItem from "@wildboar/x500/src/lib/bac/getACDFTuplesFromACIItem"; @@ -105,7 +105,6 @@ import { UpdateProblem_notAllowedOnNonLeaf, } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/UpdateProblem.ta"; import getACIItems from "../authz/getACIItems"; -import accessControlSchemesThatUseACIItems from "../authz/accessControlSchemesThatUseACIItems"; import getRelevantOperationalBindings from "../dop/getRelevantOperationalBindings"; import updateAffectedSubordinateDSAs from "../dop/updateAffectedSubordinateDSAs"; import updateSuperiorDSA from "../dop/updateSuperiorDSA"; @@ -135,6 +134,7 @@ import { SubordinateChanges, } from "@wildboar/x500/src/lib/modules/DirectoryShadowAbstractService/SubordinateChanges.ta"; import { saveIncrementalRefresh } from "../disp/saveIncrementalRefresh"; +import { acdf } from "../authz/acdf"; const PARENT: string = id_oc_parent.toString(); const CHILD: string = id_oc_child.toString(); @@ -272,10 +272,7 @@ async function removeEntry ( const accessControlScheme = [ ...state.admPoints ] // Array.reverse() works in-place, so we create a new array. .reverse() .find((ap) => ap.dse.admPoint!.accessControlScheme)?.dse.admPoint!.accessControlScheme; - if ( - accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { + if (accessControlScheme) { const relevantACIItems = await getACIItems( ctx, accessControlScheme, @@ -297,13 +294,17 @@ async function removeEntry ( isMemberOfGroup, NAMING_MATCHER, ); - const { authorized: authorizedToRemoveEntry } = bacACDF( + const authorizedToRemoveEntry = acdf( + ctx, + accessControlScheme, + assn, + target, + [ PERMISSION_CATEGORY_REMOVE ], relevantTuples, user, { entry: Array.from(target.dse.objectClass).map(ObjectIdentifier.fromString), }, - [ PERMISSION_CATEGORY_REMOVE ], bacSettings, true, ); diff --git a/apps/meerkat/src/app/distributed/searchRuleCheckProcedure_i.ts b/apps/meerkat/src/app/distributed/searchRuleCheckProcedure_i.ts index 151301c4d..25dfb6652 100644 --- a/apps/meerkat/src/app/distributed/searchRuleCheckProcedure_i.ts +++ b/apps/meerkat/src/app/distributed/searchRuleCheckProcedure_i.ts @@ -1,6 +1,6 @@ import { Context, Vertex, ServiceError, ClientAssociation, AttributeError } from "@wildboar/meerkat-types"; import type { OperationDispatcherState } from "./OperationDispatcher"; -import { ACDFTupleExtended, ACDFTuple, getACDFTuplesFromACIItem, bacACDF } from "@wildboar/x500"; +import { ACDFTupleExtended, ACDFTuple, getACDFTuplesFromACIItem } from "@wildboar/x500"; import { serviceAdminSubentry, } from "@wildboar/x500/src/lib/modules/InformationFramework/serviceAdminSubentry.oa"; @@ -162,12 +162,12 @@ import { PERMISSION_CATEGORY_INVOKE } from "@wildboar/x500/src/lib/bac/bacACDF"; import { bacSettings } from "../authz/bacSettings"; import { AttributeTypeAndValue } from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/AttributeTypeAndValue.ta"; import { getServiceAdminPoint } from "../dit/getServiceAdminPoint"; -import accessControlSchemesThatUseACIItems from "../authz/accessControlSchemesThatUseACIItems"; import { is_empty_search_rule } from "../service/is_empty_search_rule"; import { general_check_of_search_filter } from "../service/general_check_of_search_filter"; import { check_of_request_attribute_profiles } from "../service/check_of_request_attribute_profiles"; import { check_of_controls_and_hierarchy_selections } from "../service/check_of_controls_and_hierarchy_selections"; import { check_of_matching_use } from "../service/check_of_matching_use"; +import { acdf } from "../authz/acdf"; const SEARCH_RULE_BYTES: Buffer = searchRules["&id"].toBytes(); @@ -672,7 +672,7 @@ async function searchRuleCheckProcedure_i ( } const DeniedSR: SearchRule[] = []; - if (accessControlScheme && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString())) { + if (accessControlScheme) { for (const subentry of target_subentries) { const subentryDN = getDistinguishedName(subentry); const relevantSubentries: Vertex[] = (await Promise.all( @@ -703,13 +703,17 @@ async function searchRuleCheckProcedure_i ( if (!search_rules?.length) { continue; } - const { authorized: authorizedToInvokeSearchRules } = bacACDF( + const authorizedToInvokeSearchRules = acdf( + ctx, + accessControlScheme, + assn, + target, + [PERMISSION_CATEGORY_INVOKE], relevantTuples, user, { attributeType: searchRules["&id"], }, - [PERMISSION_CATEGORY_INVOKE], bacSettings, true, ); @@ -717,7 +721,12 @@ async function searchRuleCheckProcedure_i ( continue; } for (const [ undecoded, sr ] of search_rules) { - const { authorized: authorizedToInvokeThisSearchRule } = bacACDF( + const authorizedToInvokeThisSearchRule = acdf( + ctx, + accessControlScheme, + assn, + target, + [PERMISSION_CATEGORY_INVOKE], relevantTuples, user, { @@ -727,7 +736,6 @@ async function searchRuleCheckProcedure_i ( ), operational: true, }, - [PERMISSION_CATEGORY_INVOKE], bacSettings, true, ); diff --git a/apps/meerkat/src/app/distributed/search_i.ts b/apps/meerkat/src/app/distributed/search_i.ts index 7b8ed8b3e..7ca79e6a4 100644 --- a/apps/meerkat/src/app/distributed/search_i.ts +++ b/apps/meerkat/src/app/distributed/search_i.ts @@ -119,7 +119,7 @@ import { import getRelevantSubentries from "../dit/getRelevantSubentries"; import type ACDFTuple from "@wildboar/x500/src/lib/types/ACDFTuple"; import type ACDFTupleExtended from "@wildboar/x500/src/lib/types/ACDFTupleExtended"; -import bacACDF, { +import { PERMISSION_CATEGORY_BROWSE, PERMISSION_CATEGORY_RETURN_DN, PERMISSION_CATEGORY_COMPARE, @@ -237,7 +237,6 @@ import { id_oc_child, } from "@wildboar/x500/src/lib/modules/InformationFramework/id-oc-child.va"; import getACIItems from "../authz/getACIItems"; -import accessControlSchemesThatUseACIItems from "../authz/accessControlSchemesThatUseACIItems"; import type { SearchResult, } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/SearchResult.ta"; @@ -357,6 +356,7 @@ import { getEffectiveControlsFromSearchRule } from "../service/getEffectiveContr import { id_ar_serviceSpecificArea } from "@wildboar/x500/src/lib/modules/InformationFramework/id-ar-serviceSpecificArea.va"; import { ID_AR_SERVICE, ID_AUTONOMOUS } from "../../oidstr"; import { isMatchAllFilter } from "../x500/isMatchAllFilter"; +import { acdf } from "../authz/acdf"; // NOTE: This will require serious changes when service specific areas are implemented. @@ -1932,32 +1932,30 @@ async function search_i_ex ( } // TODO: REVIEW: How would this handle alias dereferencing, joins, hierarchy selection, etc? const onBaseObjectIteration: boolean = (targetDN.length === data.baseObject.rdnSequence.length); - const authorized = (permissions: number[]) => { - const { authorized } = bacACDF( - relevantTuples, - user, - { - entry: Array.from(target.dse.objectClass).map(ObjectIdentifier.fromString), - }, - permissions, - bacSettings, - true, - ); - return authorized; - }; - if ( - accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { + const authorized = (permissions: number[]) => !accessControlScheme || acdf( + ctx, + accessControlScheme, + assn, + target, + permissions, + relevantTuples, + user, + { + entry: Array.from(target.dse.objectClass).map(ObjectIdentifier.fromString), + }, + bacSettings, + true, + ); + if (accessControlScheme) { const authorizedToSearch = authorized([ PERMISSION_CATEGORY_RETURN_DN, PERMISSION_CATEGORY_BROWSE, ]); if (onBaseObjectIteration) { - const authorizedForDisclosure = authorized([ - PERMISSION_CATEGORY_DISCLOSE_ON_ERROR, - ]); if (!authorizedToSearch) { + const authorizedForDisclosure = authorized([ + PERMISSION_CATEGORY_DISCLOSE_ON_ERROR, + ]); if (authorizedForDisclosure) { throw new errors.SecurityError( ctx.i18n.t("err:not_authz_search_base_object"), @@ -2151,15 +2149,19 @@ async function search_i_ex ( return friendship ? [ ...friendship.friends ] : []; }, permittedToMatch: (attributeType: OBJECT_IDENTIFIER, value?: ASN1Element): boolean => { - if ( - !accessControlScheme - || !accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { + if (!accessControlScheme) { return true; } - const { - authorized: authorizedToMatch, - } = bacACDF( + return acdf( + ctx, + accessControlScheme, + assn, + target, + [ + PERMISSION_CATEGORY_FILTER_MATCH, + PERMISSION_CATEGORY_COMPARE, // Not required by specification. + PERMISSION_CATEGORY_READ, // Not required by specification. + ], relevantTuples, user, value @@ -2173,15 +2175,9 @@ async function search_i_ex ( : { attributeType, }, - [ - PERMISSION_CATEGORY_FILTER_MATCH, - PERMISSION_CATEGORY_COMPARE, // Not required by specification. - PERMISSION_CATEGORY_READ, // Not required by specification. - ], bacSettings, true, ); - return authorizedToMatch; }, performExactly, matchedValuesOnly: matchedValuesOnly || searchRuleReturnsMatchedValuesOnly, @@ -2261,25 +2257,31 @@ async function search_i_ex ( } if (target.dse.alias && searchAliases) { - if ( - accessControlScheme - && accessControlSchemesThatUseACIItems.has(accessControlScheme.toString()) - ) { + if (accessControlScheme) { const authorizedToReadEntry: boolean = authorized([ PERMISSION_CATEGORY_READ, ]); - const { authorized: authorizedToReadAliasedEntryName } = bacACDF( + const authorizedToReadAliasedEntryName = acdf( + ctx, + accessControlScheme, + assn, + target, + [ PERMISSION_CATEGORY_READ ], relevantTuples, user, { attributeType: id_at_aliasedEntryName, operational: false, }, - [ PERMISSION_CATEGORY_READ ], bacSettings, true, ); - const { authorized: authorizedToReadAliasedEntryNameValue } = bacACDF( + const authorizedToReadAliasedEntryNameValue = acdf( + ctx, + accessControlScheme, + assn, + target, + [ PERMISSION_CATEGORY_READ ], relevantTuples, user, { @@ -2289,7 +2291,6 @@ async function search_i_ex ( ), operational: false, }, - [ PERMISSION_CATEGORY_READ ], bacSettings, true, ); @@ -3285,16 +3286,20 @@ async function search_i_ex ( isMemberOfGroup, NAMING_MATCHER, ); - const { authorized: authorizedToDiscoverSubordinate } = bacACDF( + const authorizedToDiscoverSubordinate = !accessControlScheme || acdf( + ctx, + accessControlScheme, + assn, + target, + [ + PERMISSION_CATEGORY_BROWSE, + PERMISSION_CATEGORY_RETURN_DN, + ], relevantTuples, user, { entry: Array.from(subordinate.dse.objectClass).map(ObjectIdentifier.fromString), }, - [ - PERMISSION_CATEGORY_BROWSE, - PERMISSION_CATEGORY_RETURN_DN, - ], bacSettings, true, ); From c02c83a853883a6b2c5a9f0c5c09b5874ca55bfd Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Mon, 7 Aug 2023 06:00:20 -0400 Subject: [PATCH 05/39] feat: associate clearance with application association --- .../src/app/authn/attemptStrongAuth.ts | 10 +- apps/meerkat/src/app/authn/dsaBind.ts | 15 ++- apps/meerkat/src/app/ctx.ts | 119 +++++++++++++++++- apps/meerkat/src/app/dap/bind.ts | 15 ++- apps/meerkat/src/app/database/utils.ts | 25 +++- apps/meerkat/src/app/ldap/bind.ts | 9 +- libs/meerkat-types/src/lib/types.ts | 63 ++++++++++ 7 files changed, 245 insertions(+), 11 deletions(-) diff --git a/apps/meerkat/src/app/authn/attemptStrongAuth.ts b/apps/meerkat/src/app/authn/attemptStrongAuth.ts index 95c8589db..2a385d8be 100644 --- a/apps/meerkat/src/app/authn/attemptStrongAuth.ts +++ b/apps/meerkat/src/app/authn/attemptStrongAuth.ts @@ -50,7 +50,7 @@ import type { } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/StrongCredentials.ta"; import type { Socket } from "node:net"; import type { TLSSocket } from "node:tls"; -import { read_unique_id } from "../database/utils"; +import { read_unique_id, read_clearance } from "../database/utils"; const ID_OC_PKI_CERT_PATH: string = id_oc_pkiCertPath.toString(); @@ -199,6 +199,9 @@ async function attemptStrongAuth ( case (VT_RETURN_CODE_OK): { const foundEntry = await dnToVertex(ctx, ctx.dit.root, effectiveName); const unique_id = foundEntry && await read_unique_id(ctx, foundEntry); + const clearances = foundEntry + ? await read_clearance(ctx, foundEntry) + : []; return { boundVertex: foundEntry, boundNameAndUID: new NameAndOptionalUID( @@ -212,6 +215,7 @@ async function attemptStrongAuth ( undefined, ), }, + clearances, }; } case (VT_RETURN_CODE_MALFORMED): { @@ -275,6 +279,9 @@ async function attemptStrongAuth ( const tokenResult = await verifyToken(ctx, certPath, bind_token); if (tokenResult === VT_RETURN_CODE_OK) { const unique_id = attemptedVertex && await read_unique_id(ctx, attemptedVertex); + const clearances = attemptedVertex + ? await read_clearance(ctx, attemptedVertex) + : []; return { boundVertex: attemptedVertex, boundNameAndUID: new NameAndOptionalUID( @@ -288,6 +295,7 @@ async function attemptStrongAuth ( undefined, ), }, + clearances, }; } } diff --git a/apps/meerkat/src/app/authn/dsaBind.ts b/apps/meerkat/src/app/authn/dsaBind.ts index 8ed18945e..2db59d6d9 100644 --- a/apps/meerkat/src/app/authn/dsaBind.ts +++ b/apps/meerkat/src/app/authn/dsaBind.ts @@ -32,7 +32,7 @@ import { PwdResponseValue_error_passwordExpired, } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/PwdResponseValue-error.ta"; import stringifyDN from "../x500/stringifyDN"; -import { read_unique_id } from "../database/utils"; +import { read_unique_id, read_clearance } from "../database/utils"; import { OperationDispatcher } from "../distributed/OperationDispatcher"; import type { CompareArgument, @@ -137,6 +137,7 @@ async function bind ( undefined, ), }, + clearances: [], }; } const invalidCredentialsData = new DirectoryBindErrorData( @@ -160,6 +161,9 @@ async function bind ( ); } const unique_id = foundEntry && await read_unique_id(ctx, foundEntry); + const clearances = foundEntry + ? await read_clearance(ctx, foundEntry) + : []; return { boundVertex: foundEntry, boundNameAndUID: new NameAndOptionalUID( @@ -173,6 +177,7 @@ async function bind ( undefined, ), }, + clearances, }; } if (arg.credentials.simple.validity) { @@ -311,6 +316,9 @@ async function bind ( ); } const unique_id = foundEntry && await read_unique_id(ctx, foundEntry); + const clearances = foundEntry + ? await read_clearance(ctx, foundEntry) + : []; return { boundVertex: foundEntry, boundNameAndUID: new NameAndOptionalUID( @@ -330,6 +338,7 @@ async function bind ( pwd_resp.error, ) : undefined, + clearances, }; } const { @@ -359,6 +368,9 @@ async function bind ( ); } const unique_id = foundEntry && await read_unique_id(ctx, foundEntry); + const clearances = foundEntry + ? await read_clearance(ctx, foundEntry) + : []; return { boundVertex: foundEntry, boundNameAndUID: new NameAndOptionalUID( @@ -372,6 +384,7 @@ async function bind ( undefined, ), }, + clearances, }; } else if ("strong" in arg.credentials) { ctx.log.debug(ctx.i18n.t("log:dsa_bind_with_strong_creds", { source }), logInfo); diff --git a/apps/meerkat/src/app/ctx.ts b/apps/meerkat/src/app/ctx.ts index aef886c2e..e068d0cd8 100644 --- a/apps/meerkat/src/app/ctx.ts +++ b/apps/meerkat/src/app/ctx.ts @@ -106,9 +106,14 @@ import { decodePkiPathFromPEM } from "./utils/decodePkiPathFromPEM"; import type { PkiPath, } from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/PkiPath.ta"; -import { rootCertificates } from "tls"; -import { strict as assert } from "assert"; +import { rootCertificates } from "node:tls"; +import { strict as assert } from "node:assert"; +import { createPublicKey } from "node:crypto"; import { id_basicSecurityPolicy, simple_rbac_acdf } from "./authz/rbacACDF"; +import { subjectKeyIdentifier } from "@wildboar/x500/src/lib/modules/CertificateExtensions/subjectKeyIdentifier.oa"; +import { subjectAltName } from "@wildboar/x500/src/lib/modules/CertificateExtensions/subjectAltName.oa"; +import { Name } from "@wildboar/x500/src/lib/modules/InformationFramework/Name.ta"; +import { _encode_SubjectPublicKeyInfo } from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/SubjectPublicKeyInfo.ta"; export interface MeerkatTelemetryClient { @@ -410,10 +415,6 @@ const signingKeyFileContents: Buffer | undefined = process.env.MEERKAT_SIGNING_K ? fs.readFileSync(process.env.MEERKAT_SIGNING_KEY_FILE) : undefined; -const talFileContents: Buffer | undefined = process.env.MEERKAT_TRUST_ANCHORS_FILE - ? fs.readFileSync(process.env.MEERKAT_TRUST_ANCHORS_FILE) - : undefined; - const attrCertPathFileContents: string | undefined = process.env.MEERKAT_ATTR_CERT_CHAIN_FILE ? fs.readFileSync(process.env.MEERKAT_ATTR_CERT_CHAIN_FILE, { encoding: "utf-8" }) : undefined; @@ -434,6 +435,9 @@ const signingKey: KeyObject | null = signingKeyFileContents ? parseKey(signingKeyFileContents) : null; +const talFileContents: Buffer | undefined = process.env.MEERKAT_TRUST_ANCHORS_FILE + ? fs.readFileSync(process.env.MEERKAT_TRUST_ANCHORS_FILE) + : undefined; const trustAnchorList: TrustAnchorList = talFileContents ? [ ...parseTrustAnchorListFile(talFileContents), @@ -441,6 +445,26 @@ const trustAnchorList: TrustAnchorList = talFileContents ] : signingCACerts.map((certificate) => ({ certificate })); +const clearanceAuthoritiesFileContents: Buffer | undefined = process.env.MEERKAT_CLEARANCE_AUTHORITIES + ? fs.readFileSync(process.env.MEERKAT_CLEARANCE_AUTHORITIES) + : undefined; +const clearanceAuthorities: TrustAnchorList = clearanceAuthoritiesFileContents + ? [ + ...parseTrustAnchorListFile(clearanceAuthoritiesFileContents), + ...signingCACerts.map((certificate) => ({ certificate })), + ] + : signingCACerts.map((certificate) => ({ certificate })); + +const labelingAuthoritiesFileContents: Buffer | undefined = process.env.MEERKAT_LABELING_AUTHORITIES + ? fs.readFileSync(process.env.MEERKAT_LABELING_AUTHORITIES) + : undefined; +const labelingAuthorities: TrustAnchorList = labelingAuthoritiesFileContents + ? [ + ...parseTrustAnchorListFile(labelingAuthoritiesFileContents), + ...signingCACerts.map((certificate) => ({ certificate })), + ] + : signingCACerts.map((certificate) => ({ certificate })); + /** * If the trust anchor list is unpopulated, use the default root certificates. */ @@ -573,6 +597,12 @@ const config: Configuration = { : 0, // 0 disables this procedure. automaticallyTrustForIBRA: process.env.MEERKAT_TRUST_FOR_IBRA?.toUpperCase(), }, + rbac: { + getClearancesFromDSAIT: (process.env.MEERKAT_GET_CLEARANCES_FROM_DSAIT !== "0"), + getClearancesFromAttributeCertificates: (process.env.MEERKAT_GET_CLEARANCES_FROM_ATTR_CERTS !== "0"), + clearanceAuthorities, + labelingAuthorities, + }, log: { boundDN: (process.env.MEERKAT_LOG_BOUND_DN === "1"), level: logLevel as LogLevel, @@ -1138,4 +1168,81 @@ const ctx: MeerkatContext = { rbacPolicies: new Map([ [id_basicSecurityPolicy.toString(), simple_rbac_acdf] ]), }; +for (const la of labelingAuthorities) { + if (("certificate" in la) || ("tbsCert" in la)) { + const tbs = ("certificate" in la) + ? la.certificate.toBeSigned + : la.tbsCert; + const ski_ext = tbs.extensions + ?.find((ext) => ext.extnId.isEqualTo(subjectKeyIdentifier["&id"]!)); + if (!ski_ext) { + continue; + } + const ski_el = new DERElement(); + ski_el.fromBytes(ski_ext.extnValue); + const ski = subjectKeyIdentifier.decoderFor["&ExtnType"]!(ski_el); + + const issuerNames: Name[] = [ tbs.issuer ]; + const san_ext = tbs.extensions + ?.find((ext) => ext.extnId.isEqualTo(subjectAltName["&id"]!)); + if (san_ext) { + const san_el = new DERElement(); + san_el.fromBytes(san_ext.extnValue); + const sans = subjectAltName.decoderFor["&ExtnType"]!(san_el); + for (const san of sans) { + if ("directoryName" in san) { + issuerNames.push(san.directoryName); + } + } + } + + const spkiBytes = _encode_SubjectPublicKeyInfo(tbs.subjectPublicKeyInfo, DER).toBytes(); + const publicKey = createPublicKey({ + key: Buffer.from(spkiBytes), + format: "der", + type: "spki", + }); + + ctx.labellingAuthorities.set(Buffer.from(ski).toString("base64"), { + authorized: true, + issuerNames, + publicKey, + }); + } + else { + const anchor = la.taInfo; + const issuerNames: Name[] = []; + if (anchor.certPath?.certificate) { + issuerNames.push(anchor.certPath.certificate.toBeSigned.issuer); + } + const exts = [ + ...anchor.exts ?? [], + ...anchor.certPath?.certificate?.toBeSigned.extensions ?? [], + ]; + const san_ext = exts + ?.find((ext) => ext.extnId.isEqualTo(subjectAltName["&id"]!)); + if (san_ext) { + const san_el = new DERElement(); + san_el.fromBytes(san_ext.extnValue); + const sans = subjectAltName.decoderFor["&ExtnType"]!(san_el); + for (const san of sans) { + if ("directoryName" in san) { + issuerNames.push(san.directoryName); + } + } + } + const spkiBytes = _encode_SubjectPublicKeyInfo(anchor.pubKey, DER).toBytes(); + const publicKey = createPublicKey({ + key: Buffer.from(spkiBytes), + format: "der", + type: "spki", + }); + ctx.labellingAuthorities.set(Buffer.from(anchor.keyId).toString("base64"), { + authorized: true, + issuerNames, + publicKey, + }); + } +} + export default ctx; diff --git a/apps/meerkat/src/app/dap/bind.ts b/apps/meerkat/src/app/dap/bind.ts index a99798ad4..f17fbca7f 100644 --- a/apps/meerkat/src/app/dap/bind.ts +++ b/apps/meerkat/src/app/dap/bind.ts @@ -34,7 +34,7 @@ import { attemptStrongAuth } from "../authn/attemptStrongAuth"; import { PwdResponseValue_error_passwordExpired, } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/PwdResponseValue-error.ta"; -import { read_unique_id } from "../database/utils"; +import { read_unique_id, read_clearance } from "../database/utils"; import { OperationDispatcher } from "../distributed/OperationDispatcher"; import type { CompareArgument, @@ -147,6 +147,7 @@ async function bind ( undefined, ), }, + clearances: [], }; } if ("simple" in arg.credentials) { @@ -161,6 +162,9 @@ async function bind ( ); } const unique_id = foundEntry && await read_unique_id(ctx, foundEntry); + const clearances = foundEntry + ? await read_clearance(ctx, foundEntry) + : []; return { boundVertex: foundEntry, boundNameAndUID: new NameAndOptionalUID( @@ -174,6 +178,7 @@ async function bind ( undefined, ), }, + clearances, }; } if (arg.credentials.simple.validity) { @@ -312,6 +317,9 @@ async function bind ( ); } const unique_id = foundEntry && await read_unique_id(ctx, foundEntry); + const clearances = foundEntry + ? await read_clearance(ctx, foundEntry) + : []; return { boundVertex: foundEntry, boundNameAndUID: new NameAndOptionalUID( @@ -331,6 +339,7 @@ async function bind ( pwd_resp.error, ) : undefined, + clearances, }; } const { @@ -359,6 +368,9 @@ async function bind ( ); } const unique_id = foundEntry && await read_unique_id(ctx, foundEntry); + const clearances = foundEntry + ? await read_clearance(ctx, foundEntry) + : []; return { boundVertex: foundEntry, boundNameAndUID: new NameAndOptionalUID( @@ -373,6 +385,7 @@ async function bind ( ), }, pwdResponse, + clearances, }; } else if ("strong" in arg.credentials) { return attemptStrongAuth( diff --git a/apps/meerkat/src/app/database/utils.ts b/apps/meerkat/src/app/database/utils.ts index e02ab2aa4..73766e22b 100644 --- a/apps/meerkat/src/app/database/utils.ts +++ b/apps/meerkat/src/app/database/utils.ts @@ -1,5 +1,5 @@ import { Context, Vertex } from "@wildboar/meerkat-types"; -import { uniqueIdentifier } from "@wildboar/x500/src/lib/collections/attributes"; +import { clearance, uniqueIdentifier } from "@wildboar/x500/src/lib/collections/attributes"; import { attributeValueFromDB } from "./attributeValueFromDB"; export @@ -22,3 +22,26 @@ async function read_unique_id (ctx: Context, vertex: Vertex): Promise { + if (!ctx.config.rbac.getClearancesFromDSAIT) { + return []; + } + const clearance_bers = await ctx.db.attributeValue.findMany({ + where: { + entry_id: vertex.dse.id, + type_oid: clearance["&id"].toBytes(), + }, + select: { + tag_class: true, + constructed: true, + tag_number: true, + content_octets: true, + }, + }); + return clearance_bers + .map(attributeValueFromDB) + .map(clearance.decoderFor["&Type"]!) + ; +} diff --git a/apps/meerkat/src/app/ldap/bind.ts b/apps/meerkat/src/app/ldap/bind.ts index 81a77b8d0..98cda1164 100644 --- a/apps/meerkat/src/app/ldap/bind.ts +++ b/apps/meerkat/src/app/ldap/bind.ts @@ -32,7 +32,7 @@ import { PwdResponseValue_error_passwordExpired, } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/PwdResponseValue-error.ta"; import { SimpleCredentials } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/SimpleCredentials.ta"; -import { read_unique_id } from "../database/utils"; +import { read_unique_id, read_clearance } from "../database/utils"; export interface LDAPBindReturn extends BindReturn { @@ -101,6 +101,7 @@ async function bind ( if (version !== 3) { return { authLevel: notAuthed(), + clearances: [], result: new BindResponse( LDAPResult_resultCode_protocolError, req.name, @@ -118,6 +119,7 @@ async function bind ( if (!entry) { return { authLevel: notAuthed(), + clearances: [], result: invalidCredentialsError(invalidCredentialsMessage, req.name), }; } @@ -136,16 +138,19 @@ async function bind ( + ((tlsProtocol === "TLSv1.3") ? ctx.config.localQualifierPointsFor.usingTLSv1_3 : 0) ); const unique_id = entry && await read_unique_id(ctx, entry); + const clearances = entry && await read_clearance(ctx, entry); const ret = { boundNameAndUID: new NameAndOptionalUID( decodeLDAPDN(ctx, req.name), unique_id, // We just use the first one, whatever that is. ), boundVertex: entry, + clearances, }; const invalidCredentials: LDAPBindReturn = { ...ret, authLevel: notAuthed(localQualifierPoints), + clearances: [], result: invalidCredentialsError(invalidCredentialsMessage, req.name), }; const logInfo = { @@ -169,6 +174,7 @@ async function bind ( undefined, ), }, + clearances: [], result: new BindResponse( LDAPResult_resultCode_authMethodNotSupported, req.name, @@ -187,6 +193,7 @@ async function bind ( undefined, ), }, + clearances: [], result: simpleSuccess(successMessage, encodedDN), }; } diff --git a/libs/meerkat-types/src/lib/types.ts b/libs/meerkat-types/src/lib/types.ts index 93120dba2..eccaa42fa 100644 --- a/libs/meerkat-types/src/lib/types.ts +++ b/libs/meerkat-types/src/lib/types.ts @@ -1497,6 +1497,61 @@ interface CrossReferencesOptions { } +/** + * Configuration options pertaining to Rule-Based Access Control (RBAC). + * + * @interface + */ +export +interface RBACOptions { + + /** + * If TRUE, Meerkat DSA will associate clearances with a bound user based + * on the values of the `clearance` attribute it has for the bound entry + * in its local DSAIT. + */ + getClearancesFromDSAIT: boolean; + + /** + * If true, Meerkat DSA will associate clearances with a bound user based + * on the values of the `clearance` attribute that are present in the + * presented attribute certificates of the strong authentication argument, + * provided, of course, that the attribute certificates are valid. + * + * The attribute authorities to trust are listed in `clearanceAuthorities`. + * + * @see {@link clearanceAuthorities} + */ + getClearancesFromAttributeCertificates: boolean; + + /** + * The list of trust anchors whose signed attribute certificates will be + * seen as valid by Meerkat DSA, and whose clearances will be associated + * with bound users that supply such attribute certificates in their + * strong authentication parameters. + */ + clearanceAuthorities: TrustAnchorList; + + /** + * The list of trust anchors that are trusted for providing security labels + * via the `attributeValueSecurityLabelContext` context. When evaluating + * RBAC access control decisions, the signatures applied to the security + * labels will be verified to have originated from one of these trust + * anchors. + * + * Only the public key, issuer name, and subject key identifier are taken + * from these trust anchors. Expiration is never checked, nor are any other + * extensions, such as key-usage-related extensions. + * + * If a given security label has a `keyIdentifier`, it will be matched with + * the Subject Key Identifier in a trust anchor within this list; if that + * security label has an `issuerName` field, it will be matched with the + * `subject` field of a trust anchor within this list. + */ + labelingAuthorities: TrustAnchorList; + +} + /** * @summary Meerkat DSA configuration * @description @@ -1574,6 +1629,8 @@ interface Configuration { authn: AuthenticationConfiguration; + rbac: RBACOptions; + log: { /** * If true, Meerkat DSA will log distinguished names of bound clients, @@ -3481,6 +3538,12 @@ interface BindReturn { */ pwdResponse?: PwdResponseValue; + + /** + * The clearances associated with this user. + */ + clearances: Clearance[]; + } /** From 822848adf829dd180e797bce031a326c1b28672d Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Sat, 19 Aug 2023 05:02:05 -0400 Subject: [PATCH 06/39] feat: verify attribute certificates --- apps/meerkat/src/app/ctx.ts | 1 + .../src/app/pki/getOnOCSPRequestCallback.ts | 24 +- .../src/app/pki/isCertInTrustAnchor.ts | 59 + .../meerkat/src/app/pki/verifyAttrCertPath.ts | 1344 +++++++++++++++++ apps/meerkat/src/app/pki/verifyCertPath.ts | 50 +- apps/meerkat/src/app/pki/verifySIGNED.ts | 1 + libs/meerkat-types/src/lib/types.ts | 7 + libs/ocsp-client/src/lib/check.ts | 28 +- 8 files changed, 1477 insertions(+), 37 deletions(-) create mode 100644 apps/meerkat/src/app/pki/isCertInTrustAnchor.ts create mode 100644 apps/meerkat/src/app/pki/verifyAttrCertPath.ts diff --git a/apps/meerkat/src/app/ctx.ts b/apps/meerkat/src/app/ctx.ts index e068d0cd8..9f9c71917 100644 --- a/apps/meerkat/src/app/ctx.ts +++ b/apps/meerkat/src/app/ctx.ts @@ -1166,6 +1166,7 @@ const ctx: MeerkatContext = { updatingShadow: new Set(), labellingAuthorities: new Map(), rbacPolicies: new Map([ [id_basicSecurityPolicy.toString(), simple_rbac_acdf] ]), + alreadyAssertedAttributeCertificates: new Set(), }; for (const la of labelingAuthorities) { diff --git a/apps/meerkat/src/app/pki/getOnOCSPRequestCallback.ts b/apps/meerkat/src/app/pki/getOnOCSPRequestCallback.ts index e91b45bc0..e660fc3d3 100644 --- a/apps/meerkat/src/app/pki/getOnOCSPRequestCallback.ts +++ b/apps/meerkat/src/app/pki/getOnOCSPRequestCallback.ts @@ -38,8 +38,6 @@ export type NodeOCSPRequestCallback = ( callback: (err: Error | null, resp?: Buffer | null) => unknown, ) => unknown; -let cachedServerCert: Certificate | undefined; - /** * @summary Get a callback for use by NodeJS's `TLSSocket`'s `OCSPRequest` event. * @description @@ -68,14 +66,11 @@ function getOnOCSPRequestCallback ( issuer: Buffer, callback: (err: Error | null, resp?: Buffer | null) => unknown, ): Promise => { - const serverCert: Certificate = cachedServerCert - ?? (() => { - const el = new BERElement(); - el.fromBytes(certificate); - const cert = _decode_Certificate(el); - cachedServerCert = cert; - return cert; - })(); + const serverCert: Certificate = (() => { + const el = new BERElement(); + el.fromBytes(certificate); + return _decode_Certificate(el); + })(); const aiaExt = serverCert.toBeSigned.extensions ?.find((ext) => ext.extnId.isEqualTo(authorityInfoAccess["&id"]!)); const aia = aiaExt @@ -118,9 +113,16 @@ function getOnOCSPRequestCallback ( } requestBudget--; try { + const issuerCertEl = new BERElement(); + issuerCertEl.fromBytes(issuer); + const issuerCert = _decode_Certificate(issuerCertEl); const ocspResponse = await getOCSPResponse( url, - serverCert, + [ + issuerCert.toBeSigned.subject.rdnSequence, + issuerCert.toBeSigned.subjectPublicKeyInfo, + serverCert.toBeSigned.serialNumber, + ], undefined, (options.ocspTimeout * 1000), signFunction, diff --git a/apps/meerkat/src/app/pki/isCertInTrustAnchor.ts b/apps/meerkat/src/app/pki/isCertInTrustAnchor.ts new file mode 100644 index 000000000..fd9f0a193 --- /dev/null +++ b/apps/meerkat/src/app/pki/isCertInTrustAnchor.ts @@ -0,0 +1,59 @@ +import { + Certificate, _encode_Certificate, +} from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/Certificate.ta"; +import { + TrustAnchorChoice, +} from "@wildboar/tal/src/lib/modules/TrustAnchorInfoModule/TrustAnchorChoice.ta"; +import { DER } from "asn1-ts/dist/node/functional"; + +export +function isCertInTrustAnchor ( + cert: Certificate, + trust_anchor: TrustAnchorChoice, + certBytes?: Uint8Array, +): boolean { + const certBytes_ = certBytes + ?? cert.originalDER + ?? _encode_Certificate(cert, DER).toBytes(); + if ("certificate" in trust_anchor) { + const taBytes = trust_anchor.certificate.originalDER + ?? _encode_Certificate(trust_anchor.certificate, DER).toBytes(); + return !Buffer.compare(certBytes_, taBytes); + } + else if ("tbsCert" in trust_anchor) { + const tatbs = trust_anchor.tbsCert; + const tbs = cert.toBeSigned; + /** + * NOTE: Unlike the `certificate` alternative of the trust + * anchor choice, there is no digital signature, so comparison + * is not quite as straightforward. However, we can say that, + * among trust anchors, the tuple of + * (subject, serialNumber, subjectPublicKeyInfo) SHOULD be + * globally unique, so we can just check those fields. + */ + return ( + !Buffer.compare(tatbs.serialNumber, tbs.serialNumber) + && (tatbs.issuer.rdnSequence.length === cert.toBeSigned.issuer.rdnSequence.length) + && (tatbs.subject.rdnSequence.length === cert.toBeSigned.subject.rdnSequence.length) + && (tatbs.extensions?.length === tbs.extensions?.length) + && (tatbs.subjectPublicKeyInfo.algorithm.algorithm + .isEqualTo(tbs.subjectPublicKeyInfo.algorithm.algorithm)) + && !Buffer.compare( + Buffer.from(tatbs.subjectPublicKeyInfo.subjectPublicKey.buffer), + Buffer.from(tbs.subjectPublicKeyInfo.subjectPublicKey.buffer) + ) + ); + } + else { + return ( + (trust_anchor.taInfo.exts?.length === cert.toBeSigned.extensions?.length) + && (trust_anchor.taInfo.pubKey.algorithm.algorithm.isEqualTo(cert.toBeSigned.subjectPublicKeyInfo.algorithm.algorithm)) + && !Buffer.compare( + Buffer.from(trust_anchor.taInfo.pubKey.subjectPublicKey.buffer), + Buffer.from(cert.toBeSigned.subjectPublicKeyInfo.subjectPublicKey.buffer), + ) + ); + } +} + +export default isCertInTrustAnchor; diff --git a/apps/meerkat/src/app/pki/verifyAttrCertPath.ts b/apps/meerkat/src/app/pki/verifyAttrCertPath.ts new file mode 100644 index 000000000..7ed2515c3 --- /dev/null +++ b/apps/meerkat/src/app/pki/verifyAttrCertPath.ts @@ -0,0 +1,1344 @@ +import { + CRLIndex, + Context, + IndexableOID, + OfflinePKIConfig, + OCSPOptions, +} from "@wildboar/meerkat-types"; +import { + PkiPath, +} from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/PkiPath.ta"; +import { + ACPathData, + AttributeCertificate, + AttributeCertificationPath, + _encode_AttributeCertificate, +} from "@wildboar/x500/src/lib/modules/AttributeCertificateDefinitions/AttributeCertificationPath.ta"; +import { + compareGeneralName, + compareName, getDateFromTime, groupByOID, +} from "@wildboar/x500"; +import { + evaluateTemporalContext, +} from "@wildboar/x500/src/lib/matching/context/temporalContext"; +import getNamingMatcherGetter from "../x500/getNamingMatcherGetter"; +import { Name } from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/Name.ta"; +import { Certificate, _encode_Certificate } from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/Certificate.ta"; +import { issuerAltName } from "@wildboar/x500/src/lib/modules/CertificateExtensions/issuerAltName.oa"; +import { BOOLEAN, DERElement, packBits } from "asn1-ts"; +import { subjectAltName } from "@wildboar/x500/src/lib/modules/CertificateExtensions/subjectAltName.oa"; +import { digestOIDToNodeHash } from "./digestOIDToNodeHash"; +import { createHash } from "crypto"; +import { + ObjectDigestInfo_digestedObjectType_publicKeyCert, + ObjectDigestInfo_digestedObjectType_publicKey, +} from "@wildboar/x500/src/lib/modules/AttributeCertificateDefinitions/ObjectDigestInfo-digestedObjectType.ta"; +import { DER } from "asn1-ts/dist/node/functional"; +import { SubjectPublicKeyInfo, _encode_SubjectPublicKeyInfo } from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/SubjectPublicKeyInfo.ta"; +import { noAssertion } from "@wildboar/x500/src/lib/modules/AttributeCertificateDefinitions/noAssertion.oa"; +import { Holder } from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/Holder.ta"; +import { sOAIdentifier } from "@wildboar/x500/src/lib/modules/AttributeCertificateDefinitions/sOAIdentifier.oa"; +import { TrustAnchorChoice, TrustAnchorList } from "@wildboar/tal/src/lib/modules/TrustAnchorInfoModule/TrustAnchorList.ta"; +import { isCertInTrustAnchor } from "../pki/isCertInTrustAnchor"; +import { AttCertIssuer } from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/AttCertIssuer.ta"; +import { TBSCertificate } from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/TBSCertificate.ta"; +import { issuedOnBehalfOf } from "@wildboar/x500/src/lib/modules/AttributeCertificateDefinitions/issuedOnBehalfOf.oa"; +import stringifyDN from "../x500/stringifyDN"; +import { getReadDispatcher, verifySignature } from "./verifyCertPath"; +import { singleUse } from "@wildboar/x500/src/lib/modules/AttributeCertificateDefinitions/singleUse.oa"; +import { groupAC } from "@wildboar/x500/src/lib/modules/AttributeCertificateDefinitions/groupAC.oa"; +import { targetingInformation } from "@wildboar/x500/src/lib/modules/AttributeCertificateDefinitions/targetingInformation.oa"; +import { noRevAvail } from "@wildboar/x500/src/lib/modules/AttributeCertificateDefinitions/noRevAvail.oa"; +import { timeSpecification } from "@wildboar/x500/src/lib/modules/AttributeCertificateDefinitions/timeSpecification.oa"; +import { + _encode_TimeAssertion, +} from "@wildboar/x500/src/lib/modules/SelectedAttributeTypes/TimeAssertion.ta"; +import { GeneralName } from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/GeneralName.ta"; +import { IssuerSerial } from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/IssuerSerial.ta"; +import { ObjectDigestInfo } from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/ObjectDigestInfo.ta"; +import { MeerkatContext } from "../ctx"; +import { NameAndOptionalUID } from "@wildboar/x500/src/lib/modules/SelectedAttributeTypes/NameAndOptionalUID.ta"; +import getIsGroupMember from "../authz/getIsGroupMember"; +import { checkRemoteCRLs } from "./verifyCertPath"; +import { + cRLDistributionPoints, +} from "@wildboar/x500/src/lib/modules/CertificateExtensions/cRLDistributionPoints.oa"; +import { + authorityKeyIdentifier, +} from "@wildboar/x500/src/lib/modules/CertificateExtensions/authorityKeyIdentifier.oa"; +import { + AltSignatureAlgorithm, + _decode_AltSignatureAlgorithm, + altSignatureAlgorithm, +} from "@wildboar/x500/src/lib/modules/CertificateExtensions/altSignatureAlgorithm.oa"; +import { + altSignatureValue, +} from "@wildboar/x500/src/lib/modules/CertificateExtensions/altSignatureValue.oa"; +import { + authorityInfoAccess, +} from "@wildboar/x500/src/lib/modules/PkiPmiExternalDataTypes/authorityInfoAccess.oa"; +import { + subjectInfoAccess, +} from "@wildboar/x500/src/lib/modules/PkiPmiExternalDataTypes/subjectInfoAccess.oa"; +import { + subjectKeyIdentifier, +} from "@wildboar/x500/src/lib/modules/CertificateExtensions/subjectKeyIdentifier.oa"; +import { + _decode_SubjectAltPublicKeyInfo, + subjectAltPublicKeyInfo, +} from "@wildboar/x500/src/lib/modules/CertificateExtensions/subjectAltPublicKeyInfo.oa"; +import { Extension } from "@wildboar/x500/src/lib/modules/AuthenticationFramework/Extension.ta"; +import { _encode_AlgorithmIdentifier } from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/AlgorithmIdentifier.ta"; +import { TBSAttributeCertificate, _encode_TBSAttributeCertificate } from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/TBSAttributeCertificate.ta"; +import assert from "assert"; +import { id_ad_ocsp } from "@wildboar/x500/src/lib/modules/PkiPmiExternalDataTypes/id-ad-ocsp.va"; +import { SignFunction, getOCSPResponse } from "@wildboar/ocsp-client"; +import { generateSignature } from "./generateSignature"; +import { VOR_RETURN_OK, VOR_RETURN_REVOKED, VOR_RETURN_UNKNOWN_INTOLERABLE, verifyOCSPResponse } from "./verifyOCSPResponse"; + +export const VAC_OK: number = 0; +export const VAC_NOT_BEFORE: number = -1; +export const VAC_NOT_AFTER: number = -2; +export const VAC_MISSING_BASE_CERT: number = -3; +export const VAC_HOLDER_MISMATCH: number = -4; +export const VAC_UNSUPPORTED_HOLDER_DIGEST: number = -5; +export const VAC_UNSUPPORTED_HOLDER_DIGESTED_OBJECT: number = -6; +export const VAC_NO_ASSERTION: number = -7; +export const VAC_NO_SOA_CERT: number = -8; +export const VAC_UNTRUSTED_SOA: number = -10; +export const VAC_INTERNAL_ERROR: number = -11; +export const VAC_UNUSABLE_AC_PATH: number = -12; +export const VAC_INVALID_DELEGATION: number = -13; +export const VAC_UNSUPPORTED_SIG_ALG: number = -14; +export const VAC_INVALID_SIGNATURE: number = -15; +export const VAC_SINGLE_USE: number = -16; +export const VAC_ACERT_REVOKED: number = -17; +export const VAC_INVALID_TARGET: number = -18; +export const VAC_INVALID_TIME_SPEC: number = -19; +export const VAC_AMBIGUOUS_GROUP: number = -20; +export const VAC_NOT_GROUP_MEMBER: number = -21; +export const VAC_BAD_GROUP_SYNTAX: number = -22; +export const VAC_DUPLICATE_EXT: number = -23; +export const VAC_UNKNOWN_CRIT_EXT: number = -24; +export const VAC_INVALID_EXT_CRIT: number = -25; +export const VAC_CRL_REVOKED: number = -26; +export const VAC_OCSP_OTHER: number = -27; +export const VAC_OCSP_REVOKED: number = -27; + +export +const supportedExtensions: Set = new Set([ + issuerAltName["&id"]!.toString(), // TODO: If critical, one of the name forms must be recognized. + cRLDistributionPoints["&id"]!.toString(), // MUST be checked if critical. + authorityKeyIdentifier["&id"]!.toString(), // Always non-critical. + altSignatureAlgorithm["&id"]!.toString(), // No meaning imputed to critical/non-critical. + altSignatureValue["&id"]!.toString(), // No meaning imputed to critical/non-critical. + authorityInfoAccess["&id"]!.toString(), // Always non-critical + subjectInfoAccess["&id"]!.toString(), // Always non-critical +]); + +export +const extensionMandatoryCriticality: Map = new Map([ + [ subjectKeyIdentifier["&id"]!.toString(), false ], // Always non-critical. + [ authorityKeyIdentifier["&id"]!.toString(), false ], // Always non-critical. + [ authorityInfoAccess["&id"]!.toString(), false ], // Always non-critical. + [ subjectInfoAccess["&id"]!.toString(), false ], // Always non-critical. +]); + +// TODO: Move this to @wildboar/x500 +function compare_issuer_serial (ctx: Context, a: IssuerSerial, b: IssuerSerial): boolean { + if (!Buffer.compare(a.serial, b.serial)) { + return false; + } + if ( + a.issuerUID + && b.issuerUID + && !Buffer.compare(packBits(a.issuerUID), packBits(b.issuerUID)) + ) { + return false; + } + const namingMatcher = getNamingMatcherGetter(ctx); + const nameMatched = a.issuer.some((agn) => { + for (const bgn of b.issuer) { + if (compareGeneralName(agn, bgn, namingMatcher)) { + return true; + } + } + return false; + }); + if (!nameMatched) { + return false; + } + return true; +} + +function get_issuer_serial_from_cert (cert: Certificate): IssuerSerial { + return new IssuerSerial( + [ + { + directoryName: cert.toBeSigned.issuer, + }, + ], + cert.toBeSigned.serialNumber, + cert.toBeSigned.issuerUniqueIdentifier, + ); +} + +function general_name_matches_cert ( + ctx: Context, + cert: Certificate, + gn: GeneralName, + alt_names?: GeneralName[], +): boolean { + const namingMatcher = getNamingMatcherGetter(ctx); + if ( + ("directoryName" in gn) + && compareName(gn.directoryName, cert.toBeSigned.subject, namingMatcher) + ) { + return true; + } + let sans = alt_names; + if (!alt_names) { + const sanExt = cert.toBeSigned.extensions?.find((ext) => ext.extnId.isEqualTo(subjectAltName["&id"]!)); + if (sanExt) { + const el = new DERElement(); + el.fromBytes(sanExt.extnValue); + sans = subjectAltName.decoderFor["&ExtnType"]!(el); + } + } + for (const san of sans ?? []) { + if (compareGeneralName(san, gn, namingMatcher)) { + return true; + } + } + return false; +} + +// TODO: Replace some code below with this. +function object_digest_matches_cert (cert: Certificate, odinfo: ObjectDigestInfo): boolean { + const hash_str = digestOIDToNodeHash.get(odinfo.digestAlgorithm.algorithm.toString()); + if (!hash_str) { + return false; + } + const hasher = createHash(hash_str); + let hashBytes: Uint8Array | undefined; + switch (odinfo.digestedObjectType) { + case (ObjectDigestInfo_digestedObjectType_publicKeyCert): { + hashBytes = cert.originalDER ?? _encode_Certificate(cert, DER).toBytes(); + break; + } + case (ObjectDigestInfo_digestedObjectType_publicKey): { + hashBytes = _encode_SubjectPublicKeyInfo(cert.toBeSigned.subjectPublicKeyInfo, DER).toBytes(); + break; + } + default: return false; + } + hasher.update(hashBytes!); + const calculatedHashValue = hasher.digest(); + const suppliedHashValue = packBits(odinfo.objectDigest); + // TODO: Does this need to be timing-safe? I don't think it does. + return !Buffer.compare(calculatedHashValue, suppliedHashValue); +} + + +// TODO: This was copied from verifyCertPath. +/** + * @summary Verify an X.509 certificate's alternative digital signature. + * @description + * + * This function verifies the alternative digital signature, as defined in ITU + * Recommendation X.509 (2019), Sections 9.8 and 7.2.2. + * + * Note that ITU Recommendation X.509 (2019), Section 9.8.4 states that the + * `altSignatureValue` extension value: + * + * > shall be verified using the alternative public key of the issuer. + * + * ### Implementation + * + * - Check that the issuer cert has an alternative public key. + * - Check that the subject cert has all three needed extensions. + * - Re-encode the subject cert appropriately. + * - Check the signature. + * + * @param subjectCert + * @param issuerCert + * @returns + */ +function verifyAltSignature (subjectCert: AttributeCertificate, issuerExts?: Extension[]): boolean | undefined { + if (!issuerExts) { + return undefined; + } + const issuerAltPublicKeyExt = issuerExts.find((ext) => ext.extnId.isEqualTo(subjectAltPublicKeyInfo["&id"]!)); + if (!issuerAltPublicKeyExt) { + return undefined; // The issuer has no alternative public key to verify with. + } + let subjectAltSignatureAlgorithmExt: Extension | undefined; + let subjectAltSignatureValueExt: Extension | undefined; + for (const ext of subjectCert.toBeSigned.extensions ?? []) { + if (ext.extnId.isEqualTo(altSignatureAlgorithm["&id"]!)) { + if (subjectAltSignatureAlgorithmExt) { + // Already defined. + return undefined; // Invalid: there can be only one alternative signature. + } + subjectAltSignatureAlgorithmExt = ext; + continue; // Just to avoid checking the next if statement. + } + if (ext.extnId.isEqualTo(altSignatureValue["&id"]!)) { + if (subjectAltSignatureValueExt) { + // Already defined. + return undefined; // Invalid: there can be only one alternative signature. + } + subjectAltSignatureValueExt = ext; + } + } + if (!subjectAltSignatureAlgorithmExt || !subjectAltSignatureValueExt) { + return undefined; + } + const pubkey = _decode_SubjectAltPublicKeyInfo(issuerAltPublicKeyExt.valueElement()); + const sigAlg: AltSignatureAlgorithm = _decode_AltSignatureAlgorithm(subjectAltSignatureAlgorithmExt.valueElement()); + const sigValue: Uint8Array = packBits(subjectAltSignatureValueExt.valueElement().bitString); + const verificand = DERElement.fromSequence([ + _encode_TBSAttributeCertificate(new TBSAttributeCertificate( + subjectCert.toBeSigned.version, + subjectCert.toBeSigned.holder, + subjectCert.toBeSigned.issuer, + subjectCert.toBeSigned.signature, + subjectCert.toBeSigned.serialNumber, + subjectCert.toBeSigned.attrCertValidityPeriod, + subjectCert.toBeSigned.attributes, + subjectCert.toBeSigned.issuerUniqueID, + subjectCert._unrecognizedExtensionsList, + // The altSignatureValue extension is supposed to be removed. + subjectCert.toBeSigned.extensions + ?.filter((ext) => !ext.extnId.isEqualTo(altSignatureValue["&id"]!)), + ), DER), + _encode_AlgorithmIdentifier(subjectCert.algorithmIdentifier, DER), + // The native signature is supposed to be excluded. + ]).toBytes(); + return verifySignature(verificand, sigAlg, sigValue, pubkey); +} + +function isRevokedFromConfiguredCRLs ( + ctx: Context, + cert: AttributeCertificate, + asOf: Date, + options: CRLIndex & OfflinePKIConfig, +): boolean { + // If the index of revoked certificate serial numbers has this cert's SN, + // it is worth scanning the CRLs for which cert revoked it. + if (!options.revokedCertificateSerialNumbers.has(cert.toBeSigned.serialNumber.toString())) { + return false; + } + const namingMatcher = getNamingMatcherGetter(ctx); + return options.certificateRevocationLists + .some((crl) => ( + // We only care about CRLs that could have applied at the asserted time. + (getDateFromTime(crl.toBeSigned.thisUpdate) <= asOf) + && cert.toBeSigned.issuer.issuerName?.some((iss_name) => compareGeneralName( + { directoryName: crl.toBeSigned.issuer }, + iss_name, + namingMatcher, + )) + && crl.toBeSigned.revokedCertificates + ?.some((rc) => ( + (rc.serialNumber == cert.toBeSigned.serialNumber) + && (getDateFromTime(rc.revocationDate) <= asOf) + )) + )); +} + +export +async function checkOCSP ( + ctx: MeerkatContext, + ext: Extension, + issuer: [ Name, SubjectPublicKeyInfo ], + cert: AttributeCertificate, + options: OCSPOptions, +): Promise { + assert(ext.extnId.isEqualTo(authorityInfoAccess["&id"]!)); + const aiaEl = new DERElement(); + aiaEl.fromBytes(ext.extnValue); + const aiaValue = authorityInfoAccess.decoderFor["&ExtnType"]!(aiaEl); + const ocspEndpoints: GeneralName[] = aiaValue + .filter((ad) => ad.accessMethod.isEqualTo(id_ad_ocsp)) + .map((ad) => ad.accessLocation); + const signFunction: SignFunction | undefined = options.ocspSignRequests + ? (data: Uint8Array) => { + const key = ctx.config.signing.key; + const certPath = ctx.config.signing.certPath; + if (!key || !certPath?.length) { + return null; + } + const sig = generateSignature(key, data); + if (!sig) { + return null; + } + const [ algid, sigValue ] = sig; + return [ certPath, algid, sigValue ]; + } + : undefined; + let requestBudget: number = options.maxOCSPRequestsPerCertificate; + for (const gn of ocspEndpoints) { + if (!("uniformResourceIdentifier" in gn)) { + continue; + } + const url = new URL(gn.uniformResourceIdentifier); + if (!url.protocol.toLowerCase().startsWith("http")) { + continue; + } + if (requestBudget === 0) { + break; + } + requestBudget--; + const ocspResponse = await getOCSPResponse( + url, + [ + issuer[0].rdnSequence, + issuer[1], + cert.toBeSigned.serialNumber, + ], + undefined, + (options.ocspTimeout * 1000), + signFunction, + options.ocspResponseSizeLimit, + ); + if (!ocspResponse) { + return VAC_OCSP_OTHER; + } + const { res } = ocspResponse; + const verifyResult = await verifyOCSPResponse(ctx, res); + if (verifyResult === VOR_RETURN_OK) { + return VAC_OK; + } else if (verifyResult === VOR_RETURN_REVOKED) { + return VAC_OCSP_REVOKED; + } else if (verifyResult === VOR_RETURN_UNKNOWN_INTOLERABLE) { + return VAC_OCSP_OTHER; + } else { + continue; // Just to be explicit. + } + } + /** + * Even if we exhaust all endpoints, we return an "OK" so that outages of + * OCSP endpoints do not make TLS impossible. + */ + return VAC_OK; +} + + +async function hydrate_attr_cert_path_arc (arc: ACPathData): Promise { + // TODO: ~~If no certificate, fetch the certificate from the~~ + // Actually, nevermind. There is no efficient way to find the cert if not specified. + // TODO: If no attribute certificate, fetch the attribute certificate from the subject DN. + return arc; +} + +// Holder ::= SEQUENCE { +// baseCertificateID [0] IssuerSerial OPTIONAL, +// entityName [1] GeneralNames OPTIONAL, +// objectDigestInfo [2] ObjectDigestInfo OPTIONAL } +// TODO: Actually, the path should just be an index of DNs +async function hydrate_attr_cert_path ( + ctx: Context, + path: AttributeCertificationPath, +): Promise { + // const processed: Set = new Set(); + const byBaseCertId: Map = new Map(); + // const byGeneralName: Map = new Map(); // Not guaranteed to be unique. + // const byCertDigest: Map = new Map(); + // const byPublicKeyDigest: Map = new Map(); + + const digests = path.acPath?.map((arc) => arc.attributeCertificate?.toBeSigned.issuer.objectDigestInfo) ?? []; + // for (const digest of digests) { + // // digests. + // } + + for (const arc of path.acPath ?? []) { + if (arc.certificate) { + const issuer = stringifyDN(ctx, arc.certificate.toBeSigned.issuer.rdnSequence); + const serial = Buffer.from(arc.certificate.toBeSigned.serialNumber).toString("base64"); + const key = `${serial}:${issuer}`; + byBaseCertId.set(key, arc); + } + } + + + // const new_path: ACPathData[] = []; + // const first = path.acPath?.find((arc) => arc.) + // stringifyGN(ctx, ) + // const authorities = path.acPath?.sort((a, b) => { + // }); + return path; +} + +function is_cert_holder ( + ctx: Context, + eeCert: Certificate, + holder: Holder, + issuerCert?: Certificate, +): number { + const namingMatcher = getNamingMatcherGetter(ctx); + if (holder.baseCertificateID) { + if (!eeCert) { + return VAC_MISSING_BASE_CERT; + } + if (!Buffer.compare(eeCert.toBeSigned.serialNumber, holder.baseCertificateID.serial)) { + return VAC_HOLDER_MISMATCH; + } + if (holder.baseCertificateID.issuerUID && eeCert.toBeSigned.issuerUniqueIdentifier) { + const a = holder.baseCertificateID.issuerUID; + const b = eeCert.toBeSigned.issuerUniqueIdentifier; + const x = Buffer.from(a.buffer, a.byteOffset, a.byteLength); + const y = Buffer.from(b.buffer, b.byteOffset, b.byteLength); + if (!Buffer.compare(x, y)) { + return VAC_HOLDER_MISMATCH; + } + } + + // TODO: When extended Name syntax is supported, compare against other name forms. + const issuerNames: Name[] = holder.baseCertificateID.issuer + .map((iss) => ("directoryName" in iss) ? iss.directoryName : undefined) + .filter((iss): iss is Name => !!iss) + .slice(0, 10); // Slice to prevent DoS attacks by large inputs. + let name_matched: boolean = false; + const issuer_name = eeCert.toBeSigned.issuer; + for (const base_cert_iss_name of issuerNames) { + if (compareName(issuer_name, base_cert_iss_name, namingMatcher)) { + name_matched = true; + break; + } + } + // If the names didn't match the issuer field, we try the issuerAltNames in the EE certificate. + // ACTUALLY, I changed my mind on this. I think the use of this extension + // would allow intermediate issuers to issue names to themselves they + // might not have been granted by their CAs. + // if (!name_matched) { + // for (const ext of eeCert.toBeSigned.extensions ?? []) { + // if (!ext.extnId.isEqualTo(issuerAltName["&id"]!)) { + // continue; + // } + // const el = new DERElement(); + // el.fromBytes(ext.extnValue); + // const issuer_alt_names = issuerAltName.decoderFor["&ExtnType"]!(el); + // for (const gn of issuer_alt_names) { + // if ("directoryName" in gn) { + // if (compareName(issuer_name, gn.directoryName, namingMatcher)) { + + // } + // } + // } + // } + // return VAC_HOLDER_MISMATCH; + // } + // If the issuer name did not match the issuer field on the cert, try the issuer's subject alt names. + if (!name_matched && issuerCert) { + for (const ext of issuerCert.toBeSigned.extensions ?? []) { + if (!ext.extnId.isEqualTo(subjectAltName["&id"]!)) { + continue; + } + const el = new DERElement(); + el.fromBytes(ext.extnValue); + const sans = subjectAltName.decoderFor["&ExtnType"]!(el); + for (const san of sans) { + if ("directoryName" in san) { + if (compareName(issuer_name, san.directoryName, namingMatcher)) { + name_matched = true; + break; + } + } + } + if (name_matched) { + break; + } + } + } + if (!name_matched) { + return VAC_HOLDER_MISMATCH; + } + } + if (holder.entityName) { + const subjectNames: Name[] = [ eeCert.toBeSigned.subject ]; + for (const ext of eeCert.toBeSigned.extensions ?? []) { + if (!ext.extnId.isEqualTo(subjectAltName["&id"]!)) { + continue; + } + const el = new DERElement(); + el.fromBytes(ext.extnValue); + const sans = subjectAltName.decoderFor["&ExtnType"]!(el); + for (const san of sans) { + if ("directoryName" in san) { + subjectNames.push(san.directoryName); + } + } + } + const ens: Name[] = holder.entityName + .map((n) => ("directoryName" in n) ? n.directoryName : undefined) + .filter((n): n is Name => !!n) + .slice(0, 10); // Slice to prevent DoS attacks by large inputs. + let name_matched: boolean = false; + for (const sn of subjectNames) { + for (const en of ens) { + if (compareName(sn, en, namingMatcher)) { + name_matched = true; + break; + } + } + if (name_matched) { + break; + } + } + if (!name_matched) { + return VAC_HOLDER_MISMATCH; + } + } + if (holder.objectDigestInfo) { + const hash_str = digestOIDToNodeHash.get(holder.objectDigestInfo.digestAlgorithm.algorithm.toString()); + if (hash_str) { + const hasher = createHash(hash_str); + let hashBytes: Uint8Array | undefined; + switch (holder.objectDigestInfo.digestedObjectType) { + case (ObjectDigestInfo_digestedObjectType_publicKeyCert): { + hashBytes = eeCert.originalDER ?? _encode_Certificate(eeCert, DER).toBytes(); + break; + } + case (ObjectDigestInfo_digestedObjectType_publicKey): { + hashBytes = _encode_SubjectPublicKeyInfo(eeCert.toBeSigned.subjectPublicKeyInfo, DER).toBytes(); + break; + } + default: { + if (!holder.baseCertificateID && !holder.entityName) { + // If we verified a base certificate ID and/or entity names, it is + // okay if we don't understand the digest type. However, if the + // only holder identification we have is a hash that we do not + // support, we need to return an error. + return VAC_UNSUPPORTED_HOLDER_DIGESTED_OBJECT; + } + } + } + if (hashBytes) { + hasher.update(hashBytes); + const calculatedHashValue = hasher.digest(); + const suppliedHashValue = packBits(holder.objectDigestInfo.objectDigest); + // TODO: Does this need to be timing-safe? I don't think it does. + if (!Buffer.compare(calculatedHashValue, suppliedHashValue)) { + return VAC_HOLDER_MISMATCH; + } + } + } + else if (!holder.baseCertificateID && !holder.entityName?.length) { + // If we verified a base certificate ID and/or entity names, it is + // okay if we don't understand the digest algorithm. However, if the + // only holder identification we have is a hash that we do not + // support, we need to return an error. + return VAC_UNSUPPORTED_HOLDER_DIGEST; + } + // Otherwise, we don't recognize the hash type or digest, but it doesn't + // matter, because we verified the other holder fields. So do nothing. + } + return VAC_OK; +} + +function isAttrCertIssuerTrusted ( + ctx: Context, + issuer: AttCertIssuer, + trust_anchor: TrustAnchorChoice, + soa: boolean = false, +): boolean { + const namingMatcher = getNamingMatcherGetter(ctx); + let matched: boolean = false; + const tbs: TBSCertificate | undefined = ("taInfo" in trust_anchor) + ? trust_anchor.taInfo.certPath?.certificate?.toBeSigned + : ("certificate" in trust_anchor) + ? trust_anchor.certificate.toBeSigned + : trust_anchor.tbsCert; + if (soa) { + if (!tbs) { + return false; + } + const is_soa: boolean = tbs.extensions?.some((ext) => ext.extnId.isEqualTo(sOAIdentifier["&id"]!)) ?? false; + if (!is_soa) { + return false; + } + } + if (issuer.baseCertificateID) { + if (tbs) { + if (!Buffer.compare(tbs.serialNumber, issuer.baseCertificateID.serial)) { + return false; + } + if (issuer.baseCertificateID.issuerUID && tbs.issuerUniqueIdentifier) { + const a = issuer.baseCertificateID.issuerUID; + const b = tbs.issuerUniqueIdentifier; + const x = Buffer.from(a.buffer, a.byteOffset, a.byteLength); + const y = Buffer.from(b.buffer, b.byteOffset, b.byteLength); + if (!Buffer.compare(x, y)) { + return false; + } + } + + // TODO: When extended Name syntax is supported, compare against other name forms. + const issuerNames: Name[] = issuer.baseCertificateID.issuer + .map((iss) => ("directoryName" in iss) ? iss.directoryName : undefined) + .filter((iss): iss is Name => !!iss) + .slice(0, 10); // Slice to prevent DoS attacks by large inputs. + let name_matched: boolean = false; + const issuer_name = tbs.issuer; + for (const base_cert_iss_name of issuerNames) { + if (compareName(issuer_name, base_cert_iss_name, namingMatcher)) { + name_matched = true; + break; + } + } + if (!name_matched) { + return false; + } + matched = true; + } + } + if (issuer.issuerName) { + const subjectNames: Name[] = []; + if (tbs) { + subjectNames.push(tbs.subject); + for (const ext of tbs.extensions ?? []) { + if (!ext.extnId.isEqualTo(subjectAltName["&id"]!)) { + continue; + } + const el = new DERElement(); + el.fromBytes(ext.extnValue); + const sans = subjectAltName.decoderFor["&ExtnType"]!(el); + for (const san of sans) { + if ("directoryName" in san) { + subjectNames.push(san.directoryName); + } + } + } + } + if (("taInfo" in trust_anchor) && trust_anchor.taInfo.certPath?.taName) { + subjectNames.push(trust_anchor.taInfo.certPath.taName); + } + const iss_names: Name[] = issuer.issuerName + .map((n) => ("directoryName" in n) ? n.directoryName : undefined) + .filter((n): n is Name => !!n) + .slice(0, 10); // Slice to prevent DoS attacks by large inputs. + let name_matched: boolean = false; + for (const sn of subjectNames) { + for (const iss_name of iss_names) { + if (compareName(sn, iss_name, namingMatcher)) { + name_matched = true; + break; + } + } + if (name_matched) { + break; + } + } + if (name_matched) { + matched = true; + } else { + return false; + } + } + if (issuer.objectDigestInfo) { + const hash_str = digestOIDToNodeHash.get(issuer.objectDigestInfo.digestAlgorithm.algorithm.toString()); + if (hash_str) { + const hasher = createHash(hash_str); + let hashBytes: Uint8Array | undefined; + switch (issuer.objectDigestInfo.digestedObjectType) { + case (ObjectDigestInfo_digestedObjectType_publicKeyCert): { + hashBytes = ("certificate" in trust_anchor) + ? (trust_anchor.certificate.originalDER ?? _encode_Certificate(trust_anchor.certificate, DER).toBytes()) + : (("taInfo" in trust_anchor) && trust_anchor.taInfo.certPath?.certificate) + ? trust_anchor.taInfo.certPath.certificate.originalDER + ?? _encode_Certificate(trust_anchor.taInfo.certPath.certificate, DER).toBytes() + : undefined; + break; + } + case (ObjectDigestInfo_digestedObjectType_publicKey): { + 1 + 1; // This bug in TypeScript still exists. I have to put an expression here. + if (tbs) { + hashBytes = _encode_SubjectPublicKeyInfo(tbs.subjectPublicKeyInfo, DER).toBytes(); + } + break; + } + default: { + if (!issuer.baseCertificateID && !issuer.issuerName) { + // If we verified a base certificate ID and/or entity names, it is + // okay if we don't understand the digest type. However, if the + // only holder identification we have is a hash that we do not + // support, we need to return an error. + return false; + } + } + } + if (hashBytes) { + hasher.update(hashBytes); + const calculatedHashValue = hasher.digest(); + const suppliedHashValue = packBits(issuer.objectDigestInfo.objectDigest); + // TODO: Does this need to be timing-safe? I don't think it does. + if (!Buffer.compare(calculatedHashValue, suppliedHashValue)) { + return false; + } + matched = true; + } + } + else if (!issuer.baseCertificateID && !issuer.issuerName?.length) { + // If we verified a base certificate ID and/or entity names, it is + // okay if we don't understand the digest algorithm. However, if the + // only holder identification we have is a hash that we do not + // support, we need to return an error. + return false; + } + // Otherwise, we don't recognize the hash type or digest, but it doesn't + // matter, because we verified the other holder fields. So do nothing. + } + return matched; +} + +// TODO: Check if the cert path is even interesting at all (e.g. it has a clearance attribute) +// TODO: Option to obtain clearances from the asserted public-key certificates + +// AttributeCertificationPath ::= SEQUENCE { +// attributeCertificate AttributeCertificate, +// acPath SEQUENCE OF ACPathData OPTIONAL, +// ... } + +// ACPathData ::= SEQUENCE { +// certificate [0] Certificate OPTIONAL, +// attributeCertificate [1] AttributeCertificate OPTIONAL, +// ... } +// TODO: Report defect: `ACPathData.attributeCertificate` should support multiple ACs, since an entity may obtain priviledge from multiple ACs. +// TODO: Report defect: there should be a constraint to mandate one or the other. + +// - [ ] Verify that, for each certificate, if issuedOnBehalfOf is present, it points to the next AC +// - [ ] If the issuedOnBehalfOf extension is present, wat do? +// - [ ] It seems like all intermediary AAs are required to have the indirectIssuer extension +// - Does this mean that issuedOnBehalfOf always points to the intermediary above the end-entity's issuer? +// - Isn't this field redundant? Can't you just use the issuer field? +// - Well, if you didn't have it, might the AC appear to be an end entity cert? +// - No, because of the basic constraints. +// - I get it: attribute certificates don't "issue" other attribute certificates. Public key certs do. + +// ACs are always signed by PKCs. AAs are made by associating attributes with a PKC, for which it may then delegate further attribute certificates. +// - You pretty much treat the attributes of an attribute certificate as though they were simply added to the subjectDirectoryAttributes + +// Assume the PKI Path is valid. +export +async function verifyAttrCertPath ( + ctx: Context, + acPath: AttributeCertificationPath, + userPkiPath: PkiPath, + soas: TrustAnchorList, +): Promise { + + // #region validity + // We start by checking the validity of all certs. It is the least + // computationally expensive step, and it is the most likely step to fail. + // So it's an excellent candidate for "short-circuiting" a lot more work. + const now = new Date(); + if (now < acPath.attributeCertificate.toBeSigned.attrCertValidityPeriod.notBeforeTime) { + return VAC_NOT_BEFORE; + } + if (now > acPath.attributeCertificate.toBeSigned.attrCertValidityPeriod.notAfterTime) { + return VAC_NOT_AFTER; + } + for (const pair of acPath.acPath ?? []) { + const ac = pair.attributeCertificate; + const pkc = pair.certificate; + if (ac) { + if (now < ac.toBeSigned.attrCertValidityPeriod.notBeforeTime) { + return VAC_NOT_BEFORE; + } + if (now > ac.toBeSigned.attrCertValidityPeriod.notAfterTime) { + return VAC_NOT_AFTER; + } + } + if (pkc) { + const nbf = getDateFromTime(pkc.toBeSigned.validity.notBefore); + const naf = getDateFromTime(pkc.toBeSigned.validity.notAfter); + if (now < nbf) { + return VAC_NOT_BEFORE; + } + if (now > naf) { + return VAC_NOT_AFTER; + } + } + } + // #endregion validity + + // #region noAssertion + // Technically, end-entity ACs are not supposed to have a noAssertion + // extension, but we validate this anyway. The presence of noAssertion in + // AA ACs does not make them invalid: it just means that the AAs cannot + // assert those attributes: only issue attribute certificates that have + // them. + const ac = acPath.attributeCertificate; + for (const ext of ac.toBeSigned.extensions ?? []) { + if (!ext.extnId.isEqualTo(noAssertion["&id"]!)) { + continue; + } + return VAC_NO_ASSERTION; + } + // #endregion + + // #region trusted_soa + // This region checks that the attribute certification path originates from an SOA. + if (acPath.acPath?.length) { + const soaArc = acPath.acPath[acPath.acPath.length - 1]; + if (soaArc.certificate) { + const soa = soaArc.certificate; + const is_soa: boolean = soa?.toBeSigned.extensions + ?.some((ext) => ext.extnId.isEqualTo(sOAIdentifier["&id"]!)) ?? false; + if (!is_soa) { + return VAC_NO_SOA_CERT; + } + const soaBytes = soa.originalDER + ?? _encode_Certificate(soa, DER).toBytes(); + const trusted: boolean = soas.some((ta) => isCertInTrustAnchor(soa, ta, soaBytes)); + if (!trusted) { + return VAC_UNTRUSTED_SOA; + } + // } + // else if (soaArc.attributeCertificate) { + // // Otherwise, attempt to locate the issuer among the trusted SOAs + // // by name and check the signature. + // const soa_iss = soaArc.attributeCertificate.toBeSigned.issuer; + // const anchor = soas.find((ta) => isAttrCertIssuerTrusted(ctx, soa_iss, ta, true)); + // if (!anchor) { + // return VAC_UNTRUSTED_SOA; + // } + } else { + // The last arc should only have a PKC, because it is "assigned" any + // privileges via an attribute certificate. If there is an attribute + // certificate, it is just ignored. + return VAC_NO_SOA_CERT; + } + + } else { + const soa_iss = acPath.attributeCertificate.toBeSigned.issuer; + /** + * We intentionally set this to false, since the SOAs we are searching + * through are marked as SOAs by fiat. + */ + const require_soa: boolean = false; + const anchor = soas.find((ta) => isAttrCertIssuerTrusted(ctx, soa_iss, ta, require_soa)); + if (!anchor) { + return VAC_UNTRUSTED_SOA; + } + } + // #endregion trusted_soa + + // #region holder2pkc + // This code region verifies that the holder for every attribute + // certificate corresponds to the public key certificate. + { + const holder = ac.toBeSigned.holder; + const eeCert: Certificate | undefined = userPkiPath[userPkiPath.length - 1]; + const issuerCert: Certificate | undefined = userPkiPath[userPkiPath.length - 2]; + // const namingMatcher = getNamingMatcherGetter(ctx); + const holder_result = is_cert_holder(ctx, eeCert, holder, issuerCert); + if (holder_result !== VAC_OK) { + return holder_result; + } + } + + for (const arc of acPath.acPath ?? []) { + if (arc.certificate && arc.attributeCertificate) { + const pkc = arc.certificate; + const holder = arc.attributeCertificate.toBeSigned.holder; + const holder_result = is_cert_holder(ctx, pkc, holder); + if (holder_result !== VAC_OK) { + return holder_result; + } + } else { + // TODO: If no cert, use attribute certificate to search the DSAIT. + // TODO: If no attr cert, use the cert to search the DSAIT. + return VAC_UNUSABLE_AC_PATH; + } + } + // #endregion holder2pkc + + // #region chain_verification + // In this section, we finally verify the signatures and constraints, etc. + if (acPath.acPath?.length) { + // const pmi_path = [ ...acPath.acPath ].reverse(); + // const soa_cert = pmi_path[0]?.certificate; + // if (!soa_cert) { + // return VAC_INTERNAL_ERROR; // Internal error because this was already checked. + // } + // let subject = + const subj_tbs = acPath.attributeCertificate; + const issuer = acPath.acPath.find((arc) => { + if (!arc.certificate) { + return false; + } + const cert = arc.certificate; + // if (subj_tbs.toBeSigned.issuer.baseCertificateID) { + + // } + // if (subj_tbs.toBeSigned.issuer.) + }) + + + // TODO: I'm not actually sure the path has to have an SOA at the end of it... + // It would be better to arrange the path items in order before performing any evaluation. + // Then check if the SOA cert is present at the end. + // If there is no SOA cert, just start i off at 0. + + + // let issuer_cert: Certificate = soa_cert; + // for (let i = 1; i < pmi_path.length; i++) { + // const arc = await hydrate_attr_cert_path_arc(pmi_path[i]); + // const subj_cert = arc.certificate; + // const attr_cert = arc.attributeCertificate; + // // TODO: Check that attr_cert is issued by issuer_cert + // // TODO: Check that attr_cert has indirectIssuer extension + // // TODO: + // } + + // for (const arc of pmi_path.slice(1)) { // slice(1) because the last arc is the SOA. It is infallible. + // if (!arc.certificate || !arc.attributeCertificate) { + // // TODO: If no cert, use attribute certificate to search the DSAIT. + // // TODO: If no attr cert, use the cert to search the DSAIT. + // return VAC_UNUSABLE_AC_PATH; + // } + // const cert = arc.certificate; + // const attr_cert = arc.attributeCertificate; + // // We already checked that the attr_cert matches the certificate. + // // TODO: Check that the certificate is trusted? + // // TODO: Check that the attribute certificate has the indirectIssuer extension. + // } + + const iobo_ext = acPath.attributeCertificate.toBeSigned.extensions + ?.find((ext) => ext.extnId.isEqualTo(issuedOnBehalfOf["&id"]!)); + if (!iobo_ext) { + return VAC_INVALID_DELEGATION; + } + const iobo_el = new DERElement(); + iobo_el.fromBytes(iobo_ext.extnValue); + const iobo = issuedOnBehalfOf.decoderFor["&ExtnType"]!(iobo_el); + // TODO: Check that iobo + } + // #endregion chain_verification + + return VAC_OK; +} + + +// TODO: Could you just somehow make this recursive to verify a whole path? +// This would require: +// Ensure holder name constraints fall within the issuer's name constraints +// Ensure holder's allowed attribute assignments are a subset of the issuers +// Ensure no loops. +// Ensure indirectIssuer and issuedOnBehalfOf extensions. + +// This just verifies a single attribute certificate. +export +async function verifyAttrCert ( + ctx: MeerkatContext, + acert: AttributeCertificate, + userPkiPath: PkiPath, + soas: TrustAnchorList, +): Promise { + + const now = new Date(); + if (now < acert.toBeSigned.attrCertValidityPeriod.notBeforeTime) { + return VAC_NOT_BEFORE; + } + if (now > acert.toBeSigned.attrCertValidityPeriod.notAfterTime) { + return VAC_NOT_AFTER; + } + + const acert_bytes = acert.originalDER + ?? _encode_AttributeCertificate(acert, DER).toBytes(); + + const acert_hasher = createHash("sha256"); + acert_hasher.update(acert_bytes); + const acert_hash = acert_hasher.digest().toString("base64"); + + const extsGroupedByOID = groupByOID(acert.toBeSigned.extensions ?? [], (ext) => ext.extnId); + for (const [ extId, exts ] of Object.entries(extsGroupedByOID)) { + if (exts.length > 1) { + return VAC_DUPLICATE_EXT; + } + const ext = exts[0]; + if (ext.critical && !supportedExtensions.has(extId)) { + return VAC_UNKNOWN_CRIT_EXT; + } + const criticalityMandate = extensionMandatoryCriticality.get(extId); + if ( + (criticalityMandate !== undefined) + && (criticalityMandate !== (ext.critical ?? false)) + ) { + return VAC_INVALID_EXT_CRIT; + } + } + + let single_use: boolean = false; + let group_ac: boolean = false; + let no_rev_avail: boolean = false; + + for (const ext of acert.toBeSigned.extensions ?? []) { + // Technically, end-entity ACs are not supposed to have a noAssertion + // extension, but we validate this anyway. The presence of noAssertion in + // AA ACs does not make them invalid: it just means that the AAs cannot + // assert those attributes: only issue attribute certificates that have + // them. + if (ext.extnId.isEqualTo(noAssertion["&id"]!)) { + return VAC_NO_ASSERTION; + } + else if (ext.extnId.isEqualTo(singleUse["&id"]!)) { + if (ctx.alreadyAssertedAttributeCertificates.has(acert_hash)) { + return VAC_SINGLE_USE; + } + single_use = true; + } + else if (ext.extnId.isEqualTo(groupAC["&id"]!)) { + group_ac = true; + } + else if (ext.extnId.isEqualTo(noRevAvail["&id"]!)) { + no_rev_avail = true; + } + else if (ext.extnId.isEqualTo(timeSpecification["&id"]!)) { + const el = new DERElement(); + el.fromBytes(ext.extnValue); + if (!evaluateTemporalContext(_encode_TimeAssertion({ now: null }, DER), el)) { + return VAC_INVALID_TIME_SPEC; + } + } + return VAC_NO_ASSERTION; + } + + if (isRevokedFromConfiguredCRLs(ctx, acert, now, ctx.config.signing)) { + return VAC_CRL_REVOKED; + } + + const namingMatcher = getNamingMatcherGetter(ctx); + const isGroupMember = getIsGroupMember(ctx, namingMatcher); + const eeCert: Certificate | undefined = userPkiPath[userPkiPath.length - 1]; + if (group_ac) { + // TODO: Report non-documentation of groupAC procedures. + const directory_names = acert.toBeSigned.holder.entityName + ?.flatMap((n) => ("directoryName" in n ? [ n.directoryName ] : [])) ?? []; + if (directory_names.length !== 1) { + return VAC_AMBIGUOUS_GROUP; + } + const dirname = directory_names[0]; + const groupName = new NameAndOptionalUID( + dirname.rdnSequence, + undefined, + ); + const memberName = new NameAndOptionalUID( + eeCert.toBeSigned.subject.rdnSequence, + eeCert.toBeSigned.subjectUniqueIdentifier, + ); + try { + + const in_group: boolean | undefined = await isGroupMember(groupName, memberName); + if (!in_group) { + return VAC_NOT_GROUP_MEMBER; + } + } catch (e) { + // TODO: Log + return VAC_NOT_GROUP_MEMBER; + } + } + else { + const holder = acert.toBeSigned.holder; + const issuerCert: Certificate | undefined = userPkiPath[userPkiPath.length - 2]; + const holder_result = is_cert_holder(ctx, eeCert, holder, issuerCert); + if (holder_result !== VAC_OK) { + return holder_result; + } + } + + const soa_iss = acert.toBeSigned.issuer; + /** + * We intentionally set this to false, since the SOAs we are searching + * through are marked as SOAs by fiat. + */ + const require_soa: boolean = false; + const anchor = soas.find((ta) => isAttrCertIssuerTrusted(ctx, soa_iss, ta, require_soa)); + if (!anchor) { + return VAC_UNTRUSTED_SOA; + } + let issuerName: Name | undefined; + let spki!: SubjectPublicKeyInfo; + let issuer_exts: Extension[] | undefined; + if ("certificate" in anchor) { + issuerName = anchor.certificate.toBeSigned.subject; + spki = anchor.certificate.toBeSigned.subjectPublicKeyInfo; + issuer_exts = anchor.certificate.toBeSigned.extensions; + } + else if ("tbsCert" in anchor) { + issuerName = anchor.tbsCert.subject; + spki = anchor.tbsCert.subjectPublicKeyInfo; + issuer_exts = anchor.tbsCert.extensions; + } + else { + issuerName = anchor.taInfo.certPath?.taName; + spki = anchor.taInfo.pubKey; + issuer_exts = anchor.taInfo.exts; + } + const valid_signature: boolean | undefined = verifyAltSignature(acert, issuer_exts) + ?? verifySignature( + acert_bytes, + acert.algorithmIdentifier, + packBits(acert.signature), + spki, + ); + if (valid_signature === false) { + return VAC_INVALID_SIGNATURE; + } + if (valid_signature === undefined) { + if (!acert.altAlgorithmIdentifier || !acert.altSignature) { + return VAC_INVALID_SIGNATURE; + } + const valid_alt_signature = verifySignature( + acert_bytes, + acert.altAlgorithmIdentifier, + packBits(acert.altSignature), + spki, + ); + if (!valid_alt_signature) { + return VAC_INVALID_SIGNATURE; + } + } + + const signing_cert_path = ctx.config.signing.certPath; + const signing_cert = signing_cert_path + ? signing_cert_path[signing_cert_path.length - 1] + : undefined; + + const sanExt = (signing_cert?.toBeSigned.extensions ?? []) + .find((ext) => ext.extnId.isEqualTo(subjectAltName["&id"]!)); + let sans: GeneralName[] = []; + if (sanExt) { + const el = new DERElement(); + el.fromBytes(sanExt.extnValue); + sans = subjectAltName.decoderFor["&ExtnType"]!(el); + } + + for (const ext of acert.toBeSigned.extensions ?? []) { + if (ext.extnId.isEqualTo(targetingInformation["&id"]!)) { + if (!signing_cert) { + return VAC_INVALID_TARGET; + } + const el = new DERElement(); + el.fromBytes(ext.extnValue); + const target_groups = targetingInformation.decoderFor["&ExtnType"]!(el); + let matched_target: boolean = false; + for (const group of target_groups) { + for (const target of group) { + // Target ::= CHOICE { + // targetName [0] GeneralName, + // targetGroup [1] GeneralName, + // targetCert [2] TargetCert, + // ... } + if ( + ("targetName" in target) + && general_name_matches_cert(ctx, signing_cert, target.targetName, sans) + ) { + matched_target = true; + break; + } + else if ("targetGroup" in target) { + if (!("directoryName" in target.targetGroup)) { + return VAC_BAD_GROUP_SYNTAX; + } + const memberName = new NameAndOptionalUID( + signing_cert.toBeSigned.subject.rdnSequence, + signing_cert.toBeSigned.subjectUniqueIdentifier, + ); + const groupName = new NameAndOptionalUID( + target.targetGroup.directoryName.rdnSequence, + undefined, + ); + const dsa_in_group: boolean | undefined = await isGroupMember(groupName, memberName); + if (!dsa_in_group) { + matched_target = true; + break; + } + } + else if ("targetCert" in target) { + // TargetCert ::= SEQUENCE { + // targetCertificate IssuerSerial, + // targetName GeneralName OPTIONAL, + // certDigestInfo ObjectDigestInfo OPTIONAL } + const tcert = target.targetCert.targetCertificate; + const tname = target.targetCert.targetName; + if ( + tcert + && compare_issuer_serial(ctx, tcert, get_issuer_serial_from_cert(signing_cert)) + ) { + matched_target = true; + } + if (tname && !general_name_matches_cert(ctx, signing_cert, tname, sans)) { + matched_target = false; + } + if ( + target.targetCert.certDigestInfo + && !object_digest_matches_cert(signing_cert, target.targetCert.certDigestInfo) + ) { + matched_target = false; + } + if (matched_target) { + break; + } + } + } + if (matched_target) { + break; + } + } + if (!matched_target) { + return VAC_INVALID_TARGET; + } + } + } + + if (!no_rev_avail && issuerName) { + const aiaExt = extsGroupedByOID[authorityInfoAccess["&id"]!.toString()]?.[0]; + if (aiaExt) { + const ocspCheckiness = ctx.config.signing.bindOverrides?.ocspCheckiness + ?? ctx.config.signing.ocspCheckiness; + if (ocspCheckiness >= 0) { + const ocspResult = await checkOCSP( + ctx, + aiaExt, + [ issuerName, spki ], + acert, + ctx.config.tls, + ); + if (ocspResult) { + return ocspResult; + } + } + } + + const crldpExt = extsGroupedByOID[cRLDistributionPoints["&id"]!.toString()]?.[0]; + if (crldpExt && crldpExt.critical) { // TODO: Make config options: ignore_critical or always_check. + const readDispatcher = getReadDispatcher(ctx); + const crlResult = await checkRemoteCRLs( + ctx, + crldpExt, + acert.toBeSigned.serialNumber, + [ issuerName, spki ], + readDispatcher, + ctx.config.signing, + ); + if (crlResult) { + return crlResult; + } + } + + } + + if (single_use) { + if (ctx.alreadyAssertedAttributeCertificates.size > 100_000) { + /* Yes, this does mean that some singleUse certs can be re-used, + if they are valid but happen to be the 100000th asserted, but this + will be infrequent. */ + ctx.alreadyAssertedAttributeCertificates.clear(); + } + ctx.alreadyAssertedAttributeCertificates.add(acert_hash); + } + return VAC_OK; +} diff --git a/apps/meerkat/src/app/pki/verifyCertPath.ts b/apps/meerkat/src/app/pki/verifyCertPath.ts index df587290f..8395fd68e 100644 --- a/apps/meerkat/src/app/pki/verifyCertPath.ts +++ b/apps/meerkat/src/app/pki/verifyCertPath.ts @@ -214,6 +214,7 @@ import { VOR_RETURN_UNKNOWN_INTOLERABLE, } from "./verifyOCSPResponse"; import _ from "lodash"; +import { Name } from "@wildboar/x500/src/lib/modules/InformationFramework/Name.ta"; // So that arguments can be modified by reference. type Box = { @@ -683,7 +684,8 @@ export async function checkOCSP ( ctx: MeerkatContext, ext: Extension, - cert: Certificate, + issuer: [ Name, SubjectPublicKeyInfo ], + subjectCert: Certificate, options: OCSPOptions, ): Promise { assert(ext.extnId.isEqualTo(authorityInfoAccess["&id"]!)); @@ -723,7 +725,11 @@ async function checkOCSP ( requestBudget--; const ocspResponse = await getOCSPResponse( url, - cert, + [ + issuer[0].rdnSequence, + issuer[1], + subjectCert.toBeSigned.serialNumber, + ], undefined, (options.ocspTimeout * 1000), signFunction, @@ -755,8 +761,8 @@ export async function checkRemoteCRLs ( ctx: MeerkatContext, ext: Extension, - subjectCert: Certificate, - issuerCert: Certificate, + serialNumber: Uint8Array, + issuer: [ Name, SubjectPublicKeyInfo ], readDispatcher: ReadDispatcherFunction, options: RemoteCRLOptions, ): Promise { @@ -770,14 +776,13 @@ async function checkRemoteCRLs ( .map((dp) => crlCurl( ctx, dp, - issuerCert.toBeSigned.subject, + issuer[0], readDispatcher, options, )) )) .filter((result): result is CertificateList[] => !!result) .flat(); - const serialNumber = subjectCert.toBeSigned.serialNumber; for (const crl of crls) { for (const rc of crl.toBeSigned.revokedCertificates ?? []) { if (Buffer.compare(rc.serialNumber, serialNumber)) { @@ -794,7 +799,7 @@ async function checkRemoteCRLs ( crl.algorithmIdentifier, sigValue, // Remember: the CRL needs to be validated against the ISSUER's public key. - issuerCert.toBeSigned.subjectPublicKeyInfo, + issuer[1], ); if (!signatureIsValid) { // If sig invalid, skip to next CRL entirely. @@ -1322,7 +1327,16 @@ async function verifyBasicPublicKeyCertificateChecks ( if (aiaExt && !state.validityTime) { // We can't const ocspCheckiness = options.ocspCheckiness; if (ocspCheckiness >= 0) { - const ocspResult = await checkOCSP(ctx, aiaExt, subjectCert, ctx.config.tls); + const ocspResult = await checkOCSP( + ctx, + aiaExt, + [ + issuerCert.toBeSigned.subject, + issuerCert.toBeSigned.subjectPublicKeyInfo, + ], + subjectCert, + ctx.config.tls, + ); if (ocspResult) { return ocspResult; } @@ -1335,8 +1349,8 @@ async function verifyBasicPublicKeyCertificateChecks ( const crlResult = await checkRemoteCRLs( ctx, crldpExt, - subjectCert, - issuerCert, + subjectCert.toBeSigned.serialNumber, + [issuerCert.toBeSigned.subject, issuerCert.toBeSigned.subjectPublicKeyInfo], readDispatcher, options, ); @@ -1849,7 +1863,17 @@ async function verifyCACertificate ( const aiaExt = extsGroupedByOID[authorityInfoAccess["&id"]!.toString()]?.[0]; if (aiaExt) { - const ocspResult = await checkOCSP(ctx, aiaExt, cert, ctx.config.signing); + // It would be weird for a Root CA to revoke itself, but we'll check anyway. + const ocspResult = await checkOCSP( + ctx, + aiaExt, + [ + cert.toBeSigned.subject, + cert.toBeSigned.subjectPublicKeyInfo, + ], + cert, + ctx.config.signing, + ); if (ocspResult) { return ocspResult; } @@ -1861,8 +1885,8 @@ async function verifyCACertificate ( const crlResult = await checkRemoteCRLs( ctx, crldpExt, - cert, - cert, + cert.toBeSigned.serialNumber, + [cert.toBeSigned.subject, cert.toBeSigned.subjectPublicKeyInfo], readDispatcher, options, ); diff --git a/apps/meerkat/src/app/pki/verifySIGNED.ts b/apps/meerkat/src/app/pki/verifySIGNED.ts index ae67f25c1..2f9c4ff1a 100644 --- a/apps/meerkat/src/app/pki/verifySIGNED.ts +++ b/apps/meerkat/src/app/pki/verifySIGNED.ts @@ -116,6 +116,7 @@ async function verifySIGNED ( ctx, certPath, ctx.config.signing.acceptableCertificatePolicies, + ctx.config.signing, ); if (vacpResult.returnCode !== VCP_RETURN_OK) { ctx.log.warn(ctx.i18n.t("log:cert_path_invalid", { diff --git a/libs/meerkat-types/src/lib/types.ts b/libs/meerkat-types/src/lib/types.ts index eccaa42fa..b4c65a916 100644 --- a/libs/meerkat-types/src/lib/types.ts +++ b/libs/meerkat-types/src/lib/types.ts @@ -3248,6 +3248,13 @@ interface Context { * be found. */ labellingAuthorities: Map; + + /** + * An index of the base64-encoded SHA256 hashes of + * `AttributeCertificate`s that were already asserted successfully for + * attribute certificates containing the `singleUse` extension. + */ + alreadyAssertedAttributeCertificates: Set; } /** diff --git a/libs/ocsp-client/src/lib/check.ts b/libs/ocsp-client/src/lib/check.ts index d46823552..d4bbf0bc8 100644 --- a/libs/ocsp-client/src/lib/check.ts +++ b/libs/ocsp-client/src/lib/check.ts @@ -12,9 +12,6 @@ import { import { CertID, } from "@wildboar/ocsp/src/lib/modules/OCSP-2013-08/CertID.ta"; -import { - Certificate, -} from "@wildboar/x500/src/lib/modules/AuthenticationFramework/Certificate.ta"; import { OCSPResponse, _decode_OCSPResponse, @@ -38,9 +35,11 @@ import { AlgorithmIdentifier, } from "@wildboar/x500/src/lib/modules/AuthenticationFramework/AlgorithmIdentifier.ta"; import { + SubjectPublicKeyInfo, _encode_SubjectPublicKeyInfo, } from "@wildboar/x500/src/lib/modules/AuthenticationFramework/SubjectPublicKeyInfo.ta"; import { + DistinguishedName, _encode_DistinguishedName, } from "@wildboar/x500/src/lib/modules/InformationFramework/DistinguishedName.ta"; import { createHash } from "crypto"; @@ -56,6 +55,7 @@ import type { import { Signature, } from "@wildboar/ocsp/src/lib/modules/OCSP-2013-08/Signature.ta"; +import { CertificateSerialNumber } from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/CertificateSerialNumber.ta"; // Yes, I realize I could have done this with .reduce(), but for loops are more performant. function getReceivedDataSize (chunks: Buffer[]) { @@ -93,20 +93,21 @@ const ACCEPTABLE_RESPONSE_MIME_TYPES: string[] = [ * Generates an OCSP request from a certificate that requests the status on that * certificate. * - * @param cert The certificate whose status is to be checked + * @param issuerCert The certificate whose status is to be checked * @param sign A function that can be used to digitally sign OCSP requests * @returns An OCSP request * * @function */ export -function convertCertToOCSPRequest ( - cert: Certificate, +function convertCertAndSerialToOCSPRequest ( + issuerDN: DistinguishedName, + issuerSPKI: SubjectPublicKeyInfo, + subjectSerial: Uint8Array, sign?: SignFunction, ): OCSPRequest { - const certTBS = cert.toBeSigned; - const dnBytes = _encode_DistinguishedName(certTBS.issuer.rdnSequence, DER).toBytes(); - const spkiElement = _encode_SubjectPublicKeyInfo(certTBS.subjectPublicKeyInfo, DER); + const dnBytes = _encode_DistinguishedName(issuerDN, DER).toBytes(); + const spkiElement = _encode_SubjectPublicKeyInfo(issuerSPKI, DER); const dnHasher = createHash("sha256"); const spkiHasher = createHash("sha256"); dnHasher.update(dnBytes); @@ -123,7 +124,7 @@ function convertCertToOCSPRequest ( ), dnHasher.digest(), spkiHasher.digest(), - certTBS.serialNumber, + subjectSerial, ), undefined, ), @@ -275,7 +276,8 @@ interface CheckResponse { * to an OCSP responder to obtain an OCSP result. * * @param url The URL of the OCSP responder to query - * @param req The OCSP request to send, or a certificate that will be used to generate it + * @param req The OCSP request to send, or the issuer's DN, the issuer's public + * key, and the serial number of the certificate to be checked * @param tlsOptions Options relating to TLS, if it is used * @param timeoutInMilliseconds The timeout in milliseconds before this operation is abandoned * @param sign A function that can be used to digitally sign outbound OCSP requests @@ -288,7 +290,7 @@ interface CheckResponse { export async function getOCSPResponse ( url: URL, - req: OCSPRequest | Certificate, + req: OCSPRequest | [ DistinguishedName, SubjectPublicKeyInfo, CertificateSerialNumber ], tlsOptions?: TlsOptions, timeoutInMilliseconds: number = 5000, sign?: SignFunction, @@ -299,7 +301,7 @@ async function getOCSPResponse ( } const ocspReq = (req instanceof OCSPRequest) ? req - : convertCertToOCSPRequest(req, sign); + : convertCertAndSerialToOCSPRequest(req[0], req[1], req[2], sign); const result = await postHTTPS( url, ocspReq, From 6df56493c30851a4fce95d484e9764a5a2f076be Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Sat, 19 Aug 2023 07:56:35 -0400 Subject: [PATCH 07/39] feat: associate clearances with a strong auth user --- .../src/app/authn/attemptStrongAuth.ts | 165 ++++++++++++++++++ .../meerkat/src/app/pki/verifyAttrCertPath.ts | 5 +- .../src/assets/locales/en/language/log.json | 31 +++- 3 files changed, 197 insertions(+), 4 deletions(-) diff --git a/apps/meerkat/src/app/authn/attemptStrongAuth.ts b/apps/meerkat/src/app/authn/attemptStrongAuth.ts index 2a385d8be..c9e40a68b 100644 --- a/apps/meerkat/src/app/authn/attemptStrongAuth.ts +++ b/apps/meerkat/src/app/authn/attemptStrongAuth.ts @@ -46,14 +46,128 @@ import { } from "../pki/verifyToken"; import { strict as assert } from "assert"; import type { + AttributeCertificationPath, StrongCredentials, } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/StrongCredentials.ta"; import type { Socket } from "node:net"; import type { TLSSocket } from "node:tls"; import { read_unique_id, read_clearance } from "../database/utils"; +import { clearance } from "@wildboar/x500/src/lib/collections/attributes"; +import { + verifyAttrCert, + VAC_OK, + VAC_NOT_BEFORE, + VAC_NOT_AFTER, + VAC_MISSING_BASE_CERT, + VAC_HOLDER_MISMATCH, + VAC_UNSUPPORTED_HOLDER_DIGEST, + VAC_UNSUPPORTED_HOLDER_DIGESTED_OBJECT, + VAC_NO_ASSERTION, + VAC_NO_SOA_CERT, + VAC_UNTRUSTED_SOA, + VAC_INTERNAL_ERROR, + VAC_UNUSABLE_AC_PATH, + VAC_INVALID_DELEGATION, + VAC_UNSUPPORTED_SIG_ALG, + VAC_INVALID_SIGNATURE, + VAC_SINGLE_USE, + VAC_ACERT_REVOKED, + VAC_INVALID_TARGET, + VAC_INVALID_TIME_SPEC, + VAC_AMBIGUOUS_GROUP, + VAC_NOT_GROUP_MEMBER, + VAC_DUPLICATE_EXT, + VAC_UNKNOWN_CRIT_EXT, + VAC_INVALID_EXT_CRIT, + VAC_CRL_REVOKED, + VAC_OCSP_OTHER, + VAC_OCSP_REVOKED, +} from "../pki/verifyAttrCertPath"; +import { Clearance } from "@wildboar/x500/src/lib/modules/EnhancedSecurity/Clearance.ta"; +import { subjectDirectoryAttributes } from "@wildboar/x500/src/lib/modules/CertificateExtensions/subjectDirectoryAttributes.oa"; +import { DERElement } from "asn1-ts"; const ID_OC_PKI_CERT_PATH: string = id_oc_pkiCertPath.toString(); +async function clearancesFromAttrCertPath ( + ctx: MeerkatContext, + path: AttributeCertificationPath, + certification_path: CertificationPath, + source: string, + socket: Socket | TLSSocket, +): Promise { + const logInfo = { + host: source, + remoteFamily: socket.remoteFamily, + remoteAddress: socket.remoteAddress, + remotePort: socket.remotePort, + }; + if (path.acPath) { + ctx.log.debug(ctx.i18n.t("log:attr_cert_path_unsupported", logInfo), logInfo); + return []; + } + const acert = path.attributeCertificate; + const has_attributes_of_interest: boolean = acert + .toBeSigned + .attributes + .some((attr) => attr.type_.isEqualTo(clearance["&id"])); + if (!has_attributes_of_interest) { + ctx.log.debug(ctx.i18n.t("log:attr_cert_path_not_verified", logInfo), logInfo); + return []; + } + + const pkiPath = [ + ...certification_path.theCACertificates + ?.flatMap((arc) => arc.issuedToThisCA ?? []) ?? [], + certification_path.userCertificate, + ]; + const attr_cert_verif_code = await verifyAttrCert( + ctx, + acert, + pkiPath, + ctx.config.rbac.clearanceAuthorities, + ); + if (attr_cert_verif_code === VAC_OK) { + return acert.toBeSigned.attributes + .filter((attr) => attr.type_.isEqualTo(clearance["&id"])) + .flatMap((attr) => attr.values) + .map((value) => clearance.decoderFor["&Type"]!(value)); + } + const logMessageContext = ({ + [VAC_NOT_BEFORE]: "not_before", + [VAC_NOT_AFTER]: "not_after", + [VAC_MISSING_BASE_CERT]: "missing_base_cert", + [VAC_HOLDER_MISMATCH]: "holder_mismatch", + [VAC_UNSUPPORTED_HOLDER_DIGEST]: "unsupported_holder_digest", + [VAC_UNSUPPORTED_HOLDER_DIGESTED_OBJECT]: "unsupported_holder_digested_object", + [VAC_NO_ASSERTION]: "no_assertion", + [VAC_NO_SOA_CERT]: "no_soa_cert", + [VAC_UNTRUSTED_SOA]: "untrusted_soa", + [VAC_INTERNAL_ERROR]: "internal_error", + [VAC_UNUSABLE_AC_PATH]: "unusable_ac_path", + [VAC_INVALID_DELEGATION]: "invalid_delegation", + [VAC_UNSUPPORTED_SIG_ALG]: "unsupported_sig_alg", + [VAC_INVALID_SIGNATURE]: "invalid_signature", + [VAC_SINGLE_USE]: "single_use", + [VAC_ACERT_REVOKED]: "acert_revoked", + [VAC_INVALID_TARGET]: "invalid_target", + [VAC_INVALID_TIME_SPEC]: "invalid_time_spec", + [VAC_AMBIGUOUS_GROUP]: "ambiguous_group", + [VAC_NOT_GROUP_MEMBER]: "not_group_member", + [VAC_DUPLICATE_EXT]: "duplicate_ext", + [VAC_UNKNOWN_CRIT_EXT]: "unknown_crit_ext", + [VAC_INVALID_EXT_CRIT]: "invalid_ext_crit", + [VAC_CRL_REVOKED]: "crl_revoked", + [VAC_OCSP_OTHER]: "ocsp_other", + [VAC_OCSP_REVOKED]: "ocsp_revoked", + })[attr_cert_verif_code]; + ctx.log.debug(ctx.i18n.t("log:verify_attr_cert", { + ...logInfo, + context: logMessageContext, + }), logInfo); + return []; +} + /** * @summary Attempts strong authentication * @description @@ -89,6 +203,7 @@ async function attemptStrongAuth ( name, bind_token, certification_path, + attributeCertificationPath, } = credentials; const logInfo = { host: source, @@ -202,6 +317,31 @@ async function attemptStrongAuth ( const clearances = foundEntry ? await read_clearance(ctx, foundEntry) : []; + const sdaExt = certification_path + .userCertificate + .toBeSigned + .extensions + ?.find((ext) => ext.extnId.isEqualTo(subjectDirectoryAttributes["&id"]!)); + if (sdaExt) { + const sdaEl = new DERElement(); + sdaEl.fromBytes(sdaExt.extnValue); + const sda = subjectDirectoryAttributes.decoderFor["&ExtnType"]!(sdaEl); + const sdaClearances = sda + .filter((attr) => attr.type_.isEqualTo(clearance["&id"])) + .flatMap((attr) => attr.values) + .map((value) => clearance.decoderFor["&Type"]!(value)); + clearances.push(...sdaClearances); + } + if (attributeCertificationPath) { + const attrCertClearances = await clearancesFromAttrCertPath( + ctx, + attributeCertificationPath, + certification_path, + source, + socket, + ); + clearances.push(...attrCertClearances); + } return { boundVertex: foundEntry, boundNameAndUID: new NameAndOptionalUID( @@ -282,6 +422,31 @@ async function attemptStrongAuth ( const clearances = attemptedVertex ? await read_clearance(ctx, attemptedVertex) : []; + const sdaExt = certPath + .userCertificate + .toBeSigned + .extensions + ?.find((ext) => ext.extnId.isEqualTo(subjectDirectoryAttributes["&id"]!)); + if (sdaExt) { + const sdaEl = new DERElement(); + sdaEl.fromBytes(sdaExt.extnValue); + const sda = subjectDirectoryAttributes.decoderFor["&ExtnType"]!(sdaEl); + const sdaClearances = sda + .filter((attr) => attr.type_.isEqualTo(clearance["&id"])) + .flatMap((attr) => attr.values) + .map((value) => clearance.decoderFor["&Type"]!(value)); + clearances.push(...sdaClearances); + } + if (attributeCertificationPath) { + const attrCertClearances = await clearancesFromAttrCertPath( + ctx, + attributeCertificationPath, + certPath, + source, + socket, + ); + clearances.push(...attrCertClearances); + } return { boundVertex: attemptedVertex, boundNameAndUID: new NameAndOptionalUID( diff --git a/apps/meerkat/src/app/pki/verifyAttrCertPath.ts b/apps/meerkat/src/app/pki/verifyAttrCertPath.ts index 7ed2515c3..e00d549fd 100644 --- a/apps/meerkat/src/app/pki/verifyAttrCertPath.ts +++ b/apps/meerkat/src/app/pki/verifyAttrCertPath.ts @@ -117,13 +117,12 @@ export const VAC_INVALID_TARGET: number = -18; export const VAC_INVALID_TIME_SPEC: number = -19; export const VAC_AMBIGUOUS_GROUP: number = -20; export const VAC_NOT_GROUP_MEMBER: number = -21; -export const VAC_BAD_GROUP_SYNTAX: number = -22; export const VAC_DUPLICATE_EXT: number = -23; export const VAC_UNKNOWN_CRIT_EXT: number = -24; export const VAC_INVALID_EXT_CRIT: number = -25; export const VAC_CRL_REVOKED: number = -26; export const VAC_OCSP_OTHER: number = -27; -export const VAC_OCSP_REVOKED: number = -27; +export const VAC_OCSP_REVOKED: number = -28; export const supportedExtensions: Set = new Set([ @@ -1241,7 +1240,7 @@ async function verifyAttrCert ( } else if ("targetGroup" in target) { if (!("directoryName" in target.targetGroup)) { - return VAC_BAD_GROUP_SYNTAX; + continue; } const memberName = new NameAndOptionalUID( signing_cert.toBeSigned.subject.rdnSequence, diff --git a/apps/meerkat/src/assets/locales/en/language/log.json b/apps/meerkat/src/assets/locales/en/language/log.json index 97f198f78..55530c1c0 100644 --- a/apps/meerkat/src/assets/locales/en/language/log.json +++ b/apps/meerkat/src/assets/locales/en/language/log.json @@ -374,5 +374,34 @@ "dsp_result_invalid_sig": "DSP result for operation {{opid}} had an invalid signature. {{e}}", "failed_to_apply_xr": "Failed to apply cross references received from operation {{opid}} to the local DSAIT. {{e}}", "filtered_cross_refs": "Filtered out {{count}} cross references from DSP response for operation {{opid}}.", - "name_found": "The name found for invocation {{iid}} was {{dn}}." + "name_found": "The name found for invocation {{iid}} was {{dn}}.", + + "verify_attr_cert@not_before": "Attribute certification path asserted by {{host}} is not valid yet.", + "verify_attr_cert@not_after": "Attribute certification path asserted by {{host}} expired.", + "verify_attr_cert@missing_base_cert": "Attribute certification path asserted by {{host}} is missing the base certificate.", + "verify_attr_cert@holder_mismatch": "Attribute certification path asserted by {{host}} does not identify bound entry as the holder.", + "verify_attr_cert@unsupported_holder_digest": "Attribute certification path asserted by {{host}} used an unsupported digest algorithm to identify the holder.", + "verify_attr_cert@unsupported_holder_digested_object": "Attribute certification path asserted by {{host}} used an unsupported digested object type to identify the holder.", + "verify_attr_cert@no_assertion": "Attribute certification path asserted by {{host}} uses a noAssertion extension in the end-entity certificate, indicating that it may not be used.", + "verify_attr_cert@no_soa_cert": "Attribute certification path asserted by {{host}} does not have a certificate that is identifiable as an SOA.", + "verify_attr_cert@untrusted_soa": "Attribute certification path asserted by {{host}} does not have a trusted SOA certificate.", + "verify_attr_cert@internal_error": "Attribute certification path asserted by {{host}} failed verification due to an internal error.", + "verify_attr_cert@unusable_ac_path": "Attribute certification path asserted by {{host}} is unusable, most likely due to being too parse in information.", + "verify_attr_cert@invalid_delegation": "Attribute certification path asserted by {{host}} contains invalid delegation.", + "verify_attr_cert@unsupported_sig_alg": "Attribute certification path asserted by {{host}} uses an unsupported signature algorithm.", + "verify_attr_cert@invalid_signature": "Attribute certification path asserted by {{host}} had an invalid signature.", + "verify_attr_cert@single_use": "Attribute certification path asserted by {{host}} is authorized for a single usage, and it was used once already.", + "verify_attr_cert@acert_revoked": "Attribute certification path asserted by {{host}} contained a certificate that was revoked.", + "verify_attr_cert@invalid_target": "Attribute certification path asserted by {{host}} is not authorized for assertion to this DSA.", + "verify_attr_cert@invalid_time_spec": "Attribute certification path asserted by {{host}} is not authorized for usage at this time because of the timeSpecification extension.", + "verify_attr_cert@ambiguous_group": "Attribute certification path asserted by {{host}} identifies an ambiguous group.", + "verify_attr_cert@not_group_member": "Attribute certification path asserted by {{host}} failed verification because the asserting party is not a member of the holder group.", + "verify_attr_cert@duplicate_ext": "Attribute certification path asserted by {{host}} contained a duplicate extension.", + "verify_attr_cert@unknown_crit_ext": "Attribute certification path asserted by {{host}} contained an unrecognized critical extension.", + "verify_attr_cert@invalid_ext_crit": "Attribute certification path asserted by {{host}} contained an extension with an invalid criticality.", + "verify_attr_cert@crl_revoked": "Attribute certification path asserted by {{host}} contained a certificate that was revoked in a CRL.", + "verify_attr_cert@ocsp_other": "Attribute certification path asserted by {{host}} could not be verified due to an error in checking OCSP.", + "verify_attr_cert@ocsp_revoked": "Attribute certification path asserted by {{host}} was revoked according to OCSP.", + "attr_cert_path_not_verified": "Attribute certification path asserted by {{host}} was not verified, because it contained no clearance attributes.", + "attr_cert_path_unsupported": "Attribute certification path asserted by {{host}} could not be verified because it uses a delegation path. Meerkat DSA only supports verifying a single, directly-issued attribute certificate." } From a7408579328bc6776ed0548b4efbc584722d1a35 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Sat, 19 Aug 2023 11:51:32 -0400 Subject: [PATCH 08/39] feat: use configuration options relating to RBAC --- .../src/app/authn/attemptStrongAuth.ts | 64 +++++++++++-------- apps/meerkat/src/app/ctx.ts | 1 + libs/meerkat-types/src/lib/types.ts | 13 ++++ 3 files changed, 50 insertions(+), 28 deletions(-) diff --git a/apps/meerkat/src/app/authn/attemptStrongAuth.ts b/apps/meerkat/src/app/authn/attemptStrongAuth.ts index c9e40a68b..d655ae2ce 100644 --- a/apps/meerkat/src/app/authn/attemptStrongAuth.ts +++ b/apps/meerkat/src/app/authn/attemptStrongAuth.ts @@ -96,6 +96,9 @@ async function clearancesFromAttrCertPath ( source: string, socket: Socket | TLSSocket, ): Promise { + if (!ctx.config.rbac.getClearancesFromAttributeCertificates) { + return []; + } const logInfo = { host: source, remoteFamily: socket.remoteFamily, @@ -317,20 +320,22 @@ async function attemptStrongAuth ( const clearances = foundEntry ? await read_clearance(ctx, foundEntry) : []; - const sdaExt = certification_path - .userCertificate - .toBeSigned - .extensions - ?.find((ext) => ext.extnId.isEqualTo(subjectDirectoryAttributes["&id"]!)); - if (sdaExt) { - const sdaEl = new DERElement(); - sdaEl.fromBytes(sdaExt.extnValue); - const sda = subjectDirectoryAttributes.decoderFor["&ExtnType"]!(sdaEl); - const sdaClearances = sda - .filter((attr) => attr.type_.isEqualTo(clearance["&id"])) - .flatMap((attr) => attr.values) - .map((value) => clearance.decoderFor["&Type"]!(value)); - clearances.push(...sdaClearances); + if (ctx.config.rbac.getClearancesFromPublicKeyCert) { + const sdaExt = certification_path + .userCertificate + .toBeSigned + .extensions + ?.find((ext) => ext.extnId.isEqualTo(subjectDirectoryAttributes["&id"]!)); + if (sdaExt) { + const sdaEl = new DERElement(); + sdaEl.fromBytes(sdaExt.extnValue); + const sda = subjectDirectoryAttributes.decoderFor["&ExtnType"]!(sdaEl); + const sdaClearances = sda + .filter((attr) => attr.type_.isEqualTo(clearance["&id"])) + .flatMap((attr) => attr.values) + .map((value) => clearance.decoderFor["&Type"]!(value)); + clearances.push(...sdaClearances); + } } if (attributeCertificationPath) { const attrCertClearances = await clearancesFromAttrCertPath( @@ -422,21 +427,24 @@ async function attemptStrongAuth ( const clearances = attemptedVertex ? await read_clearance(ctx, attemptedVertex) : []; - const sdaExt = certPath - .userCertificate - .toBeSigned - .extensions - ?.find((ext) => ext.extnId.isEqualTo(subjectDirectoryAttributes["&id"]!)); - if (sdaExt) { - const sdaEl = new DERElement(); - sdaEl.fromBytes(sdaExt.extnValue); - const sda = subjectDirectoryAttributes.decoderFor["&ExtnType"]!(sdaEl); - const sdaClearances = sda - .filter((attr) => attr.type_.isEqualTo(clearance["&id"])) - .flatMap((attr) => attr.values) - .map((value) => clearance.decoderFor["&Type"]!(value)); - clearances.push(...sdaClearances); + if (ctx.config.rbac.getClearancesFromPublicKeyCert) { + const sdaExt = certPath + .userCertificate + .toBeSigned + .extensions + ?.find((ext) => ext.extnId.isEqualTo(subjectDirectoryAttributes["&id"]!)); + if (sdaExt) { + const sdaEl = new DERElement(); + sdaEl.fromBytes(sdaExt.extnValue); + const sda = subjectDirectoryAttributes.decoderFor["&ExtnType"]!(sdaEl); + const sdaClearances = sda + .filter((attr) => attr.type_.isEqualTo(clearance["&id"])) + .flatMap((attr) => attr.values) + .map((value) => clearance.decoderFor["&Type"]!(value)); + clearances.push(...sdaClearances); + } } + if (attributeCertificationPath) { const attrCertClearances = await clearancesFromAttrCertPath( ctx, diff --git a/apps/meerkat/src/app/ctx.ts b/apps/meerkat/src/app/ctx.ts index 9f9c71917..b1df67c92 100644 --- a/apps/meerkat/src/app/ctx.ts +++ b/apps/meerkat/src/app/ctx.ts @@ -600,6 +600,7 @@ const config: Configuration = { rbac: { getClearancesFromDSAIT: (process.env.MEERKAT_GET_CLEARANCES_FROM_DSAIT !== "0"), getClearancesFromAttributeCertificates: (process.env.MEERKAT_GET_CLEARANCES_FROM_ATTR_CERTS !== "0"), + getClearancesFromPublicKeyCert: (process.env.MEERKAT_GET_CLEARANCES_FROM_PKC !== "0"), clearanceAuthorities, labelingAuthorities, }, diff --git a/libs/meerkat-types/src/lib/types.ts b/libs/meerkat-types/src/lib/types.ts index b4c65a916..3723d8ccf 100644 --- a/libs/meerkat-types/src/lib/types.ts +++ b/libs/meerkat-types/src/lib/types.ts @@ -1524,6 +1524,19 @@ interface RBACOptions { */ getClearancesFromAttributeCertificates: boolean; + /** + * If true, Meerkat DSA will associate clearances with a bound user based + * on the values of the `clearance` attribute that are present in the + * presented subjectDirectoryAttributes extension of the public key + * certificate of the strong authentication argument, provided, of course, + * that the public key certification path is valid. + * + * The attribute authorities to trust are listed in `clearanceAuthorities`. + * + * @see {@link clearanceAuthorities} + */ + getClearancesFromPublicKeyCert: boolean; + /** * The list of trust anchors whose signed attribute certificates will be * seen as valid by Meerkat DSA, and whose clearances will be associated From 74f62ba698671dea755f39fb5dfd7444d1c521a1 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Sat, 19 Aug 2023 18:05:38 -0400 Subject: [PATCH 09/39] feat: associate clearances with DAP and LDAP associations --- apps/meerkat/src/app/dap/DAPConnection.ts | 1 + apps/meerkat/src/app/ldap/LDAPConnection.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/apps/meerkat/src/app/dap/DAPConnection.ts b/apps/meerkat/src/app/dap/DAPConnection.ts index 6ab3ebe59..b70ad9153 100644 --- a/apps/meerkat/src/app/dap/DAPConnection.ts +++ b/apps/meerkat/src/app/dap/DAPConnection.ts @@ -716,6 +716,7 @@ class DAPAssociation extends ClientAssociation { this.boundNameAndUID = outcome.boundNameAndUID; this.authLevel = outcome.authLevel; this.protocolVersion = arg_.versions?.[Versions_v2] ? 2 : 1; + this.clearances = outcome.clearances; if ( ("basicLevels" in outcome.authLevel) && (outcome.authLevel.basicLevels.level === AuthenticationLevel_basicLevels_level_none) diff --git a/apps/meerkat/src/app/ldap/LDAPConnection.ts b/apps/meerkat/src/app/ldap/LDAPConnection.ts index 72026110f..009a47500 100644 --- a/apps/meerkat/src/app/ldap/LDAPConnection.ts +++ b/apps/meerkat/src/app/ldap/LDAPConnection.ts @@ -797,6 +797,7 @@ class LDAPAssociation extends ClientAssociation { this.authLevel = outcome.authLevel; this.status = Status.BOUND; this.pwdReset = (outcome.pwdResponse?.error === PwdResponseValue_error_changeAfterReset); + this.clearances = outcome.clearances; const remoteHostIdentifier = `${this.socket.remoteFamily}://${this.socket.remoteAddress}/${this.socket.remotePort}`; if ( ("basicLevels" in outcome.authLevel) From afd8227281c2b94a1b02855404cdcf6dc25ed00d Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Sat, 19 Aug 2023 18:08:25 -0400 Subject: [PATCH 10/39] refactor: fix typo --- apps/meerkat/src/app/ctx.ts | 12 ++++++------ libs/meerkat-types/src/lib/types.ts | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/meerkat/src/app/ctx.ts b/apps/meerkat/src/app/ctx.ts index b1df67c92..54e8e0eb9 100644 --- a/apps/meerkat/src/app/ctx.ts +++ b/apps/meerkat/src/app/ctx.ts @@ -455,12 +455,12 @@ const clearanceAuthorities: TrustAnchorList = clearanceAuthoritiesFileContents ] : signingCACerts.map((certificate) => ({ certificate })); -const labelingAuthoritiesFileContents: Buffer | undefined = process.env.MEERKAT_LABELING_AUTHORITIES - ? fs.readFileSync(process.env.MEERKAT_LABELING_AUTHORITIES) +const labellingAuthoritiesFileContents: Buffer | undefined = process.env.MEERKAT_LABELLING_AUTHORITIES + ? fs.readFileSync(process.env.MEERKAT_LABELLING_AUTHORITIES) : undefined; -const labelingAuthorities: TrustAnchorList = labelingAuthoritiesFileContents +const labellingAuthorities: TrustAnchorList = labellingAuthoritiesFileContents ? [ - ...parseTrustAnchorListFile(labelingAuthoritiesFileContents), + ...parseTrustAnchorListFile(labellingAuthoritiesFileContents), ...signingCACerts.map((certificate) => ({ certificate })), ] : signingCACerts.map((certificate) => ({ certificate })); @@ -602,7 +602,7 @@ const config: Configuration = { getClearancesFromAttributeCertificates: (process.env.MEERKAT_GET_CLEARANCES_FROM_ATTR_CERTS !== "0"), getClearancesFromPublicKeyCert: (process.env.MEERKAT_GET_CLEARANCES_FROM_PKC !== "0"), clearanceAuthorities, - labelingAuthorities, + labellingAuthorities, }, log: { boundDN: (process.env.MEERKAT_LOG_BOUND_DN === "1"), @@ -1170,7 +1170,7 @@ const ctx: MeerkatContext = { alreadyAssertedAttributeCertificates: new Set(), }; -for (const la of labelingAuthorities) { +for (const la of labellingAuthorities) { if (("certificate" in la) || ("tbsCert" in la)) { const tbs = ("certificate" in la) ? la.certificate.toBeSigned diff --git a/libs/meerkat-types/src/lib/types.ts b/libs/meerkat-types/src/lib/types.ts index 3723d8ccf..11b377d52 100644 --- a/libs/meerkat-types/src/lib/types.ts +++ b/libs/meerkat-types/src/lib/types.ts @@ -1553,7 +1553,7 @@ interface RBACOptions { * anchors. * * Only the public key, issuer name, and subject key identifier are taken - * from these trust anchors. Expiration is never checked, nor are any other + * from these trust anchors. Expiration is never checked, nor sare any other * extensions, such as key-usage-related extensions. * * If a given security label has a `keyIdentifier`, it will be matched with @@ -1561,7 +1561,7 @@ interface RBACOptions { * security label has an `issuerName` field, it will be matched with the * `subject` field of a trust anchor within this list. */ - labelingAuthorities: TrustAnchorList; + labellingAuthorities: TrustAnchorList; } From 48646b7545c5d2293ab1d4d43e99f0eea9a9849f Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Sat, 19 Aug 2023 18:08:56 -0400 Subject: [PATCH 11/39] docs: add frequently-used OID to JSDoc comment --- libs/parity-schema/src/lib/modules/Wildboar/id-wildboar.va.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/parity-schema/src/lib/modules/Wildboar/id-wildboar.va.ts b/libs/parity-schema/src/lib/modules/Wildboar/id-wildboar.va.ts index d88cd0326..951ce7816 100644 --- a/libs/parity-schema/src/lib/modules/Wildboar/id-wildboar.va.ts +++ b/libs/parity-schema/src/lib/modules/Wildboar/id-wildboar.va.ts @@ -9,6 +9,8 @@ import { pen_wildboar } from '../Wildboar/pen-wildboar.va'; * @summary id_wildboar * @description * + * The full, expanded value of this OID is `1.3.6.1.4.1.56490`. + * * ### ASN.1 Definition: * * ```asn1 From 3f06282157fe6ada381a31e9bc02415a4a84ef77 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Sat, 19 Aug 2023 18:09:09 -0400 Subject: [PATCH 12/39] style: remove TODO comments --- apps/meerkat/src/app/authz/rbacACDF.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/meerkat/src/app/authz/rbacACDF.ts b/apps/meerkat/src/app/authz/rbacACDF.ts index 504644b40..7551a7c7e 100644 --- a/apps/meerkat/src/app/authz/rbacACDF.ts +++ b/apps/meerkat/src/app/authz/rbacACDF.ts @@ -59,7 +59,7 @@ const simple_rbac_acdf: RBAC_ACDF = ( if (classification === SecurityClassification_unclassified) { return true; // If unclassified, the user may always see it. } - const policyId = label.security_policy_identifier ?? id_basicSecurityPolicy; // TODO: Needs documentation. + const policyId = label.security_policy_identifier ?? id_basicSecurityPolicy; let highestClearanceLevel: number = 0; for (const clearance of assn.clearances) { if (!clearance.policyId.isEqualTo(policyId)) { @@ -81,7 +81,6 @@ const simple_rbac_acdf: RBAC_ACDF = ( else if (clearance.classList[ClassList_restricted] === TRUE_BIT) { return SecurityClassification_restricted; } - // TODO: Document treating unmarked as higher than unclassified. else if (clearance.classList[ClassList_unmarked] === TRUE_BIT) { return SecurityClassification_unmarked; } @@ -118,7 +117,7 @@ export function rbacACDF ( } // const applicable_clearances = assn.clearances.filter((c) => c.policyId.isEqualTo(label.)) const policyId = label.toBeSigned.securityLabel.security_policy_identifier - ?? id_basicSecurityPolicy; // TODO: Needs documentation. + ?? id_basicSecurityPolicy; const acdf = ctx.rbacPolicies.get(policyId.toString()); if (!acdf) { return false; // If the policy ID is not understood, deny access. From 1fe81cdbcc4842402728b033c8e2afea5ea9db46 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Sat, 19 Aug 2023 18:09:18 -0400 Subject: [PATCH 13/39] docs: update documentation for RBAC --- apps/meerkat-docs/docs/authorization.md | 202 ++++++++++++++++-- apps/meerkat-docs/docs/conformance.md | 3 +- apps/meerkat-docs/docs/env.md | 94 ++++++++ apps/meerkat-docs/docs/roadmap.md | 10 +- apps/meerkat/src/assets/static/conformance.md | 4 +- 5 files changed, 287 insertions(+), 26 deletions(-) diff --git a/apps/meerkat-docs/docs/authorization.md b/apps/meerkat-docs/docs/authorization.md index 85c12ac4c..e74fd373a 100644 --- a/apps/meerkat-docs/docs/authorization.md +++ b/apps/meerkat-docs/docs/authorization.md @@ -1,11 +1,16 @@ # Authorization -Meerkat currently only supports the Basic Access Control and Simplified Access -Control schemes defined in -[ITU Recommendation X.501](https://www.itu.int/rec/T-REC-X.501/en). Future -versions may implement: +Meerkat supports all access control schemes defined in the X.500 specifications, +meaning: +- Basic Access Control +- Simplified Access Control - Rule-Based Access Control +- Rule-and-Basic Access Control +- Rule-and-Simple Access Control. + +Future versions of Meerkat DSA may introduce new access control schemes, like: + - A "points-based" access control scheme - An "OpenLDAP-like" access control scheme @@ -42,21 +47,18 @@ given Access Control Specific Area (ACSA), the following must be in place: is turned on, administrators will be locked out of the entire ACSA, because there are no ACI items defined that grant them permission to it! --> 3. The access control administrative point must have an `accessControlScheme` - attribute value of `basicAccessControlScheme` (2.5.28.1) or - `simplifiedAccessControlScheme` (2.5.28.2), depending on what you want. - - Note that, if you use the `rule-and-basic-access-control` or - `rule-and-simple-access-control` access control schemes instead, they will - follow the semantics of the Basic Access Control and Simplified Access - Control schemes; the "rule-based" aspects of these access control schemes - will be ignored, however, because Meerkat DSA does not understand - rule-based access control, and some access control is usually preferrable - to none. + attribute value set to the object identifier of the access control scheme + you want to use in that administrative area. + +:::caution Note that the ACI items should be created before enabling access control. If there are no ACI items defined at all, then _nobody_ is permitted to do _anything_. It is possible for administrators to accidentally configure rules that prevent even themselves from accessing their own DSA! +::: + ## Getting Locked Out If an administrator gets locked out of Meerkat DSA by having misconfigured @@ -163,6 +165,7 @@ should be defined that: against the directory (e.g. trying to exploit a bug in access control by using aliases or attempting to enumerate entries with timing attacks via the `hierarchyParent` attribute validation.) +- Explicitly prohibit users from modifying the `clearance` attribute. ## Access Control in a Distributed Environment @@ -195,3 +198,176 @@ The environment variables that are used to configure the `localQualifier` are: - [`MEERKAT_LOCAL_QUALIFIER_POINTS_FOR_USING_TLS_1_1`](./env.md#meerkatlocalqualifierpointsforusingtls11) - [`MEERKAT_LOCAL_QUALIFIER_POINTS_FOR_USING_TLS_1_2`](./env.md#meerkatlocalqualifierpointsforusingtls12) - [`MEERKAT_LOCAL_QUALIFIER_POINTS_FOR_USING_TLS_1_3`](./env.md#meerkatlocalqualifierpointsforusingtls13) + +## Rule-Based Access Control + +Rule-Based Access Control is intentionally vague as to how a clearance value is +compared to a security label: it is open-ended by being left to the security +policy to determine how this comparison is performed. This means that Meerkat +DSA must use a means for mapping a security policy identifier (which is an +object identifier) to a function that is used to compare the user's clearance +with the security label. + +### Where Clearances Come From + +Clearances may be associated with a user in three ways: + +1. By being present as attribute values of the `clearance` attribute in the + entry within the bound DSA, so long as the + [`MEERKAT_GET_CLEARANCES_FROM_DSAIT`](./env.md#meerkat_get_clearances_from_dsait) + environment variable is not set to `0`. +2. By being present as attribute values of the `clearance` attribute in the + X.509 public key certificate asserted by the user upon successful strong + authentication, so long as the + [`MEERKAT_GET_CLEARANCES_FROM_PKC`](./env.md#meerkat_get_clearances_from_pkc) + environment variable is not set to `0`. +3. By being present as attribute values of the `clearance` attribute in the + attribute certificate asserted by the user upon successful strong + authentication, so long as the + [`MEERKAT_GET_CLEARANCES_FROM_ATTR_CERTS`](./env.md#meerkat_get_clearances_from_attr_certs) + environment variable is not set to `0`. + +### Labelling Authorities and Clearance Authorities + +Security labels are signed data structures. Their signatures are generally +produced by the public keys of "labelling authorities." Clearances do not have +to be signed, since they can be taken from the DSAIT, but generally, they should +be signed by being presented in an X.509 public key certificate or attribute +certificate asserted by a user during strong authentication. Thus, for both +verifying security labels and clearance values, there is a need for Meerkat DSA +to have a configurable set of trust anchors explicitly for the purposes of +labelling and clearance issuance. + +This can be done by pointing to Trust Anchor List files by using the +[`MEERKAT_CLEARANCE_AUTHORITIES`](./env.md#meerkat_clearance_authorities) and +[`MEERKAT_LABELLING_AUTHORITIES`](./env.md#meerkat_labelling_authorities) +environment variables. + +If either of these are unset, they default to the trust anchors used for +signing. + +### The Simple Security Policy + +For the sake of easy use of the Rule-Based Access Control (RBAC), Meerkat DSA +comes with a security policy built-in, called the "simple security policy." +It's object identifier is `1.3.6.1.4.1.56490.403.1. This security policy does +nothing with security categories, and permits access to the labeled attribute +value if the clearance level is greater than or equal to the clearance level +required by the labeled attribute value. Unless you plan to make use of security +categories, this should be a sensible default for most use cases. + +The Simple Security Policy treats the "unmarked" classification as being of +higher sensitivity than "unclassified," but of lesser sensitivity than +"restricted." The rationale for this is that "unclassified" explicitly names +something as having the most relaxed classification, whereas "unmarked" is an +absence of information, but it may also indicate that the labeled thing is not +important enough to have labeled properly in the first place, hence, it lies +between total declassification and the "restricted" classification. + +:::note + +If no security policy is listed in the security label or clearance, it defaults +to the Simple Security Policy described above. + +::: + +### Custom Security Policies + +If you want to define your own security policies, you may do so in the +[init script](./env.md#meerkat_init_js) like demonstrated below. + +```javascript + +// This is the object identifier of your security policy. +const your_security_policy_id = "1.3.6.1.4.1.99999.1"; + +// The is the Access Control Decision Function (ACDF) for your security policy. +// This determines how a clearance value compares to a security label. + +const your_custom_acdf = ( + ctx, // Context + assn, // ClientAssociation // This has a clearance field. + target, // Vertex + signedLabel, // SignedSecurityLabel + value, // ASN1Element + contexts, // X500Context[] + permissions, // number[] +): boolean => { + const label = signedLabel.toBeSigned.securityLabel; + const classification = Number(label.security_classification ?? SecurityClassification_unmarked); + if (classification === SecurityClassification_unclassified) { + return true; // If unclassified, the user may always see it. + } + let highestClearanceLevel: number = 0; + // Note that a client may be associated with multiple clearance values. + // How you handle this is up to you. + for (const clearance of assn.clearances) { + if (!clearance.policyId.toString() !== your_security_policy_id) { + // We ignore clearances that do not pertain to this security policy. + continue; + } + const clearanceLevel: SecurityClassification = (() => { + if (!clearance.classList) { + return SecurityClassification_unclassified; + } + else if (clearance.classList[ClassList_topSecret] === TRUE_BIT) { + return SecurityClassification_top_secret; + } + else if (clearance.classList[ClassList_secret] === TRUE_BIT) { + return SecurityClassification_secret; + } + else if (clearance.classList[ClassList_confidential] === TRUE_BIT) { + return SecurityClassification_confidential; + } + else if (clearance.classList[ClassList_restricted] === TRUE_BIT) { + return SecurityClassification_restricted; + } + else if (clearance.classList[ClassList_unmarked] === TRUE_BIT) { + return SecurityClassification_unmarked; + } + else { + return SecurityClassification_unclassified; + } + })(); + if (clearanceLevel > highestClearanceLevel) { + highestClearanceLevel = Number(clearanceLevel); + } + } + // Just to make sure that classification cannot be given a large, + // illegitimate value to make a protected value universally inaccessible. + if (highestClearanceLevel == SecurityClassification_top_secret) { + return true; + } + return (highestClearanceLevel >= classification); +} + +async function init(ctx) { + // Here, we associate the policy ID with the ACDF + ctx.rbacPolicies.set(your_security_policy_id, your_custom_acdf); + + // This is just logging, just to show you that you can do this. :) + ctx.log.info("Added my own custom security policy"); +} + +export default init; +``` + +The Access Control Decision Function (ACDF) associated with the security policy +takes several arguments associated with the user, attribute value, contexts, the +DSA itself, and returns a `boolean`: if this `boolean` is `true`, it means that +the user's access request was granted; if `false`, the requested access is +denied. + +### Chaining Rule-Based Access Control + +The clearances associated with a user are not preserved across the DSA boundary: +they are not chained. With Basic Access Control and Simplified Access Control, +the user's authentication level can be relayed to other DSAs, but there is no +defined mechanism for a users clearances to survive across chaining. As such, +Rule-Based Access Control is only viable for regulating access within a single +DSA. + +For security reasons, only DAP and LDAP associations will have any clearances +associated: this is so that downstream DSAs do not make access control decisions +on the basis of the upstream DSA's clearances rather than the originating DAP +requester when chaining is used. diff --git a/apps/meerkat-docs/docs/conformance.md b/apps/meerkat-docs/docs/conformance.md index bc8b03259..91fc3e224 100644 --- a/apps/meerkat-docs/docs/conformance.md +++ b/apps/meerkat-docs/docs/conformance.md @@ -334,7 +334,7 @@ Meerkat DSA supports management of the DSA Information Tree. #### X. Rule-Based Access Control -Meerkat DSA **does not** support Rule-Based Access Control. +Meerkat DSA fully supports Rule-Based Access Control (RBAC). #### Y. Integrity of Directory Operations @@ -375,7 +375,6 @@ Meerkat DSA conforms to the static requirements described in ITU Recommendation X.519 (2019), Section 13.2.2, with the following exceptions: - Meerkat DSA does not support the `multiStrand` family grouping described in X.511 7.3.2. -- Meerkat DSA does not support Rule-Based Access Control In addition to this, Meerkat DSA conforms in the following respects: diff --git a/apps/meerkat-docs/docs/env.md b/apps/meerkat-docs/docs/env.md index f1fc71759..a0d26d97f 100644 --- a/apps/meerkat-docs/docs/env.md +++ b/apps/meerkat-docs/docs/env.md @@ -256,6 +256,29 @@ re-enable bulk insert mode. This is an open-ended string that specifies the client certificate engine that OpenSSL can use to obtain a client certificate. +## MEERKAT_CLEARANCE_AUTHORITIES + +The filepath of a Trust Anchor List file. See +[IETF RFC 5914](https://datatracker.ietf.org/doc/html/rfc5914). This file +contains the trust anchors whose signed attribute certificates will be seen as +valid by Meerkat DSA, and whose clearances (values of the `clearance` attribute) +will be associated with bound users that supply such attribute certificates in +their strong authentication parameters. + +The trust anchor list shall be encapsulated in a Cryptographic Message Syntax +(CMS) message. It does not need to be the top-level object, however. It can be +nested within authenticated data, signed data, or digested data objects, as +defined in [IETF RFC 5652](https://datatracker.ietf.org/doc/html/rfc5652). + +This file may also be PEM-encoded. The PEM label must be `TRUST ANCHOR LIST`, +such that the file looks like this when opened in a text editor: + +``` +-----BEGIN TRUST ANCHOR LIST----- + +-----END TRUST ANCHOR LIST----- +``` + ## MEERKAT_CHAINING_CHECK_SIG If not set to `0`, Meerkat DSA will verify the digital signatures on received @@ -328,6 +351,51 @@ this can generally be set to a fairly low number for optimal results. If set to `1`, anonymous binds are declined entirely. +## MEERKAT_GET_CLEARANCES_FROM_ATTR_CERTS + +If not set to `0`, Meerkat DSA will associate clearances with a bound user based +on the values of the `clearance` attribute that are present in the presented +attribute certificates of the strong authentication argument, provided, of +course, that the attribute certificates are valid. + +:::note + +Meerkat DSA only supports directly-issued attribute certificates: it cannot +currently validate indirectly issued attribute certificates / delegation paths. +If a user supplies an attribute certification path that has an `acPath` +parameter, Meerkat DSA will not attempt to validate the attribute certification +path. Authentication may still succeed, but any clearances granted to the user +via that path will not be applied. + +This feature will be supported in some future release. + +::: + +## MEERKAT_GET_CLEARANCES_FROM_DSAIT + +If not set to `0`, Meerkat DSA will associate clearances with a bound user based +on the values of the `clearance` attribute it has for the bound entry +in its local DSAIT. + +:::danger + +Unfortunately, `clearance` is technically defined as a user attribute, even +though the directory uses it for making access control decisions. This means +that, if you define an access control rule that, for instance, allows a user +to edit `allUserAttributes`, they will be able to modify the `clearance` +attribute. As such, it is important to have access control rules that explicitly +forbid editing `clearance` attribute values. + +::: + +## MEERKAT_GET_CLEARANCES_FROM_PKC + +If not set to `0`, Meerkat DSA will associate clearances with a bound user based +on the values of the `clearance` attribute that are present in the presented +`subjectDirectoryAttributes` extension of the public key certificate of the +strong authentication argument, provided, of course, that the public key +certification path is valid. + ## MEERKAT_HONOR_CIPHER_ORDER If set to `1`, Meerkat DSA will attempt to use its own preferred TLS cipher @@ -515,6 +583,32 @@ transport should always be preferred, and IDMS even moreso. ::: +## MEERKAT_LABELLING_AUTHORITIES + +The filepath of the Trust Anchor List file. See +[IETF RFC 5914](https://datatracker.ietf.org/doc/html/rfc5914). This file +contains information on the trust anchors to be used for verifying the security +labels on attribute values that are applied using the +`attributeValueSecurityLabelContext` context. This is used for implementing +Rule-Based Access Control (RBAC). In other words, when the signatures on +security labels on attribute values are checked, these trust anchors provide the +public keys against which these security labels are verified and the names of +the issuers. + +The trust anchor list shall be encapsulated in a Cryptographic Message Syntax +(CMS) message. It does not need to be the top-level object, however. It can be +nested within authenticated data, signed data, or digested data objects, as +defined in [IETF RFC 5652](https://datatracker.ietf.org/doc/html/rfc5652). + +This file may also be PEM-encoded. The PEM label must be `TRUST ANCHOR LIST`, +such that the file looks like this when opened in a text editor: + +``` +-----BEGIN TRUST ANCHOR LIST----- + +-----END TRUST ANCHOR LIST----- +``` + ## MEERKAT_LCR_PARALLELISM If greater than 1, Meerkat DSA will make parallel requests in the diff --git a/apps/meerkat-docs/docs/roadmap.md b/apps/meerkat-docs/docs/roadmap.md index c32a1b95a..88e81b8af 100644 --- a/apps/meerkat-docs/docs/roadmap.md +++ b/apps/meerkat-docs/docs/roadmap.md @@ -4,7 +4,7 @@ We will not promise any particular schedule of delivery of features or bug fixes at this time. However, the very high-level roadmap for Meerkat DSA can be broken down to the following versions. -## Version 3.1.0 - Schema Update ("Wildboar Schema") +## Version 3.2.0 - Schema Update ("Wildboar Schema") This update will introduce thousands of new schema objects defined by Wildboar Software into the default schema. This is desirable so that X.500 directories @@ -16,14 +16,6 @@ using a `married` auxiliary object class that permits the presence of a administrators everywhere to define their own equivalent object classes, thereby duplicating work and reducing inter-domain compatibility. -## Version 3.2.0 - Rule Based Access Control - -This update will introduce Rule-Based Access Control (RBAC) (not to be mistaken -for the more common "Role-Based Access Control") as described in -[ITU Recommendation X.501 (2019)](https://www.itu.int/rec/T-REC-X.501/en). -This is an access control scheme that determines authorization on the basis of -clearance levels, such as "confidential" or "top secret." - ## Version 3.3.0 - SPKM Authentication This version will introduce SPKM Authentication as described in diff --git a/apps/meerkat/src/assets/static/conformance.md b/apps/meerkat/src/assets/static/conformance.md index 3b21ed953..7b3265c7c 100644 --- a/apps/meerkat/src/assets/static/conformance.md +++ b/apps/meerkat/src/assets/static/conformance.md @@ -334,7 +334,7 @@ Meerkat DSA supports management of the DSA Information Tree. #### X. Rule-Based Access Control -Meerkat DSA **does not** support Rule-Based Access Control. +Meerkat DSA fully supports Rule-Based Access Control (RBAC). #### Y. Integrity of Directory Operations @@ -375,7 +375,6 @@ Meerkat DSA conforms to the static requirements described in ITU Recommendation X.519 (2019), Section 13.2.2, with the following exceptions: - Meerkat DSA does not support the `multiStrand` family grouping described in X.511 7.3.2. -- Meerkat DSA does not support Rule-Based Access Control In addition to this, Meerkat DSA conforms in the following respects: @@ -509,3 +508,4 @@ X.519 (2019), Section 13.4.2, including providing support for the Meerkat DSA conforms to the dynamic requirements described in ITU Recommendation X.519 (2019), Section 13.4.3. The mapping of application contexts onto OSI services is conformant, and has been tested against Quipu and ISODE DUAs. +s \ No newline at end of file From 24476137b91bf74610e86952febf8d30fedf0f1c Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Mon, 21 Aug 2023 05:39:07 -0400 Subject: [PATCH 14/39] test: seed c=CA --- apps/create-test-dit/src/app/create-ca.ts | 1791 +++++++++++++++++ .../src/app/create-countries.ts | 8 +- apps/create-test-dit/src/main.ts | 16 + 3 files changed, 1814 insertions(+), 1 deletion(-) create mode 100644 apps/create-test-dit/src/app/create-ca.ts diff --git a/apps/create-test-dit/src/app/create-ca.ts b/apps/create-test-dit/src/app/create-ca.ts new file mode 100644 index 000000000..f9fc6c6a0 --- /dev/null +++ b/apps/create-test-dit/src/app/create-ca.ts @@ -0,0 +1,1791 @@ +import type { Connection, Context } from "./types"; +import { + TRUE, + FALSE, + FALSE_BIT, + unpackBits, + OBJECT_IDENTIFIER, + ObjectIdentifier, + ASN1Element, + INTEGER, +} from "asn1-ts"; +import { KeyObject, createHash, createPrivateKey, createSign, randomBytes, randomInt } from "crypto"; +import { + addEntry, +} from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/addEntry.oa"; +import { + AddEntryArgument, + _encode_AddEntryArgument, +} from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/AddEntryArgument.ta"; +import { + AddEntryArgumentData, +} from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/AddEntryArgumentData.ta"; +import { + DistinguishedName, +} from "@wildboar/x500/src/lib/modules/InformationFramework/DistinguishedName.ta"; +import { + Attribute, +} from "@wildboar/x500/src/lib/modules/InformationFramework/Attribute.ta"; +import { + AttributeTypeAndValue, _encode_AttributeTypeAndValue, +} from "@wildboar/x500/src/lib/modules/InformationFramework/AttributeTypeAndValue.ta"; +import * as selat from "@wildboar/x500/src/lib/collections/attributes"; +import * as seloc from "@wildboar/x500/src/lib/collections/objectClasses"; +import { + subentry, +} from "@wildboar/x500/src/lib/modules/InformationFramework/subentry.oa"; +import { + collectiveAttributeSubentry, +} from "@wildboar/x500/src/lib/modules/InformationFramework/collectiveAttributeSubentry.oa"; +import { + accessControlSubentry, +} from "@wildboar/x500/src/lib/modules/InformationFramework/accessControlSubentry.oa"; +import { + pwdAdminSubentry, +} from "@wildboar/x500/src/lib/modules/InformationFramework/pwdAdminSubentry.oa"; +import { + subschema, +} from "@wildboar/x500/src/lib/modules/SchemaAdministration/subschema.oa"; +import { + SubtreeSpecification, + _encode_SubtreeSpecification, +} from "@wildboar/x500/src/lib/modules/InformationFramework/SubtreeSpecification.ta"; +import { + ServiceControls, +} from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/ServiceControls.ta"; +import { + ServiceControlOptions, +} from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/ServiceControlOptions.ta"; +import { SecurityParameters } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/SecurityParameters.ta"; +import { + ProtectionRequest_none, +} from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/ProtectionRequest.ta"; +import { + ErrorProtectionRequest_none, +} from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/ErrorProtectionRequest.ta"; +import print from "./printCode"; +import { + DER, + _encodeBoolean, + _encodeInteger, + _encodeNull, + _encodeObjectIdentifier, + _encodePrintableString, + _encodeUTF8String, + // _encodeNumericString, +} from "asn1-ts/dist/node/functional"; +import { + dITStructureRules, +} from "@wildboar/x500/src/lib/modules/SchemaAdministration/dITStructureRules.oa"; +import { + DITStructureRuleDescription, + _encode_DITStructureRuleDescription, +} from "@wildboar/x500/src/lib/modules/SchemaAdministration/DITStructureRuleDescription.ta"; +import { + dITContentRules, +} from "@wildboar/x500/src/lib/modules/SchemaAdministration/dITContentRules.oa"; +import { + DITContentRuleDescription, + _encode_DITContentRuleDescription, +} from "@wildboar/x500/src/lib/modules/SchemaAdministration/DITContentRuleDescription.ta"; +import { + dITContextUse, +} from "@wildboar/x500/src/lib/modules/SchemaAdministration/dITContextUse.oa"; +import { + DITContextUseDescription, + _encode_DITContextUseDescription, +} from "@wildboar/x500/src/lib/modules/SchemaAdministration/DITContextUseDescription.ta"; +import { + DITContextUseInformation, +} from "@wildboar/x500/src/lib/modules/SchemaAdministration/DITContextUseInformation.ta"; +import * as nf from "@wildboar/x500/src/lib/collections/nameForms"; +import * as oc from "@wildboar/x500/src/lib/collections/objectClasses"; +import * as at from "@wildboar/x500/src/lib/collections/attributes"; +import * as ct from "@wildboar/x500/src/lib/collections/contexts"; +import { + id_oa_allAttributeTypes, +} from "@wildboar/x500/src/lib/modules/InformationFramework/id-oa-allAttributeTypes.va"; +import { + prescriptiveACI, +} from "@wildboar/x500/src/lib/modules/BasicAccessControl/prescriptiveACI.oa"; +import { + ANONYMOUS_BASELINE, + AUTHENTICATED_USER_BASELINE, + AUTHENTICATED_USER_SELF_BASELINE, + GLOBAL_DIRECTORY_ADMIN_BASELINE, +} from "./aci"; +import { RDNSequence } from "@wildboar/x500/src/lib/modules/InformationFramework/RDNSequence.ta"; +import compareCode from "@wildboar/x500/src/lib/utils/compareCode"; +import { + updateError, +} from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/updateError.oa"; +import { + UpdateProblem_entryAlreadyExists, UpdateProblem_namingViolation, +} from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/UpdateProblem.ta"; +import getOptionallyProtectedValue from "@wildboar/x500/src/lib/utils/getOptionallyProtectedValue"; +import { AccessPoint, Name } from "@wildboar/x500/src/lib/modules/DistributedOperations/AccessPoint.ta"; +import { + id_ar_collectiveAttributeSpecificArea, +} from "@wildboar/x500/src/lib/modules/InformationFramework/id-ar-collectiveAttributeSpecificArea.va"; +import { + id_ar_accessControlSpecificArea, +} from "@wildboar/x500/src/lib/modules/InformationFramework/id-ar-accessControlSpecificArea.va"; +import { + id_ar_autonomousArea, +} from "@wildboar/x500/src/lib/modules/InformationFramework/id-ar-autonomousArea.va"; +import { + id_ar_pwdAdminSpecificArea, +} from "@wildboar/x500/src/lib/modules/InformationFramework/id-ar-pwdAdminSpecificArea.va"; +import { + id_ar_subschemaAdminSpecificArea, +} from "@wildboar/x500/src/lib/modules/InformationFramework/id-ar-subschemaAdminSpecificArea.va"; +import { uriToNSAP } from "@wildboar/x500/src/lib/distributed/uri"; +import { + PresentationAddress, + _encode_PresentationAddress, +} from "@wildboar/x500/src/lib/modules/SelectedAttributeTypes/PresentationAddress.ta"; +import { idempotentAddEntry } from "./utils"; +import { Guide, _encode_Guide } from "@wildboar/x500/src/lib/modules/SelectedAttributeTypes/Guide.ta"; +import { + directoryAccessAC, +} from "@wildboar/x500/src/lib/modules/DirectoryOSIProtocols/directoryAccessAC.oa"; +import { + directorySystemAC, +} from "@wildboar/x500/src/lib/modules/DirectoryOSIProtocols/directorySystemAC.oa"; +import { + directoryOperationalBindingManagementAC, +} from "@wildboar/x500/src/lib/modules/DirectoryOSIProtocols/directoryOperationalBindingManagementAC.oa"; +import { + shadowSupplierInitiatedAC, +} from "@wildboar/x500/src/lib/modules/DirectoryOSIProtocols/shadowSupplierInitiatedAC.oa"; +import { + shadowConsumerInitiatedAC, +} from "@wildboar/x500/src/lib/modules/DirectoryOSIProtocols/shadowConsumerInitiatedAC.oa"; +import { + shadowSupplierInitiatedAsynchronousAC, +} from "@wildboar/x500/src/lib/modules/DirectoryOSIProtocols/shadowSupplierInitiatedAsynchronousAC.oa"; +import { + shadowConsumerInitiatedAsynchronousAC, +} from "@wildboar/x500/src/lib/modules/DirectoryOSIProtocols/shadowConsumerInitiatedAsynchronousAC.oa"; +import { + Attribute_valuesWithContext_Item, +} from "@wildboar/x500/src/lib/modules/InformationFramework/Attribute-valuesWithContext-Item.ta"; +import { + Context as X500Context, +} from "@wildboar/x500/src/lib/modules/InformationFramework/Context.ta"; +import { + languageContext, +} from "@wildboar/x500/src/lib/modules/SelectedAttributeTypes/languageContext.oa"; +import { + localeContext, +} from "@wildboar/x500/src/lib/modules/SelectedAttributeTypes/localeContext.oa"; +import { + temporalContext, +} from "@wildboar/x500/src/lib/modules/SelectedAttributeTypes/temporalContext.oa"; +import { + TimeSpecification, +} from "@wildboar/x500/src/lib/modules/SelectedAttributeTypes/TimeSpecification.ta"; +import { + TimeSpecification_time_absolute, +} from "@wildboar/x500/src/lib/modules/SelectedAttributeTypes/TimeSpecification-time-absolute.ta"; +import { addDays } from "date-fns"; +import { + commonName, +} from "@wildboar/x500/src/lib/modules/SelectedAttributeTypes/commonName.oa"; +import { commonAuxiliaryObjectClasses } from "./objectClassSets"; +import { + inetOrgPersonNameForm, +} from "@wildboar/parity-schema/src/lib/modules/InetOrgPerson/inetOrgPersonNameForm.oa"; +import { + rule_based_access_control, +} from "@wildboar/x500/src/lib/modules/BasicAccessControl/rule-based-access-control.va"; +import { HASH, SecurityLabel, SignedSecurityLabelContent, _encode_SignedSecurityLabelContent } from "@wildboar/x500/src/lib/modules/EnhancedSecurity/SignedSecurityLabelContent.ta"; +import { SIGNED, SignedSecurityLabel, _encode_SignedSecurityLabel } from "@wildboar/x500/src/lib/modules/EnhancedSecurity/SignedSecurityLabel.ta"; +import { readFileSync } from "node:fs"; +import * as path from "node:path"; +import { sha256WithRSAEncryption } from "@wildboar/x500/src/lib/modules/AlgorithmObjectIdentifiers/sha256WithRSAEncryption.va"; +import { AlgorithmIdentifier } from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/AlgorithmIdentifier.ta"; +import { SecurityClassification_confidential, SecurityClassification_restricted, SecurityClassification_secret, SecurityClassification_top_secret, SecurityClassification_unmarked } from "@wildboar/x500/src/lib/modules/EnhancedSecurity/SecurityClassification.ta"; +import { id_sha256 } from "@wildboar/x500/src/lib/modules/AlgorithmObjectIdentifiers/id-sha256.va"; +import { AttributeType, _encode_AttributeType } from "@wildboar/x500/src/lib/modules/InformationFramework/AttributeType.ta"; +import { AttCertIssuer } from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/AttCertIssuer.ta"; + +// const MOSCOW_ACCESS_POINT = new AccessPoint( +// { +// rdnSequence: [ +// [ +// new AttributeTypeAndValue( +// commonName["&id"], +// _encodeUTF8String("dsa01.moscow.mkdemo.wildboar.software", DER), +// ), +// ], +// ], +// }, +// new PresentationAddress( +// undefined, +// undefined, +// undefined, +// [ +// /** +// * Even if you plan on using LDAP to read this entry, you MUST +// * specify an X.500 URL, because DOP cannot be translated into LDAP. +// */ +// uriToNSAP("idms://dsa01.moscow.mkdemo.wildboar.software:44632", false), +// uriToNSAP("idm://dsa01.moscow.mkdemo.wildboar.software:4632", false), +// uriToNSAP("ldaps://dsa01.moscow.mkdemo.wildboar.software:636", false), +// uriToNSAP("ldap://dsa01.moscow.mkdemo.wildboar.software:389", false), +// ], +// ), +// undefined, +// ); + +const MOSCOW_ACCESS_POINT = new AccessPoint( + { + rdnSequence: [ + [ + new AttributeTypeAndValue( + commonName["&id"], + _encodeUTF8String("dsa2", DER), + ), + ], + ], + }, + new PresentationAddress( + undefined, + undefined, + undefined, + [ + uriToNSAP("idm://dsa2:4632", false), + ], + ), + undefined, +); + +const allNonSecurityContextTypes: OBJECT_IDENTIFIER[] = [ + ct.languageContext["&id"], + ct.localeContext["&id"], + ct.temporalContext["&id"], + ct.ldapAttributeOptionContext["&id"], +]; + +const serviceControlOptions: ServiceControlOptions = new Uint8ClampedArray( + Array(9).fill(FALSE_BIT)); +// Necessary to make countries administrative points. +// serviceControlOptions[ServiceControlOptions_manageDSAIT] = TRUE_BIT; +const serviceControls = new ServiceControls( + serviceControlOptions, + undefined, + 60, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, +); + +function securityParameters (): SecurityParameters { + return new SecurityParameters( + undefined, + undefined, // DSA name + { + generalizedTime: new Date(), + }, + unpackBits(randomBytes(16)), + ProtectionRequest_none, + addEntry["&operationCode"]!, + ErrorProtectionRequest_none, + undefined, + ); +} + +function addLocalityArgument ( + baseObject: DistinguishedName, + lname: string, + targetSystem?: AccessPoint, +): AddEntryArgument { + const ln = _encodeUTF8String(lname, DER); + const dn: DistinguishedName = [ + ...baseObject, + [ + new AttributeTypeAndValue( + selat.localityName["&id"]!, + ln, + ), + ], + ]; + const attributes: Attribute[] = [ + new Attribute( + selat.administrativeRole["&id"]!, + [ + _encodeObjectIdentifier(id_ar_autonomousArea, DER), + _encodeObjectIdentifier(id_ar_collectiveAttributeSpecificArea, DER), + _encodeObjectIdentifier(id_ar_accessControlSpecificArea, DER), + _encodeObjectIdentifier(id_ar_pwdAdminSpecificArea, DER), + _encodeObjectIdentifier(id_ar_subschemaAdminSpecificArea, DER), + ], + undefined, + ), + new Attribute( + selat.objectClass["&id"], + [ + _encodeObjectIdentifier(seloc.locality["&id"]!, DER), + _encodeObjectIdentifier(seloc.userPwdClass["&id"]!, DER), + ], + undefined, + ), + new Attribute( + selat.localityName["&id"]!, + [ln], + undefined, + ), + new Attribute( + selat.userPwd["&id"], + [ + selat.userPwd.encoderFor["&Type"]!({ + clear: `password4${lname}`, + }, DER), + ], + undefined, + ), + ]; + return { + unsigned: new AddEntryArgumentData( + { + rdnSequence: dn, + }, + attributes, + targetSystem, + [], + serviceControls, + securityParameters(), + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ), + }; +} + +function createAddEntryArgument ( + dn: DistinguishedName, + attributes: Attribute[], +): AddEntryArgument { + return { + unsigned: new AddEntryArgumentData( + { + rdnSequence: dn, + }, + attributes, + undefined, + [], + serviceControls, + securityParameters(), + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ), + }; +} + +// NOTE: There is no collectiveCountryName attribute. +function addCollectiveAttributeSubentryArgument ( + baseObject: DistinguishedName, + iso2c: string, +): AddEntryArgument { + const c2 = _encodePrintableString(iso2c, DER); + const cn = _encodeUTF8String(`${iso2c} Country Collective Attributes`, DER); + const dn: DistinguishedName = [ + ...baseObject, + [ + new AttributeTypeAndValue( + selat.countryName["&id"]!, + c2, + ), + ], + [ + new AttributeTypeAndValue( + selat.commonName["&id"]!, + cn, + ), + ], + ]; + const attributes: Attribute[] = [ + new Attribute( + selat.objectClass["&id"], + [ + _encodeObjectIdentifier(subentry["&id"], DER), + _encodeObjectIdentifier(collectiveAttributeSubentry["&id"], DER), + ], + undefined, + ), + new Attribute( + selat.commonName["&id"], + [cn], + undefined, + ), + new Attribute( + selat.subtreeSpecification["&id"], + [ + _encode_SubtreeSpecification(new SubtreeSpecification( + undefined, + undefined, + undefined, + undefined, + undefined, + [], + ), DER), + ], + undefined, + ), + ]; + return { + unsigned: new AddEntryArgumentData( + { + rdnSequence: dn, + }, + attributes, + undefined, + [], + serviceControls, + securityParameters(), + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ), + }; +} + +function addAccessControlSubentryArgument ( + baseObject: DistinguishedName, + iso2c: string, +): AddEntryArgument { + const c2 = _encodePrintableString(iso2c, DER); + const cn = _encodeUTF8String(`${iso2c} Country Access Control`, DER); + const dn: DistinguishedName = [ + ...baseObject, + [ + new AttributeTypeAndValue( + selat.countryName["&id"]!, + c2, + ), + ], + [ + new AttributeTypeAndValue( + selat.commonName["&id"]!, + cn, + ), + ], + ]; + const attributes: Attribute[] = [ + new Attribute( + selat.objectClass["&id"], + [ + _encodeObjectIdentifier(subentry["&id"], DER), + _encodeObjectIdentifier(accessControlSubentry["&id"], DER), + ], + undefined, + ), + new Attribute( + selat.commonName["&id"], + [cn], + undefined, + ), + new Attribute( + selat.subtreeSpecification["&id"], + [ + _encode_SubtreeSpecification(new SubtreeSpecification( + undefined, + undefined, + undefined, + undefined, + undefined, + [], + ), DER), + ], + undefined, + ), + new Attribute( + prescriptiveACI["&id"], + [ + ANONYMOUS_BASELINE, + AUTHENTICATED_USER_BASELINE, + AUTHENTICATED_USER_SELF_BASELINE, + GLOBAL_DIRECTORY_ADMIN_BASELINE, + ] + .map((aci) => prescriptiveACI.encoderFor["&Type"]!(aci, DER)), + undefined, + ), + ]; + return { + unsigned: new AddEntryArgumentData( + { + rdnSequence: dn, + }, + attributes, + undefined, + [], + serviceControls, + securityParameters(), + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ), + }; +} + +function addSubschemaSubentryArgument ( + baseObject: DistinguishedName, + iso2c: string, +): AddEntryArgument { + const c2 = _encodePrintableString(iso2c, DER); + const cn = _encodeUTF8String(`${iso2c} Subschema`, DER); + const dn: DistinguishedName = [ + ...baseObject, + [ + new AttributeTypeAndValue( + selat.countryName["&id"]!, + c2, + ), + ], + [ + new AttributeTypeAndValue( + selat.commonName["&id"]!, + cn, + ), + ], + ]; + const attributes: Attribute[] = [ + new Attribute( + selat.objectClass["&id"], + [ + _encodeObjectIdentifier(subentry["&id"], DER), + _encodeObjectIdentifier(subschema["&id"], DER), + ], + undefined, + ), + new Attribute( + selat.commonName["&id"], + [cn], + undefined, + ), + new Attribute( + selat.subtreeSpecification["&id"], + [ + _encode_SubtreeSpecification(new SubtreeSpecification(), DER), + ], + undefined, + ), + // TODO: Add all attribute types + // TODO: Add all object classes + // TODO: Add all name forms + new Attribute( + dITStructureRules["&id"], + [ + _encode_DITStructureRuleDescription(new DITStructureRuleDescription( + 1, + nf.countryNameForm["&id"], + ), DER), + _encode_DITStructureRuleDescription(new DITStructureRuleDescription( + 2, + nf.sOPNameForm["&id"], + ), DER), + _encode_DITStructureRuleDescription(new DITStructureRuleDescription( + 3, + nf.orgNameForm["&id"], + [1, 2, 8], + ), DER), + _encode_DITStructureRuleDescription(new DITStructureRuleDescription( + 4, + nf.dMDNameForm["&id"], + [1, 2, 3, 8, 11], + ), DER), + _encode_DITStructureRuleDescription(new DITStructureRuleDescription( + 5, + nf.dSANameForm["&id"], + [1, 4, 8, 11], + ), DER), + _encode_DITStructureRuleDescription(new DITStructureRuleDescription( + 6, + nf.deviceNameForm["&id"], + [1, 2, 3, 4, 5, 8, 9, 11, 12, 13], + ), DER), + _encode_DITStructureRuleDescription(new DITStructureRuleDescription( + 7, + nf.gONNameForm["&id"], + [1, 2, 3, 8, 11], + ), DER), + _encode_DITStructureRuleDescription(new DITStructureRuleDescription( + 8, + nf.locNameForm["&id"], + [1, 2, 8], + ), DER), + _encode_DITStructureRuleDescription(new DITStructureRuleDescription( + 9, + nf.orgPersonNameForm["&id"], + [1, 2, 3, 8, 11], + ), DER), + _encode_DITStructureRuleDescription(new DITStructureRuleDescription( + 10, + nf.orgRoleNameForm["&id"], + [1, 2, 3, 8, 11], + ), DER), + _encode_DITStructureRuleDescription(new DITStructureRuleDescription( + 11, + nf.orgUnitNameForm["&id"], + [1, 2, 3, 8, 11], + ), DER), + _encode_DITStructureRuleDescription(new DITStructureRuleDescription( + 12, + nf.personNameForm["&id"], + [1, 2, 3, 8, 11], + ), DER), + _encode_DITStructureRuleDescription(new DITStructureRuleDescription( + 13, + nf.resPersonNameForm["&id"], + [1, 2, 8], + ), DER), + _encode_DITStructureRuleDescription(new DITStructureRuleDescription( + 14, + nf.applProcessNameForm["&id"], + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], + ), DER), + _encode_DITStructureRuleDescription(new DITStructureRuleDescription( + 15, + nf.applEntityNameForm["&id"], + [14], + ), DER), + _encode_DITStructureRuleDescription(new DITStructureRuleDescription( + 16, + nf.cRLDistPtNameForm["&id"], + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + ), DER), + _encode_DITStructureRuleDescription(new DITStructureRuleDescription( + 17, + inetOrgPersonNameForm["&id"], + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + ), DER), + ], + undefined, + ), + new Attribute( + dITContentRules["&id"], + [ + _encode_DITContentRuleDescription(new DITContentRuleDescription( + oc.applicationEntity["&id"], + commonAuxiliaryObjectClasses, // auxiliaries + undefined, // mandatory + undefined, // optional + undefined, // precluded + ), DER), + _encode_DITContentRuleDescription(new DITContentRuleDescription( + oc.applicationProcess["&id"], + commonAuxiliaryObjectClasses, // auxiliaries + undefined, // mandatory + undefined, // optional + undefined, // precluded + ), DER), + _encode_DITContentRuleDescription(new DITContentRuleDescription( + oc.cRLDistributionPoint["&id"], + commonAuxiliaryObjectClasses, // auxiliaries + undefined, // mandatory + undefined, // optional + undefined, // precluded + ), DER), + _encode_DITContentRuleDescription(new DITContentRuleDescription( + oc.country["&id"], + commonAuxiliaryObjectClasses, // auxiliaries + undefined, // mandatory + undefined, // optional + undefined, // precluded + ), DER), + _encode_DITContentRuleDescription(new DITContentRuleDescription( + oc.dMD["&id"], + commonAuxiliaryObjectClasses, // auxiliaries + undefined, // mandatory + undefined, // optional + undefined, // precluded + ), DER), + _encode_DITContentRuleDescription(new DITContentRuleDescription( + oc.dSA["&id"], + commonAuxiliaryObjectClasses, // auxiliaries + undefined, // mandatory + undefined, // optional + undefined, // precluded + ), DER), + _encode_DITContentRuleDescription(new DITContentRuleDescription( + oc.device["&id"], + commonAuxiliaryObjectClasses, // auxiliaries + undefined, // mandatory + undefined, // optional + undefined, // precluded + ), DER), + _encode_DITContentRuleDescription(new DITContentRuleDescription( + oc.locality["&id"], + commonAuxiliaryObjectClasses, // auxiliaries + undefined, // mandatory + undefined, // optional + undefined, // precluded + ), DER), + _encode_DITContentRuleDescription(new DITContentRuleDescription( + oc.organization["&id"], + commonAuxiliaryObjectClasses, // auxiliaries + undefined, // mandatory + undefined, // optional + undefined, // precluded + ), DER), + _encode_DITContentRuleDescription(new DITContentRuleDescription( + oc.organizationalPerson["&id"], + commonAuxiliaryObjectClasses, // auxiliaries + undefined, // mandatory + undefined, // optional + undefined, // precluded + ), DER), + _encode_DITContentRuleDescription(new DITContentRuleDescription( + oc.organizationalUnit["&id"], + commonAuxiliaryObjectClasses, // auxiliaries + undefined, // mandatory + undefined, // optional + undefined, // precluded + ), DER), + _encode_DITContentRuleDescription(new DITContentRuleDescription( + oc.person["&id"], + commonAuxiliaryObjectClasses, // auxiliaries + undefined, // mandatory + undefined, // optional + undefined, // precluded + ), DER), + _encode_DITContentRuleDescription(new DITContentRuleDescription( + oc.residentialPerson["&id"], + commonAuxiliaryObjectClasses, // auxiliaries + undefined, // mandatory + undefined, // optional + undefined, // precluded + ), DER), + ], + undefined, + ), + new Attribute( + dITContextUse["&id"], + [ + _encode_DITContextUseDescription(new DITContextUseDescription( + id_oa_allAttributeTypes, + undefined, + undefined, + undefined, + new DITContextUseInformation( + undefined, + [ + ct.temporalContext["&id"], + ct.ldapAttributeOptionContext["&id"], + ], + ), + ), DER), + ...([ + at.name["&id"], + at.commonName["&id"], + at.localityName["&id"], + at.stateOrProvinceName["&id"], + at.title["&id"], + at.description["&id"], + at.serialNumber["&id"], + ].map((ctoid) => _encode_DITContextUseDescription(new DITContextUseDescription( + ctoid, + undefined, + undefined, + undefined, + new DITContextUseInformation( + undefined, + [ + ...allNonSecurityContextTypes, + ct.attributeValueSecurityLabelContext["&id"], + ct.attributeValueIntegrityInfoContext["&id"], + ], + ), + ), DER))), + ], + undefined, + ), + ]; + return { + unsigned: new AddEntryArgumentData( + { + rdnSequence: dn, + }, + attributes, + undefined, + [], + serviceControls, + securityParameters(), + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ), + }; +} + +function addPasswordAdminSubentryArgument ( + baseObject: DistinguishedName, + iso2c: string, +): AddEntryArgument { + const c2 = _encodePrintableString(iso2c, DER); + const cn = _encodeUTF8String(`${iso2c} Country Password Administration`, DER); + const dn: DistinguishedName = [ + ...baseObject, + [ + new AttributeTypeAndValue( + selat.countryName["&id"]!, + c2, + ), + ], + [ + new AttributeTypeAndValue( + selat.commonName["&id"]!, + cn, + ), + ], + ]; + const attributes: Attribute[] = [ + new Attribute( + selat.objectClass["&id"], + [ + _encodeObjectIdentifier(subentry["&id"], DER), + _encodeObjectIdentifier(pwdAdminSubentry["&id"], DER), + ], + undefined, + ), + new Attribute( + selat.commonName["&id"], + [cn], + undefined, + ), + new Attribute( + selat.subtreeSpecification["&id"], + [ + _encode_SubtreeSpecification(new SubtreeSpecification( + undefined, + undefined, + undefined, + undefined, + undefined, + [], + ), DER), + ], + undefined, + ), + new Attribute( + at.pwdAttribute["&id"], + [_encodeObjectIdentifier(at.userPwd["&id"], DER)], + undefined, + ), + new Attribute( + at.pwdModifyEntryAllowed["&id"], + [_encodeBoolean(FALSE, DER)], + undefined, + ), + new Attribute( + at.pwdChangeAllowed["&id"], + [_encodeBoolean(TRUE, DER)], + undefined, + ), + new Attribute( + at.pwdMinLength["&id"], + [_encodeInteger(8, DER)], + undefined, + ), + ]; + return { + unsigned: new AddEntryArgumentData( + { + rdnSequence: dn, + }, + attributes, + undefined, + [], + serviceControls, + securityParameters(), + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ), + }; +} + +const baseObject: DistinguishedName = [ + [ + new AttributeTypeAndValue( + selat.countryName["&id"], + _encodePrintableString("CA", DER), + ), + ], +]; + +// FIXME: This needs to be changed or removed once testing is done. +const issuerName: Name = { + rdnSequence: [ + [ + new AttributeTypeAndValue( + commonName["&id"], + _encodeUTF8String("god", DER), + ), + ], + ], +}; + +let cachedKey: KeyObject | undefined; +function createSecurityLabel (content: SignedSecurityLabelContent): SignedSecurityLabel | null { + try { + if (!cachedKey) { + const keyFileContents = readFileSync( + process.env.TEST_DIT_KEY_FILE ?? path.join("test", "pki", "god.key"), + { encoding: "ascii" }, + ); + const key = createPrivateKey({ + format: "pem", + type: "pkcs8", + key: keyFileContents, + }); + cachedKey = key; + } + const bytes = _encode_SignedSecurityLabelContent(content, DER).toBytes(); + const signer = createSign("sha256"); + signer.update(bytes); + const sig = signer.sign(cachedKey); + return new SIGNED( + content, + new AlgorithmIdentifier( + sha256WithRSAEncryption, + _encodeNull(null, DER), + ), + unpackBits(sig), + undefined, + undefined, + ); + } catch (e) { + // We log and return gracefully, so that the test DIT can still be + // created even if the keys could not be found. + console.error(e); + return null; + } +} + +function createSecurityLabelContext (content: SignedSecurityLabelContent): X500Context | null { + const label = createSecurityLabel(content); + if (!label) { + return null; + } + return new X500Context( + ct.attributeValueSecurityLabelContext["&id"], + [ + ct.attributeValueSecurityLabelContext.encoderFor["&Type"]!(label, DER), + ], + ); +} + +function secLabel ( + type: AttributeType, + value: ASN1Element, + classification: INTEGER, +): X500Context | null { + const atav = new AttributeTypeAndValue(type, value); + const atav_bytes = _encode_AttributeTypeAndValue(atav, DER).toBytes(); + const slc = new SignedSecurityLabelContent( + new HASH( + new AlgorithmIdentifier(id_sha256), + unpackBits(createHash("sha256").update(atav_bytes).digest()), + ), + issuerName, + undefined, + new SecurityLabel( + new ObjectIdentifier([ 1, 3, 6, 1, 4, 1, 56490, 403, 1 ]), + classification, + (classification === SecurityClassification_secret) ? "SECRET" : undefined, + undefined, + ), + ); + return createSecurityLabelContext(slc); +} + +// Converts "asdf" to "a***" +function censor (str: string): string { + return `${str.charAt(0)}${"*".repeat(str.length - 1)}`; +} + +function createSensitiveDeviceAttributes (serial: string, loc: string, desc: string): Attribute[] { + const desc2 = censor(desc); + const serial_sec_label = secLabel( + selat.serialNumber["&id"], + _encodeUTF8String(serial, DER), + SecurityClassification_secret, + ); + const loc_sec_label = secLabel( + selat.localityName["&id"], + _encodeUTF8String(loc, DER), + SecurityClassification_confidential, + ); + const desc_sec_label = secLabel( + selat.description["&id"], + _encodeUTF8String(desc, DER), + SecurityClassification_restricted, + ); + const desc2_sec_label = secLabel( + selat.description["&id"], + _encodeUTF8String(desc2, DER), + SecurityClassification_unmarked, + ); + return [ + new Attribute( + selat.serialNumber["&id"]!, + serial_sec_label ? [] : [ _encodeUTF8String(serial, DER) ], + serial_sec_label + ? [ + new Attribute_valuesWithContext_Item( + _encodeUTF8String(serial, DER), + [serial_sec_label], + ), + ] + : undefined, + ), + new Attribute( + selat.localityName["&id"]!, + loc_sec_label ? [] : [ _encodeUTF8String(loc, DER) ], + loc_sec_label + ? [ + new Attribute_valuesWithContext_Item( + _encodeUTF8String(loc, DER), + [loc_sec_label], + ), + ] + : undefined, + ), + new Attribute( + selat.description["&id"]!, + (desc_sec_label && desc2_sec_label) ? [] : [ _encodeUTF8String(desc, DER) ], + (desc_sec_label && desc2_sec_label) + ? [ + new Attribute_valuesWithContext_Item( + _encodeUTF8String(desc, DER), + [desc_sec_label], + ), + new Attribute_valuesWithContext_Item( + _encodeUTF8String(desc2, DER), + [desc2_sec_label], + ), + ] + : undefined, + ), + ]; +} + +export +async function seedCA ( + ctx: Context, + conn: Connection, +): Promise { + const subentries: [ (base: RDNSequence, cc: string) => AddEntryArgument, string ][] = [ + [ addPasswordAdminSubentryArgument, "password administration" ], + [ addCollectiveAttributeSubentryArgument, "collective attributes" ], + [ addSubschemaSubentryArgument, "subschema" ], + [ addAccessControlSubentryArgument, "access control" ], + ]; + for (const [ createSubentryArg, subentryType ] of subentries) { + const createSubentry = createSubentryArg([], "CA"); + const outcome = await conn.writeOperation({ + opCode: addEntry["&operationCode"], + argument: _encode_AddEntryArgument(createSubentry, DER), + }); + if ("error" in outcome) { + if (outcome.errcode) { + if (compareCode(outcome.errcode, updateError["&errorCode"]!)) { + const param = updateError.decoderFor["&ParameterType"]!(outcome.error); + const data = getOptionallyProtectedValue(param); + if (data.problem === UpdateProblem_entryAlreadyExists) { + ctx.log.warn(`Country CA already has a ${subentryType} subentry.`); + continue; + } + else if ( // This error happens because there can only be one subschema per admin area. + (subentryType === "subschema") + && (data.problem === UpdateProblem_namingViolation) + ) { + ctx.log.warn(`Country CA already has a ${subentryType} subentry.`); + continue; + } + } + ctx.log.error(print(outcome.errcode)); + process.exit(873); + } else { + ctx.log.error("Uncoded error."); + process.exit(4352); + } + } + ctx.log.info(`Created ${subentryType} subentry for country CA.`); + } + + { // C=CA,O=Armed Forces + const arg = createAddEntryArgument([ + ...baseObject, + [ + new AttributeTypeAndValue( + selat.organizationName["&id"]!, + _encodeUTF8String("Armed Forces", DER), + ), + ], + ], [ + new Attribute( + selat.objectClass["&id"], + [ + _encodeObjectIdentifier(seloc.organization["&id"]!, DER), + ], + undefined, + ), + new Attribute( + selat.organizationName["&id"]!, + [_encodeUTF8String("Armed Forces", DER)], + undefined, + ), + new Attribute( + selat.administrativeRole["&id"], + [ + _encodeObjectIdentifier(id_ar_accessControlSpecificArea, DER), + ], + ), + new Attribute( + selat.accessControlScheme["&id"], + [ + _encodeObjectIdentifier(rule_based_access_control, DER), + ], + ), + ]); + await idempotentAddEntry(ctx, conn, "C=CA,O=Armed Forces", arg); + } + + { // C=CA,CN=Justin Trudeau + const arg = createAddEntryArgument([ + ...baseObject, + [ + new AttributeTypeAndValue( + selat.commonName["&id"]!, + _encodeUTF8String("Justin Trudeau", DER), + ), + ], + ], [ + new Attribute( + selat.objectClass["&id"], + [ + _encodeObjectIdentifier(seloc.person["&id"]!, DER), + _encodeObjectIdentifier(seloc.organizationalPerson["&id"]!, DER), + ], + undefined, + ), + new Attribute( + selat.commonName["&id"]!, + [_encodeUTF8String("Justin Trudeau", DER)], + undefined, + ), + new Attribute( + selat.surname["&id"], + [ + _encodeUTF8String("Trudeau", DER), + ], + ), + new Attribute( + selat.title["&id"], + [ + _encodeUTF8String("Prime Minister", DER), + ], + ), + ]); + await idempotentAddEntry(ctx, conn, "C=CA,CN=Justin Trudeau", arg); + } + + { // C=CA,CN=Charles III + const arg = createAddEntryArgument([ + ...baseObject, + [ + new AttributeTypeAndValue( + selat.commonName["&id"]!, + _encodeUTF8String("Charles III", DER), + ), + ], + ], [ + new Attribute( + selat.objectClass["&id"], + [ + _encodeObjectIdentifier(seloc.person["&id"]!, DER), + _encodeObjectIdentifier(seloc.organizationalPerson["&id"]!, DER), + ], + undefined, + ), + new Attribute( + selat.commonName["&id"]!, + [ + _encodeUTF8String("Charles Philip Arthur George", DER), + _encodeUTF8String("Charles III", DER), + ], + undefined, + ), + new Attribute( + selat.surname["&id"], + [ + _encodeUTF8String("George", DER), + ], + ), + // new Attribute( + // selat.generationQualifier["&id"], + // [ + // _encodeUTF8String("III", DER), + // ], + // ), + new Attribute( + selat.title["&id"], + [ + _encodeUTF8String("King", DER), + ], + ), + ]); + await idempotentAddEntry(ctx, conn, "C=CA,CN=Charles III", arg); + } + + { // C=CA,CN=Mary Simon + const arg = createAddEntryArgument([ + ...baseObject, + [ + new AttributeTypeAndValue( + selat.commonName["&id"]!, + _encodeUTF8String("Mary Simon", DER), + ), + ], + ], [ + new Attribute( + selat.objectClass["&id"], + [ + _encodeObjectIdentifier(seloc.person["&id"]!, DER), + _encodeObjectIdentifier(seloc.organizationalPerson["&id"]!, DER), + ], + undefined, + ), + new Attribute( + selat.commonName["&id"]!, + [ + _encodeUTF8String("Mary Simon", DER), + ], + undefined, + ), + new Attribute( + selat.surname["&id"], + [ + _encodeUTF8String("Simon", DER), + ], + ), + new Attribute( + selat.title["&id"], + [ + _encodeUTF8String("Governor General", DER), + ], + ), + ]); + await idempotentAddEntry(ctx, conn, "C=CA,CN=Mary Simon", arg); + } + + { // C=CA,O=Armed Forces,CN=Bill Blair + const arg = createAddEntryArgument([ + ...baseObject, + [ + new AttributeTypeAndValue( + selat.organizationName["&id"]!, + _encodeUTF8String("Armed Forces", DER), + ), + ], + [ + new AttributeTypeAndValue( + selat.commonName["&id"]!, + _encodeUTF8String("Bill Blair", DER), + ), + ], + ], [ + new Attribute( + selat.objectClass["&id"], + [ + _encodeObjectIdentifier(seloc.person["&id"]!, DER), + _encodeObjectIdentifier(seloc.organizationalPerson["&id"]!, DER), + ], + undefined, + ), + new Attribute( + selat.commonName["&id"]!, + [ + _encodeUTF8String("Bill Blair", DER), + ], + undefined, + ), + new Attribute( + selat.surname["&id"], + [ + _encodeUTF8String("Blair", DER), + ], + ), + new Attribute( + selat.title["&id"], + [ + _encodeUTF8String("Minister of National Defense", DER), + ], + ), + ]); + await idempotentAddEntry(ctx, conn, "C=CA,O=Armed Forces,CN=Bill Blair", arg); + } + + { // C=CA,O=Armed Forces,CN=Wayne Eyre + const arg = createAddEntryArgument([ + ...baseObject, + [ + new AttributeTypeAndValue( + selat.organizationName["&id"]!, + _encodeUTF8String("Armed Forces", DER), + ), + ], + [ + new AttributeTypeAndValue( + selat.commonName["&id"]!, + _encodeUTF8String("Wayne Eyre", DER), + ), + ], + ], [ + new Attribute( + selat.objectClass["&id"], + [ + _encodeObjectIdentifier(seloc.person["&id"]!, DER), + _encodeObjectIdentifier(seloc.organizationalPerson["&id"]!, DER), + ], + undefined, + ), + new Attribute( + selat.commonName["&id"]!, + [ + _encodeUTF8String("Wayne Eyre", DER), + ], + undefined, + ), + new Attribute( + selat.surname["&id"], + [ + _encodeUTF8String("Eyre", DER), + ], + ), + new Attribute( + selat.title["&id"], + [ + _encodeUTF8String("Chief of the Defence Staff", DER), + ], + ), + ]); + await idempotentAddEntry(ctx, conn, "C=CA,O=Armed Forces,CN=Wayne Eyre", arg); + } + + { // C=CA,O=Armed Forces,CN=Frances J. Allen + const arg = createAddEntryArgument([ + ...baseObject, + [ + new AttributeTypeAndValue( + selat.organizationName["&id"]!, + _encodeUTF8String("Armed Forces", DER), + ), + ], + [ + new AttributeTypeAndValue( + selat.commonName["&id"]!, + _encodeUTF8String("Frances J. Allen", DER), + ), + ], + ], [ + new Attribute( + selat.objectClass["&id"], + [ + _encodeObjectIdentifier(seloc.person["&id"]!, DER), + _encodeObjectIdentifier(seloc.organizationalPerson["&id"]!, DER), + ], + undefined, + ), + new Attribute( + selat.commonName["&id"]!, + [ + _encodeUTF8String("Frances J. Allen", DER), + ], + undefined, + ), + new Attribute( + selat.surname["&id"], + [ + _encodeUTF8String("Allen", DER), + ], + ), + new Attribute( + selat.title["&id"], + [ + _encodeUTF8String("Vice Chief of the Defence Staff", DER), + ], + ), + ]); + await idempotentAddEntry(ctx, conn, "C=CA,O=Armed Forces,CN=Frances J. Allen", arg); + } + + { // C=CA,O=Armed Forces,CN=Gilles Gregoire + const arg = createAddEntryArgument([ + ...baseObject, + [ + new AttributeTypeAndValue( + selat.organizationName["&id"]!, + _encodeUTF8String("Armed Forces", DER), + ), + ], + [ + new AttributeTypeAndValue( + selat.commonName["&id"]!, + _encodeUTF8String("Gilles Gregoire", DER), + ), + ], + ], [ + new Attribute( + selat.objectClass["&id"], + [ + _encodeObjectIdentifier(seloc.person["&id"]!, DER), + _encodeObjectIdentifier(seloc.organizationalPerson["&id"]!, DER), + ], + undefined, + ), + new Attribute( + selat.commonName["&id"]!, + [ + _encodeUTF8String("Gilles Gregoire", DER), + ], + undefined, + ), + new Attribute( + selat.surname["&id"], + [ + _encodeUTF8String("Gregoire", DER), + ], + ), + new Attribute( + selat.title["&id"], + [ + _encodeUTF8String("Chief Warrant Officer", DER), + ], + ), + ]); + await idempotentAddEntry(ctx, conn, "C=CA,O=Armed Forces,CN=Gilles Gregoire", arg); + } + + // This is not a real person. + { // C=CA,O=Armed Forces,CN=Joe Schmoe + const arg = createAddEntryArgument([ + ...baseObject, + [ + new AttributeTypeAndValue( + selat.organizationName["&id"]!, + _encodeUTF8String("Armed Forces", DER), + ), + ], + [ + new AttributeTypeAndValue( + selat.commonName["&id"]!, + _encodeUTF8String("Joe Schmoe", DER), + ), + ], + ], [ + new Attribute( + selat.objectClass["&id"], + [ + _encodeObjectIdentifier(seloc.person["&id"]!, DER), + _encodeObjectIdentifier(seloc.organizationalPerson["&id"]!, DER), + ], + undefined, + ), + new Attribute( + selat.commonName["&id"]!, + [ + _encodeUTF8String("Joe Schmoe", DER), + ], + undefined, + ), + new Attribute( + selat.surname["&id"], + [ + _encodeUTF8String("Schmoe", DER), + ], + ), + new Attribute( + selat.title["&id"], + [ + _encodeUTF8String("Caffeination Engineer", DER), + ], + ), + ]); + await idempotentAddEntry(ctx, conn, "C=CA,O=Armed Forces,CN=Joe Schmoe", arg); + } + + // These entries are going to be restricted under rule-based access control. + // These all have object class `device`. + + { // C=CA,O=Armed Forces,CN=Patriot Missile System + const serial = "358508303080-P"; + const loc = "Toronto"; + const desc = "Lightly used, $400 OBO, cash offers ONLY"; + const arg = createAddEntryArgument([ + ...baseObject, + [ + new AttributeTypeAndValue( + selat.organizationName["&id"]!, + _encodeUTF8String("Armed Forces", DER), + ), + ], + [ + new AttributeTypeAndValue( + selat.commonName["&id"]!, + _encodeUTF8String("Patriot Missile System", DER), + ), + ], + ], [ + new Attribute( + selat.objectClass["&id"], + [ + _encodeObjectIdentifier(seloc.device["&id"]!, DER), + ], + undefined, + ), + new Attribute( + selat.commonName["&id"]!, + [ + _encodeUTF8String("Patriot Missile System", DER), + ], + undefined, + ), + ...createSensitiveDeviceAttributes(serial, loc, desc), + ]); + await idempotentAddEntry(ctx, conn, "C=CA,O=Armed Forces,CN=Patriot Missile System", arg); + } + + { // C=CA,O=Armed Forces,CN=Shadow Strike Missile + const serial = "S6899993-91"; + const loc = "Winnipeg"; + const desc = "Duct-taped together, but its fine."; + const arg = createAddEntryArgument([ + ...baseObject, + [ + new AttributeTypeAndValue( + selat.organizationName["&id"]!, + _encodeUTF8String("Armed Forces", DER), + ), + ], + [ + new AttributeTypeAndValue( + selat.commonName["&id"]!, + _encodeUTF8String("Shadow Strike Missile", DER), + ), + ], + ], [ + new Attribute( + selat.objectClass["&id"], + [ + _encodeObjectIdentifier(seloc.device["&id"]!, DER), + ], + undefined, + ), + new Attribute( + selat.commonName["&id"]!, + [ + _encodeUTF8String("Shadow Strike Missile", DER), + ], + undefined, + ), + ...createSensitiveDeviceAttributes(serial, loc, desc), + ]); + await idempotentAddEntry(ctx, conn, "C=CA,O=Armed Forces,CN=Shadow Strike Missile", arg); + } + + { // C=CA,O=Armed Forces,CN=Abrams Tank + const serial = "AT-00098551"; + const loc = "Churchill"; + const desc = "Stolen from the U.S. (dont tell please)"; + const arg = createAddEntryArgument([ + ...baseObject, + [ + new AttributeTypeAndValue( + selat.organizationName["&id"]!, + _encodeUTF8String("Armed Forces", DER), + ), + ], + [ + new AttributeTypeAndValue( + selat.commonName["&id"]!, + _encodeUTF8String("Abrams Tank", DER), + ), + ], + ], [ + new Attribute( + selat.objectClass["&id"], + [ + _encodeObjectIdentifier(seloc.device["&id"]!, DER), + ], + undefined, + ), + new Attribute( + selat.commonName["&id"]!, + [ + _encodeUTF8String("Abrams Tank", DER), + ], + undefined, + ), + ...createSensitiveDeviceAttributes(serial, loc, desc), + ]); + await idempotentAddEntry(ctx, conn, "C=CA,O=Armed Forces,CN=Abrams Tank", arg); + } + + { // C=CA,O=Armed Forces,CN=F-16 #3389 + const serial = "3389"; + const loc = "Ottawa"; + const desc = "The one with the shark-launcher installed"; + const arg = createAddEntryArgument([ + ...baseObject, + [ + new AttributeTypeAndValue( + selat.organizationName["&id"]!, + _encodeUTF8String("Armed Forces", DER), + ), + ], + [ + new AttributeTypeAndValue( + selat.commonName["&id"]!, + _encodeUTF8String("F-16 #3389", DER), + ), + ], + ], [ + new Attribute( + selat.objectClass["&id"], + [ + _encodeObjectIdentifier(seloc.device["&id"]!, DER), + ], + undefined, + ), + new Attribute( + selat.commonName["&id"]!, + [ + _encodeUTF8String("F-16 #3389", DER), + ], + undefined, + ), + ...createSensitiveDeviceAttributes(serial, loc, desc), + ]); + await idempotentAddEntry(ctx, conn, "C=CA,O=Armed Forces,CN=F-16 #3389", arg); + } + + // This entry is TOP SECRET. You are NOT ALLOWED to know about this. + { // C=CA,O=Armed Forces,CN=Demon Portal Siphon Gun + const cn = "Demon Portal Siphon Gun"; + const cn_sec_label = secLabel( + selat.commonName["&id"], + _encodeUTF8String(cn, DER), + SecurityClassification_top_secret, + ); + const arg = createAddEntryArgument([ + ...baseObject, + [ + new AttributeTypeAndValue( + selat.organizationName["&id"]!, + _encodeUTF8String("Armed Forces", DER), + ), + ], + [ + new AttributeTypeAndValue( + selat.commonName["&id"]!, + _encodeUTF8String(cn, DER), + ), + ], + ], [ + new Attribute( + selat.objectClass["&id"], + [ + _encodeObjectIdentifier(seloc.device["&id"]!, DER), + ], + undefined, + ), + new Attribute( + selat.commonName["&id"]!, + cn_sec_label ? [] : [ _encodeUTF8String(cn, DER) ], + cn_sec_label + ? [ + new Attribute_valuesWithContext_Item( + _encodeUTF8String(cn, DER), + [cn_sec_label], + ), + ] + : undefined, + ), + new Attribute( + selat.serialNumber["&id"]!, + [ + _encodeUTF8String("BFG9000", DER), + ], + undefined, + ), + new Attribute( + selat.localityName["&id"]!, + [ + _encodeUTF8String("Ottawa", DER), + ], + undefined, + ), + new Attribute( + selat.description["&id"]!, + [ + _encodeUTF8String("This entry is TOP SECRET. You are NOT ALLOWED to know about this.", DER), + ], + undefined, + ), + ]); + await idempotentAddEntry(ctx, conn, "C=CA,O=Armed Forces,CN=Demon Portal Siphon Gun", arg); + } + +} + +export default seedCA; diff --git a/apps/create-test-dit/src/app/create-countries.ts b/apps/create-test-dit/src/app/create-countries.ts index 2888d8842..e948a2d08 100644 --- a/apps/create-test-dit/src/app/create-countries.ts +++ b/apps/create-test-dit/src/app/create-countries.ts @@ -701,6 +701,7 @@ function addSubschemaSubentryArgument ( at.stateOrProvinceName["&id"], at.title["&id"], at.description["&id"], + at.serialNumber["&id"], unstructuredName["&id"], unstructuredAddress["&id"], ].map((ctoid) => _encode_DITContextUseDescription(new DITContextUseDescription( @@ -710,7 +711,11 @@ function addSubschemaSubentryArgument ( undefined, new DITContextUseInformation( undefined, - allNonSecurityContextTypes, + [ + ...allNonSecurityContextTypes, + ct.attributeValueSecurityLabelContext["&id"], + ct.attributeValueIntegrityInfoContext["&id"], + ], ), ), DER))), ], @@ -891,6 +896,7 @@ const countriesToCreate: [ string, AccessPoint | undefined ][] = [ [ "US", undefined ], // US will be inside of the root DSA. [ "GB", undefined ], [ "RU", RU_ACCESS_POINT ], + [ "CA", undefined ], ]; export diff --git a/apps/create-test-dit/src/main.ts b/apps/create-test-dit/src/main.ts index c7d1ec93b..9de9707e1 100644 --- a/apps/create-test-dit/src/main.ts +++ b/apps/create-test-dit/src/main.ts @@ -24,6 +24,7 @@ import { } from "@wildboar/x500/src/lib/modules/SelectedAttributeTypes/localityName.oa"; import { sleep } from "./app/utils"; import seedUN from "./app/create-un"; +import seedCA from "./app/create-ca"; program.version("1.0.0"); @@ -60,6 +61,21 @@ async function main () { await adminConnection.close(); break; } + case ("ca"): { + const bindDN: DistinguishedName = [ + [ + new AttributeTypeAndValue( + countryName["&id"], + _encodePrintableString("CA", DER), + ), + ], + ]; + const password: string = "password4CA"; + const connection = await bind(ctx, options["accessPoint"], bindDN, password); + await seedCA(ctx, connection); + await connection.close(); + break; + } case ("gb"): { const bindDN: DistinguishedName = [ [ From a2524833c17be8740fad5f66e2ae1ac6c57d5c73 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Mon, 21 Aug 2023 05:39:58 -0400 Subject: [PATCH 15/39] test: ignore updateError for already existing subschema --- apps/create-test-dit/src/app/connect.ts | 2 +- apps/create-test-dit/src/app/create-gb.ts | 11 ++++++++--- apps/create-test-dit/src/app/create-moscow.ts | 9 ++++++++- apps/create-test-dit/src/app/create-ru.ts | 9 ++++++++- apps/create-test-dit/src/app/create-un.ts | 9 ++++++++- 5 files changed, 33 insertions(+), 7 deletions(-) diff --git a/apps/create-test-dit/src/app/connect.ts b/apps/create-test-dit/src/app/connect.ts index 48ef3d803..13f800a3d 100644 --- a/apps/create-test-dit/src/app/connect.ts +++ b/apps/create-test-dit/src/app/connect.ts @@ -114,7 +114,7 @@ async function connect ( idm.events.on("error_", (e) => { console.error(`Invocation ${e.invokeID} returned an error with code ${printCode(e.errcode)}`); console.error(`Error parameter: ${Buffer.from(e.error.toBytes()).toString("hex")}`); - process.exit(34); + // process.exit(34); }); idm.events.on("reject", (r) => { console.error(`Invocation ${r.invokeID} rejected with reason: ${r.reason}`); diff --git a/apps/create-test-dit/src/app/create-gb.ts b/apps/create-test-dit/src/app/create-gb.ts index 7f25a2a68..a1f817797 100644 --- a/apps/create-test-dit/src/app/create-gb.ts +++ b/apps/create-test-dit/src/app/create-gb.ts @@ -3,10 +3,8 @@ import { TRUE, FALSE, FALSE_BIT, - unpackBits, OBJECT_IDENTIFIER, } from "asn1-ts"; -import { randomBytes } from "crypto"; import { addEntry, } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/addEntry.oa"; @@ -117,7 +115,7 @@ import { updateError, } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/updateError.oa"; import { - UpdateProblem_entryAlreadyExists, + UpdateProblem_entryAlreadyExists, UpdateProblem_namingViolation, } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/UpdateProblem.ta"; import getOptionallyProtectedValue from "@wildboar/x500/src/lib/utils/getOptionallyProtectedValue"; import { createMockPersonAttributes } from "./mock-entries"; @@ -833,6 +831,13 @@ async function seedGB ( ctx.log.warn(`Country GB already has a ${subentryType} subentry.`); continue; } + else if ( // This error happens because there can only be one subschema per admin area. + (subentryType === "subschema") + && (data.problem === UpdateProblem_namingViolation) + ) { + ctx.log.warn(`Country GB already has a ${subentryType} subentry.`); + continue; + } } ctx.log.error(print(outcome.errcode)); process.exit(8482); diff --git a/apps/create-test-dit/src/app/create-moscow.ts b/apps/create-test-dit/src/app/create-moscow.ts index 216a975fd..44a0bcb2f 100644 --- a/apps/create-test-dit/src/app/create-moscow.ts +++ b/apps/create-test-dit/src/app/create-moscow.ts @@ -116,7 +116,7 @@ import { updateError, } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/updateError.oa"; import { - UpdateProblem_entryAlreadyExists, + UpdateProblem_entryAlreadyExists, UpdateProblem_namingViolation, } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/UpdateProblem.ta"; import getOptionallyProtectedValue from "@wildboar/x500/src/lib/utils/getOptionallyProtectedValue"; import { commonAuxiliaryObjectClasses } from "./objectClassSets"; @@ -827,6 +827,13 @@ async function seedMoscow ( ctx.log.warn(`Locality Moscow already has a ${subentryType} subentry.`); continue; } + else if ( // This error happens because there can only be one subschema per admin area. + (subentryType === "subschema") + && (data.problem === UpdateProblem_namingViolation) + ) { + ctx.log.warn(`Locality Moscow already has a ${subentryType} subentry.`); + continue; + } } ctx.log.error(print(outcome.errcode)); process.exit(843); diff --git a/apps/create-test-dit/src/app/create-ru.ts b/apps/create-test-dit/src/app/create-ru.ts index bae647594..ac60fc0be 100644 --- a/apps/create-test-dit/src/app/create-ru.ts +++ b/apps/create-test-dit/src/app/create-ru.ts @@ -116,7 +116,7 @@ import { updateError, } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/updateError.oa"; import { - UpdateProblem_entryAlreadyExists, + UpdateProblem_entryAlreadyExists, UpdateProblem_namingViolation, } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/UpdateProblem.ta"; import getOptionallyProtectedValue from "@wildboar/x500/src/lib/utils/getOptionallyProtectedValue"; import { AccessPoint } from "@wildboar/x500/src/lib/modules/DistributedOperations/AccessPoint.ta"; @@ -965,6 +965,13 @@ async function seedRU ( ctx.log.warn(`Country RU already has a ${subentryType} subentry.`); continue; } + else if ( // This error happens because there can only be one subschema per admin area. + (subentryType === "subschema") + && (data.problem === UpdateProblem_namingViolation) + ) { + ctx.log.warn(`Country RU already has a ${subentryType} subentry.`); + continue; + } } ctx.log.error(print(outcome.errcode)); process.exit(873); diff --git a/apps/create-test-dit/src/app/create-un.ts b/apps/create-test-dit/src/app/create-un.ts index 3656caefc..a6a3a3011 100644 --- a/apps/create-test-dit/src/app/create-un.ts +++ b/apps/create-test-dit/src/app/create-un.ts @@ -131,7 +131,7 @@ import { updateError, } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/updateError.oa"; import { - UpdateProblem_entryAlreadyExists, + UpdateProblem_entryAlreadyExists, UpdateProblem_namingViolation, } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/UpdateProblem.ta"; import getOptionallyProtectedValue from "@wildboar/x500/src/lib/utils/getOptionallyProtectedValue"; import { @@ -889,6 +889,13 @@ async function seedUN ( ctx.log.warn(`O=UN already has a ${subentryType} subentry.`); continue; } + else if ( // This error happens because there can only be one subschema per admin area. + (subentryType === "subschema") + && (data.problem === UpdateProblem_namingViolation) + ) { + ctx.log.warn(`O=UN already has a ${subentryType} subentry.`); + continue; + } } ctx.log.error(print(outcome.errcode)); process.exit(87841); From 1ee1d2824add0ef7ba1117765d5533c15add6ab8 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Mon, 21 Aug 2023 05:40:26 -0400 Subject: [PATCH 16/39] fix: tolerate an empty acPath in an attribute certification path --- apps/meerkat/src/app/authn/attemptStrongAuth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meerkat/src/app/authn/attemptStrongAuth.ts b/apps/meerkat/src/app/authn/attemptStrongAuth.ts index d655ae2ce..5710e4c42 100644 --- a/apps/meerkat/src/app/authn/attemptStrongAuth.ts +++ b/apps/meerkat/src/app/authn/attemptStrongAuth.ts @@ -105,7 +105,7 @@ async function clearancesFromAttrCertPath ( remoteAddress: socket.remoteAddress, remotePort: socket.remotePort, }; - if (path.acPath) { + if (path.acPath?.length) { ctx.log.debug(ctx.i18n.t("log:attr_cert_path_unsupported", logInfo), logInfo); return []; } From 52689f08b41ce8ae0089bd67ef4f6b85ca68b007 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Mon, 21 Aug 2023 05:41:41 -0400 Subject: [PATCH 17/39] fix(x500-cli): correctly load and construct attr cert path --- apps/x500-cli/src/net/bind.ts | 5 +++-- apps/x500-cli/src/net/connect.ts | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/x500-cli/src/net/bind.ts b/apps/x500-cli/src/net/bind.ts index 6fa3e7d04..aec46ab90 100644 --- a/apps/x500-cli/src/net/bind.ts +++ b/apps/x500-cli/src/net/bind.ts @@ -82,7 +82,7 @@ function parseAttrCertPath (data: string): AttributeCertificationPath { })(); const path: ACPathData[] = []; let pkc: Certificate | undefined; - for (const pem of pems) { + for (const pem of pems.slice(1)) { const el = new BERElement(); el.fromBytes(pem.data); // TODO: Check for extra bytes, just to make sure everything is valid. if (pem.label === "ATTRIBUTE CERTIFICATE") { @@ -103,7 +103,7 @@ function parseAttrCertPath (data: string): AttributeCertificationPath { } return new AttributeCertificationPath( userCert, - path, + path.length ? path : undefined, ); } @@ -197,6 +197,7 @@ async function createConnection ( certPath, key, called_ae_title, + attrCertPath, ); if (!connection) { ctx.log.warn(`Could not create connection to this access point: ${accessPoint.url}.`); diff --git a/apps/x500-cli/src/net/connect.ts b/apps/x500-cli/src/net/connect.ts index 8d2f96cc7..86df59017 100644 --- a/apps/x500-cli/src/net/connect.ts +++ b/apps/x500-cli/src/net/connect.ts @@ -20,6 +20,7 @@ import { SimpleCredentials, } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/SimpleCredentials.ta"; import { + AttributeCertificationPath, StrongCredentials, } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/StrongCredentials.ta"; import { @@ -64,6 +65,7 @@ async function connect ( certPath?: CertificationPath, signingKey?: KeyObject | null, aeTitle?: DistinguishedName, + attrCertPath?: AttributeCertificationPath, // TODO: Config file ): Promise { const bindDN_ = destringifyDN(ctx, bindDN ?? ""); @@ -142,6 +144,7 @@ async function connect ( certPath, token, eeCert?.toBeSigned.subject.rdnSequence, + attrCertPath, ), } : { From 558629fc32bf467bf6ea31e5549f9558d01e1a4b Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Mon, 21 Aug 2023 05:42:35 -0400 Subject: [PATCH 18/39] chore: create PKI / PMI for testing RBAC in c=CA in test DIT --- test/create-pki.sh | 6 +++ test/pki/ca/ca.db.certs/1240.pem | 54 ++++++++++++++++++++ test/pki/ca/ca.db.index | 1 + test/pki/ca/ca.db.index.old | 1 + test/pki/ca/ca.db.serial | 2 +- test/pki/ca/ca.db.serial.old | 2 +- test/pki/jt.acrt | 12 +++++ test/pki/jt.chain.crt | 33 ++++++++++++ test/pki/jt.crt | 54 ++++++++++++++++++++ test/pki/jt.key.pem | 16 ++++++ test/pki/reqjt.pem | 10 ++++ test/pki/trudeau-clearance-confidential.acrt | 12 +++++ test/pki/trudeau-clearance-restricted.acrt | 12 +++++ test/pki/trudeau-clearance-secret.acrt | 12 +++++ test/pki/trudeau-clearance-topSecret.acrt | 12 +++++ test/pki/trudeau-clearance-unclassified.acrt | 12 +++++ test/pki/trudeau-clearance-unmarked.acrt | 12 +++++ 17 files changed, 261 insertions(+), 2 deletions(-) create mode 100644 test/pki/ca/ca.db.certs/1240.pem create mode 100644 test/pki/jt.acrt create mode 100644 test/pki/jt.chain.crt create mode 100644 test/pki/jt.crt create mode 100644 test/pki/jt.key.pem create mode 100644 test/pki/reqjt.pem create mode 100644 test/pki/trudeau-clearance-confidential.acrt create mode 100644 test/pki/trudeau-clearance-restricted.acrt create mode 100644 test/pki/trudeau-clearance-secret.acrt create mode 100644 test/pki/trudeau-clearance-topSecret.acrt create mode 100644 test/pki/trudeau-clearance-unclassified.acrt create mode 100644 test/pki/trudeau-clearance-unmarked.acrt diff --git a/test/create-pki.sh b/test/create-pki.sh index 73a4af0e0..51268da9f 100644 --- a/test/create-pki.sh +++ b/test/create-pki.sh @@ -25,3 +25,9 @@ openssl x509 -in ./dsa3.crt > dsa3.chain.crt cat god.crt >> dsa1.chain.crt cat god.crt >> dsa2.chain.crt cat god.crt >> dsa3.chain.crt + +# This is for testing Rule-Based Access Control with a set of attribute certificates I created separately. +openssl req -new -newkey rsa:1024 -nodes -keyout jt.key.pem -out reqjt.pem -subj "/C=CA/CN=Justin Trudeau" +openssl ca -batch -config openssl.cnf -md sha256 -out jt.crt -infiles reqjt.pem +openssl x509 -in ./jt.crt > jt.chain.crt +cat god.crt >> jt.chain.crt diff --git a/test/pki/ca/ca.db.certs/1240.pem b/test/pki/ca/ca.db.certs/1240.pem new file mode 100644 index 000000000..7589c94c0 --- /dev/null +++ b/test/pki/ca/ca.db.certs/1240.pem @@ -0,0 +1,54 @@ +Certificate: + Data: + Version: 1 (0x0) + Serial Number: 4672 (0x1240) + Signature Algorithm: sha256WithRSAEncryption + Issuer: CN=god + Validity + Not Before: Aug 20 21:32:16 2023 GMT + Not After : Aug 17 21:32:16 2033 GMT + Subject: C=CA, CN=Justin Trudeau + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (1024 bit) + Modulus: + 00:a5:55:24:11:6e:ef:13:e6:4c:63:c7:9e:39:fd: + da:59:9e:84:38:b3:f7:2e:f0:22:4d:96:8d:1f:af: + c2:67:97:56:a2:4a:83:fe:ba:80:8f:42:25:c0:c4: + dd:a4:e7:8f:53:34:b3:6d:b7:5b:21:82:c0:fa:3a: + 82:42:d6:05:b4:96:ec:2c:6e:31:cc:d7:1c:8a:8b: + 7c:5c:a5:3f:6e:d8:a8:94:ab:21:b8:7a:2a:3e:ba: + 8f:64:51:78:24:80:dc:a7:81:92:8f:9e:c2:7b:d0: + 30:91:4a:02:34:50:34:ec:67:72:3f:98:1a:7b:60: + 41:76:70:e2:74:cb:11:c6:27 + Exponent: 65537 (0x10001) + Signature Algorithm: sha256WithRSAEncryption + 01:fe:a9:5b:f5:fa:86:60:fc:f3:cf:c0:75:86:41:13:50:5e: + d0:1f:b1:07:51:fa:18:0f:a7:68:2f:e8:c3:1d:e4:a8:7f:15: + 59:9b:19:77:ab:ee:fa:4e:76:99:73:d0:f6:01:09:99:13:8d: + 3b:ce:cd:9f:e8:b2:90:55:e4:88:07:0d:f0:e4:25:98:af:0e: + a0:8f:f2:a0:06:41:c6:09:db:5b:03:67:c9:1e:9f:48:c8:25: + c5:1a:ce:23:63:59:fd:c6:01:e1:e6:18:fb:2d:86:f4:2c:5b: + 54:53:52:41:c8:7a:38:40:b4:63:42:f8:03:13:ab:dd:25:95: + c1:e4:28:47:7c:f4:39:be:22:31:f2:45:1c:cb:cd:4c:f4:77: + 96:c4:81:60:81:b9:d9:9c:33:e1:9e:28:44:16:8e:72:bf:16: + fb:9c:b9:37:ed:2c:e4:bc:bb:f0:55:8e:a1:21:15:2d:86:74: + 3d:d5:c5:b1:68:9a:24:06:4c:cb:80:f6:6e:86:d9:b6:6b:75: + 48:17:5d:da:86:7b:ff:35:05:a6:41:1a:eb:0e:5c:26:11:d8: + a2:02:15:73:b9:3b:2c:ca:dc:33:06:da:af:af:23:07:0b:12: + 32:b9:3f:8d:32:06:ee:10:be:9d:9c:ca:51:79:47:38:e7:da: + 5d:b9:0d:dc +-----BEGIN CERTIFICATE----- +MIICJTCCAQ0CAhJAMA0GCSqGSIb3DQEBCwUAMA4xDDAKBgNVBAMMA2dvZDAeFw0y +MzA4MjAyMTMyMTZaFw0zMzA4MTcyMTMyMTZaMCYxCzAJBgNVBAYTAkNBMRcwFQYD +VQQDDA5KdXN0aW4gVHJ1ZGVhdTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA +pVUkEW7vE+ZMY8eeOf3aWZ6EOLP3LvAiTZaNH6/CZ5dWokqD/rqAj0IlwMTdpOeP +UzSzbbdbIYLA+jqCQtYFtJbsLG4xzNcciot8XKU/btiolKshuHoqPrqPZFF4JIDc +p4GSj57Ce9AwkUoCNFA07GdyP5gae2BBdnDidMsRxicCAwEAATANBgkqhkiG9w0B +AQsFAAOCAQEAAf6pW/X6hmD888/AdYZBE1Be0B+xB1H6GA+naC/owx3kqH8VWZsZ +d6vu+k52mXPQ9gEJmRONO87Nn+iykFXkiAcN8OQlmK8OoI/yoAZBxgnbWwNnyR6f +SMglxRrOI2NZ/cYB4eYY+y2G9CxbVFNSQch6OEC0Y0L4AxOr3SWVweQoR3z0Ob4i +MfJFHMvNTPR3lsSBYIG52Zwz4Z4oRBaOcr8W+5y5N+0s5Ly78FWOoSEVLYZ0PdXF +sWiaJAZMy4D2bobZtmt1SBdd2oZ7/zUFpkEa6w5cJhHYogIVc7k7LMrcMwbar68j +BwsSMrk/jTIG7hC+nZzKUXlHOOfaXbkN3A== +-----END CERTIFICATE----- diff --git a/test/pki/ca/ca.db.index b/test/pki/ca/ca.db.index index 54c527a03..41aff6d33 100644 --- a/test/pki/ca/ca.db.index +++ b/test/pki/ca/ca.db.index @@ -1,3 +1,4 @@ V 330625105501Z 123D unknown /CN=dsa1 V 330625105518Z 123E unknown /CN=dsa2 V 330707072907Z 123F unknown /CN=dsa3 +V 330817213216Z 1240 unknown /C=CA/CN=Justin Trudeau diff --git a/test/pki/ca/ca.db.index.old b/test/pki/ca/ca.db.index.old index 76b3a50b7..54c527a03 100644 --- a/test/pki/ca/ca.db.index.old +++ b/test/pki/ca/ca.db.index.old @@ -1,2 +1,3 @@ V 330625105501Z 123D unknown /CN=dsa1 V 330625105518Z 123E unknown /CN=dsa2 +V 330707072907Z 123F unknown /CN=dsa3 diff --git a/test/pki/ca/ca.db.serial b/test/pki/ca/ca.db.serial index 39ccce404..183f97e61 100644 --- a/test/pki/ca/ca.db.serial +++ b/test/pki/ca/ca.db.serial @@ -1 +1 @@ -1240 +1241 diff --git a/test/pki/ca/ca.db.serial.old b/test/pki/ca/ca.db.serial.old index 1233cbb61..39ccce404 100644 --- a/test/pki/ca/ca.db.serial.old +++ b/test/pki/ca/ca.db.serial.old @@ -1 +1 @@ -123F +1240 diff --git a/test/pki/jt.acrt b/test/pki/jt.acrt new file mode 100644 index 000000000..d1282efa7 --- /dev/null +++ b/test/pki/jt.acrt @@ -0,0 +1,12 @@ +-----BEGIN ATTRIBUTE CERTIFICATE----- +MIIBtzCBoAIBATAsoSqkKDAmMQswCQYDVQQGEwJDQTEXMBUGA1UEAwwOSnVzdGlu +IFRydWRlYXWgFDASpBAwDjEMMAoGA1UEAwwDZ29kMA0GCSqGSIb3DQEBBQUAAgQM +IjhOMCIYDzIwMjMwODIxMDE0NDQ3WhgPMjAyNTA4MjEwMTQ0NDdaMBwwGgYDVQQ3 +MRMwEQYLKwYBBAGDuSqDEwEDAgIQMAAwDQYJKoZIhvcNAQEFBQADggEBACoDNj+a +ZGkneUKyBgeXaIqT0BWgWZuv+E+fKde9L3BDPS21sqGZDAlAfJLdr/UC1VYHj8xl +NWjJrJgOfnIugEcZtmFvSXjCM9lvqblUeMq/0teI3voBI69gBFjVze5v7zgVyzwK +WaNXD9zDfrOGfbHMh1UvOucIpU45nWMFCGWQKmkEiO1fSKwmTYxCDn5BmdiJwYBt +SZF4fRv6xpxH31g06uxcJTpfYw73ZdGqKOrzyz982mydl0yQnTka+pJ0dbQJwz3o +wB1eA9oDYwr690iCFPySqoXi2/4nYBPPEy4sXRnIBECeFTzMkECzBN/9CXTnDYgQ +v08plPUlIXLYgjY= +-----END ATTRIBUTE CERTIFICATE----- diff --git a/test/pki/jt.chain.crt b/test/pki/jt.chain.crt new file mode 100644 index 000000000..bbcd420ae --- /dev/null +++ b/test/pki/jt.chain.crt @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIICJTCCAQ0CAhJAMA0GCSqGSIb3DQEBCwUAMA4xDDAKBgNVBAMMA2dvZDAeFw0y +MzA4MjAyMTMyMTZaFw0zMzA4MTcyMTMyMTZaMCYxCzAJBgNVBAYTAkNBMRcwFQYD +VQQDDA5KdXN0aW4gVHJ1ZGVhdTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA +pVUkEW7vE+ZMY8eeOf3aWZ6EOLP3LvAiTZaNH6/CZ5dWokqD/rqAj0IlwMTdpOeP +UzSzbbdbIYLA+jqCQtYFtJbsLG4xzNcciot8XKU/btiolKshuHoqPrqPZFF4JIDc +p4GSj57Ce9AwkUoCNFA07GdyP5gae2BBdnDidMsRxicCAwEAATANBgkqhkiG9w0B +AQsFAAOCAQEAAf6pW/X6hmD888/AdYZBE1Be0B+xB1H6GA+naC/owx3kqH8VWZsZ +d6vu+k52mXPQ9gEJmRONO87Nn+iykFXkiAcN8OQlmK8OoI/yoAZBxgnbWwNnyR6f +SMglxRrOI2NZ/cYB4eYY+y2G9CxbVFNSQch6OEC0Y0L4AxOr3SWVweQoR3z0Ob4i +MfJFHMvNTPR3lsSBYIG52Zwz4Z4oRBaOcr8W+5y5N+0s5Ly78FWOoSEVLYZ0PdXF +sWiaJAZMy4D2bobZtmt1SBdd2oZ7/zUFpkEa6w5cJhHYogIVc7k7LMrcMwbar68j +BwsSMrk/jTIG7hC+nZzKUXlHOOfaXbkN3A== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIC/TCCAeWgAwIBAgIURzzyhKPsMnE/aL0n2zPNKdH9NTIwDQYJKoZIhvcNAQEL +BQAwDjEMMAoGA1UEAwwDZ29kMB4XDTIzMDYyODAyMDYzNloXDTMzMDYyNTAyMDYz +NlowDjEMMAoGA1UEAwwDZ29kMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEA2mlA8is79guN7y3PppS9Gjuj1bekNbPjRrpdv7Q9koIYVZm5DFqJKOMhhGjU +YZZJjfPx8tI4H8mgD4a+g+WXjqfTgu3oYIOxSNXhkpAN89QE3vi201YJlpGPRR/y +JNbGkFi0tYhZmZxTYMC22lATSWPOa/tbp7lU+X5EIlLxDUsq5Fv8LW8PfsQGInlS +BA4TmxS2igdUqckY6rfxmrtjG3aJuYHL8x7zfCM3M+4GXDOt6j3EpzC/oSD62kXT +VVojCEWKcnuFrDhiwQBOboYLF2AEwdjQswb2mLzQtmze//y02MzjVN31/tzVFYac +PZqMvJTGewV/Tat/9h8nPSm2FwIDAQABo1MwUTAdBgNVHQ4EFgQUVgZAa7669Q69 +sZxdVZvBqih1gKwwHwYDVR0jBBgwFoAUVgZAa7669Q69sZxdVZvBqih1gKwwDwYD +VR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAOIeu2q5JoYAaEcCu08Bo +1YWMHfSwnB6p6ccuzD1oiG4RTWzEexRWz6HzEXkW+8tpEWILCy2Dk6NZ+6QI+vB5 +srXu91pWGyESQjv/+VgwQ5HYP40HuNYQbLOeb4IP6lIE/VpXC+iRi++FASrRqTEY +xOClXMaV+85rbFT+AjDpellQ64ixYPa+8WMLhj/tYvjGNodKGofmCrnOn3z1sMzn +8YK+rZ0jzsUIlLsxJJY7qAtXHCseo3/WLsH6yoT4dYPbZOvVGGoj6Yl9lhTwc63D +C58RHmQIRWIfJZp6eTXFBFRDb6RrZcq7PxUk5CEBVz6r5gcgIaf0OccMAfsjTIb6 +8g== +-----END CERTIFICATE----- diff --git a/test/pki/jt.crt b/test/pki/jt.crt new file mode 100644 index 000000000..7589c94c0 --- /dev/null +++ b/test/pki/jt.crt @@ -0,0 +1,54 @@ +Certificate: + Data: + Version: 1 (0x0) + Serial Number: 4672 (0x1240) + Signature Algorithm: sha256WithRSAEncryption + Issuer: CN=god + Validity + Not Before: Aug 20 21:32:16 2023 GMT + Not After : Aug 17 21:32:16 2033 GMT + Subject: C=CA, CN=Justin Trudeau + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (1024 bit) + Modulus: + 00:a5:55:24:11:6e:ef:13:e6:4c:63:c7:9e:39:fd: + da:59:9e:84:38:b3:f7:2e:f0:22:4d:96:8d:1f:af: + c2:67:97:56:a2:4a:83:fe:ba:80:8f:42:25:c0:c4: + dd:a4:e7:8f:53:34:b3:6d:b7:5b:21:82:c0:fa:3a: + 82:42:d6:05:b4:96:ec:2c:6e:31:cc:d7:1c:8a:8b: + 7c:5c:a5:3f:6e:d8:a8:94:ab:21:b8:7a:2a:3e:ba: + 8f:64:51:78:24:80:dc:a7:81:92:8f:9e:c2:7b:d0: + 30:91:4a:02:34:50:34:ec:67:72:3f:98:1a:7b:60: + 41:76:70:e2:74:cb:11:c6:27 + Exponent: 65537 (0x10001) + Signature Algorithm: sha256WithRSAEncryption + 01:fe:a9:5b:f5:fa:86:60:fc:f3:cf:c0:75:86:41:13:50:5e: + d0:1f:b1:07:51:fa:18:0f:a7:68:2f:e8:c3:1d:e4:a8:7f:15: + 59:9b:19:77:ab:ee:fa:4e:76:99:73:d0:f6:01:09:99:13:8d: + 3b:ce:cd:9f:e8:b2:90:55:e4:88:07:0d:f0:e4:25:98:af:0e: + a0:8f:f2:a0:06:41:c6:09:db:5b:03:67:c9:1e:9f:48:c8:25: + c5:1a:ce:23:63:59:fd:c6:01:e1:e6:18:fb:2d:86:f4:2c:5b: + 54:53:52:41:c8:7a:38:40:b4:63:42:f8:03:13:ab:dd:25:95: + c1:e4:28:47:7c:f4:39:be:22:31:f2:45:1c:cb:cd:4c:f4:77: + 96:c4:81:60:81:b9:d9:9c:33:e1:9e:28:44:16:8e:72:bf:16: + fb:9c:b9:37:ed:2c:e4:bc:bb:f0:55:8e:a1:21:15:2d:86:74: + 3d:d5:c5:b1:68:9a:24:06:4c:cb:80:f6:6e:86:d9:b6:6b:75: + 48:17:5d:da:86:7b:ff:35:05:a6:41:1a:eb:0e:5c:26:11:d8: + a2:02:15:73:b9:3b:2c:ca:dc:33:06:da:af:af:23:07:0b:12: + 32:b9:3f:8d:32:06:ee:10:be:9d:9c:ca:51:79:47:38:e7:da: + 5d:b9:0d:dc +-----BEGIN CERTIFICATE----- +MIICJTCCAQ0CAhJAMA0GCSqGSIb3DQEBCwUAMA4xDDAKBgNVBAMMA2dvZDAeFw0y +MzA4MjAyMTMyMTZaFw0zMzA4MTcyMTMyMTZaMCYxCzAJBgNVBAYTAkNBMRcwFQYD +VQQDDA5KdXN0aW4gVHJ1ZGVhdTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA +pVUkEW7vE+ZMY8eeOf3aWZ6EOLP3LvAiTZaNH6/CZ5dWokqD/rqAj0IlwMTdpOeP +UzSzbbdbIYLA+jqCQtYFtJbsLG4xzNcciot8XKU/btiolKshuHoqPrqPZFF4JIDc +p4GSj57Ce9AwkUoCNFA07GdyP5gae2BBdnDidMsRxicCAwEAATANBgkqhkiG9w0B +AQsFAAOCAQEAAf6pW/X6hmD888/AdYZBE1Be0B+xB1H6GA+naC/owx3kqH8VWZsZ +d6vu+k52mXPQ9gEJmRONO87Nn+iykFXkiAcN8OQlmK8OoI/yoAZBxgnbWwNnyR6f +SMglxRrOI2NZ/cYB4eYY+y2G9CxbVFNSQch6OEC0Y0L4AxOr3SWVweQoR3z0Ob4i +MfJFHMvNTPR3lsSBYIG52Zwz4Z4oRBaOcr8W+5y5N+0s5Ly78FWOoSEVLYZ0PdXF +sWiaJAZMy4D2bobZtmt1SBdd2oZ7/zUFpkEa6w5cJhHYogIVc7k7LMrcMwbar68j +BwsSMrk/jTIG7hC+nZzKUXlHOOfaXbkN3A== +-----END CERTIFICATE----- diff --git a/test/pki/jt.key.pem b/test/pki/jt.key.pem new file mode 100644 index 000000000..18de99398 --- /dev/null +++ b/test/pki/jt.key.pem @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKVVJBFu7xPmTGPH +njn92lmehDiz9y7wIk2WjR+vwmeXVqJKg/66gI9CJcDE3aTnj1M0s223WyGCwPo6 +gkLWBbSW7CxuMczXHIqLfFylP27YqJSrIbh6Kj66j2RReCSA3KeBko+ewnvQMJFK +AjRQNOxncj+YGntgQXZw4nTLEcYnAgMBAAECgYBBGWYrSx/uGPcpLrd5pB/uu8Da +Rtpka/9Fx6rnuB/3BBsDqg2RJkRqpCQTZDDVtquzmoOwBstmRYLQxGr4ACecUu8b +xLSx4F3BVeIvLhr8JitsfnxEtfR3Nkl3hHjFlrmbNM1cdRdGo13ZnowsNBgAnyuR +lSdh1RwyX4MyMPhsCQJBAM/H1Q/LNxrEbePlrjQRbD+qtyVEMlnvco8aaAb2T+XI +LKP4Ujm43Efouvy99i+vsrdBvywhztGlgNDp+fSEimUCQQDLs3zrfwiGoMXnJ4Vb +sziLSSLMj8PADFFs1i3dW7mbZUD5oeLQxH/ZgVtNhEM0JrH2n5/QK1u6ucQVofm6 +9N+bAkBrqimN+IgCN7yVdYMx+cE0sFocVl0c2wVqf41d5i36bCItiNPakx6ZqE+T +/T7e8NTTPp832ADaAz9fgY4ClNc9AkEAxWNuP31fs6WDYFUpowxuVHpQYE1HcIf/ +NynsU74Yg36AkeXPNmHTSd9RrDHoNVbxyHwxjrUVNEHiwtusdP/o1QJAGtq0O+0u +/hUtkoqO0j7oxM0qdifNc+lIYauRHnetTzg7YCWJuqTV5dl9N5LIUk9HNp+JQnXc +j01kR942dr9IZw== +-----END PRIVATE KEY----- diff --git a/test/pki/reqjt.pem b/test/pki/reqjt.pem new file mode 100644 index 000000000..d2c78a7c1 --- /dev/null +++ b/test/pki/reqjt.pem @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBZTCBzwIBADAmMQswCQYDVQQGEwJDQTEXMBUGA1UEAwwOSnVzdGluIFRydWRl +YXUwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKVVJBFu7xPmTGPHnjn92lme +hDiz9y7wIk2WjR+vwmeXVqJKg/66gI9CJcDE3aTnj1M0s223WyGCwPo6gkLWBbSW +7CxuMczXHIqLfFylP27YqJSrIbh6Kj66j2RReCSA3KeBko+ewnvQMJFKAjRQNOxn +cj+YGntgQXZw4nTLEcYnAgMBAAGgADANBgkqhkiG9w0BAQsFAAOBgQAno0fhd0BV +HWWKLYblj+O1vWdSuEGBCl97K3lmr221bhhy6PEOwV5tQZrATyaSkewYsZ8TEYdB +futUKlOFQhSqcoaSypBBIFbYqeUeZwtmyxOO++eV6hKBRi/e9Yo1HKnm1ejrh2PH +8JSr9g+wZZL/yc1Eb95q5C0rEwXc30H/rw== +-----END CERTIFICATE REQUEST----- diff --git a/test/pki/trudeau-clearance-confidential.acrt b/test/pki/trudeau-clearance-confidential.acrt new file mode 100644 index 000000000..daec45efd --- /dev/null +++ b/test/pki/trudeau-clearance-confidential.acrt @@ -0,0 +1,12 @@ +-----BEGIN ATTRIBUTE CERTIFICATE----- +MIIBtzCBoAIBATAsoSqkKDAmMQswCQYDVQQGEwJDQTEXMBUGA1UEAwwOSnVzdGlu +IFRydWRlYXWgFDASpBAwDjEMMAoGA1UEAwwDZ29kMA0GCSqGSIb3DQEBBQUAAgQM +IjhOMCIYDzIwMjMwODIxMDE0NDQ3WhgPMjAyNTA4MjEwMTQ0NDdaMBwwGgYDVQQ3 +MRMwEQYLKwYBBAGDuSqDEwEDAgIQMAAwDQYJKoZIhvcNAQEFBQADggEBACoDNj+a +ZGkneUKyBgeXaIqT0BWgWZuv+E+fKde9L3BDPS21sqGZDAlAfJLdr/UC1VYHj8xl +NWjJrJgOfnIugEcZtmFvSXjCM9lvqblUeMq/0teI3voBI69gBFjVze5v7zgVyzwK +WaNXD9zDfrOGfbHMh1UvOucIpU45nWMFCGWQKmkEiO1fSKwmTYxCDn5BmdiJwYBt +SZF4fRv6xpxH31g06uxcJTpfYw73ZdGqKOrzyz982mydl0yQnTka+pJ0dbQJwz3o +wB1eA9oDYwr690iCFPySqoXi2/4nYBPPEy4sXRnIBECeFTzMkECzBN/9CXTnDYgQ +v08plPUlIXLYgjY= +-----END ATTRIBUTE CERTIFICATE----- \ No newline at end of file diff --git a/test/pki/trudeau-clearance-restricted.acrt b/test/pki/trudeau-clearance-restricted.acrt new file mode 100644 index 000000000..a2a169f07 --- /dev/null +++ b/test/pki/trudeau-clearance-restricted.acrt @@ -0,0 +1,12 @@ +-----BEGIN ATTRIBUTE CERTIFICATE----- +MIIBtzCBoAIBATAsoSqkKDAmMQswCQYDVQQGEwJDQTEXMBUGA1UEAwwOSnVzdGlu +IFRydWRlYXWgFDASpBAwDjEMMAoGA1UEAwwDZ29kMA0GCSqGSIb3DQEBBQUAAgQM +IjhOMCIYDzIwMjMwODIxMDE0NDQ3WhgPMjAyNTA4MjEwMTQ0NDdaMBwwGgYDVQQ3 +MRMwEQYLKwYBBAGDuSqDEwEDAgIgMAAwDQYJKoZIhvcNAQEFBQADggEBAHSZTm3a +G8NQNywPJy5vqugcjlKeIFomOIti5mpOY43PTQO35Uv5HIUl+qLAh8JUjW9R9O5/ +oPX1u2p4gWhB1OjR1L00ctLBbno13syr+8M5t5aIFpLt+UVXpBoSW2P99Avy8uSA +v5inSE6Ek8Tl7dl2q2I5K8IIyRc3JBrFIQEGKktcK1vXKntuT914Or2rN9MZcxS1 +i5PuezzkvFhECwhsoHuL18T54ya5DGr/a//b2Fp36DmzaQFUXG3scomjeyu8/OC1 +dqpA7VTv7FIgA4LJSUoz20S2edF8QFYww7Bb+/1jaK4advrLr6XfdSQDvseWkT/Q +FQ6719lMj/LXLEs= +-----END ATTRIBUTE CERTIFICATE----- \ No newline at end of file diff --git a/test/pki/trudeau-clearance-secret.acrt b/test/pki/trudeau-clearance-secret.acrt new file mode 100644 index 000000000..bccb33960 --- /dev/null +++ b/test/pki/trudeau-clearance-secret.acrt @@ -0,0 +1,12 @@ +-----BEGIN ATTRIBUTE CERTIFICATE----- +MIIBtzCBoAIBATAsoSqkKDAmMQswCQYDVQQGEwJDQTEXMBUGA1UEAwwOSnVzdGlu +IFRydWRlYXWgFDASpBAwDjEMMAoGA1UEAwwDZ29kMA0GCSqGSIb3DQEBBQUAAgQM +IjhOMCIYDzIwMjMwODIxMDE0NDQ3WhgPMjAyNTA4MjEwMTQ0NDdaMBwwGgYDVQQ3 +MRMwEQYLKwYBBAGDuSqDEwEDAgIIMAAwDQYJKoZIhvcNAQEFBQADggEBAEq4IV6N +vC1fypYFfMa5+VMz5eW+OG6CQsBwpw2L4R+d6CdOFDwEiFrCWa8OQmfqqRlemXFC +nG5/rFRA7q6M6ys+Jjr1eEe85iR9QyvytKPLL+1uaVzbrEdsptNN6pY74I6DrqIH +4f6cB7t4R6HDAt3s/5MIVhtXoD0lRR0vCM3sjg4gYElZ/ozlKUY7CS2svwO3HiEa +Yu530qHYFi6GyKXzH0T/plO4gUvU//AvQLw3iKAmZPYKYdCscCFrTrDnZYW7+SSu +KugLKuxEf1vCryF9vvq5BCZF2r2uQup8s2+k6KFzyJdHCaugO7xA9ITV7LUWnPOg +EOFuZVCe9T7urlk= +-----END ATTRIBUTE CERTIFICATE----- \ No newline at end of file diff --git a/test/pki/trudeau-clearance-topSecret.acrt b/test/pki/trudeau-clearance-topSecret.acrt new file mode 100644 index 000000000..303549050 --- /dev/null +++ b/test/pki/trudeau-clearance-topSecret.acrt @@ -0,0 +1,12 @@ +-----BEGIN ATTRIBUTE CERTIFICATE----- +MIIBtzCBoAIBATAsoSqkKDAmMQswCQYDVQQGEwJDQTEXMBUGA1UEAwwOSnVzdGlu +IFRydWRlYXWgFDASpBAwDjEMMAoGA1UEAwwDZ29kMA0GCSqGSIb3DQEBBQUAAgQM +IjhOMCIYDzIwMjMwODIxMDE0NDQ3WhgPMjAyNTA4MjEwMTQ0NDdaMBwwGgYDVQQ3 +MRMwEQYLKwYBBAGDuSqDEwEDAgIEMAAwDQYJKoZIhvcNAQEFBQADggEBAA8PXXgx +UHyqyEHsytpIuB74Od28hS/glmpiIRm0Fya60fXLGIYP9tSqedkqb2kVOBq6CXEw +FG7LncZLP+w1CBaUjhNd/O5+jiDZ2BmUI+zeLuK/NYvJJf8yVA0p4qaur02GGTkO +T7/6VH4NMT5k5atvRBIf2MRi7fEZB+o7mGsHYWeJ2kdSYpsTAOXuUKEa5PKK4XPC +/B7gYXaNfQdnLyS7JgDQdPbuMXCaPEyPPT51gPHPYt6waaKXbEW1rw9RwPwh2wyE +SFErCFSBveDL3JlwMFxrL4SJtksfum1noqoxnxc9bjUOT33jrHUp1K++HyDfqS7i +2mlsYqRy51eMveE= +-----END ATTRIBUTE CERTIFICATE----- \ No newline at end of file diff --git a/test/pki/trudeau-clearance-unclassified.acrt b/test/pki/trudeau-clearance-unclassified.acrt new file mode 100644 index 000000000..55acbdb0a --- /dev/null +++ b/test/pki/trudeau-clearance-unclassified.acrt @@ -0,0 +1,12 @@ +-----BEGIN ATTRIBUTE CERTIFICATE----- +MIIBtzCBoAIBATAsoSqkKDAmMQswCQYDVQQGEwJDQTEXMBUGA1UEAwwOSnVzdGlu +IFRydWRlYXWgFDASpBAwDjEMMAoGA1UEAwwDZ29kMA0GCSqGSIb3DQEBBQUAAgQM +IjhOMCIYDzIwMjMwODIxMDE0NDQ3WhgPMjAyNTA4MjEwMTQ0NDdaMBwwGgYDVQQ3 +MRMwEQYLKwYBBAGDuSqDEwEDAgJAMAAwDQYJKoZIhvcNAQEFBQADggEBAJHtnf1R +zGVs3ur51azdExMj0d3a69830Vl/34m4JAmKK3QiVEb85RCkArBv1LyP9T5do5y3 +bT8Zgn2u8Ah1jj2I6+cejUkkpmFPz9z5Nuerln55RVMgHDtPOpCWMcxj+lnkx60E +/u3EGbZsX2F1nUFelViBOnCq5CQI/QAc/zfYXFr1KW5vu9eFYqiSgV3yxmPa0zur +vwRP3T8kwHpGQim+vl+peziwSK3/pS9omtXAKuuHC5oA17vDsCvhVqkfR1IJs1HW +BXlhKCbX4rqXIY/KYhWygoFEy0HNUmRfjtpuFJbUC0CMvaaL+ua/R95qsvUzXl5+ +kDk7VLzq/rHpbjQ= +-----END ATTRIBUTE CERTIFICATE----- \ No newline at end of file diff --git a/test/pki/trudeau-clearance-unmarked.acrt b/test/pki/trudeau-clearance-unmarked.acrt new file mode 100644 index 000000000..a77c7cd21 --- /dev/null +++ b/test/pki/trudeau-clearance-unmarked.acrt @@ -0,0 +1,12 @@ +-----BEGIN ATTRIBUTE CERTIFICATE----- +MIIBtzCBoAIBATAsoSqkKDAmMQswCQYDVQQGEwJDQTEXMBUGA1UEAwwOSnVzdGlu +IFRydWRlYXWgFDASpBAwDjEMMAoGA1UEAwwDZ29kMA0GCSqGSIb3DQEBBQUAAgQM +IjhOMCIYDzIwMjMwODIxMDE0NDQ3WhgPMjAyNTA4MjEwMTQ0NDdaMBwwGgYDVQQ3 +MRMwEQYLKwYBBAGDuSqDEwEDAgKAMAAwDQYJKoZIhvcNAQEFBQADggEBACopv7Xq +gcOKwYGx3g/Xko6WJMrThzzlD/AG07O1xHfB/Uh2ezxdkfDtvbocDpNSG5kT5C/r +P21DpNLZmTOCcLuqPXR5DlFHKVMFOhyZLHFoHlzSad168199ABUTQKu7+1op4C6z +kRrovkiKqtLustSAfSHS9Ze3JxffZuKqHPw6kC4lx55OnqlqM2xGrJR4y/zNGeJi +UArrh0A98poiHL/qgQyNvOuz6UfCOnOWHWXJZ6Yrxjp41P1SqKV2AXChRK+L5YAs +rovcCBn8tL805LsJ3bs186oIyRTj4P0fv8kS8OxQ3XdJtbXBhaNKEoWaO2xenjxI +Mlg1lwhuEg5GFSE= +-----END ATTRIBUTE CERTIFICATE----- \ No newline at end of file From dd90e93c25da0dea0a62bba3abb3853bbf09d0b8 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Mon, 21 Aug 2023 05:43:23 -0400 Subject: [PATCH 19/39] fix: verify signature of TBS, not whole attribute certificate --- apps/meerkat/src/app/pki/verifyAttrCertPath.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/meerkat/src/app/pki/verifyAttrCertPath.ts b/apps/meerkat/src/app/pki/verifyAttrCertPath.ts index e00d549fd..6a31f3652 100644 --- a/apps/meerkat/src/app/pki/verifyAttrCertPath.ts +++ b/apps/meerkat/src/app/pki/verifyAttrCertPath.ts @@ -1046,7 +1046,13 @@ async function verifyAttrCert ( } const acert_bytes = acert.originalDER - ?? _encode_AttributeCertificate(acert, DER).toBytes(); + ? (() => { + const el = new DERElement(); + el.fromBytes(acert.originalDER); + const tbs = el.sequence[0]; + return tbs.toBytes(); + })() + : _encode_TBSAttributeCertificate(acert.toBeSigned, DER).toBytes(); const acert_hasher = createHash("sha256"); acert_hasher.update(acert_bytes); From 2f6e690d26e687938240d93029b420284a2d2147 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Mon, 21 Aug 2023 20:45:19 -0400 Subject: [PATCH 20/39] fix: missing RBAC from accessControlSchemesThatUseRBAC --- apps/meerkat/src/app/authz/accessControlSchemesThatUseRBAC.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/meerkat/src/app/authz/accessControlSchemesThatUseRBAC.ts b/apps/meerkat/src/app/authz/accessControlSchemesThatUseRBAC.ts index 8b1c561be..d20093bdd 100644 --- a/apps/meerkat/src/app/authz/accessControlSchemesThatUseRBAC.ts +++ b/apps/meerkat/src/app/authz/accessControlSchemesThatUseRBAC.ts @@ -1,3 +1,6 @@ +import { + rule_based_access_control, +} from "@wildboar/x500/src/lib/modules/BasicAccessControl/rule-based-access-control.va"; import { rule_and_basic_access_control, } from "@wildboar/x500/src/lib/modules/BasicAccessControl/rule-and-basic-access-control.va"; @@ -18,6 +21,7 @@ import { IndexableOID } from "@wildboar/meerkat-types"; */ export const accessControlSchemesThatUseRBAC: Set = new Set([ + rule_based_access_control.toString(), rule_and_basic_access_control.toString(), rule_and_simple_access_control.toString(), ]); From 325533c17b48944dc4052d49d932817b483825bc Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Mon, 21 Aug 2023 20:46:24 -0400 Subject: [PATCH 21/39] fix: non-display of any attribute values when using RBAC --- .../src/app/database/entry/readPermittedEntryInformation.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/meerkat/src/app/database/entry/readPermittedEntryInformation.ts b/apps/meerkat/src/app/database/entry/readPermittedEntryInformation.ts index 5eeba57e1..b6a9e5b20 100644 --- a/apps/meerkat/src/app/database/entry/readPermittedEntryInformation.ts +++ b/apps/meerkat/src/app/database/entry/readPermittedEntryInformation.ts @@ -133,8 +133,7 @@ async function readPermittedEntryInformation ( } let incompleteEntry: boolean = true; let discloseIncompleteEntry: boolean = false; - const permittedEinfo: EntryInformation_information_Item[] = []; - const permittedEinfoViaRbac: EntryInformation_information_Item[] = []; + let permittedEinfo: EntryInformation_information_Item[] = []; if (accessControlSchemesThatUseACIItems.has(acs)) { for (const info of einfo) { if ("attribute" in info) { @@ -242,6 +241,8 @@ async function readPermittedEntryInformation ( continue; } } + } else { + permittedEinfo = einfo; } if (!accessControlSchemesThatUseRBAC.has(acs) || !assn) { @@ -253,6 +254,7 @@ async function readPermittedEntryInformation ( } const typesOnly: boolean = (options?.selection?.infoTypes === attributeTypesOnly); + const permittedEinfoViaRbac: EntryInformation_information_Item[] = []; for (const info of permittedEinfo) { if (!("attribute" in info)) { // This should actually never happen. From eae65e4130a4ca257e9ed0829280454d07c3ed86 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Mon, 21 Aug 2023 20:47:08 -0400 Subject: [PATCH 22/39] fix: observe RBAC when determining access to entries --- apps/meerkat/src/app/authz/acdf.ts | 66 +++++++++---- .../app/authz/get_security_labels_for_rdn.ts | 96 +++++++++++++++++++ apps/meerkat/src/app/distributed/findDSE.ts | 22 ++++- apps/meerkat/src/app/distributed/list_i.ts | 7 ++ apps/meerkat/src/app/distributed/list_ii.ts | 7 ++ apps/meerkat/src/app/distributed/search_i.ts | 17 +++- apps/meerkat/src/app/distributed/search_ii.ts | 2 + 7 files changed, 195 insertions(+), 22 deletions(-) create mode 100644 apps/meerkat/src/app/authz/get_security_labels_for_rdn.ts diff --git a/apps/meerkat/src/app/authz/acdf.ts b/apps/meerkat/src/app/authz/acdf.ts index d9afaea11..643b6526d 100644 --- a/apps/meerkat/src/app/authz/acdf.ts +++ b/apps/meerkat/src/app/authz/acdf.ts @@ -1,6 +1,7 @@ -import { Context, Vertex, ClientAssociation } from "@wildboar/meerkat-types"; +import { Context, Vertex, ClientAssociation, Value } from "@wildboar/meerkat-types"; import { bacACDF } from "@wildboar/x500"; import { + SignedSecurityLabel, _decode_SignedSecurityLabel, } from "@wildboar/x500/src/lib/modules/EnhancedSecurity/SignedSecurityLabel.ta"; import { OBJECT_IDENTIFIER } from "asn1-ts"; @@ -13,7 +14,6 @@ import accessControlSchemesThatUseACIItems from "./accessControlSchemesThatUseAC import accessControlSchemesThatUseRBAC from "./accessControlSchemesThatUseRBAC"; import { rbacACDF } from "./rbacACDF"; import { attributeValueSecurityLabelContext } from "@wildboar/x500/src/lib/collections/contexts"; -// import { PERMISSION_CATEGORY_DISCLOSE_ON_ERROR } from "@wildboar/x500/src/lib/bac/bacACDF"; // attributeValueSecurityLabelContext const avslc = attributeValueSecurityLabelContext["&id"]; @@ -31,6 +31,7 @@ function acdf ( settings: EvaluateFilterSettings, tuplesAlreadySplit?: boolean, addingEntry: boolean = false, + rdnSecurityLabels?: [ Value, SignedSecurityLabel[] ][], ): boolean { const acs = accessControlScheme.toString(); if (accessControlSchemesThatUseACIItems.has(acs)) { @@ -48,29 +49,54 @@ function acdf ( } if ( accessControlSchemesThatUseRBAC.has(acs) - && ("value" in request) && assn && !addingEntry // RBAC basically has no effect on adding entries unless the superior is hidden. // TODO: Find DSE basically runs this same code twice. I want to find some optimization to avoid that. // && (permissions.length !== 1 || permissions[0] !== PERMISSION_CATEGORY_DISCLOSE_ON_ERROR) ) { - const labelContext = request.contexts - ?.find((c) => c.contextType.isEqualTo(avslc)); - if (labelContext?.contextValues.length) { - // return true; // If there is no label, access is allowed. - const label = _decode_SignedSecurityLabel(labelContext.contextValues[0]); - const authorized: boolean = rbacACDF( - ctx, - assn, - target, - label, - request.value.type_, - request.value.value, - request.contexts ?? [], - permissions, - ); - if (!authorized) { - return false; + if ( + ("value" in request) + // RBAC only applies to values that have a context. + && (request.contexts?.some((c) => c.contextType.isEqualTo(attributeValueSecurityLabelContext["&id"]))) + ) { + const labelContext = request.contexts + ?.find((c) => c.contextType.isEqualTo(avslc)); + if (labelContext?.contextValues.length) { + // return true; // If there is no label, access is allowed. + const label = _decode_SignedSecurityLabel(labelContext.contextValues[0]); + const authorized: boolean = rbacACDF( + ctx, + assn, + target, + label, + request.value.type_, + request.value.value, + request.contexts ?? [], + permissions, + ); + if (!authorized) { + return false; + } + } + } + else if (("entry" in request) && rdnSecurityLabels?.length) { + for (const atav_and_labels of rdnSecurityLabels) { + const [ atav, labels ] = atav_and_labels; + for (const label of labels) { + const authorized: boolean = rbacACDF( + ctx, + assn, + target, + label, + atav.type, + atav.value, + atav.contexts ?? [], + permissions, + ); + if (!authorized) { + return false; + } + } } } } diff --git a/apps/meerkat/src/app/authz/get_security_labels_for_rdn.ts b/apps/meerkat/src/app/authz/get_security_labels_for_rdn.ts new file mode 100644 index 000000000..2699e5f8c --- /dev/null +++ b/apps/meerkat/src/app/authz/get_security_labels_for_rdn.ts @@ -0,0 +1,96 @@ +import { Context, MistypedArgumentError, Value } from "@wildboar/meerkat-types"; +import { attributeValueSecurityLabelContext } from "@wildboar/x500/src/lib/collections/contexts"; +import { RelativeDistinguishedName } from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/RelativeDistinguishedName.ta"; +import { attributeValueFromDB } from "../database/attributeValueFromDB"; +import { + SignedSecurityLabel, _decode_SignedSecurityLabel, +} from "@wildboar/x500/src/lib/modules/EnhancedSecurity/SignedSecurityLabel.ta"; +import { + Context as X500Context, +} from "@wildboar/x500/src/lib/modules/InformationFramework/Context.ta"; +import { groupByOID } from "@wildboar/x500"; +import getNamingMatcherGetter from "../x500/getNamingMatcherGetter"; +import { BERElement, ObjectIdentifier } from "asn1-ts"; + +const AVSLC: string = attributeValueSecurityLabelContext["&id"].toString(); + +export +async function get_security_labels_for_rdn ( + ctx: Context, + rdn: RelativeDistinguishedName, +): Promise<[ Value, SignedSecurityLabel[] ][]> { + const rdnValuesWithContexts = await ctx.db.attributeValue.findMany({ + where: { + type_oid: { + in: rdn.map((atav) => atav.type_.toBytes()), + }, + }, + select: { + type_oid: true, + tag_class: true, + tag_number: true, + constructed: true, + content_octets: true, + ContextValue: { + select: { + type: true, + tag_class: true, + tag_number: true, + constructed: true, + ber: true, + fallback: true, + }, + }, + }, + }); + const atavsByType = groupByOID(rdn, (atav) => atav.type_); + const namingMatcher = getNamingMatcherGetter(ctx); + const ret: [ Value, SignedSecurityLabel[] ][] = []; + for (const dbval of rdnValuesWithContexts) { + if (dbval.ContextValue.length === 0) { + continue; + } + const secLabelContexts = dbval.ContextValue.filter((cv) => (cv.type === AVSLC)); + if (secLabelContexts.length === 0) { + continue; + } + const type = ObjectIdentifier.fromBytes(dbval.type_oid); + const type_str = type.toString(); + const value = attributeValueFromDB(dbval); + const atavs = atavsByType[type_str]; + if (atavs.length > 1) { + throw new MistypedArgumentError(); + } + const atav = atavs[0]; + if (atav && namingMatcher(type)?.(value, atav.value)) { + // If this value is the one that's in the RDN, we return it with + // its security labels. + const labels = secLabelContexts.map((c) => { + const el = new BERElement(); + el.fromBytes(c.ber); + return _decode_SignedSecurityLabel(el); + }); + const groupedContexts = groupByOID(dbval.ContextValue, (c) => c.type); + const contexts = Object.entries(groupedContexts).map(([ type_str, c ]) => { + return new X500Context( + ObjectIdentifier.fromString(type_str), + c.map((cv) => { + const el = new BERElement(); + el.fromBytes(cv.ber); + return el; + }), + c[0]?.fallback, + ); + }); + ret.push([ { + type: atav.type_, + value: atav.value, + contexts, + }, labels ]); + delete atavsByType[type_str]; + } + } + return ret; +} + +export default get_security_labels_for_rdn; diff --git a/apps/meerkat/src/app/distributed/findDSE.ts b/apps/meerkat/src/app/distributed/findDSE.ts index b3cc90c9f..9cc62a1ae 100644 --- a/apps/meerkat/src/app/distributed/findDSE.ts +++ b/apps/meerkat/src/app/distributed/findDSE.ts @@ -141,6 +141,8 @@ import { isModificationOperation } from "@wildboar/x500"; import { EXT_BIT_USE_ALIAS_ON_UPDATE } from "@wildboar/x500/src/lib/dap/extensions"; import stringifyDN from "../x500/stringifyDN"; import { acdf } from "../authz/acdf"; +import accessControlSchemesThatUseRBAC from "../authz/accessControlSchemesThatUseRBAC"; +import { get_security_labels_for_rdn } from "../authz/get_security_labels_for_rdn"; const autonomousArea: string = id_ar_autonomousArea.toString(); @@ -1019,7 +1021,9 @@ export const objectClasses = Array .from(matchedVertex.dse.objectClass) .map(ObjectIdentifier.fromString); - + const rdn_sec_labels = accessControlSchemesThatUseRBAC.has(accessControlScheme.toString()) + ? await get_security_labels_for_rdn(ctx, matchedVertex.dse.rdn) + : undefined; const authorizedToDiscover: boolean = acdf( ctx, accessControlScheme, @@ -1034,6 +1038,8 @@ export { entry: objectClasses }, bacSettings, true, + false, + rdn_sec_labels, ); if (!authorizedToDiscover) { const authorizedToDiscoverOnError: boolean = acdf( @@ -1047,6 +1053,8 @@ export { entry: objectClasses }, bacSettings, true, + false, + rdn_sec_labels, ); if (authorizedToDiscoverOnError) { throw new errors.SecurityError( @@ -1193,6 +1201,9 @@ export NAMING_MATCHER, ); const objectClasses = Array.from(child.dse.objectClass).map(ObjectIdentifier.fromString); + const rdn_sec_labels = accessControlSchemesThatUseRBAC.has(accessControlScheme.toString()) + ? await get_security_labels_for_rdn(ctx, child.dse.rdn) + : undefined; /** * We ignore entries for which browse and returnDN permissions * are not granted. This is not specified in the Find DSE @@ -1213,6 +1224,8 @@ export { entry: objectClasses }, bacSettings, true, + false, + rdn_sec_labels, ); if (!authorizedToDiscover) { @@ -1231,6 +1244,8 @@ export { entry: objectClasses }, bacSettings, true, + false, + rdn_sec_labels, ); if (authorizedToDiscoverOnError) { throw new errors.SecurityError( @@ -1326,6 +1341,9 @@ export let discloseOnError: boolean = true; if (!ctx.config.bulkInsertMode && accessControlScheme) { + const rdn_sec_labels = accessControlSchemesThatUseRBAC.has(accessControlScheme.toString()) + ? await get_security_labels_for_rdn(ctx, dse_i.dse.rdn) + : undefined; const currentDN = getDistinguishedName(dse_i); const relevantSubentries: Vertex[] = (await Promise.all( state.admPoints.map((ap) => getRelevantSubentries(ctx, dse_i, currentDN, ap)), @@ -1365,6 +1383,8 @@ export { entry: objectClasses }, bacSettings, true, + false, + rdn_sec_labels, ); discloseOnError = authorizedToDiscloseOnError; } diff --git a/apps/meerkat/src/app/distributed/list_i.ts b/apps/meerkat/src/app/distributed/list_i.ts index 5da7fc0af..686275e02 100644 --- a/apps/meerkat/src/app/distributed/list_i.ts +++ b/apps/meerkat/src/app/distributed/list_i.ts @@ -150,6 +150,8 @@ import { import DSPAssociation from "../dsp/DSPConnection"; import { entryACI, prescriptiveACI, subentryACI } from "@wildboar/x500/src/lib/collections/attributes"; import { acdf } from "../authz/acdf"; +import accessControlSchemesThatUseRBAC from "../authz/accessControlSchemesThatUseRBAC"; +import { get_security_labels_for_rdn } from "../authz/get_security_labels_for_rdn"; const BYTES_IN_A_UUID: number = 16; const PARENT: string = parent["&id"].toString(); @@ -603,6 +605,9 @@ async function list_i ( NAMING_MATCHER, ); const objectClasses = Array.from(subordinate.dse.objectClass).map(ObjectIdentifier.fromString); + const rdn_sec_labels = accessControlSchemesThatUseRBAC.has(effectiveAccessControlScheme.toString()) + ? await get_security_labels_for_rdn(ctx, subordinate.dse.rdn) + : undefined; const authorizedToList = acdf( ctx, effectiveAccessControlScheme, @@ -617,6 +622,8 @@ async function list_i ( { entry: objectClasses }, bacSettings, true, + false, + rdn_sec_labels, ); if (!authorizedToList) { continue; diff --git a/apps/meerkat/src/app/distributed/list_ii.ts b/apps/meerkat/src/app/distributed/list_ii.ts index da7a0830a..44dff1c7c 100644 --- a/apps/meerkat/src/app/distributed/list_ii.ts +++ b/apps/meerkat/src/app/distributed/list_ii.ts @@ -152,6 +152,8 @@ import DSPAssociation from "../dsp/DSPConnection"; import { generateSignature } from "../pki/generateSignature"; import { SIGNED } from "@wildboar/x500/src/lib/modules/AuthenticationFramework/SIGNED.ta"; import { acdf } from "../authz/acdf"; +import accessControlSchemesThatUseRBAC from "../authz/accessControlSchemesThatUseRBAC"; +import { get_security_labels_for_rdn } from "../authz/get_security_labels_for_rdn"; const BYTES_IN_A_UUID: number = 16; const PARENT: string = parent["&id"].toString(); @@ -575,6 +577,9 @@ async function list_ii ( NAMING_MATCHER, ); const objectClasses = Array.from(subordinate.dse.objectClass).map(ObjectIdentifier.fromString); + const rdn_sec_labels = accessControlSchemesThatUseRBAC.has(effectiveAccessControlScheme.toString()) + ? await get_security_labels_for_rdn(ctx, subordinate.dse.rdn) + : undefined; const authorizedToList = acdf( ctx, effectiveAccessControlScheme, @@ -589,6 +594,8 @@ async function list_ii ( { entry: objectClasses }, bacSettings, true, + false, + rdn_sec_labels, ); if (!authorizedToList) { continue; diff --git a/apps/meerkat/src/app/distributed/search_i.ts b/apps/meerkat/src/app/distributed/search_i.ts index 7ca79e6a4..154660013 100644 --- a/apps/meerkat/src/app/distributed/search_i.ts +++ b/apps/meerkat/src/app/distributed/search_i.ts @@ -357,6 +357,8 @@ import { id_ar_serviceSpecificArea } from "@wildboar/x500/src/lib/modules/Inform import { ID_AR_SERVICE, ID_AUTONOMOUS } from "../../oidstr"; import { isMatchAllFilter } from "../x500/isMatchAllFilter"; import { acdf } from "../authz/acdf"; +import accessControlSchemesThatUseRBAC from "../authz/accessControlSchemesThatUseRBAC"; +import { get_security_labels_for_rdn } from "../authz/get_security_labels_for_rdn"; // NOTE: This will require serious changes when service specific areas are implemented. @@ -1932,6 +1934,9 @@ async function search_i_ex ( } // TODO: REVIEW: How would this handle alias dereferencing, joins, hierarchy selection, etc? const onBaseObjectIteration: boolean = (targetDN.length === data.baseObject.rdnSequence.length); + const rdn_sec_labels = (accessControlScheme && accessControlSchemesThatUseRBAC.has(accessControlScheme.toString())) + ? await get_security_labels_for_rdn(ctx, target.dse.rdn) + : undefined; const authorized = (permissions: number[]) => !accessControlScheme || acdf( ctx, accessControlScheme, @@ -1945,6 +1950,8 @@ async function search_i_ex ( }, bacSettings, true, + false, + rdn_sec_labels, ); if (accessControlScheme) { const authorizedToSearch = authorized([ @@ -3286,11 +3293,17 @@ async function search_i_ex ( isMemberOfGroup, NAMING_MATCHER, ); + const rdn_sec_labels = ( + accessControlScheme + && accessControlSchemesThatUseRBAC.has(accessControlScheme.toString()) + ) + ? await get_security_labels_for_rdn(ctx, subordinate.dse.rdn) + : undefined; const authorizedToDiscoverSubordinate = !accessControlScheme || acdf( ctx, accessControlScheme, assn, - target, + subordinate, [ PERMISSION_CATEGORY_BROWSE, PERMISSION_CATEGORY_RETURN_DN, @@ -3302,6 +3315,8 @@ async function search_i_ex ( }, bacSettings, true, + false, + rdn_sec_labels, ); if (!authorizedToDiscoverSubordinate) { continue; diff --git a/apps/meerkat/src/app/distributed/search_ii.ts b/apps/meerkat/src/app/distributed/search_ii.ts index 391de337d..32c13af4d 100644 --- a/apps/meerkat/src/app/distributed/search_ii.ts +++ b/apps/meerkat/src/app/distributed/search_ii.ts @@ -78,6 +78,8 @@ import getEntryExistsFilter from "../database/entryExistsFilter"; import { searchRules } from "@wildboar/x500/src/lib/collections/attributes"; import { attributeValueFromDB } from "../database/attributeValueFromDB"; import { MAX_RESULTS } from "../constants"; +import accessControlSchemesThatUseRBAC from "../authz/accessControlSchemesThatUseRBAC"; +import { get_security_labels_for_rdn } from "../authz/get_security_labels_for_rdn"; const BYTES_IN_A_UUID: number = 16; From 7ff32798c83a073353294fe8cdb6300181f7ce15 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Mon, 21 Aug 2023 21:45:11 -0400 Subject: [PATCH 23/39] fix: ignore alt signatures --- apps/meerkat/src/app/pki/verifyAttrCertPath.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/apps/meerkat/src/app/pki/verifyAttrCertPath.ts b/apps/meerkat/src/app/pki/verifyAttrCertPath.ts index 6a31f3652..9ccb16026 100644 --- a/apps/meerkat/src/app/pki/verifyAttrCertPath.ts +++ b/apps/meerkat/src/app/pki/verifyAttrCertPath.ts @@ -1193,18 +1193,7 @@ async function verifyAttrCert ( return VAC_INVALID_SIGNATURE; } if (valid_signature === undefined) { - if (!acert.altAlgorithmIdentifier || !acert.altSignature) { - return VAC_INVALID_SIGNATURE; - } - const valid_alt_signature = verifySignature( - acert_bytes, - acert.altAlgorithmIdentifier, - packBits(acert.altSignature), - spki, - ); - if (!valid_alt_signature) { - return VAC_INVALID_SIGNATURE; - } + return VAC_INVALID_SIGNATURE; } const signing_cert_path = ctx.config.signing.certPath; From 45771b788380a678b279dac74d9b1e3b0ce27bb9 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Mon, 21 Aug 2023 22:26:54 -0400 Subject: [PATCH 24/39] fix: do not check alt signatures on security labels --- apps/meerkat/src/app/authz/rbacACDF.ts | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/apps/meerkat/src/app/authz/rbacACDF.ts b/apps/meerkat/src/app/authz/rbacACDF.ts index 7551a7c7e..382090742 100644 --- a/apps/meerkat/src/app/authz/rbacACDF.ts +++ b/apps/meerkat/src/app/authz/rbacACDF.ts @@ -222,20 +222,7 @@ export function rbacACDF ( publicKey, ); if (!sig_valid) { - if (label.altAlgorithmIdentifier && label.altSignature) { - const alt_sig_value = packBits(label.signature); - const alt_sig_valid = verifySignature( - tbs_bytes, - label.altAlgorithmIdentifier, - alt_sig_value, - publicKey, - ); - if (!alt_sig_valid) { - return false; - } - } else { - return false; - } + return false; } // At this point, we know that the label is correctly bound to the value, // so we can use the policy-specific RBAC ACDF. From fcfbbecd05beff731343d5f557ff2e20e4a9df88 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Mon, 21 Aug 2023 22:27:28 -0400 Subject: [PATCH 25/39] docs: JSDoc for RBAC --- apps/meerkat/src/app/authz/acdf.ts | 25 ++++++++++++- .../app/authz/get_security_labels_for_rdn.ts | 16 +++++++++ apps/meerkat/src/app/authz/rbacACDF.ts | 36 +++++++++++++++++++ .../src/app/pki/isCertInTrustAnchor.ts | 15 ++++++++ .../meerkat/src/app/pki/verifyAttrCertPath.ts | 18 ++++++++++ 5 files changed, 109 insertions(+), 1 deletion(-) diff --git a/apps/meerkat/src/app/authz/acdf.ts b/apps/meerkat/src/app/authz/acdf.ts index 643b6526d..ec147296d 100644 --- a/apps/meerkat/src/app/authz/acdf.ts +++ b/apps/meerkat/src/app/authz/acdf.ts @@ -15,9 +15,32 @@ import accessControlSchemesThatUseRBAC from "./accessControlSchemesThatUseRBAC"; import { rbacACDF } from "./rbacACDF"; import { attributeValueSecurityLabelContext } from "@wildboar/x500/src/lib/collections/contexts"; -// attributeValueSecurityLabelContext const avslc = attributeValueSecurityLabelContext["&id"]; +/** + * @summary Access Control Decision Function - determine allow-or-deny + * @description + * + * This function evaluates whether a user is authorized under a given access + * control scheme to perform some operation on an object by virtue of having + * the necessary permissions. + * + * @param ctx The context object + * @param accessControlScheme The effective access control scheme + * @param assn The client association, if any + * @param target The target object for which authorization is being evaluated + * @param permissions The permissions on the protected item being requested + * @param tuples The basic access control tuples + * @param requester The name of the entry making the request for authorization + * @param request The protected item to which authorization is sought + * @param settings The settings for evaluating a filter + * @param tuplesAlreadySplit Whether the tuples were already split to allow and deny + * @param addingEntry Whether this request is for adding an entry + * @param rdnSecurityLabels The security labels (if any) associated with RDN values + * @returns A boolean indicating whether access is granted + * + * @function + */ export function acdf ( ctx: Context, diff --git a/apps/meerkat/src/app/authz/get_security_labels_for_rdn.ts b/apps/meerkat/src/app/authz/get_security_labels_for_rdn.ts index 2699e5f8c..c2d99cc16 100644 --- a/apps/meerkat/src/app/authz/get_security_labels_for_rdn.ts +++ b/apps/meerkat/src/app/authz/get_security_labels_for_rdn.ts @@ -14,6 +14,22 @@ import { BERElement, ObjectIdentifier } from "asn1-ts"; const AVSLC: string = attributeValueSecurityLabelContext["&id"].toString(); +/** + * @summary Get the security labels associated with the RDN values + * @description + * + * This function gets the security labels associated with the distinguished + * attribute values of the relative distinguished name. This is for Meerkat DSA + * to evaluate the ability to discover an entry under Rule-Based Access Control + * (RBAC). + * + * @param ctx The context object + * @param rdn The RDN whose security labels are to be returned + * @returns The attribute types and values along with security labels, if any + * + * @async + * @function + */ export async function get_security_labels_for_rdn ( ctx: Context, diff --git a/apps/meerkat/src/app/authz/rbacACDF.ts b/apps/meerkat/src/app/authz/rbacACDF.ts index 382090742..8571aecb3 100644 --- a/apps/meerkat/src/app/authz/rbacACDF.ts +++ b/apps/meerkat/src/app/authz/rbacACDF.ts @@ -44,6 +44,22 @@ import { // TODO: Add this to the registry. export const id_basicSecurityPolicy = new ObjectIdentifier([ 403, 1 ], id_wildboar); +/** + * @summary A simple Rule-Based Access Control ACDF + * @description + * + * This RBAC ACDF is built-in to Meerkat DSA to provide a sensible default and + * sample ACDF. + * + * @param ctx The context object + * @param assn The client association + * @param target The target object + * @param signedLabel The security label, after being verified + * @param _value The value to which authorization is sought + * @param _contexts The contexts associated with the value + * @param _permissions The permissions sought on the attribute value + * @returns A `boolean` indicating whether access is granted to the value + */ export const simple_rbac_acdf: RBAC_ACDF = ( ctx: Context, @@ -101,6 +117,26 @@ const simple_rbac_acdf: RBAC_ACDF = ( }; // TODO: Log invalid hashes and such so admins can know if they are locked out of values. + +/** + * @summary Rule-Based Access Control Decision Function + * @description + * + * This function evaluates access to an attribute value according to + * Rule-Based Access Control (RBAC). + * + * @param ctx The context object + * @param assn The association + * @param target The target object + * @param label The security label + * @param attributeType the attribute type to which authorization is requested + * @param value The attribute value to which authorization is requested + * @param contexts The contexts associated with the attribute value + * @param permissions The permissions requested on the attribute value + * @returns A `boolean` indicating whether access is granted to the value + * + * @function + */ export function rbacACDF ( ctx: Context, assn: ClientAssociation, // This has a clearance field. diff --git a/apps/meerkat/src/app/pki/isCertInTrustAnchor.ts b/apps/meerkat/src/app/pki/isCertInTrustAnchor.ts index fd9f0a193..749d0234d 100644 --- a/apps/meerkat/src/app/pki/isCertInTrustAnchor.ts +++ b/apps/meerkat/src/app/pki/isCertInTrustAnchor.ts @@ -6,6 +6,21 @@ import { } from "@wildboar/tal/src/lib/modules/TrustAnchorInfoModule/TrustAnchorChoice.ta"; import { DER } from "asn1-ts/dist/node/functional"; +/** + * @summary Determine whether a certificate matches a trust anchor + * @description + * + * This function determines whether a public key certificate matches a trust + * anchor. + * + * @param cert The certificate that may or may not be a trust anchor + * @param trust_anchor A single trust anchor + * @param certBytes The raw bytes of the certificate + * @returns A `boolean` indicating whether the asserted certificate matches the + * trust anchor. + * + * @function + */ export function isCertInTrustAnchor ( cert: Certificate, diff --git a/apps/meerkat/src/app/pki/verifyAttrCertPath.ts b/apps/meerkat/src/app/pki/verifyAttrCertPath.ts index 9ccb16026..609c52ab3 100644 --- a/apps/meerkat/src/app/pki/verifyAttrCertPath.ts +++ b/apps/meerkat/src/app/pki/verifyAttrCertPath.ts @@ -1029,6 +1029,24 @@ async function verifyAttrCertPath ( // Ensure indirectIssuer and issuedOnBehalfOf extensions. // This just verifies a single attribute certificate. + +/** + * @summary Verify a single attribute certificate + * @description + * + * This function verifies a single attribute certificate. It does not support + * indirect issuance: it does not check the delegation path where an SOA issues + * an AA beneath it to serve as an indirect issuer. + * + * @param ctx The context object + * @param acert The attribute certificate being verified + * @param userPkiPath The user PKI path + * @param soas The trust anchors that can serve as SOAs + * @returns A promise resolving to a return code + * + * @async + * @function + */ export async function verifyAttrCert ( ctx: MeerkatContext, From 6ab5f025e5e2e8b123c028d41d11f0d84da02dfe Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Tue, 22 Aug 2023 08:00:16 -0400 Subject: [PATCH 26/39] chore: generalize checkOCSP to also support attr certs --- .../meerkat/src/app/pki/verifyAttrCertPath.ts | 96 +++---------------- apps/meerkat/src/app/pki/verifyCertPath.ts | 41 ++++++-- 2 files changed, 46 insertions(+), 91 deletions(-) diff --git a/apps/meerkat/src/app/pki/verifyAttrCertPath.ts b/apps/meerkat/src/app/pki/verifyAttrCertPath.ts index 609c52ab3..fe469c27f 100644 --- a/apps/meerkat/src/app/pki/verifyAttrCertPath.ts +++ b/apps/meerkat/src/app/pki/verifyAttrCertPath.ts @@ -90,11 +90,13 @@ import { import { Extension } from "@wildboar/x500/src/lib/modules/AuthenticationFramework/Extension.ta"; import { _encode_AlgorithmIdentifier } from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/AlgorithmIdentifier.ta"; import { TBSAttributeCertificate, _encode_TBSAttributeCertificate } from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/TBSAttributeCertificate.ta"; -import assert from "assert"; -import { id_ad_ocsp } from "@wildboar/x500/src/lib/modules/PkiPmiExternalDataTypes/id-ad-ocsp.va"; -import { SignFunction, getOCSPResponse } from "@wildboar/ocsp-client"; -import { generateSignature } from "./generateSignature"; -import { VOR_RETURN_OK, VOR_RETURN_REVOKED, VOR_RETURN_UNKNOWN_INTOLERABLE, verifyOCSPResponse } from "./verifyOCSPResponse"; +import { + checkOCSP, + VCP_RETURN_OCSP_REVOKED, + VCP_RETURN_OCSP_OTHER, + VCP_RETURN_CRL_REVOKED, + VCP_RETURN_CRL_UNREACHABLE, +} from "./verifyCertPath"; export const VAC_OK: number = 0; export const VAC_NOT_BEFORE: number = -1; @@ -123,6 +125,10 @@ export const VAC_INVALID_EXT_CRIT: number = -25; export const VAC_CRL_REVOKED: number = -26; export const VAC_OCSP_OTHER: number = -27; export const VAC_OCSP_REVOKED: number = -28; +export const VAC_RETURN_OCSP_REVOKED: number = VCP_RETURN_OCSP_REVOKED; +export const VAC_RETURN_OCSP_OTHER: number = VCP_RETURN_OCSP_OTHER; +export const VAC_RETURN_CRL_REVOKED: number = VCP_RETURN_CRL_REVOKED; +export const VAC_RETURN_CRL_UNREACHABLE: number = VCP_RETURN_CRL_UNREACHABLE; export const supportedExtensions: Set = new Set([ @@ -346,84 +352,6 @@ function isRevokedFromConfiguredCRLs ( )); } -export -async function checkOCSP ( - ctx: MeerkatContext, - ext: Extension, - issuer: [ Name, SubjectPublicKeyInfo ], - cert: AttributeCertificate, - options: OCSPOptions, -): Promise { - assert(ext.extnId.isEqualTo(authorityInfoAccess["&id"]!)); - const aiaEl = new DERElement(); - aiaEl.fromBytes(ext.extnValue); - const aiaValue = authorityInfoAccess.decoderFor["&ExtnType"]!(aiaEl); - const ocspEndpoints: GeneralName[] = aiaValue - .filter((ad) => ad.accessMethod.isEqualTo(id_ad_ocsp)) - .map((ad) => ad.accessLocation); - const signFunction: SignFunction | undefined = options.ocspSignRequests - ? (data: Uint8Array) => { - const key = ctx.config.signing.key; - const certPath = ctx.config.signing.certPath; - if (!key || !certPath?.length) { - return null; - } - const sig = generateSignature(key, data); - if (!sig) { - return null; - } - const [ algid, sigValue ] = sig; - return [ certPath, algid, sigValue ]; - } - : undefined; - let requestBudget: number = options.maxOCSPRequestsPerCertificate; - for (const gn of ocspEndpoints) { - if (!("uniformResourceIdentifier" in gn)) { - continue; - } - const url = new URL(gn.uniformResourceIdentifier); - if (!url.protocol.toLowerCase().startsWith("http")) { - continue; - } - if (requestBudget === 0) { - break; - } - requestBudget--; - const ocspResponse = await getOCSPResponse( - url, - [ - issuer[0].rdnSequence, - issuer[1], - cert.toBeSigned.serialNumber, - ], - undefined, - (options.ocspTimeout * 1000), - signFunction, - options.ocspResponseSizeLimit, - ); - if (!ocspResponse) { - return VAC_OCSP_OTHER; - } - const { res } = ocspResponse; - const verifyResult = await verifyOCSPResponse(ctx, res); - if (verifyResult === VOR_RETURN_OK) { - return VAC_OK; - } else if (verifyResult === VOR_RETURN_REVOKED) { - return VAC_OCSP_REVOKED; - } else if (verifyResult === VOR_RETURN_UNKNOWN_INTOLERABLE) { - return VAC_OCSP_OTHER; - } else { - continue; // Just to be explicit. - } - } - /** - * Even if we exhaust all endpoints, we return an "OK" so that outages of - * OCSP endpoints do not make TLS impossible. - */ - return VAC_OK; -} - - async function hydrate_attr_cert_path_arc (arc: ACPathData): Promise { // TODO: ~~If no certificate, fetch the certificate from the~~ // Actually, nevermind. There is no efficient way to find the cert if not specified. @@ -1316,7 +1244,7 @@ async function verifyAttrCert ( ctx, aiaExt, [ issuerName, spki ], - acert, + acert.toBeSigned.serialNumber, ctx.config.tls, ); if (ocspResult) { diff --git a/apps/meerkat/src/app/pki/verifyCertPath.ts b/apps/meerkat/src/app/pki/verifyCertPath.ts index 8395fd68e..a20fc116c 100644 --- a/apps/meerkat/src/app/pki/verifyCertPath.ts +++ b/apps/meerkat/src/app/pki/verifyCertPath.ts @@ -224,10 +224,10 @@ type Box = { export type VCPReturnCode = number; export const VCP_RETURN_OK: VCPReturnCode = 0; export const VCP_RETURN_INVALID_SIG: VCPReturnCode = -1; -export const VCP_RETURN_OCSP_REVOKED: VCPReturnCode = -2; -export const VCP_RETURN_OCSP_OTHER: VCPReturnCode = -3; // Unreachable, Unauthorized, etc. -export const VCP_RETURN_CRL_REVOKED: VCPReturnCode = -4; -export const VCP_RETURN_CRL_UNREACHABLE: VCPReturnCode = -5; +// export const VCP_RETURN_OCSP_REVOKED: VCPReturnCode = -2; +// export const VCP_RETURN_OCSP_OTHER: VCPReturnCode = -3; // Unreachable, Unauthorized, etc. +// export const VCP_RETURN_CRL_REVOKED: VCPReturnCode = -4; +// export const VCP_RETURN_CRL_UNREACHABLE: VCPReturnCode = -5; export const VCP_RETURN_MALFORMED: VCPReturnCode = -6; export const VCP_RETURN_BAD_KEY_USAGE: VCPReturnCode = -7; export const VCP_RETURN_BAD_EXT_KEY_USAGE: VCPReturnCode = -8; @@ -248,6 +248,12 @@ export const VCP_RETURN_POLICY_NOT_ACCEPTABLE: VCPReturnCode = -22; export const VCP_RETURN_NO_AUTHORIZED_POLICIES: VCPReturnCode = -23; export const VCP_RETURN_NO_BASIC_CONSTRAINTS_CA: VCPReturnCode = -24; +// The -100s are shared between verifyCertPath and verifyAttrCert. +export const VCP_RETURN_OCSP_REVOKED: VCPReturnCode = -102; +export const VCP_RETURN_OCSP_OTHER: VCPReturnCode = -103; // Unreachable, Unauthorized, etc. +export const VCP_RETURN_CRL_REVOKED: VCPReturnCode = -104; +export const VCP_RETURN_CRL_UNREACHABLE: VCPReturnCode = -105; + export const supportedExtensions: Set = new Set([ subjectDirectoryAttributes["&id"]!.toString(), // TODO: If critical, at least one attr must be understood. @@ -680,15 +686,36 @@ const sigAlgOidToNodeJSDigest: Map = new Map([ [ id_Ed25519.toString(), null ], ]); +/** + * @summary Check the OCSP status of a certificate + * @description + * + * This function sends requests to OCSP responders in the authorityInfoAccess + * extension to determine if a certificate is still valid. It's signature may + * seem weird, but it is purposefully designed to be general enough to be used + * for checking OCSP for public key certificates and attribute certificates. + * + * @param ctx The context object + * @param ext The authorityInfoAccess extension in the subject certificate + * @param issuer The issuer's Name and SubjectPublicKeyInfo + * @param serialNumber The subject's serial number + * @param options OCSP-related options + * @returns A promise resolving to an return code. + * + * @async + * @function + */ export async function checkOCSP ( ctx: MeerkatContext, ext: Extension, issuer: [ Name, SubjectPublicKeyInfo ], - subjectCert: Certificate, + serialNumber: Uint8Array, options: OCSPOptions, ): Promise { - assert(ext.extnId.isEqualTo(authorityInfoAccess["&id"]!)); + if (!ext.extnId.isEqualTo(authorityInfoAccess["&id"]!)) { + return VCP_RETURN_OCSP_OTHER; + } const aiaEl = new DERElement(); aiaEl.fromBytes(ext.extnValue); const aiaValue = authorityInfoAccess.decoderFor["&ExtnType"]!(aiaEl); @@ -728,7 +755,7 @@ async function checkOCSP ( [ issuer[0].rdnSequence, issuer[1], - subjectCert.toBeSigned.serialNumber, + serialNumber, ], undefined, (options.ocspTimeout * 1000), From 015aedebb71845d83cbd2238b7acdd8e2e49d642 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Tue, 22 Aug 2023 08:00:28 -0400 Subject: [PATCH 27/39] fix: correctly verify remote CRLs --- apps/meerkat/src/app/pki/verifyCertPath.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/meerkat/src/app/pki/verifyCertPath.ts b/apps/meerkat/src/app/pki/verifyCertPath.ts index a20fc116c..c8da7eadc 100644 --- a/apps/meerkat/src/app/pki/verifyCertPath.ts +++ b/apps/meerkat/src/app/pki/verifyCertPath.ts @@ -818,8 +818,14 @@ async function checkRemoteCRLs ( if (ctx.config.signing.disableAllSignatureVerification) { return VCP_RETURN_CRL_REVOKED; } - const bytes = crl.originalDER // FIXME: This is incorrect. - ?? _encode_CertificateList(crl, DER).toBytes(); + const bytes = crl.originalDER + ? (() => { + const el = new DERElement(); + el.fromBytes(crl.originalDER); + const tbs = el.sequence[0]; + return tbs.toBytes(); + })() + : _encode_CertificateListContent(crl.toBeSigned, DER).toBytes(); const sigValue = packBits(crl.signature); const signatureIsValid: boolean | undefined = verifySignature( bytes, From 11dc6ab81b593ed1b8d9dd37c2ec130566c726fa Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Tue, 22 Aug 2023 08:00:48 -0400 Subject: [PATCH 28/39] docs: JSDoc for everything in verifyCertPath.ts --- apps/meerkat/src/app/pki/verifyCertPath.ts | 361 ++++++++++++++++++++- 1 file changed, 346 insertions(+), 15 deletions(-) diff --git a/apps/meerkat/src/app/pki/verifyCertPath.ts b/apps/meerkat/src/app/pki/verifyCertPath.ts index c8da7eadc..8d137999c 100644 --- a/apps/meerkat/src/app/pki/verifyCertPath.ts +++ b/apps/meerkat/src/app/pki/verifyCertPath.ts @@ -135,7 +135,7 @@ import { OperationDispatcher } from "../distributed/OperationDispatcher"; import type { MeerkatContext } from "../ctx"; import getOptionallyProtectedValue from "@wildboar/x500/src/lib/utils/getOptionallyProtectedValue"; import { - CertificateList, _encode_CertificateList, + CertificateList, _encode_CertificateList, _encode_CertificateListContent, } from "@wildboar/x500/src/lib/modules/AuthenticationFramework/CertificateList.ta"; import { KeyUsage, @@ -254,6 +254,12 @@ export const VCP_RETURN_OCSP_OTHER: VCPReturnCode = -103; // Unreachable, Unauth export const VCP_RETURN_CRL_REVOKED: VCPReturnCode = -104; export const VCP_RETURN_CRL_UNREACHABLE: VCPReturnCode = -105; +/** + * This set contains the extensions that Meerkat DSA observes when processing + * the certification path. Essentially, if an extension is encountered with its + * `critical` field set to `TRUE` and it is not in this set, Meerkat DSA does + * not know how to handle that critical extension and MUST fail the validation. + */ export const supportedExtensions: Set = new Set([ subjectDirectoryAttributes["&id"]!.toString(), // TODO: If critical, at least one attr must be understood. @@ -280,6 +286,10 @@ const supportedExtensions: Set = new Set([ subjectInfoAccess["&id"]!.toString(), // Always non-critical ]); +/** + * Where extensions are defined to have a specification-mandated criticality, + * this map indicates what the criticality value should be for each extension. + */ export const extensionMandatoryCriticality: Map = new Map([ [ subjectKeyIdentifier["&id"]!.toString(), false ], // Always non-critical. @@ -292,6 +302,13 @@ const extensionMandatoryCriticality: Map = new Map([ const ietfUserNoticeOID: OBJECT_IDENTIFIER = new ObjectIdentifier([ 1, 3, 6, 1, 5, 5, 7, 2, 2 ]); +/** + * @summary Convert `DisplayText` to a `string` + * @param dt Converts `DisplayText` to a `string`. + * @returns The string representation of the `DisplayText` + * + * @function + */ function displayTextToString (dt: ASN1Element): string | null { if (dt.tagClass !== ASN1TagClass.universal) { return null; @@ -313,6 +330,13 @@ function displayTextToString (dt: ASN1Element): string | null { } } +/** + * Meant to mirror the policy tree-related types from OpenSSL's + * `crypto/x509/pcy_local.h` and using the algorithm described in IETF RFC 5280, + * since the description of the algorithm for evaluating certification policy + * compliance in ITU-T Recommendation X.509 (2019), section 12, is absolutely + * obtuse. + */ interface ValidPolicyData { // flags valid_policy: OBJECT_IDENTIFIER; @@ -320,12 +344,30 @@ interface ValidPolicyData { expected_policy_set: Set; } +/** + * Meant to mirror the policy tree-related types from OpenSSL's + * `crypto/x509/pcy_local.h` and using the algorithm described in IETF RFC 5280, + * since the description of the algorithm for evaluating certification policy + * compliance in ITU-T Recommendation X.509 (2019), section 12, is absolutely + * obtuse. + */ interface ValidPolicyNode extends ValidPolicyData { // data handled by the above "extends" parent?: ValidPolicyNode; + + /** + * The number of direct child nodes. + */ nchild: number; } +/** + * Meant to mirror the policy tree-related types from OpenSSL's + * `crypto/x509/pcy_local.h` and using the algorithm described in IETF RFC 5280, + * since the description of the algorithm for evaluating certification policy + * compliance in ITU-T Recommendation X.509 (2019), section 12, is absolutely + * obtuse. + */ interface ValidPolicyLevel { cert: Certificate; nodes: ValidPolicyNode[]; @@ -333,7 +375,13 @@ interface ValidPolicyLevel { // flags? } -// Meant to mirror the policy tree-related types from OpenSSL's crypto/x509/pcy_local.h. +/** + * Meant to mirror the policy tree-related types from OpenSSL's + * `crypto/x509/pcy_local.h` and using the algorithm described in IETF RFC 5280, + * since the description of the algorithm for evaluating certification policy + * compliance in ITU-T Recommendation X.509 (2019), section 12, is absolutely + * obtuse. + */ interface ValidPolicyTree { levels: ValidPolicyLevel[]; anyPolicy: boolean; @@ -472,6 +520,19 @@ interface VerifyCertPathArgs { readonly initial_required_name_forms: NAME_FORM["&id"][]; } +/** + * @summary Get a function that performs a `read` operation + * @description + * + * This is a higher-order function that returns an async function that takes + * a `ReadArgument`, performs the X.500 `read` operation using it, and returns + * the `ReadResult`. + * + * @param ctx The context object + * @returns An async function that performs a `read` operation + * + * @function + */ export function getReadDispatcher (ctx: MeerkatContext): ReadDispatcherFunction { return async ( @@ -483,6 +544,19 @@ function getReadDispatcher (ctx: MeerkatContext): ReadDispatcherFunction { }; } +/** + * @summary Create a `VerifyCertPathResult` failure result + * @description + * + * This function creates a `VerifyCertPathResult` failure result from just a + * return code. It exists just to fill in all the other fields of the result + * with null-ish values. + * + * @param returnCode The return code to instantiate the `returnCode` field + * @returns A `VerifyCertPathResult` + * + * @function + */ export function verifyCertPathFail (returnCode: number): VerifyCertPathResult { return { @@ -569,6 +643,12 @@ interface VerifyCertPathResult { readonly userNotices: string[]; } +/** + * Values of the variable defined in ITU-T Recommendation X.509 (2019), Section + * 12.3.i. + * + * @interface + */ export interface PendingConstraint { pending: boolean; @@ -634,20 +714,55 @@ interface VerifyCertPathState { */ valid_policy_tree: ValidPolicyTree | null; + /** + * The permitted subtrees granted by name constraints in the + * `nameConstraints` extension. + */ permitted_subtrees: GeneralSubtrees; + /** + * The excluded subtress forbidden by name constraints in the + * `nameConstraints` extension. + */ excluded_subtrees: GeneralSubtrees; + /** + * The required name forms. + */ required_name_forms: NAME_FORM["&id"][]; + /** + * Indicates whether an acceptable policy needs to be explicitly identified + * in every public-key certificate in the path. + */ explicit_policy_indicator: boolean; + /** + * An integer equal to one more than the number of public-key certificates + * in the certification path for which processing has been completed. + */ path_depth: number; + /** + * Indicates whether policy mapping is inhibited. + */ policy_mapping_inhibit_indicator: boolean; + /** + * Indicates whether the special value anyPolicy is considered a match for + * any specific certificate policy. + */ inhibit_any_policy_indicator: boolean; + /** + * Details of explicit-policy inhibit-policy-mapping and/or + * inhibit-any-policy constraints which have been stipulated but have yet to + * take effect. There are three one-bit indicators called + * explicit-policy-pending, policy-mapping-inhibit-pending and + * inhibit-any-policy-pending together with, for each, an integer called + * skip-certificates which gives the number of public-key certificates yet + * to skip before the constraint takes effect. + */ pending_constraints: { explicit_policy: PendingConstraint; @@ -658,16 +773,36 @@ interface VerifyCertPathState { }; + /** + * What key usages are granted to the end-entity, given by the + * `keyUsage` extension. + */ endEntityKeyUsage?: KeyUsage; + /** + * What extended key usages are granted to the end-entity, given by the + * `extKeyUsage` extension. + */ endEntityExtKeyUsage?: KeyPurposeId[]; + /** + * The notBefore time of the end entity's private key, given by the + * `privateKeyUsagePeriod` extension. + */ endEntityPrivateKeyNotBefore?: GeneralizedTime; + /** + * The notAfter time of the end entity's private key, given by the + * `privateKeyUsagePeriod` extension. + */ endEntityPrivateKeyNotAfter?: GeneralizedTime; } +/** + * A mapping of the signature algorithm object identifiers to their NodeJS + * digest algorithm string identifiers. + */ const sigAlgOidToNodeJSDigest: Map = new Map([ [ sha1WithRSAEncryption.toString(), "sha1" ], [ sha224WithRSAEncryption.toString(), "sha224" ], @@ -784,6 +919,24 @@ async function checkOCSP ( return VCP_RETURN_OK; } +/** + * @summary Check a certificate's revocation status among remote CRLs. + * @description + * + * This function issues HTTP, FTP, LDAP, and DAP requests, etc. to obtain remote + * CRLs to check if a certificate is revoked in said CRLs. + * + * @param ctx The context object + * @param ext The cRLDistributionPoints extension + * @param serialNumber The serial number of the certificate to be checked + * @param issuer The issuer's Name and SubjectPublicKeyInfo + * @param readDispatcher A function that dispatches a local `read` request + * @param options CRL options + * @returns A promise that resolves to a return code. + * + * @async + * @function + */ export async function checkRemoteCRLs ( ctx: MeerkatContext, @@ -846,6 +999,20 @@ async function checkRemoteCRLs ( // We check validity time first just because we do not want to verify the // digital signature (it is computationally expensive) if we do not have to. +/** + * @summary Check whether a point in time falls between a certificate's validity times. + * @description + * + * This function checks whether an asserted point in time--the `asOf` time--falls + * between a certificate's validity `notBefore` and `notAfter` times. + * + * @param cert The certificate whose validity time is to be checked. + * @param asOf The point in time against which the validity time is to be checked. + * @returns A `boolean` indicating whether the `asOf` time falls within the + * certificate's validity times. + * + * @function + */ export function certIsValidTime (cert: Certificate, asOf: Date): boolean { const notBefore = getDateFromTime(cert.toBeSigned.validity.notBefore); @@ -859,6 +1026,21 @@ function certIsValidTime (cert: Certificate, asOf: Date): boolean { return true; } +/** + * @summary Verify a generic digital signature over raw bytes + * @description + * + * This function verifies a digital signature over raw bytes of data. + * + * @param bytes The raw bytes that are to be verified + * @param alg The algorithm identifier of the signature + * @param sigValue The signature value + * @param keyOrSPKI The key object or SubjectPublicKeyInfo + * @returns A `boolean` indicating whether the signature is valid or not, or + * `undefined` if it cannot be determined (e.g. unrecognized algorithm). + * + * @function + */ export function verifySignature ( bytes: Uint8Array, @@ -968,9 +1150,16 @@ function verifyAltSignature (subjectCert: Certificate, issuerCert: Certificate): } /** + * @summary Verify the signature on a subject's certificate, given an issuer certificate + * @description * - * @param subjectCert - * @param issuerCert + * This signature verifies that the asserted subject's certificate was signed + * by the issuer identified in the issuer certificate. + * + * @param subjectCert The subject certificate + * @param issuerCert The issuer certificate + * + * @function */ export function verifyNativeSignature (subjectCert: Certificate, issuerCert: Certificate): boolean | undefined { @@ -1005,6 +1194,21 @@ function verifyNativeSignature (subjectCert: Certificate, issuerCert: Certificat ); } +/** + * @summary Check if a certificate is revoked in locally-configured CRLs. + * @description + * + * This function checks if a certificate has been revoked (by having its + * serial number present) in one of the locally-configured CRLs. + * + * @param ctx The context object + * @param cert The certificate + * @param asOf The point-in-time to check revocation + * @param options Options pertaining to CRL checking + * @returns A `boolean` indicating whether the certificate is revoked in any CRL. + * + * @function + */ export function isRevokedFromConfiguredCRLs ( ctx: Context, @@ -1035,7 +1239,22 @@ function isRevokedFromConfiguredCRLs ( )); } -// Section 12.5.1 +/** + * @summary Verify a public key certificate per ITU-T Rec. X.509, Section 12.5.1 + * @description + * + * This function is an implementation of the public key certificate verification + * procedures described in ITU-T Recommendation X.509 (2019), Section 12.5.1. + * + * @param ctx The context object + * @param state The certification path processing state + * @param subjectCert The subject certificate + * @param issuerCert The issuer certificate + * @param subjectIndex ? + * @param readDispatcher An async function that dispatches a local `read` request + * @param options Options pertaining to signing + * @returns A promise that resolves to a return code + */ export async function verifyBasicPublicKeyCertificateChecks ( ctx: MeerkatContext, @@ -1367,7 +1586,7 @@ async function verifyBasicPublicKeyCertificateChecks ( issuerCert.toBeSigned.subject, issuerCert.toBeSigned.subjectPublicKeyInfo, ], - subjectCert, + subjectCert.toBeSigned.serialNumber, ctx.config.tls, ); if (ocspResult) { @@ -1453,7 +1672,20 @@ async function verifyBasicPublicKeyCertificateChecks ( return VCP_RETURN_OK; } -// Section 12.5.2 +/** + * @summary Verify a public key certificate per ITU-T Rec. X.509, Section 12.5.2 + * @description + * + * This function is an implementation of the public key certificate verification + * procedures described in ITU-T Recommendation X.509 (2019), Section 12.5.2. + * + * @param ctx The context object + * @param state The certification path processing state + * @param subjectCert The subject certificate + * @returns A new certification path processing state + * + * @function + */ export function processIntermediateCertificates ( ctx: Context, @@ -1654,7 +1886,21 @@ function processIntermediateCertificates ( }; } -// Section 12.5.3 +/** + * @summary Verify a public key certificate per ITU-T Rec. X.509, Section 12.5.3 + * @description + * + * This function is an implementation of the public key certificate verification + * procedures described in ITU-T Recommendation X.509 (2019), Section 12.5.3. + * + * @param ctx The context object + * @param state The certification path processing state + * @param subjectCert The subject certificate + * @param subjectIndex ? + * @returns A new certification path processing state + * + * @function + */ export function processExplicitPolicyIndicator ( ctx: Context, @@ -1719,6 +1965,24 @@ function processExplicitPolicyIndicator ( return state; } +/** + * @summary Verify an end-entity certificate + * @description + * + * This function verifies an end-entity public key certificate in a + * certification path. + * + * @param ctx The context object + * @param state The certification path processing state + * @param subjectCert The subject certificate + * @param issuerCert The issuer certificate + * @param readDispatcher An async function that dispatches a local `read` request + * @param options Options pertaining to signing + * @returns A promise resolving to a new certification path processing state + * + * @async + * @function + */ export async function verifyEndEntityCertificate ( ctx: MeerkatContext, @@ -1746,6 +2010,25 @@ async function verifyEndEntityCertificate ( return state; } +/** + * @summary Verify an intermediate certificate + * @description + * + * This function verifies an intermediate public key certificate in a + * certification path. + * + * @param ctx The context object + * @param state The certification path processing state + * @param subjectCert The subject certificate + * @param issuerCert The issuer certificate + * @param subjectIndex ? + * @param readDispatcher An async function that dispatches a local `read` request + * @param options Options pertaining to signing + * @returns A promise resolving to a new certification path processing state + * + * @async + * @function + */ export async function verifyIntermediateCertificate ( ctx: MeerkatContext, @@ -1781,9 +2064,12 @@ async function verifyIntermediateCertificate ( * Interestingly, X.509 does not say to do any validation of trust anchors... * We are still going to do a little anyway. * - * @param ctx - * @param cert - * @returns + * @param ctx The context object + * @param cert The certificate that may or may not be a trust anchor + * @returns A promise resolving to a return code + * + * @async + * @function */ export async function verifyCACertificate ( @@ -1904,7 +2190,7 @@ async function verifyCACertificate ( cert.toBeSigned.subject, cert.toBeSigned.subjectPublicKeyInfo, ], - cert, + cert.toBeSigned.serialNumber, ctx.config.signing, ); if (ocspResult) { @@ -1933,6 +2219,23 @@ async function verifyCACertificate ( } // From OpenSSL in `crypto/x509/pcy_tree.c`. +/** + * @summary Calculate the set of valid user certificate policies. + * @description + * + * I basically just paraphrased this function from OpenSSL's + * `crypto/x509/pcy_tree.c`, which is an implementation of the algorithm in + * IETF RFC 5280, since the description of the algorithm for evaluating + * certification policy compliance in ITU-T Recommendation X.509 (2019), section + * 12, is absolutely obtuse. + * + * @param tree The valid policy tree + * @param policy_oids The initial-policy-set + * @param auth_nodes The valid policy nodes + * @returns A set of new valid policy nodes + * + * @function + */ export function tree_calculate_user_set ( tree: ValidPolicyTree, @@ -2005,7 +2308,21 @@ function tree_calculate_user_set ( return userPolicies; } -// From OpenSSL in `crypto/x509/pcy_tree.c`. +/** + * @summary Calculate the authority set of certificate policies + * @description + * + * I basically just paraphrased this function from OpenSSL's + * `crypto/x509/pcy_tree.c`, which is an implementation of the algorithm in + * IETF RFC 5280, since the description of the algorithm for evaluating + * certification policy compliance in ITU-T Recommendation X.509 (2019), section + * 12, is absolutely obtuse. + * + * @param tree The valid policy tree + * @param pnodes The valid policy nodes + * + * @function + */ export function tree_calculate_authority_set ( tree: ValidPolicyTree, @@ -2136,7 +2453,9 @@ function tree_calculate_authority_set ( pnodes.ref = tree.auth_policies; } -// From OpenSSL in `crypto/x509/pcy_lib.c`. +/** + * From OpenSSL in `crypto/x509/pcy_lib.c`. + */ export function X509_policy_tree_get0_user_policies ( tree: ValidPolicyTree | null | undefined, @@ -2152,7 +2471,19 @@ function X509_policy_tree_get0_user_policies ( } } -// ITU Recommendation X.509, Section 12.5.4. +/** + * @summary Verify a public key certificate per ITU-T Rec. X.509, Section 12.5.4 + * @description + * + * This function is an implementation of the public key certificate verification + * procedures described in ITU-T Recommendation X.509 (2019), Section 12.5.4. + * + * @param args The original arguments object to `verifyCertPath` + * @param state The certification path processing state + * @returns A `verifyCertPath` result. + * + * @function + */ export function finalProcessing ( args: VerifyCertPathArgs, From 53ae6b3335e9b0c5225a4f52df0375fa5607b4b1 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Tue, 22 Aug 2023 08:00:59 -0400 Subject: [PATCH 29/39] docs: RBAC access to entries --- apps/meerkat-docs/docs/authorization.md | 20 +++++++++++++++++++- apps/meerkat-docs/docs/deviations-nuances.md | 8 ++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/apps/meerkat-docs/docs/authorization.md b/apps/meerkat-docs/docs/authorization.md index e74fd373a..38da0133c 100644 --- a/apps/meerkat-docs/docs/authorization.md +++ b/apps/meerkat-docs/docs/authorization.md @@ -208,6 +208,17 @@ DSA must use a means for mapping a security policy identifier (which is an object identifier) to a function that is used to compare the user's clearance with the security label. +### Controlling Access to Entries + +The X.500 specifications state that access to a given entry is denied under +Rule-Based Access Control when access to all attribute values is denied. +However, enforcing this would be devastating from a performance perspective. +When performing a `list` operation, Meerkat DSA would have to check what might +be thousands of attributes per entry. Instead, Meerkat DSA denies access to +an entry if access to any of its distinguished values are denied. This is much +faster, since usually only one single value is evaluated, and it is technically +more strict from a security perspective. + ### Where Clearances Come From Clearances may be associated with a user in three ways: @@ -250,7 +261,7 @@ signing. For the sake of easy use of the Rule-Based Access Control (RBAC), Meerkat DSA comes with a security policy built-in, called the "simple security policy." -It's object identifier is `1.3.6.1.4.1.56490.403.1. This security policy does +It's object identifier is `1.3.6.1.4.1.56490.403.1`. This security policy does nothing with security categories, and permits access to the labeled attribute value if the clearance level is greater than or equal to the clearance level required by the labeled attribute value. Unless you plan to make use of security @@ -264,6 +275,13 @@ absence of information, but it may also indicate that the labeled thing is not important enough to have labeled properly in the first place, hence, it lies between total declassification and the "restricted" classification. +:::caution + +The Simple Security Policy does not treat reads and writes differently: if +access is granted to read an entry, access is also granted to modify an entry. + +::: + :::note If no security policy is listed in the security label or clearance, it defaults diff --git a/apps/meerkat-docs/docs/deviations-nuances.md b/apps/meerkat-docs/docs/deviations-nuances.md index 84a888483..f542d2605 100644 --- a/apps/meerkat-docs/docs/deviations-nuances.md +++ b/apps/meerkat-docs/docs/deviations-nuances.md @@ -262,6 +262,14 @@ noted below are nuances in Meerkat DSA: shadowing agreement, but this is used generally just to indicate where the master DSA can be reached. As such, Meerkat DSA will assume that, if this field is present, the correspondent DSA is _not_ the master. +- The X.500 specifications state that access to a given entry is denied under + Rule-Based Access Control when access to all attribute values is denied. + However, enforcing this would be devastating from a performance perspective. + When performing a `list` operation, Meerkat DSA would have to check what might + be thousands of attributes per entry. Instead, Meerkat DSA denies access to an + entry if access to any of its distinguished values are denied. This is much + faster, since usually only one single value is evaluated, and it is + technically more strict from a security perspective. ## The "Never Contributing" Bug From 0de8e7fb81965f26238ca23fc812357f6fcb3210 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Tue, 22 Aug 2023 08:13:20 -0400 Subject: [PATCH 30/39] docs: finish JSDoc in verifyAttrCertPath.ts --- .../meerkat/src/app/pki/verifyAttrCertPath.ts | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/apps/meerkat/src/app/pki/verifyAttrCertPath.ts b/apps/meerkat/src/app/pki/verifyAttrCertPath.ts index fe469c27f..630364934 100644 --- a/apps/meerkat/src/app/pki/verifyAttrCertPath.ts +++ b/apps/meerkat/src/app/pki/verifyAttrCertPath.ts @@ -150,6 +150,20 @@ const extensionMandatoryCriticality: Map = new Map([ ]); // TODO: Move this to @wildboar/x500 +/** + * @summary Compare two `IssuerSerial`s + * @description + * + * This function compares two `IssuerSerial`s for equality, returning `true` if + * they match. + * + * @param ctx The context object + * @param a An IssuerSerial + * @param b The other `IssuerSerial` + * @returns A `boolean` indicating whether the two `IssuerSerial`s match. + * + * @function + */ function compare_issuer_serial (ctx: Context, a: IssuerSerial, b: IssuerSerial): boolean { if (!Buffer.compare(a.serial, b.serial)) { return false; @@ -176,6 +190,15 @@ function compare_issuer_serial (ctx: Context, a: IssuerSerial, b: IssuerSerial): return true; } +/** + * @summary Create an `IssuerSerial` from a public key certificate + * @description + * + * This function creates an `IssuerSerial` from a public key certificate. + * + * @param cert A public key certificate whose `IssuerSerial` is to be determined. + * @returns An `IssuerSerial` for the certificate + */ function get_issuer_serial_from_cert (cert: Certificate): IssuerSerial { return new IssuerSerial( [ @@ -188,6 +211,22 @@ function get_issuer_serial_from_cert (cert: Certificate): IssuerSerial { ); } +/** + * @summary Determine whether a general name matches a public key certificate + * @description + * + * This function returns `true` if an asserted `GeneralName` matches a public + * key certificate. In addition to the `subject` field, this function also + * checks the subject alternative names, if there are any. + * + * @param ctx The context object + * @param cert The public key certificate + * @param gn The general name to check against the public key certificate + * @param alt_names Any alternative names (to avoid recalculating this between iterations) + * @returns A `boolean` indicating whether the general name matches the certificate + * + * @function + */ function general_name_matches_cert ( ctx: Context, cert: Certificate, @@ -219,6 +258,20 @@ function general_name_matches_cert ( } // TODO: Replace some code below with this. +/** + * @summary Check whether an object digest matches a certificate + * @description + * + * This function checks whether an `ObjectDigestInfo` refers to the asserted + * certificate. + * + * @param cert The public key certificate whose object digest is to be checked. + * @param odinfo The object digest info + * @returns A `boolean` indicating whether the certificate is identified by the + * object digest info + * + * @function + */ function object_digest_matches_cert (cert: Certificate, odinfo: ObjectDigestInfo): boolean { const hash_str = digestOIDToNodeHash.get(odinfo.digestAlgorithm.algorithm.toString()); if (!hash_str) { @@ -323,6 +376,21 @@ function verifyAltSignature (subjectCert: AttributeCertificate, issuerExts?: Ext return verifySignature(verificand, sigAlg, sigValue, pubkey); } +/** + * @summary Check if a certificate is revoked in locally-configured CRLs. + * @description + * + * This function checks if an attribute certificate has been revoked (by having + * its serial number present) in one of the locally-configured CRLs. + * + * @param ctx The context object + * @param cert The attribute certificate + * @param asOf The point-in-time to check revocation + * @param options Options pertaining to CRL checking + * @returns A `boolean` indicating whether the certificate is revoked in any CRL. + * + * @function + */ function isRevokedFromConfiguredCRLs ( ctx: Context, cert: AttributeCertificate, @@ -397,6 +465,19 @@ async function hydrate_attr_cert_path ( return path; } +/** + * @summary Check if a public key certificate is the holder of an attribute certificate + * @description + * + * This function checks if the `holder` field of an attribute certificate refers + * to the asserted public key certificate. + * + * @param ctx The context object + * @param eeCert The end-entity certificate + * @param holder The `holder` field of an attribute certificate + * @param issuerCert The issuer certificate of the end-entity + * @returns A return code + */ function is_cert_holder ( ctx: Context, eeCert: Certificate, @@ -564,6 +645,21 @@ function is_cert_holder ( return VAC_OK; } +/** + * @summary Determine whether an attribute certificate issuer is trusted. + * @description + * + * This function determines whether an attribute certificate issuer is trusted, + * including checking whether it is an SOA, if it needs to be. + * + * @param ctx The context object + * @param issuer The `AttCertIssuer` + * @param trust_anchor A single trust anchor + * @param soa Whether the trust anchor must be a Source of Authority (SOA) + * @returns A boolean indicating whether the attribute certificate issuer is trusted + * + * @function + */ function isAttrCertIssuerTrusted ( ctx: Context, issuer: AttCertIssuer, @@ -747,6 +843,16 @@ function isAttrCertIssuerTrusted ( // - You pretty much treat the attributes of an attribute certificate as though they were simply added to the subjectDirectoryAttributes // Assume the PKI Path is valid. +/** + * ## Do not use this function. It is not done. + * + * @deprecated This is not fully implemented yet. + * @param ctx + * @param acPath + * @param userPkiPath + * @param soas + * @returns + */ export async function verifyAttrCertPath ( ctx: Context, From 92f3128ac197d75695ad23acc7ae67fc6c9ca3e6 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Tue, 22 Aug 2023 20:43:29 -0400 Subject: [PATCH 31/39] chore: update helm chart to Meerkat 3.1.0 --- k8s/charts/meerkat-dsa/Chart.yaml | 4 +-- k8s/charts/meerkat-dsa/templates/config.yaml | 9 +++++ .../meerkat-dsa/templates/deployment.yaml | 34 +++++++++++++++++++ k8s/charts/meerkat-dsa/values.yaml | 7 +++- 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/k8s/charts/meerkat-dsa/Chart.yaml b/k8s/charts/meerkat-dsa/Chart.yaml index 151181c8d..6a034cd77 100644 --- a/k8s/charts/meerkat-dsa/Chart.yaml +++ b/k8s/charts/meerkat-dsa/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: meerkat-dsa description: X.500 Directory Server (DSA) and LDAP Server by Wildboar Software. type: application -version: 2.15.0 -appVersion: 3.0.0 +version: 2.16.0 +appVersion: 3.1.0 home: https://wildboarsoftware.com keywords: - directory diff --git a/k8s/charts/meerkat-dsa/templates/config.yaml b/k8s/charts/meerkat-dsa/templates/config.yaml index 4be07830d..1fb50efc8 100644 --- a/k8s/charts/meerkat-dsa/templates/config.yaml +++ b/k8s/charts/meerkat-dsa/templates/config.yaml @@ -19,6 +19,9 @@ data: chaining_check_sig: {{ .Values.chaining_check_sig | default true | ternary 1 0 | quote }} chaining_sign_requests: {{ .Values.chaining_sign_requests | default true | ternary 1 0 | quote }} chaining_tls_optional: {{ .Values.chaining_tls_optional | ternary 1 0 | quote }} + {{- if .Values.clearance_authorities }} + clearance_authorities: {{ .Values.clearance_authorities | quote }} + {{- end }} default_entry_ttl: {{ .Values.default_entry_ttl | default "60" | quote }} enable_disp: {{ .Values.enable_disp | ternary 1 0 | quote }} enable_dop: {{ .Values.enable_dop | ternary 1 0 | quote }} @@ -29,6 +32,9 @@ data: enable_dap: {{ .Values.enable_dap | ternary 1 0 | quote }} entries_per_subordinates_page: {{ .Values.entries_per_subordinates_page | default 100 | quote }} forbid_anonymous_bind: {{ .Values.forbid_anonymous_bind | ternary 1 0 | quote }} + get_clearances_from_attr_certs: {{ .Values.get_clearances_from_attr_certs | default true | ternary 1 0 | quote }} + get_clearances_from_dsait: {{ .Values.get_clearances_from_dsait | default true | ternary 1 0 | quote }} + get_clearances_from_pkc: {{ .Values.get_clearances_from_pkc | default true | ternary 1 0 | quote }} honor_cipher_order: {{ .Values.honor_cipher_order | ternary 1 0 | quote }} idm_buffer_size: {{ .Values.idm_buffer_size | default "1000000" | quote }} idm_port: {{ .Values.idm_port | default 4632 | quote }} @@ -50,6 +56,9 @@ data: itot_acse_password: {{ .Values.itot_acse_password | quote }} {{- end }} disable_itot_chaining: {{ .Values.disable_itot_chaining | ternary 1 0 | quote }} + {{- if .Values.labelling_authorities }} + labelling_authorities: {{ .Values.labelling_authorities | quote }} + {{- end }} lcr_parallelism: {{ .Values.lcr_parallelism | default 0 | quote }} ldap_buffer_size: {{ .Values.ldap_buffer_size | default "1000000" | quote }} ldap_port: {{ .Values.ldap_port | default 389 | quote }} diff --git a/k8s/charts/meerkat-dsa/templates/deployment.yaml b/k8s/charts/meerkat-dsa/templates/deployment.yaml index e631cf34b..ee0da4df0 100644 --- a/k8s/charts/meerkat-dsa/templates/deployment.yaml +++ b/k8s/charts/meerkat-dsa/templates/deployment.yaml @@ -173,6 +173,14 @@ spec: name: {{ include "meerkat-dsa.fullname" . }}-config key: chaining_tls_optional optional: true + {{- if .Values.clearance_authorities }} + - name: MEERKAT_CLEARANCE_AUTHORITIES + valueFrom: + configMapKeyRef: + name: {{ include "meerkat-dsa.fullname" . }}-config + key: clearance_authorities + optional: true + {{- end }} - name: MEERKAT_DEFAULT_ENTRY_TTL valueFrom: configMapKeyRef: @@ -221,6 +229,24 @@ spec: name: {{ include "meerkat-dsa.fullname" . }}-config key: forbid_anonymous_bind optional: true + - name: MEERKAT_GET_CLEARANCES_FROM_ATTR_CERTS + valueFrom: + configMapKeyRef: + name: {{ include "meerkat-dsa.fullname" . }}-config + key: get_clearances_from_attr_certs + optional: true + - name: MEERKAT_GET_CLEARANCES_FROM_DSAIT + valueFrom: + configMapKeyRef: + name: {{ include "meerkat-dsa.fullname" . }}-config + key: get_clearances_from_dsait + optional: true + - name: MEERKAT_GET_CLEARANCES_FROM_PKC + valueFrom: + configMapKeyRef: + name: {{ include "meerkat-dsa.fullname" . }}-config + key: get_clearances_from_pkc + optional: true - name: MEERKAT_HONOR_CIPHER_ORDER valueFrom: configMapKeyRef: @@ -293,6 +319,14 @@ spec: name: {{ include "meerkat-dsa.fullname" . }}-config key: disable_itot_chaining optional: true + {{- if .Values.labelling_authorities }} + - name: MEERKAT_LABELLING_AUTHORITIES + valueFrom: + configMapKeyRef: + name: {{ include "meerkat-dsa.fullname" . }}-config + key: labelling_authorities + optional: true + {{- end }} - name: MEERKAT_LCR_PARALLELISM valueFrom: configMapKeyRef: diff --git a/k8s/charts/meerkat-dsa/values.yaml b/k8s/charts/meerkat-dsa/values.yaml index 3f97e6a74..51aabb584 100644 --- a/k8s/charts/meerkat-dsa/values.yaml +++ b/k8s/charts/meerkat-dsa/values.yaml @@ -1,6 +1,6 @@ image: repository: ghcr.io/wildboar-software/meerkat-dsa - pullPolicy: Always # TODO: Change this to IfNotPresent after Beta is over. + pullPolicy: IfNotPresent imagePullSecrets: [] nameOverride: "" fullnameOverride: "" @@ -114,6 +114,7 @@ bulk_insert_mode: false chaining_check_sig: true chaining_sign_requests: true chaining_tls_optional: false +# clearance_authorities: /path/to/trust-anchor-list.pem default_entry_ttl: 60 # ecdh_curves: P-521:P-384:P-256 enable_dap: true @@ -122,6 +123,9 @@ enable_dop: true enable_dsp: true entries_per_subordinates_page: 100 forbid_anonymous_bind: false +get_clearances_from_attr_certs: true +get_clearances_from_dsait: true +get_clearances_from_pkc: true honor_cipher_order: true idm_buffer_size: "1000000" idm_port: 4632 @@ -137,6 +141,7 @@ itot_abort_timeout_in_seconds: 3 itot_max_presentation_contexts: 10 # itot_acse_password: banana disable_itot_chaining: false +# labelling_authorities: /path/to/trust-anchor-list.pem lcr_parallelism: 0 ldap_buffer_size: "1000000" ldap_port: 389 From f1ae2c1cc41d951e79c60df6ebafa3d52d65c66f Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Tue, 22 Aug 2023 20:44:12 -0400 Subject: [PATCH 32/39] chore: version bump to 3.1.0 --- .github/workflows/meerkat.yml | 2 +- apps/meerkat-docs/docs/changelog-meerkat.md | 21 +++++++++++++++++++ apps/meerkat-docs/docs/conformance.md | 4 ++-- apps/meerkat/package.json | 2 +- apps/meerkat/src/assets/static/conformance.md | 4 ++-- pkg/control | 2 +- pkg/docker-compose.yaml | 2 +- pkg/meerkat-dsa.rb | 2 +- snap/snapcraft.yaml | 2 +- 9 files changed, 31 insertions(+), 10 deletions(-) diff --git a/.github/workflows/meerkat.yml b/.github/workflows/meerkat.yml index f23d37255..dd544f3b6 100644 --- a/.github/workflows/meerkat.yml +++ b/.github/workflows/meerkat.yml @@ -808,7 +808,7 @@ jobs: --set enable_dsp=true \ --set administrator_email=jonathan@wilbur.space \ --set administrator_email_public=true \ - --set vendor_version='3.0.0' \ + --set vendor_version='3.1.0' \ --set signing_required_for_chaining=false \ --set tcp_timeout_in_seconds=300 \ --set min_transfer_speed_bytes_per_minute=10 \ diff --git a/apps/meerkat-docs/docs/changelog-meerkat.md b/apps/meerkat-docs/docs/changelog-meerkat.md index 1cc52b593..1c2880ce1 100644 --- a/apps/meerkat-docs/docs/changelog-meerkat.md +++ b/apps/meerkat-docs/docs/changelog-meerkat.md @@ -1,5 +1,26 @@ # Changelog for Meerkat DSA +## Version 3.1.0 + +This version introduces support for Rule-Based Access Control (RBAC), thereby +enabling Meerkat DSA to enforce the `rule-based-access-control`, +`rule-and-basic-access-control` and `rule-and-simple-access-control` access +control schemes defined in +[ITU Recommendation X.501 (2019)](https://www.itu.int/rec/T-REC-X.501/en). + +### New Features / Improvements + +- Support for Rule-Based Access Control +- Slight performance improvement when creating a new entry + +### Bug Fixes + +- Fix invalid OCSP requests + - OCSP requests were made using the subject's `subjectPublicKeyInfo` rather than the issuer's. +- Fix invalid OCSP verification + - Verification would occur using the same issuer certificate over and over again +- Fix invalid invalid remote CRL signature validation + ## Version 3.0.0 The defining aspect of this version is support for cross references. Cross diff --git a/apps/meerkat-docs/docs/conformance.md b/apps/meerkat-docs/docs/conformance.md index 91fc3e224..aea11e43d 100644 --- a/apps/meerkat-docs/docs/conformance.md +++ b/apps/meerkat-docs/docs/conformance.md @@ -1,7 +1,7 @@ # Conformance -In the statements below, the term "Meerkat DSA" refers to version 3.0.0 of -Meerkat DSA, hence these statements are only claimed for version 3.0.0 of +In the statements below, the term "Meerkat DSA" refers to version 3.1.0 of +Meerkat DSA, hence these statements are only claimed for version 3.1.0 of Meerkat DSA. ## X.519 Conformance Statement diff --git a/apps/meerkat/package.json b/apps/meerkat/package.json index da0ac461b..b991935f8 100644 --- a/apps/meerkat/package.json +++ b/apps/meerkat/package.json @@ -12,7 +12,7 @@ "url": "https://github.com/JonathanWilbur" } ], - "version": "3.0.0", + "version": "3.1.0", "license": "MIT", "bin": { "meerkat": "./main.js" diff --git a/apps/meerkat/src/assets/static/conformance.md b/apps/meerkat/src/assets/static/conformance.md index 7b3265c7c..865276497 100644 --- a/apps/meerkat/src/assets/static/conformance.md +++ b/apps/meerkat/src/assets/static/conformance.md @@ -1,7 +1,7 @@ # Conformance -In the statements below, the term "Meerkat DSA" refers to version 3.0.0 of -Meerkat DSA, hence these statements are only claimed for version 3.0.0 of +In the statements below, the term "Meerkat DSA" refers to version 3.1.0 of +Meerkat DSA, hence these statements are only claimed for version 3.1.0 of Meerkat DSA. ## X.519 Conformance Statement diff --git a/pkg/control b/pkg/control index 2912347f3..74058929e 100644 --- a/pkg/control +++ b/pkg/control @@ -1,5 +1,5 @@ Package: meerkat-dsa -Version: 3.0.0 +Version: 3.1.0 Section: database Priority: optional Architecture: i386 diff --git a/pkg/docker-compose.yaml b/pkg/docker-compose.yaml index 32e8418b4..f710a36c6 100644 --- a/pkg/docker-compose.yaml +++ b/pkg/docker-compose.yaml @@ -20,7 +20,7 @@ services: labels: author: Wildboar Software app: meerkat - version: "3.0.0" + version: "3.1.0" ports: - '1389:389/tcp' # LDAP TCP Port - '4632:4632/tcp' # IDM Socket diff --git a/pkg/meerkat-dsa.rb b/pkg/meerkat-dsa.rb index 5c08c810d..806f86197 100644 --- a/pkg/meerkat-dsa.rb +++ b/pkg/meerkat-dsa.rb @@ -2,7 +2,7 @@ class MeerkatDSA < Formula desc "X.500 Directory Server (DSA) and LDAP Server by Wildboar Software" homepage "https://github.com/Wildboar-Software/directory" url "https://github.com/Wildboar-Software/directory/archive/v1.1.0.tar.gz" - version = "3.0.0" + version = "3.1.0" # sha256 "e86694b2e15d8d4da2477c44e584fb5e860666787d010801199a0a77bcf28a2d" def install diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 3c19eeac2..8de9b72cd 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,6 +1,6 @@ name: meerkat-dsa base: core20 -version: '3.0.0' +version: '3.1.0' summary: X.500 Directory (DSA) and LDAP Server description: | Fully-featured X.500 directory server / directory system agent (DSA) From 2b809df7c8d9abf1fb8c4cb4ace110d8d0781bde Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Tue, 22 Aug 2023 21:11:06 -0400 Subject: [PATCH 33/39] refactor: remove unused imports in create-ca.ts --- apps/create-test-dit/src/app/create-ca.ts | 189 +--------------------- 1 file changed, 4 insertions(+), 185 deletions(-) diff --git a/apps/create-test-dit/src/app/create-ca.ts b/apps/create-test-dit/src/app/create-ca.ts index f9fc6c6a0..d7fefa7de 100644 --- a/apps/create-test-dit/src/app/create-ca.ts +++ b/apps/create-test-dit/src/app/create-ca.ts @@ -9,7 +9,7 @@ import { ASN1Element, INTEGER, } from "asn1-ts"; -import { KeyObject, createHash, createPrivateKey, createSign, randomBytes, randomInt } from "crypto"; +import { KeyObject, createHash, createPrivateKey, createSign, randomBytes } from "crypto"; import { addEntry, } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/addEntry.oa"; @@ -123,72 +123,17 @@ import { UpdateProblem_entryAlreadyExists, UpdateProblem_namingViolation, } from "@wildboar/x500/src/lib/modules/DirectoryAbstractService/UpdateProblem.ta"; import getOptionallyProtectedValue from "@wildboar/x500/src/lib/utils/getOptionallyProtectedValue"; -import { AccessPoint, Name } from "@wildboar/x500/src/lib/modules/DistributedOperations/AccessPoint.ta"; -import { - id_ar_collectiveAttributeSpecificArea, -} from "@wildboar/x500/src/lib/modules/InformationFramework/id-ar-collectiveAttributeSpecificArea.va"; +import { Name } from "@wildboar/x500/src/lib/modules/DistributedOperations/AccessPoint.ta"; import { id_ar_accessControlSpecificArea, } from "@wildboar/x500/src/lib/modules/InformationFramework/id-ar-accessControlSpecificArea.va"; -import { - id_ar_autonomousArea, -} from "@wildboar/x500/src/lib/modules/InformationFramework/id-ar-autonomousArea.va"; -import { - id_ar_pwdAdminSpecificArea, -} from "@wildboar/x500/src/lib/modules/InformationFramework/id-ar-pwdAdminSpecificArea.va"; -import { - id_ar_subschemaAdminSpecificArea, -} from "@wildboar/x500/src/lib/modules/InformationFramework/id-ar-subschemaAdminSpecificArea.va"; -import { uriToNSAP } from "@wildboar/x500/src/lib/distributed/uri"; -import { - PresentationAddress, - _encode_PresentationAddress, -} from "@wildboar/x500/src/lib/modules/SelectedAttributeTypes/PresentationAddress.ta"; import { idempotentAddEntry } from "./utils"; -import { Guide, _encode_Guide } from "@wildboar/x500/src/lib/modules/SelectedAttributeTypes/Guide.ta"; -import { - directoryAccessAC, -} from "@wildboar/x500/src/lib/modules/DirectoryOSIProtocols/directoryAccessAC.oa"; -import { - directorySystemAC, -} from "@wildboar/x500/src/lib/modules/DirectoryOSIProtocols/directorySystemAC.oa"; -import { - directoryOperationalBindingManagementAC, -} from "@wildboar/x500/src/lib/modules/DirectoryOSIProtocols/directoryOperationalBindingManagementAC.oa"; -import { - shadowSupplierInitiatedAC, -} from "@wildboar/x500/src/lib/modules/DirectoryOSIProtocols/shadowSupplierInitiatedAC.oa"; -import { - shadowConsumerInitiatedAC, -} from "@wildboar/x500/src/lib/modules/DirectoryOSIProtocols/shadowConsumerInitiatedAC.oa"; -import { - shadowSupplierInitiatedAsynchronousAC, -} from "@wildboar/x500/src/lib/modules/DirectoryOSIProtocols/shadowSupplierInitiatedAsynchronousAC.oa"; -import { - shadowConsumerInitiatedAsynchronousAC, -} from "@wildboar/x500/src/lib/modules/DirectoryOSIProtocols/shadowConsumerInitiatedAsynchronousAC.oa"; import { Attribute_valuesWithContext_Item, } from "@wildboar/x500/src/lib/modules/InformationFramework/Attribute-valuesWithContext-Item.ta"; import { Context as X500Context, } from "@wildboar/x500/src/lib/modules/InformationFramework/Context.ta"; -import { - languageContext, -} from "@wildboar/x500/src/lib/modules/SelectedAttributeTypes/languageContext.oa"; -import { - localeContext, -} from "@wildboar/x500/src/lib/modules/SelectedAttributeTypes/localeContext.oa"; -import { - temporalContext, -} from "@wildboar/x500/src/lib/modules/SelectedAttributeTypes/temporalContext.oa"; -import { - TimeSpecification, -} from "@wildboar/x500/src/lib/modules/SelectedAttributeTypes/TimeSpecification.ta"; -import { - TimeSpecification_time_absolute, -} from "@wildboar/x500/src/lib/modules/SelectedAttributeTypes/TimeSpecification-time-absolute.ta"; -import { addDays } from "date-fns"; import { commonName, } from "@wildboar/x500/src/lib/modules/SelectedAttributeTypes/commonName.oa"; @@ -200,66 +145,14 @@ import { rule_based_access_control, } from "@wildboar/x500/src/lib/modules/BasicAccessControl/rule-based-access-control.va"; import { HASH, SecurityLabel, SignedSecurityLabelContent, _encode_SignedSecurityLabelContent } from "@wildboar/x500/src/lib/modules/EnhancedSecurity/SignedSecurityLabelContent.ta"; -import { SIGNED, SignedSecurityLabel, _encode_SignedSecurityLabel } from "@wildboar/x500/src/lib/modules/EnhancedSecurity/SignedSecurityLabel.ta"; +import { SIGNED, SignedSecurityLabel } from "@wildboar/x500/src/lib/modules/EnhancedSecurity/SignedSecurityLabel.ta"; import { readFileSync } from "node:fs"; import * as path from "node:path"; import { sha256WithRSAEncryption } from "@wildboar/x500/src/lib/modules/AlgorithmObjectIdentifiers/sha256WithRSAEncryption.va"; import { AlgorithmIdentifier } from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/AlgorithmIdentifier.ta"; import { SecurityClassification_confidential, SecurityClassification_restricted, SecurityClassification_secret, SecurityClassification_top_secret, SecurityClassification_unmarked } from "@wildboar/x500/src/lib/modules/EnhancedSecurity/SecurityClassification.ta"; import { id_sha256 } from "@wildboar/x500/src/lib/modules/AlgorithmObjectIdentifiers/id-sha256.va"; -import { AttributeType, _encode_AttributeType } from "@wildboar/x500/src/lib/modules/InformationFramework/AttributeType.ta"; -import { AttCertIssuer } from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/AttCertIssuer.ta"; - -// const MOSCOW_ACCESS_POINT = new AccessPoint( -// { -// rdnSequence: [ -// [ -// new AttributeTypeAndValue( -// commonName["&id"], -// _encodeUTF8String("dsa01.moscow.mkdemo.wildboar.software", DER), -// ), -// ], -// ], -// }, -// new PresentationAddress( -// undefined, -// undefined, -// undefined, -// [ -// /** -// * Even if you plan on using LDAP to read this entry, you MUST -// * specify an X.500 URL, because DOP cannot be translated into LDAP. -// */ -// uriToNSAP("idms://dsa01.moscow.mkdemo.wildboar.software:44632", false), -// uriToNSAP("idm://dsa01.moscow.mkdemo.wildboar.software:4632", false), -// uriToNSAP("ldaps://dsa01.moscow.mkdemo.wildboar.software:636", false), -// uriToNSAP("ldap://dsa01.moscow.mkdemo.wildboar.software:389", false), -// ], -// ), -// undefined, -// ); - -const MOSCOW_ACCESS_POINT = new AccessPoint( - { - rdnSequence: [ - [ - new AttributeTypeAndValue( - commonName["&id"], - _encodeUTF8String("dsa2", DER), - ), - ], - ], - }, - new PresentationAddress( - undefined, - undefined, - undefined, - [ - uriToNSAP("idm://dsa2:4632", false), - ], - ), - undefined, -); +import { AttributeType } from "@wildboar/x500/src/lib/modules/InformationFramework/AttributeType.ta"; const allNonSecurityContextTypes: OBJECT_IDENTIFIER[] = [ ct.languageContext["&id"], @@ -299,80 +192,6 @@ function securityParameters (): SecurityParameters { ); } -function addLocalityArgument ( - baseObject: DistinguishedName, - lname: string, - targetSystem?: AccessPoint, -): AddEntryArgument { - const ln = _encodeUTF8String(lname, DER); - const dn: DistinguishedName = [ - ...baseObject, - [ - new AttributeTypeAndValue( - selat.localityName["&id"]!, - ln, - ), - ], - ]; - const attributes: Attribute[] = [ - new Attribute( - selat.administrativeRole["&id"]!, - [ - _encodeObjectIdentifier(id_ar_autonomousArea, DER), - _encodeObjectIdentifier(id_ar_collectiveAttributeSpecificArea, DER), - _encodeObjectIdentifier(id_ar_accessControlSpecificArea, DER), - _encodeObjectIdentifier(id_ar_pwdAdminSpecificArea, DER), - _encodeObjectIdentifier(id_ar_subschemaAdminSpecificArea, DER), - ], - undefined, - ), - new Attribute( - selat.objectClass["&id"], - [ - _encodeObjectIdentifier(seloc.locality["&id"]!, DER), - _encodeObjectIdentifier(seloc.userPwdClass["&id"]!, DER), - ], - undefined, - ), - new Attribute( - selat.localityName["&id"]!, - [ln], - undefined, - ), - new Attribute( - selat.userPwd["&id"], - [ - selat.userPwd.encoderFor["&Type"]!({ - clear: `password4${lname}`, - }, DER), - ], - undefined, - ), - ]; - return { - unsigned: new AddEntryArgumentData( - { - rdnSequence: dn, - }, - attributes, - targetSystem, - [], - serviceControls, - securityParameters(), - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - undefined, - ), - }; -} - function createAddEntryArgument ( dn: DistinguishedName, attributes: Attribute[], From 0488e052dd9eedf8d358e2898d354cf9bfed2dd2 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Wed, 23 Aug 2023 07:10:12 -0400 Subject: [PATCH 34/39] refactor: rename default RBAC security policy --- apps/meerkat/src/app/authz/rbacACDF.ts | 13 +++++++++++-- apps/meerkat/src/app/ctx.ts | 4 ++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/apps/meerkat/src/app/authz/rbacACDF.ts b/apps/meerkat/src/app/authz/rbacACDF.ts index 8571aecb3..e93a00e85 100644 --- a/apps/meerkat/src/app/authz/rbacACDF.ts +++ b/apps/meerkat/src/app/authz/rbacACDF.ts @@ -42,7 +42,8 @@ import { } from "@wildboar/parity-schema/src/lib/modules/Wildboar/id-wildboar.va"; // TODO: Add this to the registry. -export const id_basicSecurityPolicy = new ObjectIdentifier([ 403, 1 ], id_wildboar); +export const id_simpleSecurityPolicy = new ObjectIdentifier([ 403, 1 ], id_wildboar); + /** * @summary A simple Rule-Based Access Control ACDF @@ -75,7 +76,15 @@ const simple_rbac_acdf: RBAC_ACDF = ( if (classification === SecurityClassification_unclassified) { return true; // If unclassified, the user may always see it. } - const policyId = label.security_policy_identifier ?? id_basicSecurityPolicy; + /* "unmarked" is treated as more sensitive than "unclassified," but it is + numerically lower than "unclassified" among the named integers of the + `SecurityClassification` type. We perform the swap here. */ + if (classification === SecurityClassification_unclassified) { + classification = 0; + } else if (classification === SecurityClassification_unmarked) { + classification = 1; + } + const policyId = label.security_policy_identifier ?? id_simpleSecurityPolicy; let highestClearanceLevel: number = 0; for (const clearance of assn.clearances) { if (!clearance.policyId.isEqualTo(policyId)) { diff --git a/apps/meerkat/src/app/ctx.ts b/apps/meerkat/src/app/ctx.ts index 54e8e0eb9..9b863a88a 100644 --- a/apps/meerkat/src/app/ctx.ts +++ b/apps/meerkat/src/app/ctx.ts @@ -109,7 +109,7 @@ import type { import { rootCertificates } from "node:tls"; import { strict as assert } from "node:assert"; import { createPublicKey } from "node:crypto"; -import { id_basicSecurityPolicy, simple_rbac_acdf } from "./authz/rbacACDF"; +import { id_simpleSecurityPolicy, simple_rbac_acdf } from "./authz/rbacACDF"; import { subjectKeyIdentifier } from "@wildboar/x500/src/lib/modules/CertificateExtensions/subjectKeyIdentifier.oa"; import { subjectAltName } from "@wildboar/x500/src/lib/modules/CertificateExtensions/subjectAltName.oa"; import { Name } from "@wildboar/x500/src/lib/modules/InformationFramework/Name.ta"; @@ -1166,7 +1166,7 @@ const ctx: MeerkatContext = { shadowUpdateCycles: new Map(), updatingShadow: new Set(), labellingAuthorities: new Map(), - rbacPolicies: new Map([ [id_basicSecurityPolicy.toString(), simple_rbac_acdf] ]), + rbacPolicies: new Map([ [id_simpleSecurityPolicy.toString(), simple_rbac_acdf] ]), alreadyAssertedAttributeCertificates: new Set(), }; From 63989944772f5ba38134f40a8119b6f040d69578 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Wed, 23 Aug 2023 07:13:31 -0400 Subject: [PATCH 35/39] feat: change rbac edge case behavior --- apps/meerkat/src/app/authz/rbacACDF.ts | 83 ++++++++++++++++++-------- 1 file changed, 59 insertions(+), 24 deletions(-) diff --git a/apps/meerkat/src/app/authz/rbacACDF.ts b/apps/meerkat/src/app/authz/rbacACDF.ts index e93a00e85..08a266194 100644 --- a/apps/meerkat/src/app/authz/rbacACDF.ts +++ b/apps/meerkat/src/app/authz/rbacACDF.ts @@ -40,10 +40,21 @@ import { import { id_wildboar, } from "@wildboar/parity-schema/src/lib/modules/Wildboar/id-wildboar.va"; +import { attributeValueSecurityLabelContext } from "@wildboar/x500/src/lib/collections/contexts"; +import { + PERMISSION_CATEGORY_ADD, + PERMISSION_CATEGORY_MODIFY, + PERMISSION_CATEGORY_REMOVE, +} from "@wildboar/x500/src/lib/bac/bacACDF"; // TODO: Add this to the registry. export const id_simpleSecurityPolicy = new ObjectIdentifier([ 403, 1 ], id_wildboar); +const modification_permissions: number[] = [ + PERMISSION_CATEGORY_ADD, + PERMISSION_CATEGORY_MODIFY, + PERMISSION_CATEGORY_REMOVE, +]; /** * @summary A simple Rule-Based Access Control ACDF @@ -68,11 +79,11 @@ const simple_rbac_acdf: RBAC_ACDF = ( target: Vertex, signedLabel: SignedSecurityLabel, _value: ASN1Element, - _contexts: X500Context[], - _permissions: number[], + contexts: X500Context[], + permissions: number[], ): boolean => { const label = signedLabel.toBeSigned.securityLabel; - const classification = Number(label.security_classification ?? SecurityClassification_unmarked); + let classification = Number(label.security_classification ?? SecurityClassification_unmarked); if (classification === SecurityClassification_unclassified) { return true; // If unclassified, the user may always see it. } @@ -92,7 +103,7 @@ const simple_rbac_acdf: RBAC_ACDF = ( } const clearanceLevel: SecurityClassification = (() => { if (!clearance.classList) { - return SecurityClassification_unclassified; + return 0; } else if (clearance.classList[ClassList_topSecret] === TRUE_BIT) { return SecurityClassification_top_secret; @@ -107,22 +118,36 @@ const simple_rbac_acdf: RBAC_ACDF = ( return SecurityClassification_restricted; } else if (clearance.classList[ClassList_unmarked] === TRUE_BIT) { - return SecurityClassification_unmarked; - } - else { - return SecurityClassification_unclassified; + /* Under the Simple Security Policy, unmarked is treated as + being a level higher than unclassified. */ + return 1; } + return 0; })(); + // Just to make sure that classification cannot be given a large, + // illegitimate value to make a protected value universally inaccessible. + // This also short circuits this function. + if (clearanceLevel == SecurityClassification_top_secret) { + return true; + } if (clearanceLevel > highestClearanceLevel) { highestClearanceLevel = Number(clearanceLevel); } } - // Just to make sure that classification cannot be given a large, - // illegitimate value to make a protected value universally inaccessible. - if (highestClearanceLevel == SecurityClassification_top_secret) { - return true; + + if (highestClearanceLevel < classification) { + return false; } - return (highestClearanceLevel >= classification); + /* If the user does not have top-secret clearance, but does have generally + sufficient clearance, the only remaining reason we have to block them is if + they are modifying the security labels themselves, in which case, access is + denied, because, under the Simple Security Policy, you need top-secret + clearance to modify the security labels. */ + const is_modification: boolean = permissions.some((p) => modification_permissions.includes(p)); + return ( + !is_modification + || !contexts.some((c) => c.contextType.isEqualTo(attributeValueSecurityLabelContext["&id"])) + ); }; // TODO: Log invalid hashes and such so admins can know if they are locked out of values. @@ -157,20 +182,28 @@ export function rbacACDF ( permissions: number[], ): boolean { if (!assn.clearances.length) { - // If the user has no clearance, only allow access for things unmarked. + // If the user has no clearance, only allow access for things unclassified. return (label.toBeSigned.securityLabel.security_classification == SecurityClassification_unclassified); } - // const applicable_clearances = assn.clearances.filter((c) => c.policyId.isEqualTo(label.)) const policyId = label.toBeSigned.securityLabel.security_policy_identifier - ?? id_basicSecurityPolicy; + ?? id_simpleSecurityPolicy; const acdf = ctx.rbacPolicies.get(policyId.toString()); + const relevantClearances = assn.clearances.filter((c) => c.policyId.isEqualTo(policyId)); + const userHasTopSecretClearance: boolean = relevantClearances + .some((c) => (c.classList?.[ClassList_topSecret] === TRUE_BIT)); if (!acdf) { - return false; // If the policy ID is not understood, deny access. + /* If the policy is not understood, allow the request if unclassified or + if the user has top-secret clearance for that policy. Otherwise, deny + access. */ + return ( + (label.toBeSigned.securityLabel.security_classification === SecurityClassification_unclassified) + || userHasTopSecretClearance + ); } const atav_hash_alg = digestOIDToNodeHash.get(label.toBeSigned.attHash.algorithmIdentifier.algorithm.toString()); if (!atav_hash_alg) { - return false; // Hash algorithm not understood. + return userHasTopSecretClearance; // Hash algorithm not understood. } const atav = new AttributeTypeAndValue(attributeType, value); const atav_bytes = _encode_AttributeTypeAndValue(atav, DER).toBytes(); @@ -185,7 +218,7 @@ export function rbacACDF ( ); if (Buffer.compare(calculated_digest, provided_digest)) { - return false; // The hashes don't match up. + return userHasTopSecretClearance; // The hashes don't match up. } const namingMatcher = getNamingMatcherGetter(ctx); @@ -206,7 +239,7 @@ export function rbacACDF ( const key_id = Buffer.from(label.toBeSigned.keyIdentifier).toString("base64"); const authority = ctx.labellingAuthorities.get(key_id); if (authority === null) { - return false; + return userHasTopSecretClearance; } const issuer = label.toBeSigned.issuer; const authority_is_valid: boolean = !!( @@ -222,7 +255,7 @@ export function rbacACDF ( ) ); if (!authority_is_valid) { - return false; + return userHasTopSecretClearance; } publicKey = authority?.publicKey; } else if (label.toBeSigned.issuer) { @@ -246,10 +279,12 @@ export function rbacACDF ( publicKey = authority.publicKey; } } else { // This should actually be unreachable. - return false; + // We grant access for only top-secret clearance, just to ensure that + // problems can be rectified. + return userHasTopSecretClearance; } if (!publicKey) { - return false; + return userHasTopSecretClearance; } const tbs_bytes = label.originalDER ? (() => { @@ -267,7 +302,7 @@ export function rbacACDF ( publicKey, ); if (!sig_valid) { - return false; + return userHasTopSecretClearance; } // At this point, we know that the label is correctly bound to the value, // so we can use the policy-specific RBAC ACDF. From a51193edfecd35f1892d76b0f7cd41046daaaa82 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Wed, 23 Aug 2023 07:13:42 -0400 Subject: [PATCH 36/39] docs: document RBAC edge case behavior --- apps/meerkat-docs/docs/authorization.md | 73 +++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/apps/meerkat-docs/docs/authorization.md b/apps/meerkat-docs/docs/authorization.md index 38da0133c..4d74a4afc 100644 --- a/apps/meerkat-docs/docs/authorization.md +++ b/apps/meerkat-docs/docs/authorization.md @@ -275,12 +275,10 @@ absence of information, but it may also indicate that the labeled thing is not important enough to have labeled properly in the first place, hence, it lies between total declassification and the "restricted" classification. -:::caution - The Simple Security Policy does not treat reads and writes differently: if access is granted to read an entry, access is also granted to modify an entry. - -::: +There is an exception, however: any modification operation that affects a value +with security labels MUST be performed with top-secret clearance. :::note @@ -389,3 +387,70 @@ For security reasons, only DAP and LDAP associations will have any clearances associated: this is so that downstream DSAs do not make access control decisions on the basis of the upstream DSA's clearances rather than the originating DAP requester when chaining is used. + +### Users with No Clearance + +Users with no clearances at all will automatically be given access only to +attribute values having a security label with a class of `unclassified`. + +### Invalid Hashes, Untrusted Labels, and Untrusted Labelling Authorities + +In Meerkat DSA, an invalid hash, invalid signature, or untrusted labelling +authority having issued a security label will make the security label +inaccessible to all users not having top-secret clearance for the identified +security policy. + +:::info + +When establishing the above behavior, I had--broadly speaking--two choices: + +1. Generally allow access (with possible caveats) when a security label is + invalid for some reason. +2. Generally deny access (with possible caveats) when a security label is + invalid for some reason. + +If I chose option 1, there would be a risk of disclosing classified information +as a result of accidentally malformed labels or bugs in the DER encoding, +hashing, or signing. If I chose option 2, there would be a risk of nefarious +users either creating illegitimate labels or copying otherwise legitimate +labels from correct values to cripple directory access to legitimate users. + +I chose option 2 for these reasons: + +1. Not all malformed labels are malicious; some are produced by accident, and + the risk of accidentally disclosing classified information could be much + greater than temporarily having lost access to part of the directory. +2. Since labelled values are generally going to be created by people that + already have clearance for that information, there would be no immediate + feedback to indicate that something was wrong if access were granted for + invalid labels; the value would still appear to that user as expected. By + restricting access for invalid labels, there may be a visible consequence + that users can seek to rectify. +3. There is no real way to know if a label is valid until you submit it to + Meerkat DSA. This means that users would have to add the classified + information to Meerkat DSA, then determine if the labels are valid after the + fact being checking if the classified information is disclosed to + unauthorized users. +4. At least with the Simple Security Policy, modifying security labels in the + first place is only granted to users with top-secret clearance. + +::: + +Note that, as a result of the above, changing the configured +[labelling authorities](./env.md#meerkat_labelling_authorities) could invalidate +existing security labels, making values "disappear" to users that would +otherwise have access to them. + +### Unrecognized Policies + +When Meerkat DSA encounters an unrecognized policy on a security label, it +only grants access to the protected value if the label indicates that the +item is `unclassified` or if the user has top-secret clearance. + +### Multiple Security Labels + +[ITU Recommendation X.501 (2019)](https://www.itu.int/rec/T-REC-X.501/en) +mandates a limit of one security label context per value. Meerkat DSA does not +enforce this. The behavior of Meerkat DSA in the presence of multiple security +labels for a given value will remain undefined, but it usually results in the +first one being used exclusively, and the remainder ignored. From 252675340fee127f441705645efbd4a96421735e Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Wed, 23 Aug 2023 07:25:48 -0400 Subject: [PATCH 37/39] docs: document subjectKeyIdentifier requirement among labelling authorities --- apps/meerkat-docs/docs/authorization.md | 4 ++++ libs/ocsp-client/package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/meerkat-docs/docs/authorization.md b/apps/meerkat-docs/docs/authorization.md index 4d74a4afc..41f56035f 100644 --- a/apps/meerkat-docs/docs/authorization.md +++ b/apps/meerkat-docs/docs/authorization.md @@ -257,6 +257,10 @@ environment variables. If either of these are unset, they default to the trust anchors used for signing. +To be used as a labelling authority, the configured trust anchors **MUST** have +a `subjectKeyIdentifier` extension (or the equivalent `keyId` field of the +`taInfo` alternative); those that do not match this requirement will be ignored. + ### The Simple Security Policy For the sake of easy use of the Rule-Based Access Control (RBAC), Meerkat DSA diff --git a/libs/ocsp-client/package.json b/libs/ocsp-client/package.json index 441cb460f..d636ef875 100644 --- a/libs/ocsp-client/package.json +++ b/libs/ocsp-client/package.json @@ -1,5 +1,5 @@ { "name": "@wildboar/ocsp-client", - "version": "1.0.0", + "version": "1.1.0", "license": "MIT" } From db97e0495fda0bf26cd05cbb575b75a6782a6b47 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Wed, 23 Aug 2023 08:02:55 -0400 Subject: [PATCH 38/39] refactor: remove unused imports --- apps/meerkat/src/app/pki/verifyAttrCertPath.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/apps/meerkat/src/app/pki/verifyAttrCertPath.ts b/apps/meerkat/src/app/pki/verifyAttrCertPath.ts index 630364934..9ef03138e 100644 --- a/apps/meerkat/src/app/pki/verifyAttrCertPath.ts +++ b/apps/meerkat/src/app/pki/verifyAttrCertPath.ts @@ -3,7 +3,6 @@ import { Context, IndexableOID, OfflinePKIConfig, - OCSPOptions, } from "@wildboar/meerkat-types"; import { PkiPath, @@ -12,7 +11,6 @@ import { ACPathData, AttributeCertificate, AttributeCertificationPath, - _encode_AttributeCertificate, } from "@wildboar/x500/src/lib/modules/AttributeCertificateDefinitions/AttributeCertificationPath.ta"; import { compareGeneralName, @@ -92,10 +90,6 @@ import { _encode_AlgorithmIdentifier } from "@wildboar/pki-stub/src/lib/modules/ import { TBSAttributeCertificate, _encode_TBSAttributeCertificate } from "@wildboar/pki-stub/src/lib/modules/PKI-Stub/TBSAttributeCertificate.ta"; import { checkOCSP, - VCP_RETURN_OCSP_REVOKED, - VCP_RETURN_OCSP_OTHER, - VCP_RETURN_CRL_REVOKED, - VCP_RETURN_CRL_UNREACHABLE, } from "./verifyCertPath"; export const VAC_OK: number = 0; @@ -125,10 +119,10 @@ export const VAC_INVALID_EXT_CRIT: number = -25; export const VAC_CRL_REVOKED: number = -26; export const VAC_OCSP_OTHER: number = -27; export const VAC_OCSP_REVOKED: number = -28; -export const VAC_RETURN_OCSP_REVOKED: number = VCP_RETURN_OCSP_REVOKED; -export const VAC_RETURN_OCSP_OTHER: number = VCP_RETURN_OCSP_OTHER; -export const VAC_RETURN_CRL_REVOKED: number = VCP_RETURN_CRL_REVOKED; -export const VAC_RETURN_CRL_UNREACHABLE: number = VCP_RETURN_CRL_UNREACHABLE; +export const VAC_RETURN_OCSP_REVOKED: number = -102; +export const VAC_RETURN_OCSP_OTHER: number = -103; +export const VAC_RETURN_CRL_REVOKED: number = -104; +export const VAC_RETURN_CRL_UNREACHABLE: number = -105; export const supportedExtensions: Set = new Set([ From ec769fe40b74b6ba08e5bbe2fab623346ee6a2f3 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Wilbur" Date: Wed, 23 Aug 2023 08:03:39 -0400 Subject: [PATCH 39/39] chore: bump version on x500-cli --- apps/x500-cli/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/x500-cli/package.json b/apps/x500-cli/package.json index e507bf7a9..b5199ec19 100644 --- a/apps/x500-cli/package.json +++ b/apps/x500-cli/package.json @@ -1,6 +1,6 @@ { "name": "@wildboar/x500-cli", - "version": "0.10.0", + "version": "0.10.1", "bin": { "x500": "./main.js" },