Skip to content

Commit 6eb70c1

Browse files
feat(lib): offline abac KAO configuration (#349)
1 parent ced163d commit 6eb70c1

File tree

11 files changed

+836
-145
lines changed

11 files changed

+836
-145
lines changed

lib/src/policy/attributes.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
export type Metadata = {
2+
/**
3+
* created_at set by server (entity who created will recorded in an audit event)
4+
* Format: date-time
5+
*/
6+
createdAt?: string;
7+
8+
/**
9+
* updated_at set by server (entity who updated will recorded in an audit event)
10+
* Format: date-time
11+
*/
12+
updatedAt?: string;
13+
14+
/** optional short description */
15+
labels?: Record<string, string>;
16+
};
17+
18+
export type KasPublicKeyAlgorithm =
19+
| 'KAS_PUBLIC_KEY_ALG_ENUM_UNSPECIFIED'
20+
| 'KAS_PUBLIC_KEY_ALG_ENUM_RSA_2048'
21+
| 'KAS_PUBLIC_KEY_ALG_ENUM_EC_SECP256R1';
22+
23+
export type KasPublicKey = {
24+
/** x509 ASN.1 content in PEM envelope, usually */
25+
pem: string;
26+
/** A unique string identifier for this key */
27+
kid: string;
28+
/**
29+
* @description A known algorithm type with any additional parameters encoded.
30+
* To start, these may be `rsa:2048` for encrypting ZTDF files and
31+
* `ec:secp256r1` for nanoTDF, but more formats may be added as needed.
32+
*/
33+
alg: KasPublicKeyAlgorithm;
34+
};
35+
36+
export type KasPublicKeySet = {
37+
keys: KasPublicKey[];
38+
};
39+
40+
export type PublicKey = {
41+
/** kas public key url - optional since can also be retrieved via public key */
42+
remote?: string;
43+
/** public key; PEM of RSA public key; prefer `cached` */
44+
local?: string;
45+
/** public key with additional information. Current preferred version */
46+
cached?: KasPublicKeySet;
47+
};
48+
49+
export type KeyAccessServer = {
50+
id?: string;
51+
/** Address of a KAS instance */
52+
uri: string;
53+
publicKey?: PublicKey;
54+
metadata?: Metadata;
55+
};
56+
57+
export type Namespace = {
58+
/** uuid */
59+
id?: string;
60+
/** used to partition Attribute Definitions, support by namespace AuthN and enable federation */
61+
name?: string;
62+
fqn: string;
63+
/** active by default until explicitly deactivated */
64+
active?: boolean;
65+
metadata?: Metadata;
66+
grants?: KeyAccessServer[];
67+
};
68+
69+
export type AttributeRuleType =
70+
| 'ATTRIBUTE_RULE_TYPE_ENUM_UNSPECIFIED'
71+
| 'ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF'
72+
| 'ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF'
73+
| 'ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY';
74+
75+
export type Attribute = {
76+
/** UUID */
77+
id?: string;
78+
namespace?: Namespace;
79+
/** attribute name */
80+
name?: string;
81+
/** attribute rule enum */
82+
rule?: AttributeRuleType;
83+
values?: Value[];
84+
grants?: KeyAccessServer[];
85+
fqn: string;
86+
/** active by default until explicitly deactivated */
87+
active?: boolean;
88+
/** Common metadata */
89+
metadata?: Metadata;
90+
};
91+
92+
export type Value = {
93+
id?: string;
94+
attribute?: Attribute;
95+
value?: string;
96+
/** list of key access servers */
97+
grants?: KeyAccessServer[];
98+
fqn: string;
99+
/** active by default until explicitly deactivated */
100+
active?: boolean;
101+
/** Common metadata */
102+
metadata?: Metadata;
103+
};

lib/src/policy/granter.ts

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { Attribute, AttributeRuleType, KeyAccessServer, Value } from './attributes.js';
2+
3+
export type KeySplitStep = {
4+
kas: KeyAccessServer;
5+
sid?: string;
6+
};
7+
8+
type AttributeClause = {
9+
def: Attribute;
10+
values: string[];
11+
};
12+
13+
type AndClause = {
14+
op: 'allOf';
15+
kases: string[];
16+
};
17+
18+
type HeirarchyClause = {
19+
op: 'hierarchy';
20+
kases: string[];
21+
};
22+
23+
type OrClause = {
24+
op: 'anyOf';
25+
kases: string[];
26+
};
27+
28+
type BooleanClause = AndClause | OrClause | HeirarchyClause;
29+
30+
type BooleanOperator = BooleanClause['op'];
31+
32+
type ComplexBooleanClause = {
33+
op: BooleanOperator;
34+
children: BooleanClause[];
35+
};
36+
37+
export function booleanOperatorFor(rule?: AttributeRuleType): BooleanOperator {
38+
if (!rule) {
39+
return 'allOf';
40+
}
41+
switch (rule) {
42+
case 'ATTRIBUTE_RULE_TYPE_ENUM_UNSPECIFIED':
43+
case 'ATTRIBUTE_RULE_TYPE_ENUM_ALL_OF':
44+
return 'allOf';
45+
case 'ATTRIBUTE_RULE_TYPE_ENUM_ANY_OF':
46+
return 'anyOf';
47+
case 'ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY':
48+
return 'hierarchy';
49+
}
50+
}
51+
52+
export function plan(dataAttrs: Value[]): KeySplitStep[] {
53+
// KASes by value
54+
const grants: Record<string, Set<string>> = {};
55+
// KAS detail by KAS url
56+
const kasInfo: Record<string, KeyAccessServer> = {};
57+
// Attribute definitions in use
58+
const prefixes: Set<string> = new Set();
59+
// Values grouped by normalized attribute prefix
60+
const allClauses: Record<string, AttributeClause> = {};
61+
// Values by normalized FQN
62+
const allValues: Record<string, Value> = {};
63+
64+
const addGrants = (val: string, gs?: KeyAccessServer[]): boolean => {
65+
if (!gs?.length) {
66+
if (!(val in grants)) {
67+
grants[val] = new Set();
68+
}
69+
return false;
70+
}
71+
for (const g of gs) {
72+
if (val in grants) {
73+
grants[val].add(g.uri);
74+
} else {
75+
grants[val] = new Set([g.uri]);
76+
}
77+
kasInfo[g.uri] = g;
78+
}
79+
return true;
80+
};
81+
82+
for (const v of dataAttrs) {
83+
const { attribute, fqn } = v;
84+
if (!attribute) {
85+
throw new Error(`attribute not defined for [${fqn}]`);
86+
}
87+
const valFqn = fqn.toLowerCase();
88+
const attrFqn = attribute.fqn.toLowerCase();
89+
if (!prefixes.has(attrFqn)) {
90+
prefixes.add(attrFqn);
91+
allClauses[attrFqn] = {
92+
def: attribute,
93+
values: [],
94+
};
95+
}
96+
allClauses[attrFqn].values.push(valFqn);
97+
allValues[valFqn] = v;
98+
if (!addGrants(valFqn, v.grants)) {
99+
if (!addGrants(valFqn, attribute.grants)) {
100+
addGrants(valFqn, attribute.namespace?.grants);
101+
}
102+
}
103+
}
104+
const kcs: ComplexBooleanClause[] = [];
105+
for (const attrClause of Object.values(allClauses)) {
106+
const ccv: BooleanClause[] = [];
107+
for (const term of attrClause.values) {
108+
const grantsForTerm = Array.from(grants[term] || []);
109+
if (grantsForTerm?.length) {
110+
ccv.push({
111+
op: 'anyOf',
112+
kases: grantsForTerm,
113+
});
114+
}
115+
}
116+
const op = booleanOperatorFor(attrClause.def.rule);
117+
kcs.push({
118+
op,
119+
children: ccv,
120+
});
121+
}
122+
return simplify(kcs, kasInfo);
123+
}
124+
125+
function simplify(
126+
clauses: ComplexBooleanClause[],
127+
kasInfo: Record<string, KeyAccessServer>
128+
): KeySplitStep[] {
129+
const conjunction: Record<string, string[]> = {};
130+
function keyFor(kases: string[]): string {
131+
const k = Array.from(new Set([kases])).sort();
132+
return k.join('|');
133+
}
134+
for (const { op, children } of clauses) {
135+
if (!children) {
136+
continue;
137+
}
138+
if (op === 'anyOf') {
139+
const anyKids = [];
140+
for (const bc of children) {
141+
if (bc.op != 'anyOf') {
142+
throw new Error('inversion');
143+
}
144+
if (!bc.kases?.length) {
145+
continue;
146+
}
147+
anyKids.push(...bc.kases);
148+
}
149+
if (!anyKids?.length) {
150+
continue;
151+
}
152+
const k = keyFor(anyKids);
153+
conjunction[k] = anyKids;
154+
} else {
155+
for (const bc of children) {
156+
if (bc.op != 'anyOf') {
157+
throw new Error('inversion');
158+
}
159+
if (!bc.kases?.length) {
160+
continue;
161+
}
162+
const k = keyFor(bc.kases);
163+
conjunction[k] = bc.kases;
164+
}
165+
}
166+
}
167+
const t: KeySplitStep[] = [];
168+
let i = 0;
169+
for (const k of Object.keys(conjunction).sort()) {
170+
if (!conjunction[k]) {
171+
continue;
172+
}
173+
i += 1;
174+
const sid = '' + i;
175+
for (const kas of conjunction[k]) {
176+
t.push({ sid, kas: kasInfo[kas] });
177+
}
178+
}
179+
return t;
180+
}

lib/tdf3/src/client/builders.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import { PemKeyPair } from '../crypto/declarations.js';
88
import { EntityObject } from '../../../src/tdf/EntityObject.js';
99
import { DecoratedReadableStream } from './DecoratedReadableStream.js';
1010
import { type Chunker } from '../utils/chunkers.js';
11+
import { Value } from '../../../src/policy/attributes.js';
1112

1213
export const DEFAULT_SEGMENT_SIZE: number = 1024 * 1024;
1314
export type Scope = {
1415
dissem?: string[];
1516
policyId?: string;
1617
policyObject?: Policy;
1718
attributes?: (string | AttributeObject)[];
19+
attributeValues?: Value[];
1820
};
1921

2022
export type EncryptKeyMiddleware = (...args: unknown[]) => Promise<{

0 commit comments

Comments
 (0)