@kya-brasil/core 0.7.1 → 0.9.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
@@ -66,6 +66,43 @@ the network already gives (network token reference, Payment Account Reference),
66
66
  With the hosted issuer, the same check comes back signed from
67
67
  `GET /v1/agents/{id}/allowed?payment_network=visa&payment_ref_sha256=…`, which is evidence for a later dispute.
68
68
 
69
+ ## Sign as the e-CNPJ holder (A1 and A3)
70
+
71
+ The company's side of the challenge, without the hosted page. The issuer only ever receives the signature.
72
+
73
+ ```ts
74
+ import { openPfx, ecnpjSigner, readSigningLink } from "@kya-brasil/core";
75
+
76
+ // A1: the .pfx / .p12 file (legacy 3DES/RC2 PKCS#12 included)
77
+ const a1 = await openPfx(pfxBytes, password);
78
+ const ecnpj_signature = await a1.sign(challengeText); // detached CMS, base64: POST it to /v1/agents/{id}/prove
79
+
80
+ // A3 token, smart card, HSM or cloud certificate: anything that signs bytes with the certificate's RSA key
81
+ // (PKCS#1 v1.5 over SHA-256), plus the certificate chain in DER.
82
+ const a3 = ecnpjSigner({ certificates: [certDer, ...chainDer], sign: (bytes) => myToken.sign("SHA256withRSA", bytes) });
83
+ const sig = await a3.sign(readSigningLink(link).text); // the text from a /sign link the agent sent
84
+ ```
85
+
86
+ `sign` refuses a text whose `cnpj:` line names another company. For PKCS#11 tokens on Node,
87
+ `openToken({ library, pin })` in `@kya-brasil/agent-tools` (and the `kya-sign --pkcs11` command) already wires
88
+ `C_Sign` into `ecnpjSigner`.
89
+
90
+ ### Cloud certificates (PSC)
91
+
92
+ `CloudSignatureProvider` is the interface for an e-CNPJ held by an ICP-Brasil PSC: `authorizationUrl` (OAuth with
93
+ PKCE), `exchangeCode` returning a session with `certificates()` and `signHash(sha256)`. No PSC is implemented here
94
+ (each has its own contract); implement it for yours from
95
+ [examples/cloud-provider](https://github.com/celfons/kya-brasil/tree/main/examples/cloud-provider) and then:
96
+
97
+ ```ts
98
+ import { cloudSigner, pkcePair, newState } from "@kya-brasil/core";
99
+ const { verifier, challenge } = await pkcePair();
100
+ const url = await provider.authorizationUrl({ redirectUri, state: newState(), codeChallenge: challenge });
101
+ // … the person approves, your redirect URI gets ?code=…&state=…
102
+ const signer = await cloudSigner(await provider.exchangeCode({ code, redirectUri, codeVerifier: verifier }));
103
+ const ecnpj_signature = await signer.sign(challengeText);
104
+ ```
105
+
69
106
  ## Run your own issuer
70
107
 
71
108
  ```ts
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Cloud e-CNPJ (D-28): the certificate lives at an ICP-Brasil PSC (prestador de serviço de confiança) and signs
3
+ * after the holder approves, usually on the phone. Each PSC has its own contract, registration and API details,
4
+ * so this project ships the interface only: whoever has an agreement with a PSC implements CloudSignatureProvider
5
+ * for it and plugs it into the three entry points (this library, `kya-sign --cloud`, the /sign page of a
6
+ * self-hosted issuer). The KYA side (PKCE, single-use state, the CMS, what is signed) is done here.
7
+ *
8
+ * Typical PSC flow (OAuth 2.0 authorization code with PKCE): send the person to authorizationUrl, get a code at
9
+ * the redirect URI, exchange it for a session, read the certificate, sign hashes. Ask the PSC for the narrowest
10
+ * session it offers (one signature, minutes of lifetime).
11
+ */
12
+ import { type ECnpjSigner } from "./ecnpj-sign.js";
13
+ /** An authorized session at the PSC: the certificate and a way to sign one SHA-256 hash. */
14
+ export interface CloudSession {
15
+ /** Signer certificate first, then the chain if the PSC returns it (DER). */
16
+ certificates(): Promise<Uint8Array[]>;
17
+ /**
18
+ * RSA PKCS#1 v1.5 signature over a SHA-256 hash (32 bytes) computed by the caller: what PSC APIs usually
19
+ * call a "raw" or "PKCS1" signature of a hash. Return the signature bytes.
20
+ */
21
+ signHash(sha256: Uint8Array): Promise<Uint8Array>;
22
+ /** Revoke or forget the session at the PSC, when it supports it. */
23
+ close?(): Promise<void>;
24
+ }
25
+ /** One PSC. Implement it with your own registration (client id/secret, redirect URIs) and the PSC's API. */
26
+ export interface CloudSignatureProvider {
27
+ /** Short id used in URLs and on the command line, e.g. "my-psc". Lowercase letters, digits, "-". */
28
+ readonly id: string;
29
+ /** Name shown to the person, e.g. "BirdID", "VIDaaS". */
30
+ readonly name: string;
31
+ /**
32
+ * Where to send the person to log in and approve. Must carry `state` and the PKCE `codeChallenge` (S256) so the
33
+ * code comes back to `redirectUri` bound to this request. `loginHint` may carry a CNPJ/CPF when the PSC accepts it.
34
+ */
35
+ authorizationUrl(p: {
36
+ redirectUri: string;
37
+ state: string;
38
+ codeChallenge: string;
39
+ loginHint?: string;
40
+ }): string | Promise<string>;
41
+ /** Exchanges the code for a session (server or CLI side: this is where a client secret would be used). */
42
+ exchangeCode(p: {
43
+ code: string;
44
+ redirectUri: string;
45
+ codeVerifier: string;
46
+ }): Promise<CloudSession>;
47
+ }
48
+ /** PKCE (RFC 7636, S256): a random verifier kept by us and the challenge sent to the PSC. */
49
+ export declare function pkcePair(): Promise<{
50
+ verifier: string;
51
+ challenge: string;
52
+ }>;
53
+ /** A random, single-use OAuth state value. */
54
+ export declare const newState: () => string;
55
+ /** Valid provider ids: what can safely go in a URL path or a CLI flag. */
56
+ export declare const isProviderId: (id: string) => boolean;
57
+ /**
58
+ * An e-CNPJ signer over a PSC session: the CMS is built here, the PSC only signs the SHA-256 of the signed
59
+ * attributes. Same interface as openPfx (A1) and openToken (A3), so everything downstream is unchanged.
60
+ */
61
+ export declare function cloudSigner(session: CloudSession): Promise<ECnpjSigner>;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Cloud e-CNPJ (D-28): the certificate lives at an ICP-Brasil PSC (prestador de serviço de confiança) and signs
3
+ * after the holder approves, usually on the phone. Each PSC has its own contract, registration and API details,
4
+ * so this project ships the interface only: whoever has an agreement with a PSC implements CloudSignatureProvider
5
+ * for it and plugs it into the three entry points (this library, `kya-sign --cloud`, the /sign page of a
6
+ * self-hosted issuer). The KYA side (PKCE, single-use state, the CMS, what is signed) is done here.
7
+ *
8
+ * Typical PSC flow (OAuth 2.0 authorization code with PKCE): send the person to authorizationUrl, get a code at
9
+ * the redirect URI, exchange it for a session, read the certificate, sign hashes. Ask the PSC for the narrowest
10
+ * session it offers (one signature, minutes of lifetime).
11
+ */
12
+ import { ecnpjSigner } from "./ecnpj-sign.js";
13
+ import { KyaError } from "./types.js";
14
+ const toArrayBuffer = (u) => u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
15
+ const b64url = (u) => {
16
+ let s = "";
17
+ for (const b of u)
18
+ s += String.fromCharCode(b);
19
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
20
+ };
21
+ /** PKCE (RFC 7636, S256): a random verifier kept by us and the challenge sent to the PSC. */
22
+ export async function pkcePair() {
23
+ const verifier = b64url(crypto.getRandomValues(new Uint8Array(32)));
24
+ const challenge = b64url(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier))));
25
+ return { verifier, challenge };
26
+ }
27
+ /** A random, single-use OAuth state value. */
28
+ export const newState = () => b64url(crypto.getRandomValues(new Uint8Array(24)));
29
+ /** Valid provider ids: what can safely go in a URL path or a CLI flag. */
30
+ export const isProviderId = (id) => /^[a-z0-9][a-z0-9-]{0,31}$/.test(id);
31
+ /**
32
+ * An e-CNPJ signer over a PSC session: the CMS is built here, the PSC only signs the SHA-256 of the signed
33
+ * attributes. Same interface as openPfx (A1) and openToken (A3), so everything downstream is unchanged.
34
+ */
35
+ export async function cloudSigner(session) {
36
+ const certificates = await session.certificates();
37
+ if (!certificates.length)
38
+ throw new KyaError("CERTIFICATE_INVALID", "the cloud provider returned no certificate");
39
+ return ecnpjSigner({
40
+ certificates,
41
+ sign: async (data) => {
42
+ const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", toArrayBuffer(data)));
43
+ const sig = await session.signHash(hash);
44
+ if (!sig?.length)
45
+ throw new KyaError("UPSTREAM_ERROR", "the cloud provider returned an empty signature");
46
+ return sig;
47
+ }
48
+ });
49
+ }
@@ -0,0 +1,57 @@
1
+ /** Signs `data` with the certificate's RSA private key: PKCS#1 v1.5 over SHA-256 (the signer hashes). */
2
+ export type RawSigner = (data: Uint8Array) => Promise<Uint8Array>;
3
+ /**
4
+ * Detached CMS/PKCS#7 SignedData (base64 DER) over `text`, with signed attributes (content type, message
5
+ * digest, signing time) and the given certificates (signer first, then its chain, DER). The issuer accepts it as
6
+ * `ecnpj_signature`.
7
+ */
8
+ export declare function buildDetachedCms(p: {
9
+ text: string;
10
+ certificates: Uint8Array[];
11
+ sign: RawSigner;
12
+ signingTime?: Date;
13
+ }): Promise<string>;
14
+ /** What a certificate holder exposes to sign: subject, CNPJ, validity, chain and the sign step. */
15
+ export interface ECnpjSigner {
16
+ /** Subject of the e-CNPJ certificate, e.g. "CN=ACME LTDA:11222333000181, ...". */
17
+ subject: string;
18
+ /** CNPJ read from the certificate's common name ("NAME:CNPJ"), when present. */
19
+ cnpj: string | null;
20
+ notAfter: Date;
21
+ /** Signer certificate first, then the chain found with it (DER). */
22
+ certificates: Uint8Array[];
23
+ /** Detached CMS (base64 DER) over `text`; refuses a text whose "cnpj:" line names another company. */
24
+ sign(text: string): Promise<string>;
25
+ }
26
+ /** CNPJ in a common name ending in ":<14 digits>", the ICP-Brasil convention for e-CNPJ. */
27
+ export declare const cnpjFromCommonName: (cn: string) => string | null;
28
+ /** Wraps a raw signer and its certificates into an ECnpjSigner (used for A1 files, tokens, HSMs, cloud). */
29
+ export declare function ecnpjSigner(p: {
30
+ certificates: Uint8Array[];
31
+ sign: RawSigner;
32
+ }): ECnpjSigner;
33
+ /** Opens an A1 e-CNPJ file (.pfx / .p12 bytes) with its password. The key stays in memory, inside Web Crypto. */
34
+ export declare function openPfx(pfx: Uint8Array, password: string): Promise<ECnpjSigner>;
35
+ /** One call: sign `text` with the e-CNPJ in an A1 file. Returns the base64 DER the issuer expects as ecnpj_signature. */
36
+ export declare function signWithECnpjPfx(pfx: Uint8Array, password: string, text: string): Promise<string>;
37
+ /**
38
+ * Link to an issuer's signing page (<issuer>/sign) for the person who holds the company's e-CNPJ. The agent
39
+ * registers, signs its half and sends this link; the person sees what they authorize and signs in the browser.
40
+ */
41
+ export declare function signingLink(p: {
42
+ issuer: string;
43
+ credentialId: string;
44
+ challenge: {
45
+ id: string;
46
+ text: string;
47
+ };
48
+ agentSignature?: string;
49
+ }): string;
50
+ /** The challenge text carried by a signing link (inverse of signingLink), with its ids. */
51
+ export declare function readSigningLink(link: string): {
52
+ issuer: string;
53
+ credentialId: string | null;
54
+ challengeId: string | null;
55
+ agentSignature: string | null;
56
+ text: string;
57
+ };
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Signing with the company's e-CNPJ, for whoever holds it (D-26, D-27). One CMS builder for every kind of
3
+ * certificate: the caller gives the certificate chain and a function that signs bytes with the private key.
4
+ * A1 (.pfx / .p12 file): openPfx / signWithECnpjPfx here (node-forge reads the legacy 3DES/RC2 PKCS#12
5
+ * many Brazilian A1 files use; the key is then used through Web Crypto);
6
+ * A3 (token, smart card), HSM, cloud certificate: anything that can produce an RSA PKCS#1 v1.5 SHA-256
7
+ * signature (PKCS#11 C_Sign with CKM_SHA256_RSA_PKCS, a cloud signing API, ...) plugs into
8
+ * buildDetachedCms. The kya-sign command in @kya-brasil/agent-tools does it for PKCS#11 tokens.
9
+ * Pure: no storage, no network. The issuer never receives the certificate or its password.
10
+ */
11
+ import * as asn1js from "asn1js";
12
+ import * as pkijs from "pkijs";
13
+ import forge from "node-forge";
14
+ import { KyaError } from "./types.js";
15
+ const OID_DATA = "1.2.840.113549.1.7.1";
16
+ const OID_SIGNED_DATA = "1.2.840.113549.1.7.2";
17
+ const OID_SHA256 = "2.16.840.1.101.3.4.2.1";
18
+ const OID_SHA256_RSA = "1.2.840.113549.1.1.11";
19
+ const OID_CONTENT_TYPE = "1.2.840.113549.1.9.3";
20
+ const OID_MESSAGE_DIGEST = "1.2.840.113549.1.9.4";
21
+ const OID_SIGNING_TIME = "1.2.840.113549.1.9.5";
22
+ const toArrayBuffer = (u) => u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength);
23
+ function ensureEngine() {
24
+ pkijs.setEngine("kya-sign", new pkijs.CryptoEngine({ name: "kya-sign", crypto: globalThis.crypto }));
25
+ }
26
+ /**
27
+ * Detached CMS/PKCS#7 SignedData (base64 DER) over `text`, with signed attributes (content type, message
28
+ * digest, signing time) and the given certificates (signer first, then its chain, DER). The issuer accepts it as
29
+ * `ecnpj_signature`.
30
+ */
31
+ export async function buildDetachedCms(p) {
32
+ ensureEngine();
33
+ if (!p.certificates.length)
34
+ throw new KyaError("INVALID_INPUT", "no signer certificate");
35
+ const certs = p.certificates.map((der) => pkijs.Certificate.fromBER(toArrayBuffer(der)));
36
+ const leaf = certs[0];
37
+ const content = new TextEncoder().encode(p.text);
38
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", content));
39
+ const signedAttrs = new pkijs.SignedAndUnsignedAttributes({
40
+ type: 0,
41
+ attributes: [
42
+ new pkijs.Attribute({ type: OID_CONTENT_TYPE, values: [new asn1js.ObjectIdentifier({ value: OID_DATA })] }),
43
+ new pkijs.Attribute({ type: OID_MESSAGE_DIGEST, values: [new asn1js.OctetString({ valueHex: toArrayBuffer(digest) })] }),
44
+ new pkijs.Attribute({ type: OID_SIGNING_TIME, values: [new asn1js.UTCTime({ valueDate: p.signingTime ?? new Date() })] })
45
+ ]
46
+ });
47
+ // What is signed is the DER of the attributes as a SET (tag 0x31), not the [0] IMPLICIT form they travel in.
48
+ const toBeSigned = new Uint8Array(signedAttrs.toSchema().toBER(false));
49
+ toBeSigned[0] = 0x31;
50
+ const signature = await p.sign(toBeSigned);
51
+ const signerInfo = new pkijs.SignerInfo({
52
+ version: 1,
53
+ sid: new pkijs.IssuerAndSerialNumber({ issuer: leaf.issuer, serialNumber: leaf.serialNumber }),
54
+ digestAlgorithm: new pkijs.AlgorithmIdentifier({ algorithmId: OID_SHA256 }),
55
+ signedAttrs,
56
+ signatureAlgorithm: new pkijs.AlgorithmIdentifier({ algorithmId: OID_SHA256_RSA }),
57
+ signature: new asn1js.OctetString({ valueHex: toArrayBuffer(signature) })
58
+ });
59
+ const signed = new pkijs.SignedData({
60
+ version: 1,
61
+ digestAlgorithms: [new pkijs.AlgorithmIdentifier({ algorithmId: OID_SHA256 })],
62
+ encapContentInfo: new pkijs.EncapsulatedContentInfo({ eContentType: OID_DATA }),
63
+ certificates: certs,
64
+ signerInfos: [signerInfo]
65
+ });
66
+ const ci = new pkijs.ContentInfo({ contentType: OID_SIGNED_DATA, content: signed.toSchema(true) });
67
+ const der = new Uint8Array(ci.toSchema().toBER(false));
68
+ let bin = "";
69
+ for (const b of der)
70
+ bin += String.fromCharCode(b);
71
+ return btoa(bin);
72
+ }
73
+ /** CNPJ in a common name ending in ":<14 digits>", the ICP-Brasil convention for e-CNPJ. */
74
+ export const cnpjFromCommonName = (cn) => /:(\d{14})\s*$/.exec(cn)?.[1] ?? null;
75
+ /** Wraps a raw signer and its certificates into an ECnpjSigner (used for A1 files, tokens, HSMs, cloud). */
76
+ export function ecnpjSigner(p) {
77
+ const leaf = forge.pki.certificateFromAsn1(forge.asn1.fromDer(forge.util.binary.raw.encode(p.certificates[0])));
78
+ const cn = String(leaf.subject.getField("CN")?.value ?? "");
79
+ const cnpj = cnpjFromCommonName(cn);
80
+ return {
81
+ subject: leaf.subject.attributes.map((a) => `${a.shortName ?? a.name}=${a.value}`).join(", "),
82
+ cnpj,
83
+ notAfter: leaf.validity.notAfter,
84
+ certificates: p.certificates,
85
+ async sign(text) {
86
+ const asked = /^cnpj: (\d{14})$/m.exec(text)?.[1];
87
+ if (asked && cnpj && asked !== cnpj)
88
+ throw new KyaError("CNPJ_MISMATCH", `this certificate is for CNPJ ${cnpj}, the text is for ${asked}`);
89
+ return buildDetachedCms({ text, certificates: p.certificates, sign: p.sign });
90
+ }
91
+ };
92
+ }
93
+ const certDer = (c) => new Uint8Array(forge.util.binary.raw.decode(forge.asn1.toDer(forge.pki.certificateToAsn1(c)).getBytes()));
94
+ /** Opens an A1 e-CNPJ file (.pfx / .p12 bytes) with its password. The key stays in memory, inside Web Crypto. */
95
+ export async function openPfx(pfx, password) {
96
+ let p12;
97
+ try {
98
+ p12 = forge.pkcs12.pkcs12FromAsn1(forge.asn1.fromDer(forge.util.binary.raw.encode(pfx)), false, password);
99
+ }
100
+ catch (e) {
101
+ throw new KyaError("INVALID_INPUT", `could not open the certificate file (wrong password or not a .pfx/.p12): ${e.message}`);
102
+ }
103
+ const oids = forge.pki.oids;
104
+ const keyBag = [oids.pkcs8ShroudedKeyBag, oids.keyBag].flatMap((t) => p12.getBags({ bagType: t })[t] ?? []).find((b) => b.key);
105
+ if (!keyBag?.key)
106
+ throw new KyaError("INVALID_INPUT", "the certificate file has no private key");
107
+ const key = keyBag.key;
108
+ const certs = (p12.getBags({ bagType: oids.certBag })[oids.certBag] ?? []).map((b) => b.cert).filter((c) => !!c);
109
+ // The signer is the certificate whose public key matches the private key; the others are its chain.
110
+ const leaf = certs.find((c) => c.publicKey.n?.equals(key.n));
111
+ if (!leaf)
112
+ throw new KyaError("INVALID_INPUT", "no certificate in the file matches its private key");
113
+ const pkcs8 = forge.util.binary.raw.decode(forge.asn1.toDer(forge.pki.wrapRsaPrivateKey(forge.pki.privateKeyToAsn1(key))).getBytes());
114
+ const cryptoKey = await crypto.subtle.importKey("pkcs8", toArrayBuffer(pkcs8), { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["sign"]);
115
+ return ecnpjSigner({
116
+ certificates: [leaf, ...certs.filter((c) => c !== leaf)].map(certDer),
117
+ sign: async (data) => new Uint8Array(await crypto.subtle.sign({ name: "RSASSA-PKCS1-v1_5" }, cryptoKey, toArrayBuffer(data)))
118
+ });
119
+ }
120
+ /** One call: sign `text` with the e-CNPJ in an A1 file. Returns the base64 DER the issuer expects as ecnpj_signature. */
121
+ export async function signWithECnpjPfx(pfx, password, text) {
122
+ return (await openPfx(pfx, password)).sign(text);
123
+ }
124
+ /**
125
+ * Link to an issuer's signing page (<issuer>/sign) for the person who holds the company's e-CNPJ. The agent
126
+ * registers, signs its half and sends this link; the person sees what they authorize and signs in the browser.
127
+ */
128
+ export function signingLink(p) {
129
+ const u = new URL("/sign", p.issuer);
130
+ u.searchParams.set("id", p.credentialId);
131
+ u.searchParams.set("challenge_id", p.challenge.id);
132
+ let bin = "";
133
+ for (const b of new TextEncoder().encode(p.challenge.text))
134
+ bin += String.fromCharCode(b);
135
+ u.searchParams.set("text", btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""));
136
+ if (p.agentSignature)
137
+ u.searchParams.set("agent_signature", p.agentSignature);
138
+ return u.toString();
139
+ }
140
+ /** The challenge text carried by a signing link (inverse of signingLink), with its ids. */
141
+ export function readSigningLink(link) {
142
+ const u = new URL(link);
143
+ const t = u.searchParams.get("text");
144
+ if (!t)
145
+ throw new KyaError("INVALID_INPUT", "the signing link has no text");
146
+ const bin = atob(t.replace(/-/g, "+").replace(/_/g, "/"));
147
+ return {
148
+ issuer: u.origin,
149
+ credentialId: u.searchParams.get("id"),
150
+ challengeId: u.searchParams.get("challenge_id"),
151
+ agentSignature: u.searchParams.get("agent_signature"),
152
+ text: new TextDecoder().decode(Uint8Array.from(bin, (c) => c.charCodeAt(0)))
153
+ };
154
+ }
package/dist/index.d.ts CHANGED
@@ -7,3 +7,5 @@ export * from "./credential.js";
7
7
  export * from "./icp-brasil.js";
8
8
  export * from "./revocation.js";
9
9
  export * from "./erc8004.js";
10
+ export * from "./ecnpj-sign.js";
11
+ export * from "./cloud-sign.js";
package/dist/index.js CHANGED
@@ -7,3 +7,5 @@ export * from "./credential.js";
7
7
  export * from "./icp-brasil.js";
8
8
  export * from "./revocation.js";
9
9
  export * from "./erc8004.js";
10
+ export * from "./ecnpj-sign.js";
11
+ export * from "./cloud-sign.js";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kya-brasil/core",
3
- "version": "0.7.1",
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. Pure functions: storage and network are yours.",
3
+ "version": "0.9.0",
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",
7
7
  "exports": {
@@ -57,12 +57,14 @@
57
57
  "@peculiar/asn1-x509": "^2.3.15",
58
58
  "@peculiar/x509": "^2.1.0",
59
59
  "asn1js": "^3.0.6",
60
+ "node-forge": "1.4.0",
60
61
  "pkijs": "^3.4.1",
61
62
  "reflect-metadata": "^0.2.2",
62
63
  "viem": "^2.57.5"
63
64
  },
64
65
  "devDependencies": {
65
66
  "@types/node": "^22.20.5",
67
+ "@types/node-forge": "^1.3.14",
66
68
  "typescript": "^5.9.3",
67
69
  "vitest": "^4.1.11"
68
70
  },