229 lines
7.2 KiB
TypeScript
229 lines
7.2 KiB
TypeScript
/**
|
|
* DTP-21 — KeyGrantEnvelope validator (issuer / recipient / signature
|
|
* / expiry checks in spec-mandated order).
|
|
*/
|
|
|
|
import { sign } from "@suaveplan/crypto-curve/ed25519";
|
|
import { describe, expect, it } from "@suaveplan/testing/runner";
|
|
import type { KeyGrantPayload } from "../src/index.js";
|
|
import {
|
|
canonicalSerializeKeyGrant,
|
|
type DriveTemplateError,
|
|
DTP_CODES,
|
|
validateKeyGrant,
|
|
} from "../src/index.js";
|
|
import { makeKeys } from "./_helpers.js";
|
|
|
|
const NOW = 2_000_000_000;
|
|
|
|
function makeGrant(
|
|
issuerHex: string,
|
|
recipientHex: string,
|
|
issuerSecret: Uint8Array,
|
|
overrides: Partial<KeyGrantPayload> = {}
|
|
): KeyGrantPayload {
|
|
const base: Omit<KeyGrantPayload, "issuerSignature"> = {
|
|
fileId: "f".repeat(64),
|
|
recipient: recipientHex,
|
|
wrappedKey: new Uint8Array(48).fill(7),
|
|
issuer: issuerHex,
|
|
expiresAt: NOW + 60_000,
|
|
...overrides,
|
|
};
|
|
const canonical = canonicalSerializeKeyGrant(base);
|
|
const issuerSignature = sign(canonical, issuerSecret);
|
|
return { ...base, issuerSignature, ...overrides };
|
|
}
|
|
|
|
describe("DTP-21 — key-grant validator", () => {
|
|
it("valid grant passes", { timeout: 5000 }, () => {
|
|
const issuer = makeKeys();
|
|
const recipient = makeKeys();
|
|
const grant = makeGrant(
|
|
issuer.publicHex,
|
|
recipient.publicHex,
|
|
issuer.secret
|
|
);
|
|
expect(
|
|
validateKeyGrant(grant, {
|
|
expectedKeyAuthority: issuer.publicHex,
|
|
ourPubkey: recipient.publicHex,
|
|
clock: () => NOW,
|
|
})
|
|
).toBe(true);
|
|
});
|
|
|
|
it("issuer mismatch rejected (DMP-21 step 1)", { timeout: 5000 }, () => {
|
|
const issuer = makeKeys();
|
|
const recipient = makeKeys();
|
|
const grant = makeGrant(
|
|
issuer.publicHex,
|
|
recipient.publicHex,
|
|
issuer.secret
|
|
);
|
|
try {
|
|
validateKeyGrant(grant, {
|
|
expectedKeyAuthority: "different-authority",
|
|
ourPubkey: recipient.publicHex,
|
|
clock: () => NOW,
|
|
});
|
|
throw new Error("expected rejection");
|
|
} catch (err) {
|
|
expect((err as DriveTemplateError).code).toBe(
|
|
DTP_CODES.KEY_GRANT_REJECTED
|
|
);
|
|
}
|
|
});
|
|
|
|
it("recipient mismatch rejected (step 2)", { timeout: 5000 }, () => {
|
|
const issuer = makeKeys();
|
|
const recipient = makeKeys();
|
|
const someoneElse = makeKeys();
|
|
const grant = makeGrant(
|
|
issuer.publicHex,
|
|
recipient.publicHex,
|
|
issuer.secret
|
|
);
|
|
try {
|
|
validateKeyGrant(grant, {
|
|
expectedKeyAuthority: issuer.publicHex,
|
|
ourPubkey: someoneElse.publicHex,
|
|
clock: () => NOW,
|
|
});
|
|
throw new Error("expected rejection");
|
|
} catch (err) {
|
|
expect((err as DriveTemplateError).code).toBe(
|
|
DTP_CODES.KEY_GRANT_REJECTED
|
|
);
|
|
}
|
|
});
|
|
|
|
it("invalid issuer hex rejected", { timeout: 5000 }, () => {
|
|
const issuer = makeKeys();
|
|
const recipient = makeKeys();
|
|
const grant: KeyGrantPayload = {
|
|
fileId: "f",
|
|
recipient: recipient.publicHex,
|
|
wrappedKey: new Uint8Array(8),
|
|
issuer: "not-hex-zzz",
|
|
issuerSignature: new Uint8Array(64),
|
|
expiresAt: NOW + 60_000,
|
|
};
|
|
try {
|
|
validateKeyGrant(grant, {
|
|
expectedKeyAuthority: "not-hex-zzz",
|
|
ourPubkey: recipient.publicHex,
|
|
clock: () => NOW,
|
|
});
|
|
throw new Error("expected rejection");
|
|
} catch (err) {
|
|
expect((err as DriveTemplateError).code).toBe(
|
|
DTP_CODES.KEY_GRANT_REJECTED
|
|
);
|
|
}
|
|
// Suppress unused-issuer lint
|
|
void issuer;
|
|
});
|
|
|
|
it("bad signature rejected (step 3)", { timeout: 5000 }, () => {
|
|
const issuer = makeKeys();
|
|
const recipient = makeKeys();
|
|
const grant = makeGrant(
|
|
issuer.publicHex,
|
|
recipient.publicHex,
|
|
issuer.secret
|
|
);
|
|
const tampered = {
|
|
...grant,
|
|
wrappedKey: new Uint8Array(48).fill(0xff),
|
|
};
|
|
try {
|
|
validateKeyGrant(tampered, {
|
|
expectedKeyAuthority: issuer.publicHex,
|
|
ourPubkey: recipient.publicHex,
|
|
clock: () => NOW,
|
|
});
|
|
throw new Error("expected rejection");
|
|
} catch (err) {
|
|
expect((err as DriveTemplateError).code).toBe(
|
|
DTP_CODES.KEY_GRANT_REJECTED
|
|
);
|
|
}
|
|
});
|
|
|
|
it("expired grant rejected (step 4)", { timeout: 5000 }, () => {
|
|
const issuer = makeKeys();
|
|
const recipient = makeKeys();
|
|
const grant = makeGrant(
|
|
issuer.publicHex,
|
|
recipient.publicHex,
|
|
issuer.secret,
|
|
{ expiresAt: NOW - 1 }
|
|
);
|
|
try {
|
|
validateKeyGrant(grant, {
|
|
expectedKeyAuthority: issuer.publicHex,
|
|
ourPubkey: recipient.publicHex,
|
|
clock: () => NOW,
|
|
});
|
|
throw new Error("expected expired");
|
|
} catch (err) {
|
|
expect((err as DriveTemplateError).code).toBe(
|
|
DTP_CODES.KEY_GRANT_REJECTED
|
|
);
|
|
}
|
|
});
|
|
|
|
it("malformed signature buffer triggers verify catch arm", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const issuer = makeKeys();
|
|
const recipient = makeKeys();
|
|
const grant: KeyGrantPayload = {
|
|
fileId: "f",
|
|
recipient: recipient.publicHex,
|
|
wrappedKey: new Uint8Array(8),
|
|
issuer: issuer.publicHex,
|
|
issuerSignature: new Uint8Array(8),
|
|
};
|
|
try {
|
|
validateKeyGrant(grant, {
|
|
expectedKeyAuthority: issuer.publicHex,
|
|
ourPubkey: recipient.publicHex,
|
|
clock: () => NOW,
|
|
});
|
|
throw new Error("expected rejection");
|
|
} catch (err) {
|
|
expect((err as DriveTemplateError).code).toBe(
|
|
DTP_CODES.KEY_GRANT_REJECTED
|
|
);
|
|
}
|
|
});
|
|
|
|
it("grant without expiresAt accepted indefinitely", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const issuer = makeKeys();
|
|
const recipient = makeKeys();
|
|
// Build without expiresAt then re-sign over the same canonical bytes.
|
|
const base: Omit<KeyGrantPayload, "issuerSignature"> = {
|
|
fileId: "f".repeat(64),
|
|
recipient: recipient.publicHex,
|
|
wrappedKey: new Uint8Array(48).fill(7),
|
|
issuer: issuer.publicHex,
|
|
};
|
|
const canonical = canonicalSerializeKeyGrant(base);
|
|
const grant: KeyGrantPayload = {
|
|
...base,
|
|
issuerSignature: sign(canonical, issuer.secret),
|
|
};
|
|
expect(
|
|
validateKeyGrant(grant, {
|
|
expectedKeyAuthority: issuer.publicHex,
|
|
ourPubkey: recipient.publicHex,
|
|
clock: () => NOW,
|
|
})
|
|
).toBe(true);
|
|
});
|
|
});
|