Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export async function GET(
return NextResponse.json({ error: "jobId must be 1-160 characters" }, { status: 400 });
}
const candidates = loadScanState().bundleBindings?.[jobId] ?? [];
const bindings = (await Promise.all(candidates.map(verifyBundleBinding)))
const bindings = (await Promise.all(candidates.map((binding) => verifyBundleBinding(binding))))
.filter((binding) => binding !== null);
return NextResponse.json({ bindings });
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export async function POST(req: NextRequest) {
// Verify at ingress as well as at use so invalid carrier data never enters
// persistent registration state.
if (body.bundleBindings) {
const verified = await Promise.all(body.bundleBindings.map(verifyBundleBinding));
const verified = await Promise.all(body.bundleBindings.map((binding) => verifyBundleBinding(binding)));
if (verified.some((binding) => binding === null)) {
return NextResponse.json({ error: "one or more bundleBindings fail DACS-5 BB-4" }, { status: 400 });
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { createHash } from "node:crypto";
import { contentHash } from "@kynesyslabs/dacs/canonical";
import { ed25519Verify, publicKeyFromRaw } from "@kynesyslabs/dacs/crypto";
import { canonicalDemosAgentClaim } from "./claimRef.js";
import {
resolvePrimaryClaimKey,
resolveDemosPrimaryClaimKey,
sameResolvedPrimaryClaim,
verifyResolvedPrimaryClaimSignature,
type ResolvePrimaryClaimKey,
} from "./primaryClaimKey.js";
import type { BundleBinding } from "./types.js";

const BINDING_DOMAIN = "dacs-bundle-binding:v1:";
Expand Down Expand Up @@ -43,7 +49,10 @@ function strictSignatureBytes(value: unknown): Uint8Array | null {
* BB-4 plus structural ingress. Unknown top-level members remain in the
* signed scope so a newer-minor field can never be silently stripped.
*/
export async function verifyBundleBinding(value: unknown): Promise<BundleBinding | null> {
export async function verifyBundleBinding(
value: unknown,
resolveKey: ResolvePrimaryClaimKey = resolveDemosPrimaryClaimKey,
): Promise<BundleBinding | null> {
const raw = record(value);
const signature = record(raw?.signature);
if (
Expand All @@ -59,27 +68,21 @@ export async function verifyBundleBinding(value: unknown): Promise<BundleBinding
typeof signature.signer !== "string" || typeof signature.value !== "string"
) return null;

const signer = canonicalDemosAgentClaim(raw.signer);
const signatureSigner = canonicalDemosAgentClaim(signature.signer);
if (!signer || !signatureSigner || signer !== signatureSigner) return null;
const keyHex = signer.slice(-64);
const signer = await resolvePrimaryClaimKey(raw.signer, signature.algorithm, resolveKey);
const signatureSigner = await resolvePrimaryClaimKey(signature.signer, signature.algorithm, resolveKey);
if (!signer || !signatureSigner || !sameResolvedPrimaryClaim(signer, signatureSigner)) return null;
const sig = strictSignatureBytes(signature.value);
if (!sig) return null;

const scope = { ...raw };
delete scope.signature;
const hash = contentHash(scope);
let ok = false;
try {
ok = await ed25519Verify(
Buffer.from(BINDING_DOMAIN + hash, "utf8"),
sig,
publicKeyFromRaw(Uint8Array.from(Buffer.from(keyHex, "hex"))),
);
} catch {
return null;
}
return ok ? raw as BundleBinding : null;
const verified = verifyResolvedPrimaryClaimSignature(
Buffer.from(BINDING_DOMAIN + hash, "utf8"),
sig,
signer,
);
return verified ? raw as BundleBinding : null;
}

const bindingOrder = (left: BundleBinding, right: BundleBinding): number =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { contentHash, stripSignature } from "@kynesyslabs/dacs/canonical";
import { ed25519Verify, publicKeyFromRaw } from "@kynesyslabs/dacs/crypto";
import type { AttestationBundle } from "@kynesyslabs/dacs/artifacts";
import type { BundleVerification } from "../../vendor/dacs-sdk/dist/agent/verifyBundleCore.js";

import { bundleSignerPolicy, demosSigningIdentity } from "./bundleSignerPolicy.js";
import { verifyListing } from "./listingVerification.js";
import {
resolvePrimaryClaimKey,
resolveDemosPrimaryClaimKey,
verifyPrimaryClaimSignature,
type ResolvePrimaryClaimKey,
} from "./primaryClaimKey.js";
import type { DealRecord, RegisteredDeal } from "./types.js";

// This is the legacy SDK compatibility path. Current-profile agreement artifacts,
Expand All @@ -28,16 +33,6 @@ function decode(value: string): Uint8Array | null {
}
}

function keyFor(claim: string): ReturnType<typeof publicKeyFromRaw> | null {
const hex = claim.match(/([0-9a-fA-F]{64})$/)?.[1];
if (!hex) return null;
try {
return publicKeyFromRaw(Uint8Array.from(Buffer.from(hex, "hex")));
} catch {
return null;
}
}

export function hasRequiredBundleSignatures(result: BundleVerification, rawBundle: unknown): boolean {
const bundle = result.bundle;
if (!bundle || !rawBundle || typeof rawBundle !== "object" || Array.isArray(rawBundle)) return false;
Expand All @@ -54,7 +49,8 @@ export function hasRequiredBundleSignatures(result: BundleVerification, rawBundl
}
const valid = new Set(result.signatures.filter((s) => s.verdict === "valid").map((s) => s.party));
return bundleSignerPolicy(bundle, valid, result.signatures.length > 0 &&
result.signatures.every((signature) => signature.verdict === "valid"));
result.signatures.every((signature) => signature.verdict === "valid"),
{ legacyDemosAliases: true });
}

/** Return the role registered for this exact copy address, if the directory knows it. */
Expand Down Expand Up @@ -175,8 +171,9 @@ function expectedArtifacts(verification: BundleVerification): ExpectedArtifact[]
export async function verifyReferencedArtifactSignature(
artifact: ResolvedArtifact,
partyClaims: Set<string> = new Set(),
resolveKey: ResolvePrimaryClaimKey = resolveDemosPrimaryClaimKey,
): Promise<boolean> {
if (artifact.kind === "dacs-1-listing") return (await verifyListing(artifact.raw)) !== null;
if (artifact.kind === "dacs-1-listing") return (await verifyListing(artifact.raw, resolveKey)) !== null;
const separator = SEPARATORS[artifact.kind];
if (!separator) return false;
const raw = artifact.raw;
Expand All @@ -197,26 +194,36 @@ export async function verifyReferencedArtifactSignature(
const s = entry as Record<string, unknown>;
const signer = typeof s.signer === "string" ? s.signer : typeof s.party === "string" ? s.party : null;
if (s.algorithm !== "ed25519" || !signer || typeof s.value !== "string") return false;
const key = keyFor(signer);
const signature = decode(s.value);
if (!key || !signature || !(await ed25519Verify(message, signature, key))) return false;
validSigners.add(signer);
if (!signature) return false;
const verified = await verifyPrimaryClaimSignature(
message, signature, signer, s.algorithm, resolveKey,
);
if (!verified) return false;
validSigners.add(verified.canonicalClaim);
}
if (artifact.kind === "dacs-3-agreement") {
const buyer = scope.buyer;
const seller = scope.seller;
return typeof buyer === "string" && typeof seller === "string" &&
validSigners.has(buyer) && validSigners.has(seller);
if (typeof buyer !== "string" || typeof seller !== "string") return false;
const resolved = await Promise.all([
resolvePrimaryClaimKey(buyer, "ed25519", resolveKey),
resolvePrimaryClaimKey(seller, "ed25519", resolveKey),
]);
return resolved.every((claim) => claim && validSigners.has(claim.canonicalClaim));
}
if (artifact.kind === "dacs-4-evidence") {
return [...validSigners].some((s) => partyClaims.has(s));
const parties = await Promise.all([...partyClaims].map((claim) =>
resolvePrimaryClaimKey(claim, "ed25519", resolveKey)));
return parties.some((claim) => claim && validSigners.has(claim.canonicalClaim));
}
return true;
}

export async function refsPassStrictPolicy(
verification: BundleVerification,
artifacts: ResolvedArtifact[],
resolveKey: ResolvePrimaryClaimKey = resolveDemosPrimaryClaimKey,
): Promise<boolean> {
if (!verification.ok || verification.refs.some((r) => r.verdict !== "ok")) return false;
const expected = expectedArtifacts(verification);
Expand All @@ -242,7 +249,7 @@ export async function refsPassStrictPolicy(
}
const partyClaims = new Set((verification.bundle?.parties ?? []).map((p) => p.primaryClaim));
const checks = await Promise.all(
artifacts.map((a) => verifyReferencedArtifactSignature(a, partyClaims)),
artifacts.map((a) => verifyReferencedArtifactSignature(a, partyClaims, resolveKey)),
);
return checks.every(Boolean);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { canonicalSigningIdentity } from "./primaryClaimKey.js";

const ROLES = new Set(["buyer", "seller", "orchestrator"] as const);
const OUTCOMES = new Set([
"completed", "failed-perm", "failed-counterparty", "failed-substrate", "aborted-by-self", "aborted-by-other",
Expand All @@ -11,7 +13,7 @@ interface BundleLike {
parties?: unknown;
}

/** Collapse the accepted Demos claim spellings to one key identity. */
/** Explicit compatibility identity used only by the legacy SDK reader. */
export function demosSigningIdentity(claim: string): string {
const key = claim.match(/^(?:did:demos:agent:|0x)?([0-9a-f]{64})$/i)?.[1];
return key ? key.toLowerCase() : claim;
Expand All @@ -25,6 +27,7 @@ export function bundleSignerPolicy(
bundle: BundleLike,
validSigners: Iterable<string>,
allPresentedSignaturesValid: boolean,
options: { legacyDemosAliases?: boolean } = {},
): boolean {
if (!allPresentedSignaturesValid || !Array.isArray(bundle.parties)) return false;
if (typeof bundle.outcome !== "string" || !OUTCOMES.has(bundle.outcome)) return false;
Expand All @@ -40,18 +43,19 @@ export function bundleSignerPolicy(
byRole.set(role as BundleRole, claim);
}
if (!byRole.has("buyer") || !byRole.has("seller")) return false;
if (demosSigningIdentity(byRole.get("buyer")!) === demosSigningIdentity(byRole.get("seller")!)) return false;
const identity = options.legacyDemosAliases ? demosSigningIdentity : canonicalSigningIdentity;
if (identity(byRole.get("buyer")!) === identity(byRole.get("seller")!)) return false;

const anchoredByRole = bundle.anchoredByRole;
if (typeof anchoredByRole !== "string" || !ROLES.has(anchoredByRole as BundleRole)) return false;
const anchorClaim = byRole.get(anchoredByRole as BundleRole);
if (!anchorClaim) return false;

const partyClaims = new Set([...byRole.values()].map(demosSigningIdentity));
const valid = new Set([...validSigners].map(demosSigningIdentity));
const partyClaims = new Set([...byRole.values()].map(identity));
const valid = new Set([...validSigners].map(identity));
if (valid.size === 0 || [...valid].some((claim) => !partyClaims.has(claim))) return false;

const allRequired = valid.size === partyClaims.size && [...partyClaims].every((claim) => valid.has(claim));
const abort = bundle.outcome === "aborted-by-self" || bundle.outcome === "aborted-by-other";
return abort ? (valid.size === 1 && valid.has(demosSigningIdentity(anchorClaim))) || allRequired : allRequired;
return abort ? (valid.size === 1 && valid.has(identity(anchorClaim))) || allRequired : allRequired;
}
Loading