feat(domain): bind runtime policy into signed manifests
This commit is contained in:
parent
6f0902846d
commit
fbff657d88
4 changed files with 189 additions and 5 deletions
|
|
@ -14,14 +14,27 @@ import {
|
|||
const MANIFEST_SCHEMA = 'gh-aios.domain-manifest/v1' as const;
|
||||
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
||||
const COMMIT_PATTERN = /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/;
|
||||
const POLICY_SCOPE_PATTERN = /^[a-z][a-z0-9-]*(?::[a-z][a-z0-9-]*)?$/;
|
||||
const POLICY_REFERENCE_PATTERN = /^[a-z][a-z0-9+.-]*:\/\/[A-Za-z0-9._~:/-]{1,240}$/;
|
||||
const cryptographicallyVerifiedManifests = new WeakSet<object>();
|
||||
|
||||
export type DomainNodeType = 'local-terminal' | 'cloud-resident';
|
||||
export type DomainThemeOwner = 'domain-team' | 'fifth-domain';
|
||||
|
||||
export interface DomainRuntimeManifestPolicy {
|
||||
allowedSessionScopes: readonly string[];
|
||||
forbiddenDataScopes: readonly string[];
|
||||
permissionPolicyRef: string;
|
||||
routeRef: string;
|
||||
themeOwner: DomainThemeOwner;
|
||||
themePackageRef: string;
|
||||
}
|
||||
|
||||
export interface DomainManifestEvidence {
|
||||
digest: string;
|
||||
domainId: string;
|
||||
repositoryId: string;
|
||||
runtimePolicy: DomainRuntimeManifestPolicy;
|
||||
schema: typeof MANIFEST_SCHEMA;
|
||||
signature: string;
|
||||
signerId: string;
|
||||
|
|
@ -38,6 +51,7 @@ export interface DomainManifestEvidence {
|
|||
export interface DomainManifestSignedPayload {
|
||||
domainId: string;
|
||||
repositoryId: string;
|
||||
runtimePolicy: DomainRuntimeManifestPolicy;
|
||||
schema: typeof MANIFEST_SCHEMA;
|
||||
signerId: string;
|
||||
sourceCommit: string;
|
||||
|
|
@ -108,6 +122,18 @@ function isIdentifier(value: unknown): value is string {
|
|||
return typeof value === 'string' && value.length > 0 && value.length <= 160;
|
||||
}
|
||||
|
||||
function isPolicyReference(value: unknown): value is string {
|
||||
return typeof value === 'string' && POLICY_REFERENCE_PATTERN.test(value);
|
||||
}
|
||||
|
||||
function isPolicyScopeList(value: unknown): value is string[] {
|
||||
return Array.isArray(value)
|
||||
&& value.length > 0
|
||||
&& value.length <= 64
|
||||
&& value.every((scope) => typeof scope === 'string' && POLICY_SCOPE_PATTERN.test(scope))
|
||||
&& new Set(value).size === value.length;
|
||||
}
|
||||
|
||||
function invalidHandoff(): never {
|
||||
throw new Error('domain_access_handoff_invalid');
|
||||
}
|
||||
|
|
@ -116,6 +142,14 @@ export function domainManifestSigningBytes(payload: DomainManifestSignedPayload)
|
|||
return Buffer.from(JSON.stringify({
|
||||
domainId: payload.domainId,
|
||||
repositoryId: payload.repositoryId,
|
||||
runtimePolicy: {
|
||||
allowedSessionScopes: payload.runtimePolicy.allowedSessionScopes,
|
||||
forbiddenDataScopes: payload.runtimePolicy.forbiddenDataScopes,
|
||||
permissionPolicyRef: payload.runtimePolicy.permissionPolicyRef,
|
||||
routeRef: payload.runtimePolicy.routeRef,
|
||||
themeOwner: payload.runtimePolicy.themeOwner,
|
||||
themePackageRef: payload.runtimePolicy.themePackageRef,
|
||||
},
|
||||
schema: payload.schema,
|
||||
signerId: payload.signerId,
|
||||
sourceCommit: payload.sourceCommit,
|
||||
|
|
@ -140,9 +174,13 @@ export function parseDomainAccessHandoff(
|
|||
const manifest = input.manifest;
|
||||
const capability = input.sessionCapability;
|
||||
const connection = input.connectionReceipt;
|
||||
if (!isRecord(manifest) || !hasExactKeys(manifest, ['digest', 'domainId', 'repositoryId', 'schema', 'signature', 'signerId', 'sourceCommit'])) invalidHandoff();
|
||||
if (!isRecord(manifest) || !hasExactKeys(manifest, ['digest', 'domainId', 'repositoryId', 'runtimePolicy', 'schema', 'signature', 'signerId', 'sourceCommit'])) invalidHandoff();
|
||||
if (!isRecord(capability) || !hasExactKeys(capability, ['capabilityId', 'domainId', 'expiresAt', 'nodeId', 'nodeType', 'scopes'])) invalidHandoff();
|
||||
if (!isRecord(connection) || !hasExactKeys(connection, ['connectionId', 'domainId', 'manifestDigest', 'nodeId', 'nodeType', 'receiptId', 'state'])) invalidHandoff();
|
||||
const runtimePolicy = manifest.runtimePolicy;
|
||||
if (!isRecord(runtimePolicy) || !hasExactKeys(runtimePolicy, ['allowedSessionScopes', 'forbiddenDataScopes', 'permissionPolicyRef', 'routeRef', 'themeOwner', 'themePackageRef'])) invalidHandoff();
|
||||
const allowedSessionScopes = runtimePolicy.allowedSessionScopes;
|
||||
const forbiddenDataScopes = runtimePolicy.forbiddenDataScopes;
|
||||
|
||||
const digest = manifest.digest;
|
||||
if (manifest.schema !== MANIFEST_SCHEMA
|
||||
|
|
@ -156,9 +194,21 @@ export function parseDomainAccessHandoff(
|
|||
|| typeof manifest.sourceCommit !== 'string' || !COMMIT_PATTERN.test(manifest.sourceCommit)
|
||||
|| typeof manifest.signature !== 'string') invalidHandoff();
|
||||
|
||||
if (!isPolicyScopeList(allowedSessionScopes)
|
||||
|| !allowedSessionScopes.includes('domain:enter')
|
||||
|| !isPolicyScopeList(forbiddenDataScopes)
|
||||
|| !isPolicyReference(runtimePolicy.permissionPolicyRef)
|
||||
|| !isPolicyReference(runtimePolicy.routeRef)
|
||||
|| !isPolicyReference(runtimePolicy.themePackageRef)
|
||||
|| (runtimePolicy.themeOwner !== 'domain-team' && runtimePolicy.themeOwner !== 'fifth-domain')
|
||||
|| (expectedDomainId === 'DOM-FIFTH-0001'
|
||||
? runtimePolicy.themeOwner !== 'fifth-domain' || !runtimePolicy.themePackageRef.startsWith('theme://origin-domain/')
|
||||
: runtimePolicy.themeOwner !== 'domain-team' || runtimePolicy.themePackageRef.startsWith('theme://origin-domain/'))) invalidHandoff();
|
||||
|
||||
const signedPayload = domainManifestSigningBytes({
|
||||
domainId: manifest.domainId,
|
||||
repositoryId: manifest.repositoryId,
|
||||
runtimePolicy: runtimePolicy as unknown as DomainRuntimeManifestPolicy,
|
||||
schema: manifest.schema,
|
||||
signerId: manifest.signerId,
|
||||
sourceCommit: manifest.sourceCommit,
|
||||
|
|
@ -180,7 +230,9 @@ export function parseDomainAccessHandoff(
|
|||
|| typeof capability.expiresAt !== 'number' || !Number.isSafeInteger(capability.expiresAt) || capability.expiresAt <= now
|
||||
|| !Array.isArray(capability.scopes)
|
||||
|| capability.scopes.some((scope) => !isIdentifier(scope))
|
||||
|| !capability.scopes.includes('domain:enter')) invalidHandoff();
|
||||
|| new Set(capability.scopes).size !== capability.scopes.length
|
||||
|| !capability.scopes.includes('domain:enter')
|
||||
|| capability.scopes.some((scope) => !allowedSessionScopes.includes(scope))) invalidHandoff();
|
||||
|
||||
if (!isIdentifier(connection.connectionId)
|
||||
|| !isIdentifier(connection.receiptId)
|
||||
|
|
@ -193,7 +245,21 @@ export function parseDomainAccessHandoff(
|
|||
const handoff: DomainAccessHandoff = {
|
||||
connectionReceipt: connection as unknown as DomainConnectionReceipt,
|
||||
manifest: {
|
||||
...(manifest as unknown as Omit<DomainManifestEvidence, 'verifierReceipt'>),
|
||||
digest,
|
||||
domainId: manifest.domainId as string,
|
||||
repositoryId: manifest.repositoryId as string,
|
||||
runtimePolicy: Object.freeze({
|
||||
allowedSessionScopes: Object.freeze([...allowedSessionScopes]),
|
||||
forbiddenDataScopes: Object.freeze([...forbiddenDataScopes]),
|
||||
permissionPolicyRef: runtimePolicy.permissionPolicyRef as string,
|
||||
routeRef: runtimePolicy.routeRef as string,
|
||||
themeOwner: runtimePolicy.themeOwner as DomainThemeOwner,
|
||||
themePackageRef: runtimePolicy.themePackageRef as string,
|
||||
}),
|
||||
schema: manifest.schema,
|
||||
signature: manifest.signature,
|
||||
signerId: manifest.signerId as string,
|
||||
sourceCommit: manifest.sourceCommit,
|
||||
verifierReceipt: {
|
||||
manifestDigest: digest,
|
||||
signerId: manifest.signerId,
|
||||
|
|
@ -265,7 +331,8 @@ export function evaluateDomainAccess(evidence: DomainAccessEvidence, now = Date.
|
|||
|| capability.nodeId !== evidence.nodeId
|
||||
|| capability.nodeType !== evidence.nodeType
|
||||
|| capability.expiresAt <= now
|
||||
|| !capability.scopes.includes('domain:enter')) {
|
||||
|| !capability.scopes.includes('domain:enter')
|
||||
|| capability.scopes.some((scope) => !manifest?.runtimePolicy.allowedSessionScopes.includes(scope))) {
|
||||
blockers.push('scoped_session_capability_missing');
|
||||
}
|
||||
const receipt = evidence.connectionReceipt;
|
||||
|
|
|
|||
Loading…
Reference in a new issue