@kya-brasil/core 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -46,6 +46,25 @@ const verdict = checkAction(credential.mandate, {
46
46
  // verdict.allowed, verdict.checks (every rule evaluated, with its reason), verdict.not_evaluated (extensions)
47
47
  ```
48
48
 
49
+ ## Without trusting the issuer
50
+
51
+ Since D-33 the e-CNPJ / e-CPF signs the agent and the mandate's hash, and the principal keeps the signed text and the
52
+ `.p7s`. When the principal hands them to its agent, you can check the authorization against ICP-Brasil alone (SPEC
53
+ section 19), and the issuer's key vouches only for the credential's status:
54
+
55
+ ```ts
56
+ import { decodePrincipalProofHeader, verifyPrincipalProof, checkRevocation } from "@kya-brasil/core";
57
+
58
+ const proof = decodePrincipalProofHeader(request.headers.get("KYA-Principal-Proof")!);
59
+ const r = await verifyPrincipalProof({ credential, proof, anchors: icpBrasilAnchors, now: trustedClock() });
60
+ // r.ok; r.checks: signature, certificate, agent, principal, issuer, mandate (each with its reason)
61
+ // then the certificate's revocation, with a CRL you fetched from r.revocation.crl_urls:
62
+ // await checkRevocation({ crlDer, serial: r.revocation!.serial_number, issuerCertificate: r.revocation!.issuer_certificate!, pastNextUpdate: "refuse" })
63
+ ```
64
+
65
+ The CMS carries the signer's certificate (personal data): the principal decides when to share it, and you use it for
66
+ this check only. The result carries nothing personal.
67
+
49
68
  ## Delegation chains (sub-agents)
50
69
 
51
70
  A mandate with `delegation: { max_depth, max_ttl_seconds }` lets the agent hand a narrower mandate to another key
package/dist/index.d.ts CHANGED
@@ -12,3 +12,4 @@ export * from "./cloud-sign.js";
12
12
  export * from "./person.js";
13
13
  export * from "./delegation.js";
14
14
  export * from "./freshness.js";
15
+ export * from "./principal-proof.js";
package/dist/index.js CHANGED
@@ -12,3 +12,4 @@ export * from "./cloud-sign.js";
12
12
  export * from "./person.js";
13
13
  export * from "./delegation.js";
14
14
  export * from "./freshness.js";
15
+ export * from "./principal-proof.js";
@@ -0,0 +1,38 @@
1
+ import { type Credential } from "./types.js";
2
+ /** What the agent presents: the exact challenge text the principal signed, and the detached CMS (base64 DER). */
3
+ export interface PrincipalProof {
4
+ text: string;
5
+ cms: string;
6
+ }
7
+ export interface PrincipalProofCheck {
8
+ rule: "signature" | "certificate" | "agent" | "principal" | "issuer" | "mandate";
9
+ ok: boolean;
10
+ detail: string;
11
+ }
12
+ export interface PrincipalProofResult {
13
+ ok: boolean;
14
+ checks: PrincipalProofCheck[];
15
+ /** For the caller's revocation check of the signing certificate; null when the signature did not verify. */
16
+ revocation: {
17
+ serial_number: string;
18
+ issuer_certificate: string | null;
19
+ crl_urls: string[];
20
+ } | null;
21
+ }
22
+ /**
23
+ * Checks, against the credential and without the issuer, that the principal's own signature authorizes this
24
+ * agent with this mandate: the CMS verifies over the text up to the ICP-Brasil anchors; the certificate is the
25
+ * one the credential records; the text names the credential's agent, principal and issuer; and the mandate hash
26
+ * in the text equals the hash of the credential's mandate. `anchors`, `now` and the revocation check are the
27
+ * caller's. Never throws on a bad proof: every check is reported.
28
+ */
29
+ export declare function verifyPrincipalProof(p: {
30
+ credential: Pick<Credential, "agent" | "principal" | "authorized_by" | "mandate" | "issuer">;
31
+ proof: PrincipalProof;
32
+ anchors: string[];
33
+ now: Date;
34
+ }): Promise<PrincipalProofResult>;
35
+ /** Header value for a request: base64url of the JSON { text, cms }. */
36
+ export declare function encodePrincipalProofHeader(proof: PrincipalProof): string;
37
+ /** Parses the header value; throws INVALID_INPUT when it is not a proof. */
38
+ export declare function decodePrincipalProofHeader(value: string): PrincipalProof;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Verification without trusting the issuer (D-34, SPEC section 19).
3
+ *
4
+ * Since D-33 the principal signs, with the e-CNPJ or e-CPF, a challenge that names the agent and carries the
5
+ * mandate's SHA-256, and keeps the signed text and the detached CMS (.p7s). When the principal chooses to hand
6
+ * them to its agent, the agent can present them with a request, and any verifier can check by itself, with the
7
+ * ICP-Brasil trust anchors, that this principal authorized this agent with these powers: the issuer's key is
8
+ * then needed for nothing but the credential's status.
9
+ *
10
+ * Pure functions, no network. The certificate's revocation is the caller's: this module returns what is needed
11
+ * to fetch and check the CRL (checkRevocation, evidenceFromRevocation), never personal data. The CMS carries the
12
+ * signer's certificate, so the principal decides when to share it, and the verifier treats it as personal data.
13
+ */
14
+ import { base64url, mandateSha256OfChallenge } from "./challenge.js";
15
+ import { mandateSha256 } from "./credential.js";
16
+ import { verifyIcpPersonSignature, verifyIcpSignature } from "./icp-brasil.js";
17
+ import { KyaError, isPersonPrincipal } from "./types.js";
18
+ const lineOf = (text, key) => {
19
+ const lines = text.split("\n").filter((l) => l.startsWith(`${key}: `));
20
+ return lines.length === 1 ? lines[0].slice(key.length + 2) : null;
21
+ };
22
+ /**
23
+ * Checks, against the credential and without the issuer, that the principal's own signature authorizes this
24
+ * agent with this mandate: the CMS verifies over the text up to the ICP-Brasil anchors; the certificate is the
25
+ * one the credential records; the text names the credential's agent, principal and issuer; and the mandate hash
26
+ * in the text equals the hash of the credential's mandate. `anchors`, `now` and the revocation check are the
27
+ * caller's. Never throws on a bad proof: every check is reported.
28
+ */
29
+ export async function verifyPrincipalProof(p) {
30
+ const { credential: c, proof } = p;
31
+ const checks = [];
32
+ const push = (rule, ok, detail) => checks.push({ rule, ok, detail });
33
+ if (!proof || typeof proof.text !== "string" || typeof proof.cms !== "string") {
34
+ push("signature", false, "proof must carry the signed text and the CMS");
35
+ return { ok: false, checks, revocation: null };
36
+ }
37
+ if (!(p.now instanceof Date) || Number.isNaN(p.now.getTime())) {
38
+ push("signature", false, "the verifier's clock is not a valid date");
39
+ return { ok: false, checks, revocation: null };
40
+ }
41
+ const person = isPersonPrincipal(c.principal);
42
+ let certificateSha256 = null;
43
+ let revocation = null;
44
+ try {
45
+ const icp = person
46
+ ? await verifyIcpPersonSignature({ cmsBase64: proof.cms, challenge: proof.text, anchors: p.anchors, now: p.now })
47
+ : await verifyIcpSignature({ cmsBase64: proof.cms, challenge: proof.text, expectedCnpj: c.principal.cnpj, anchors: p.anchors, now: p.now });
48
+ certificateSha256 = icp.certificate_sha256;
49
+ revocation = { serial_number: icp.serial_number, issuer_certificate: icp.issuer_certificate, crl_urls: icp.crl_urls };
50
+ push("signature", true, `${person ? "e-CPF" : "e-CNPJ"} signature over the text verifies up to the ICP-Brasil anchors`);
51
+ }
52
+ catch (e) {
53
+ push("signature", false, e instanceof KyaError ? e.message : `signature does not verify: ${e.message}`);
54
+ }
55
+ if (certificateSha256 !== null) {
56
+ const same = certificateSha256 === c.authorized_by.certificate_sha256;
57
+ push("certificate", same, same ? "signed with the certificate the credential records" : "signed with another certificate than the one the credential records");
58
+ }
59
+ const agent = lineOf(proof.text, "agent");
60
+ const expectedAgent = `${c.agent.kind}:${c.agent.id}`;
61
+ push("agent", agent === expectedAgent, agent === expectedAgent ? `text names the credential's agent ${expectedAgent}` : `text names agent ${agent ?? "(none)"}, the credential ${expectedAgent}`);
62
+ if (person) {
63
+ const ok = lineOf(proof.text, "principal") === "person (e-CPF)";
64
+ push("principal", ok, ok ? "text is a natural person's authorization" : "text is not a natural person's authorization");
65
+ }
66
+ else {
67
+ const cnpj = lineOf(proof.text, "cnpj");
68
+ const expected = c.principal.cnpj;
69
+ push("principal", cnpj === expected, cnpj === expected ? `text names CNPJ ${expected}` : `text names CNPJ ${cnpj ?? "(none)"}, the credential ${expected}`);
70
+ }
71
+ const issuer = lineOf(proof.text, "issuer");
72
+ push("issuer", issuer === c.issuer, issuer === c.issuer ? `text names issuer ${c.issuer}` : `text names issuer ${issuer ?? "(none)"}, the credential ${c.issuer}`);
73
+ const signed = mandateSha256OfChallenge(proof.text);
74
+ const actual = await mandateSha256(c.mandate);
75
+ const recorded = c.authorized_by.mandate_sha256;
76
+ const mandateOk = signed !== null && signed === actual && (!recorded || recorded === signed);
77
+ push("mandate", mandateOk, signed === null ? "text carries no mandate hash (signed before D-33)" : mandateOk ? "text commits to the credential's mandate" : "text commits to another mandate than the credential's");
78
+ return { ok: checks.every((x) => x.ok), checks, revocation };
79
+ }
80
+ /** Header value for a request: base64url of the JSON { text, cms }. */
81
+ export function encodePrincipalProofHeader(proof) {
82
+ return base64url.encode(new TextEncoder().encode(JSON.stringify({ text: proof.text, cms: proof.cms })));
83
+ }
84
+ /** Parses the header value; throws INVALID_INPUT when it is not a proof. */
85
+ export function decodePrincipalProofHeader(value) {
86
+ let parsed;
87
+ try {
88
+ parsed = JSON.parse(new TextDecoder().decode(base64url.decode(value.trim())));
89
+ }
90
+ catch {
91
+ throw new KyaError("INVALID_INPUT", "KYA-Principal-Proof is not base64url JSON");
92
+ }
93
+ const o = parsed;
94
+ if (!o || typeof o.text !== "string" || typeof o.cms !== "string")
95
+ throw new KyaError("INVALID_INPUT", "KYA-Principal-Proof must carry text and cms");
96
+ return { text: o.text, cms: o.cms };
97
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kya-brasil/core",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "KYA Brasil as a library: verify and issue know-your-agent credentials for Brazilian companies without any hosted service. e-CNPJ (ICP-Brasil) CMS signature and chain validation, CRL revocation, CNPJ, agent key challenges (EVM, Ed25519 / Web Bot Auth, ERC-8004), mandate schema and evaluation (including payment instruments for card networks and acquirers), credential build, Ed25519 signing and verification, and e-CNPJ signing for the holder (A1 .pfx, or any token/HSM/cloud signer through a raw-sign callback). Pure functions: storage and network are yours.",
5
5
  "license": "MIT",
6
6
  "type": "module",