@kya-brasil/agent-tools 0.6.0 → 0.8.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 +42 -11
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +96 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +14 -0
- package/dist/pkcs11.d.ts +36 -0
- package/dist/pkcs11.js +124 -0
- package/package.json +12 -5
package/README.md
CHANGED
|
@@ -103,7 +103,7 @@ if (!callee?.ok) throw new Error("unsigned or invalid callee"); // callee.
|
|
|
103
103
|
## Company: give your own agent a credential
|
|
104
104
|
|
|
105
105
|
```ts
|
|
106
|
-
import { createKyaClient, signChallengeWithEvmKey,
|
|
106
|
+
import { createKyaClient, signChallengeWithEvmKey, signingLink, signWithECnpjPfx } from "@kya-brasil/agent-tools";
|
|
107
107
|
import { readFileSync } from "node:fs";
|
|
108
108
|
|
|
109
109
|
const kya = createKyaClient({ privateKey: payerPk });
|
|
@@ -120,19 +120,50 @@ const reg = await kya.register({ // $0
|
|
|
120
120
|
});
|
|
121
121
|
|
|
122
122
|
const agent_signature = await signChallengeWithEvmKey(agentPk, reg.challenge.text);
|
|
123
|
-
const ecnpj_signature = await signWithECnpj({
|
|
124
|
-
certificatePem: readFileSync("ecnpj.crt.pem", "utf8"),
|
|
125
|
-
privateKeyPem: readFileSync("ecnpj.key.pem", "utf8"),
|
|
126
|
-
chainPem: [readFileSync("ecnpj.chain.pem", "utf8")]
|
|
127
|
-
}, reg.challenge.text);
|
|
128
123
|
|
|
129
|
-
|
|
124
|
+
// Usual case: a person holds the e-CNPJ (owner, accountant). Send them the link; they sign in the browser
|
|
125
|
+
// (.pfx, or a .p7s from any ICP-Brasil signer for A3 tokens and cloud certificates) and the credential is issued.
|
|
126
|
+
const link = signingLink({ credentialId: reg.credential_id, challenge: reg.challenge, agentSignature: agent_signature });
|
|
127
|
+
// (reg.sign_url is the same link without the agent signature)
|
|
128
|
+
|
|
129
|
+
// Or in code, when you hold the A1 file yourself:
|
|
130
|
+
const ecnpj_signature = signWithECnpjPfx(readFileSync("empresa.pfx"), process.env.PFX_PASSWORD!, reg.challenge.text);
|
|
131
|
+
const { credential, warnings } = await kya.prove(reg.credential_id, { challenge_id: reg.challenge.id, agent_signature, ecnpj_signature }); // free
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`signWithECnpjPfx` reads the `.pfx`/`.p12` directly, including the legacy 3DES/RC2 encryption many Brazilian A1
|
|
135
|
+
files use; `signWithECnpj` still takes PEM. On a terminal, the `kya-sign` command does the same without code:
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
npx -p @kya-brasil/agent-tools kya-sign --pfx empresa.pfx --link "<sign_url>&agent_signature=…" # signs and issues
|
|
139
|
+
npx -p @kya-brasil/agent-tools kya-sign --pfx empresa.pfx --text-file desafio.txt # prints the signature
|
|
130
140
|
```
|
|
131
141
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
142
|
+
The password comes from `KYA_PFX_PASSWORD` or is asked for without echo. Everything runs locally.
|
|
143
|
+
|
|
144
|
+
**A3 (token or smart card).** Through PKCS#11, the interface every ICP-Brasil token driver ships. Install the
|
|
145
|
+
optional `pkcs11js` and point to the vendor's library (e.g. SafeNet eToken `libeTPkcs11.so` / `eTPKCS11.dll`):
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
npm install pkcs11js
|
|
149
|
+
npx -p @kya-brasil/agent-tools kya-sign --pkcs11 /usr/lib/libeTPkcs11.so --list # certificates on the token
|
|
150
|
+
npx -p @kya-brasil/agent-tools kya-sign --pkcs11 /usr/lib/libeTPkcs11.so --link "<sign_url>&agent_signature=…"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const token = await openToken({ library: "/usr/lib/libeTPkcs11.so", pin: process.env.TOKEN_PIN! });
|
|
155
|
+
try { ecnpj_signature = await token.sign(reg.challenge.text); } finally { token.close(); }
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The PIN comes from `KYA_TOKEN_PIN` or is asked for. The key never leaves the token. Tested against SoftHSM2 (a
|
|
159
|
+
software PKCS#11 token) in CI. The signing code is in `@kya-brasil/core` (`openPfx`, `ecnpjSigner`,
|
|
160
|
+
`buildDetachedCms`), re-exported here; any other token or cloud signer plugs into `ecnpjSigner`.
|
|
161
|
+
|
|
162
|
+
**From the agent's side**, `kya.signLink(credentialId, { challenge_id, agent_signature })` (free) returns the
|
|
163
|
+
`sign_url` to send, the same as `signingLink(...)` offline and the MCP tool `kya_sign_link`.
|
|
164
|
+
|
|
165
|
+
An agent should not hold the company's e-CNPJ to sign for itself: that signature is the company's consent.
|
|
166
|
+
Let the agent prepare and send the link, and a person sign.
|
|
136
167
|
|
|
137
168
|
Renew every 90 days (`kya.renew` then `kya.renewProve`), revoke at once (`kya.revoke` with `revocationText(id)` signed by the e-CNPJ).
|
|
138
169
|
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* kya-sign: sign a KYA challenge (or a company request text) with the company's e-CNPJ, A1 or A3.
|
|
4
|
+
* A1 file: kya-sign --pfx empresa.pfx --link "<sign_url>&agent_signature=…"
|
|
5
|
+
* A3 token: kya-sign --pkcs11 /usr/lib/libeTPkcs11.so --link "<sign_url>&agent_signature=…"
|
|
6
|
+
* kya-sign --pkcs11 <library> --list (certificates on the token)
|
|
7
|
+
* Text instead of a link: --text-file desafio.txt or --text "<text>" (prints the signature).
|
|
8
|
+
* The password (A1) comes from KYA_PFX_PASSWORD and the PIN (A3) from KYA_TOKEN_PIN, or are asked for without
|
|
9
|
+
* echo. With a link carrying agent_signature, the proof is sent and the issued credential printed. Everything
|
|
10
|
+
* happens on this machine; the certificate and its password or PIN are never sent anywhere.
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
import { openPfx, readSigningLink } from "@kya-brasil/core";
|
|
14
|
+
import { listTokenCertificates, openToken } from "./pkcs11.js";
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
const opt = (name) => {
|
|
17
|
+
const i = args.indexOf(`--${name}`);
|
|
18
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
19
|
+
};
|
|
20
|
+
const die = (msg) => {
|
|
21
|
+
process.stderr.write(`kya-sign: ${msg}\n`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
};
|
|
24
|
+
async function askSecret(envName, prompt) {
|
|
25
|
+
if (process.env[envName] !== undefined)
|
|
26
|
+
return process.env[envName];
|
|
27
|
+
if (!process.stdin.isTTY)
|
|
28
|
+
die(`set ${envName} (no terminal to ask for it)`);
|
|
29
|
+
process.stderr.write(prompt);
|
|
30
|
+
process.stdin.setRawMode(true);
|
|
31
|
+
process.stdin.resume();
|
|
32
|
+
let secret = "";
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
process.stdin.on("data", (b) => {
|
|
35
|
+
for (const ch of b.toString("utf8")) {
|
|
36
|
+
if (ch === "\r" || ch === "\n") {
|
|
37
|
+
process.stdin.setRawMode(false);
|
|
38
|
+
process.stdin.pause();
|
|
39
|
+
process.stderr.write("\n");
|
|
40
|
+
return resolve(secret);
|
|
41
|
+
}
|
|
42
|
+
if (ch === "\u0003")
|
|
43
|
+
process.exit(130);
|
|
44
|
+
secret = ch === "\u007f" ? secret.slice(0, -1) : secret + ch;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const USAGE = `usage:
|
|
50
|
+
kya-sign --pfx <file.pfx> (--link <signing link> | --text-file <file> | --text <text>)
|
|
51
|
+
kya-sign --pkcs11 <library> [--slot N] [--label L] (--link … | --text-file … | --text …)
|
|
52
|
+
kya-sign --pkcs11 <library> --list
|
|
53
|
+
`;
|
|
54
|
+
async function main() {
|
|
55
|
+
if (args.includes("--help") || args.includes("-h"))
|
|
56
|
+
return void process.stdout.write(USAGE);
|
|
57
|
+
const pfxPath = opt("pfx");
|
|
58
|
+
const library = opt("pkcs11");
|
|
59
|
+
if (!pfxPath && !library) {
|
|
60
|
+
process.stdout.write(USAGE);
|
|
61
|
+
process.exit(2);
|
|
62
|
+
}
|
|
63
|
+
const slot = opt("slot") !== undefined ? Number(opt("slot")) : undefined;
|
|
64
|
+
if (library && args.includes("--list")) {
|
|
65
|
+
for (const c of await listTokenCertificates({ library, slot }))
|
|
66
|
+
process.stdout.write(`${c.label}\t${c.cnpj ?? "-"}\t${c.notAfter.toISOString().slice(0, 10)}\t${c.subject}\n`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const link = opt("link") ? readSigningLink(opt("link")) : null;
|
|
70
|
+
const text = link ? link.text : opt("text-file") ? readFileSync(opt("text-file"), "utf8") : (opt("text") ?? die("give --link, --text-file or --text"));
|
|
71
|
+
let signer;
|
|
72
|
+
if (library)
|
|
73
|
+
signer = await openToken({ library, slot, label: opt("label"), pin: await askSecret("KYA_TOKEN_PIN", "PIN do token / token PIN: ") });
|
|
74
|
+
else
|
|
75
|
+
signer = await openPfx(new Uint8Array(readFileSync(pfxPath)), await askSecret("KYA_PFX_PASSWORD", "Senha do certificado / certificate password: "));
|
|
76
|
+
try {
|
|
77
|
+
process.stderr.write(`certificate: ${signer.subject}\nsigning:\n${text.split("\n").map((l) => ` ${l}`).join("\n")}\n`);
|
|
78
|
+
const signature = await signer.sign(text);
|
|
79
|
+
if (!link?.credentialId || !link.challengeId || !link.agentSignature) {
|
|
80
|
+
process.stdout.write(`${signature}\n`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const res = await fetch(new URL(`/v1/agents/${encodeURIComponent(link.credentialId)}/prove`, link.issuer), {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: { "content-type": "application/json" },
|
|
86
|
+
body: JSON.stringify({ challenge_id: link.challengeId, agent_signature: link.agentSignature, ecnpj_signature: signature })
|
|
87
|
+
});
|
|
88
|
+
process.stdout.write(`${await res.text()}\n`);
|
|
89
|
+
if (!res.ok)
|
|
90
|
+
process.exitCode = 1;
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
signer.close?.();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
main().catch((e) => die(e.message));
|
package/dist/index.d.ts
CHANGED
|
@@ -141,6 +141,8 @@ export interface Registration {
|
|
|
141
141
|
pt: string;
|
|
142
142
|
};
|
|
143
143
|
mandate_schema: string;
|
|
144
|
+
/** Link for the e-CNPJ holder; append &agent_signature=… (or use signingLink / signLink). */
|
|
145
|
+
sign_url?: string;
|
|
144
146
|
}
|
|
145
147
|
export interface Proof {
|
|
146
148
|
challenge_id: string;
|
|
@@ -204,6 +206,16 @@ export interface KyaClient {
|
|
|
204
206
|
warnings?: string[]; /** /v1 compatibility, same as warnings. */
|
|
205
207
|
kyb_warnings: string[];
|
|
206
208
|
}>;
|
|
209
|
+
/** Free: the link for the e-CNPJ holder (owner, accountant), with the agent's signature already in it. */
|
|
210
|
+
signLink(credentialId: string, q: {
|
|
211
|
+
challenge_id: string;
|
|
212
|
+
agent_signature?: string;
|
|
213
|
+
}): Promise<{
|
|
214
|
+
credential_id: string;
|
|
215
|
+
challenge_id: string;
|
|
216
|
+
expires_at: string;
|
|
217
|
+
sign_url: string;
|
|
218
|
+
}>;
|
|
207
219
|
/** $0.10: renewal challenge (then renewProve). */
|
|
208
220
|
renew(credentialId: string): Promise<{
|
|
209
221
|
credential_id: string;
|
|
@@ -361,3 +373,20 @@ export declare function verifyCredentialOffline(credential: Credential, opts?: {
|
|
|
361
373
|
now?: Date;
|
|
362
374
|
}): Promise<OfflineCheck>;
|
|
363
375
|
export * from "./auth.js";
|
|
376
|
+
export { buildDetachedCms, cnpjFromCommonName, ecnpjSigner, openPfx, readSigningLink, signWithECnpjPfx, type ECnpjSigner, type RawSigner } from "@kya-brasil/core";
|
|
377
|
+
export { listTokenCertificates, openToken, type TokenCertificate, type TokenOptions } from "./pkcs11.js";
|
|
378
|
+
/**
|
|
379
|
+
* Link to the issuer's signing page (<issuer>/sign) for the person who holds the company's e-CNPJ, usually the
|
|
380
|
+
* owner or the accountant. The agent registers, signs its half and sends this link; the person opens it, sees
|
|
381
|
+
* which agent, company and mandate they are authorizing, signs with the .pfx in the browser (the file never
|
|
382
|
+
* leaves the computer) or uploads a .p7s from any ICP-Brasil signer, and the credential is issued.
|
|
383
|
+
*/
|
|
384
|
+
export declare function signingLink(p: {
|
|
385
|
+
credentialId: string;
|
|
386
|
+
challenge: {
|
|
387
|
+
id: string;
|
|
388
|
+
text: string;
|
|
389
|
+
};
|
|
390
|
+
agentSignature?: string;
|
|
391
|
+
baseUrl?: string;
|
|
392
|
+
}): string;
|
package/dist/index.js
CHANGED
|
@@ -88,6 +88,7 @@ export function createKyaClient(opts = {}) {
|
|
|
88
88
|
allowed: (id, a) => call("GET", `/v1/agents/${encodeURIComponent(id)}/allowed${q(a)}`, undefined, true, 0.01),
|
|
89
89
|
register: (input) => call("POST", "/v1/agents", input, true, 0.25),
|
|
90
90
|
prove: (id, proof) => call("POST", `/v1/agents/${encodeURIComponent(id)}/prove`, proof),
|
|
91
|
+
signLink: (id, a) => call("GET", `/v1/agents/${encodeURIComponent(id)}/sign-link${q(a)}`),
|
|
91
92
|
renew: (id) => call("POST", `/v1/agents/${encodeURIComponent(id)}/renew`, {}, true, 0.1),
|
|
92
93
|
renewProve: (id, proof) => call("POST", `/v1/agents/${encodeURIComponent(id)}/renew/prove`, proof),
|
|
93
94
|
revoke: (id, input) => call("POST", `/v1/agents/${encodeURIComponent(id)}/revoke`, input),
|
|
@@ -250,3 +251,16 @@ export async function verifyCredentialOffline(credential, opts = {}) {
|
|
|
250
251
|
return { signature, within_validity, live_status: "unknown", usable_offline: signature.valid && within_validity && credential.status === "active" };
|
|
251
252
|
}
|
|
252
253
|
export * from "./auth.js";
|
|
254
|
+
// e-CNPJ signing (D-26, D-27): the implementation lives in @kya-brasil/core, shared with anyone using the library.
|
|
255
|
+
export { buildDetachedCms, cnpjFromCommonName, ecnpjSigner, openPfx, readSigningLink, signWithECnpjPfx } from "@kya-brasil/core";
|
|
256
|
+
export { listTokenCertificates, openToken } from "./pkcs11.js";
|
|
257
|
+
import { signingLink as coreSigningLink } from "@kya-brasil/core";
|
|
258
|
+
/**
|
|
259
|
+
* Link to the issuer's signing page (<issuer>/sign) for the person who holds the company's e-CNPJ, usually the
|
|
260
|
+
* owner or the accountant. The agent registers, signs its half and sends this link; the person opens it, sees
|
|
261
|
+
* which agent, company and mandate they are authorizing, signs with the .pfx in the browser (the file never
|
|
262
|
+
* leaves the computer) or uploads a .p7s from any ICP-Brasil signer, and the credential is issued.
|
|
263
|
+
*/
|
|
264
|
+
export function signingLink(p) {
|
|
265
|
+
return coreSigningLink({ issuer: p.baseUrl ?? DEFAULT_BASE_URL, credentialId: p.credentialId, challenge: p.challenge, agentSignature: p.agentSignature });
|
|
266
|
+
}
|
package/dist/pkcs11.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A3 e-CNPJ (token or smart card) through PKCS#11, the interface every ICP-Brasil token driver ships
|
|
3
|
+
* (e.g. SafeNet eToken: libeTPkcs11.so / eTPKCS11.dll; GD/Watchdata/Oberthur: their own libraries). The
|
|
4
|
+
* private key never leaves the device: only the bytes to sign go in and the signature comes out. Node only;
|
|
5
|
+
* needs the optional package pkcs11js (npm install pkcs11js) and the vendor's library path.
|
|
6
|
+
*/
|
|
7
|
+
import { type ECnpjSigner } from "@kya-brasil/core";
|
|
8
|
+
export interface TokenOptions {
|
|
9
|
+
/** Path to the vendor's PKCS#11 library (.so / .dylib / .dll). */
|
|
10
|
+
library: string;
|
|
11
|
+
/** The token's user PIN. */
|
|
12
|
+
pin: string;
|
|
13
|
+
/** Index among the slots with a token present (default 0). */
|
|
14
|
+
slot?: number;
|
|
15
|
+
/** Pick the certificate by its label on the token; default: the one whose common name carries a CNPJ. */
|
|
16
|
+
label?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface TokenCertificate {
|
|
19
|
+
label: string;
|
|
20
|
+
subject: string;
|
|
21
|
+
cnpj: string | null;
|
|
22
|
+
notAfter: Date;
|
|
23
|
+
}
|
|
24
|
+
/** Certificates on the token (no PIN needed on most tokens), to pick one with `label`. */
|
|
25
|
+
export declare function listTokenCertificates(o: {
|
|
26
|
+
library: string;
|
|
27
|
+
slot?: number;
|
|
28
|
+
pin?: string;
|
|
29
|
+
}): Promise<TokenCertificate[]>;
|
|
30
|
+
/**
|
|
31
|
+
* Opens the token and returns an e-CNPJ signer (same interface as openPfx). Call close() when done; it logs out
|
|
32
|
+
* and releases the token.
|
|
33
|
+
*/
|
|
34
|
+
export declare function openToken(o: TokenOptions): Promise<ECnpjSigner & {
|
|
35
|
+
close(): void;
|
|
36
|
+
}>;
|
package/dist/pkcs11.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A3 e-CNPJ (token or smart card) through PKCS#11, the interface every ICP-Brasil token driver ships
|
|
3
|
+
* (e.g. SafeNet eToken: libeTPkcs11.so / eTPKCS11.dll; GD/Watchdata/Oberthur: their own libraries). The
|
|
4
|
+
* private key never leaves the device: only the bytes to sign go in and the signature comes out. Node only;
|
|
5
|
+
* needs the optional package pkcs11js (npm install pkcs11js) and the vendor's library path.
|
|
6
|
+
*/
|
|
7
|
+
import { ecnpjSigner } from "@kya-brasil/core";
|
|
8
|
+
// DigestInfo prefix for SHA-256 (RFC 8017), for tokens that only offer raw CKM_RSA_PKCS.
|
|
9
|
+
const SHA256_DIGEST_INFO = Uint8Array.from([0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20]);
|
|
10
|
+
async function load() {
|
|
11
|
+
try {
|
|
12
|
+
const mod = (await import("pkcs11js"));
|
|
13
|
+
return mod.default ?? mod;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
throw new Error("A3 tokens need the optional package pkcs11js: npm install pkcs11js");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function openSession(o) {
|
|
20
|
+
const p11 = await load();
|
|
21
|
+
const lib = new p11.PKCS11();
|
|
22
|
+
lib.load(o.library);
|
|
23
|
+
lib.C_Initialize();
|
|
24
|
+
const finalize = () => {
|
|
25
|
+
try {
|
|
26
|
+
lib.C_Finalize();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
/* already finalized */
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
try {
|
|
33
|
+
const slots = lib.C_GetSlotList(true);
|
|
34
|
+
const slot = slots[o.slot ?? 0];
|
|
35
|
+
if (!slot)
|
|
36
|
+
throw new Error(slots.length ? `no token in slot ${o.slot}` : "no token found: is it plugged in, and is this the right library?");
|
|
37
|
+
const session = lib.C_OpenSession(slot, p11.CKF_SERIAL_SESSION);
|
|
38
|
+
if (o.pin !== undefined)
|
|
39
|
+
lib.C_Login(session, p11.CKU_USER, o.pin);
|
|
40
|
+
lib.C_FindObjectsInit(session, [{ type: p11.CKA_CLASS, value: p11.CKO_CERTIFICATE }]);
|
|
41
|
+
const handles = lib.C_FindObjects(session, 64);
|
|
42
|
+
lib.C_FindObjectsFinal(session);
|
|
43
|
+
const certs = handles.map((handle) => {
|
|
44
|
+
const [value, id, label] = lib.C_GetAttributeValue(session, handle, [{ type: p11.CKA_VALUE }, { type: p11.CKA_ID }, { type: p11.CKA_LABEL }]);
|
|
45
|
+
const der = new Uint8Array(value.value);
|
|
46
|
+
let signer = null;
|
|
47
|
+
try {
|
|
48
|
+
signer = ecnpjSigner({ certificates: [der], sign: async () => new Uint8Array() });
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* not an X.509 certificate we can read */
|
|
52
|
+
}
|
|
53
|
+
return { handle, der, id: id.value ?? Buffer.alloc(0), label: label.value?.toString("utf8") ?? "", signer };
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
p11,
|
|
57
|
+
lib,
|
|
58
|
+
session,
|
|
59
|
+
certs,
|
|
60
|
+
close() {
|
|
61
|
+
try {
|
|
62
|
+
if (o.pin !== undefined)
|
|
63
|
+
lib.C_Logout(session);
|
|
64
|
+
lib.C_CloseSession(session);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
/* ignore */
|
|
68
|
+
}
|
|
69
|
+
finalize();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
finalize();
|
|
75
|
+
throw e;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Certificates on the token (no PIN needed on most tokens), to pick one with `label`. */
|
|
79
|
+
export async function listTokenCertificates(o) {
|
|
80
|
+
const s = await openSession(o);
|
|
81
|
+
try {
|
|
82
|
+
return s.certs.filter((c) => c.signer).map((c) => ({ label: c.label, subject: c.signer.subject, cnpj: c.signer.cnpj, notAfter: c.signer.notAfter }));
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
s.close();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Opens the token and returns an e-CNPJ signer (same interface as openPfx). Call close() when done; it logs out
|
|
90
|
+
* and releases the token.
|
|
91
|
+
*/
|
|
92
|
+
export async function openToken(o) {
|
|
93
|
+
const s = await openSession(o);
|
|
94
|
+
try {
|
|
95
|
+
const { p11, lib, session } = s;
|
|
96
|
+
const cert = o.label ? s.certs.find((c) => c.label === o.label) : s.certs.find((c) => c.signer?.cnpj);
|
|
97
|
+
if (!cert?.signer)
|
|
98
|
+
throw new Error(o.label ? `no certificate labelled "${o.label}" on the token` : "no e-CNPJ certificate on the token (none with a CNPJ in its name); pass a label");
|
|
99
|
+
lib.C_FindObjectsInit(session, [{ type: p11.CKA_CLASS, value: p11.CKO_PRIVATE_KEY }, { type: p11.CKA_ID, value: cert.id }]);
|
|
100
|
+
const [key] = lib.C_FindObjects(session, 1);
|
|
101
|
+
lib.C_FindObjectsFinal(session);
|
|
102
|
+
if (!key)
|
|
103
|
+
throw new Error("the token has no private key for that certificate (wrong PIN or key not on this token)");
|
|
104
|
+
// The chain: the other certificates on the token, when any (the issuer also has the ICP-Brasil bundle).
|
|
105
|
+
const chain = s.certs.filter((c) => c !== cert && c.signer).map((c) => c.der);
|
|
106
|
+
const signRaw = async (data) => {
|
|
107
|
+
try {
|
|
108
|
+
lib.C_SignInit(session, { mechanism: p11.CKM_SHA256_RSA_PKCS }, key);
|
|
109
|
+
return new Uint8Array(lib.C_Sign(session, Buffer.from(data), Buffer.alloc(1024)));
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Older tokens: hash here and sign the DigestInfo with raw PKCS#1 v1.5.
|
|
113
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", data));
|
|
114
|
+
lib.C_SignInit(session, { mechanism: p11.CKM_RSA_PKCS }, key);
|
|
115
|
+
return new Uint8Array(lib.C_Sign(session, Buffer.concat([SHA256_DIGEST_INFO, digest]), Buffer.alloc(1024)));
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
return { ...ecnpjSigner({ certificates: [cert.der, ...chain], sign: signRaw }), close: () => s.close() };
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
s.close();
|
|
122
|
+
throw e;
|
|
123
|
+
}
|
|
124
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kya-brasil/agent-tools",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "KYA Brasil for AI agents: verify that an agent acts for a Brazilian company (e-CNPJ-signed credential, revocation and company registration checked daily against the Receita's open data, no third-party service), check an action against its mandate, or give your own agent a credential. Sign in with KYA middleware (Hono, Express, Workers) and request signing. x402 payments (USDC on Base) handled for you. Adapters for Vercel AI SDK, LangChain, OpenAI and Anthropic.",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "KYA Brasil for AI agents: verify that an agent acts for a Brazilian company (e-CNPJ-signed credential, revocation and company registration checked daily against the Receita's open data, no third-party service), check an action against its mandate, or give your own agent a credential (sign with the company's e-CNPJ: A1 .pfx or A3 token via PKCS#11, in code or with the kya-sign command). Sign in with KYA middleware (Hono, Express, Workers) and request signing. x402 payments (USDC on Base) handled for you. Adapters for Vercel AI SDK, LangChain, OpenAI and Anthropic.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -55,16 +55,23 @@
|
|
|
55
55
|
"@x402/evm": "^2.28.0",
|
|
56
56
|
"@x402/fetch": "^2.28.0",
|
|
57
57
|
"pkijs": "^3.4.1",
|
|
58
|
-
"viem": "^2.57.5"
|
|
58
|
+
"viem": "^2.57.5",
|
|
59
|
+
"@kya-brasil/core": "^0.8.0"
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
61
|
-
"@kya-brasil/core": "workspace:*",
|
|
62
62
|
"@peculiar/asn1-schema": "^2.3.15",
|
|
63
63
|
"@peculiar/asn1-x509": "^2.3.15",
|
|
64
64
|
"@peculiar/x509": "^2.1.0",
|
|
65
65
|
"@types/node": "^22.20.5",
|
|
66
66
|
"reflect-metadata": "^0.2.2",
|
|
67
67
|
"typescript": "^5.9.3",
|
|
68
|
-
"vitest": "^4.1.11"
|
|
68
|
+
"vitest": "^4.1.11",
|
|
69
|
+
"pkcs11js": "2.1.7"
|
|
70
|
+
},
|
|
71
|
+
"bin": {
|
|
72
|
+
"kya-sign": "./dist/cli.js"
|
|
73
|
+
},
|
|
74
|
+
"optionalDependencies": {
|
|
75
|
+
"pkcs11js": "2.1.7"
|
|
69
76
|
}
|
|
70
77
|
}
|