@kya-brasil/core 0.9.0 → 0.10.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.
@@ -20,6 +20,19 @@ export declare function buildCredential(p: {
20
20
  status?: CredentialStatus;
21
21
  status_reason?: string | null;
22
22
  }): Credential;
23
+ /** D-29: credential of a natural person. Nothing personal: no name, no CPF, no certificate subject. */
24
+ export declare function buildPersonCredential(p: {
25
+ credential_id: string;
26
+ agent: AgentKey;
27
+ certificate: {
28
+ sha256: string;
29
+ issuer: string;
30
+ signed_at: string;
31
+ };
32
+ mandate: Mandate;
33
+ issuer: string;
34
+ now?: Date;
35
+ }): Credential;
23
36
  export declare function signCredential(c: Credential, privateKey: CryptoKey, keyId: string, now?: Date): Promise<Credential>;
24
37
  export declare function verifyCredentialSignature(c: Credential, publicJwk: JsonWebKey): Promise<{
25
38
  valid: boolean;
@@ -29,6 +29,29 @@ export function buildCredential(p) {
29
29
  revocation_url: `${p.issuer}/v1/agents/${p.credential_id}/status`
30
30
  };
31
31
  }
32
+ /** D-29: credential of a natural person. Nothing personal: no name, no CPF, no certificate subject. */
33
+ export function buildPersonCredential(p) {
34
+ const now = p.now ?? new Date();
35
+ const ttl = new Date(now.getTime() + CREDENTIAL_TTL_DAYS * 86400000);
36
+ const mandateEnd = p.mandate.valid_until ? new Date(p.mandate.valid_until) : null;
37
+ const expires = mandateEnd && mandateEnd < ttl ? mandateEnd : ttl;
38
+ return {
39
+ credential_id: p.credential_id,
40
+ version: 1,
41
+ agent: p.agent,
42
+ principal: { kind: "person", verified_by: "icp-brasil-e-cpf" },
43
+ authorized_by: { method: "icp-brasil-e-cpf", certificate_sha256: p.certificate.sha256, issuer: p.certificate.issuer, signed_at: p.certificate.signed_at },
44
+ mandate: p.mandate,
45
+ kyb_snapshot: null,
46
+ status: "active",
47
+ status_reason: null,
48
+ issued_at: now.toISOString(),
49
+ expires_at: expires.toISOString(),
50
+ issuer: p.issuer,
51
+ verify_url: `${p.issuer}/v1/agents/${p.credential_id}`,
52
+ revocation_url: `${p.issuer}/v1/agents/${p.credential_id}/status`
53
+ };
54
+ }
32
55
  export async function signCredential(c, privateKey, keyId, now = new Date()) {
33
56
  const { attestation: _a, ...payload } = c;
34
57
  const attestation = await signAttestation(payload, privateKey, keyId, `${c.issuer}/v1/verify`, now);
@@ -29,10 +29,16 @@ export interface IcpProof {
29
29
  /** Base64 DER of the AC certificate that issued it (verifies the CRL), or null when not found. */
30
30
  issuer_certificate: string | null;
31
31
  }
32
- /** CNPJ carried by the certificate's ICP-Brasil otherName, or null when it is not a PJ certificate. */
33
32
  export declare function cnpjFromCertificate(cert: X509Certificate): string | null;
33
+ /** OID 2.16.76.1.3.1: data of the e-CPF holder, "ddmmaaaa" birth date followed by the 11-digit CPF (DOC-ICP-04). */
34
+ export declare const OID_ICP_PERSON = "2.16.76.1.3.1";
34
35
  /**
35
- * Verifies a CMS SignedData (base64 DER) over `challenge` and returns the proof. Throws KyaError
36
+ * CPF of an e-CPF (natural person) certificate: from the 2.16.76.1.3.1 otherName (positions 9 to 19), or the
37
+ * common name suffix ":<11 digits>" as a fallback. Null for an e-CNPJ or any other certificate.
38
+ */
39
+ export declare function cpfFromCertificate(cert: X509Certificate): string | null;
40
+ /**
41
+ * Verifies a CMS SignedData (base64 DER) over `challenge` by an e-CNPJ and returns the proof. Throws KyaError
36
42
  * CERTIFICATE_INVALID / CHALLENGE_FAILED / CNPJ_MISMATCH. `anchors` are PEM or DER (base64) roots.
37
43
  */
38
44
  export declare function verifyIcpSignature(p: {
@@ -43,3 +49,23 @@ export declare function verifyIcpSignature(p: {
43
49
  now?: Date;
44
50
  requireChain?: boolean;
45
51
  }): Promise<IcpProof>;
52
+ /** e-CPF proof (D-29). `cpf` is for the caller to key-hash at once and forget: never store or return it. No subject (name). */
53
+ export interface IcpPersonProof {
54
+ cpf: string;
55
+ certificate_sha256: string;
56
+ issuer: string;
57
+ not_before: string;
58
+ not_after: string;
59
+ signed_at: string;
60
+ serial_number: string;
61
+ crl_urls: string[];
62
+ issuer_certificate: string | null;
63
+ }
64
+ /** Verifies a CMS over `challenge` by a natural person's e-CPF (D-29). Refuses an e-CNPJ. */
65
+ export declare function verifyIcpPersonSignature(p: {
66
+ cmsBase64: string;
67
+ challenge: string;
68
+ anchors: string[];
69
+ now?: Date;
70
+ requireChain?: boolean;
71
+ }): Promise<IcpPersonProof>;
@@ -31,7 +31,8 @@ const unb64 = (s) => Uint8Array.from(atob(s.replace(/\s+/g, "")), (c) => c.charC
31
31
  const b64 = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf)));
32
32
  const hex = (buf) => [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
33
33
  /** CNPJ carried by the certificate's ICP-Brasil otherName, or null when it is not a PJ certificate. */
34
- export function cnpjFromCertificate(cert) {
34
+ /** Digits of an ICP-Brasil otherName in the Subject Alternative Name (OID 2.16.76.1.3.<n>), or null. */
35
+ function otherNameDigits(cert, oidSuffix) {
35
36
  const san = cert.extensions.find((e) => e.type === OID_SAN);
36
37
  if (!san)
37
38
  return null;
@@ -47,27 +48,44 @@ export function cnpjFromCertificate(cert) {
47
48
  const inner = block.valueBlock.value;
48
49
  const oid = inner[0];
49
50
  const oidStr = oid?.valueBlock?.toString?.() ?? "";
50
- if (!/2\.16\.76\.1\.3\.3$/.test(oidStr))
51
+ if (oidStr !== `2.16.76.1.3.${oidSuffix}`)
51
52
  continue;
52
53
  const explicit = inner[1];
53
54
  const valueBlock = explicit?.valueBlock?.value?.[0];
54
55
  if (!valueBlock)
55
56
  continue;
56
57
  const raw = "valueBlock" in valueBlock && "valueHexView" in valueBlock.valueBlock ? new TextDecoder().decode(valueBlock.valueBlock.valueHexView) : String(valueBlock.valueBlock.value);
57
- const digits = raw.replace(/\D/g, "");
58
- if (digits.length === 14)
59
- return digits;
58
+ return raw.replace(/\D/g, "");
60
59
  }
61
60
  return null;
62
61
  }
62
+ export function cnpjFromCertificate(cert) {
63
+ const digits = otherNameDigits(cert, "3");
64
+ return digits && digits.length === 14 ? digits : null;
65
+ }
66
+ /** OID 2.16.76.1.3.1: data of the e-CPF holder, "ddmmaaaa" birth date followed by the 11-digit CPF (DOC-ICP-04). */
67
+ export const OID_ICP_PERSON = "2.16.76.1.3.1";
68
+ /**
69
+ * CPF of an e-CPF (natural person) certificate: from the 2.16.76.1.3.1 otherName (positions 9 to 19), or the
70
+ * common name suffix ":<11 digits>" as a fallback. Null for an e-CNPJ or any other certificate.
71
+ */
72
+ export function cpfFromCertificate(cert) {
73
+ if (cnpjFromCertificate(cert))
74
+ return null;
75
+ const data = otherNameDigits(cert, "1");
76
+ if (data && data.length >= 19)
77
+ return data.slice(8, 19);
78
+ const cn = /CN=([^,]+)/.exec(cert.subject)?.[1] ?? "";
79
+ return /:(\d{11})\s*$/.exec(cn)?.[1] ?? null;
80
+ }
63
81
  async function sha256Hex(bytes) {
64
82
  return hex(await crypto.subtle.digest("SHA-256", bytes.slice().buffer));
65
83
  }
66
84
  /**
67
- * Verifies a CMS SignedData (base64 DER) over `challenge` and returns the proof. Throws KyaError
68
- * CERTIFICATE_INVALID / CHALLENGE_FAILED / CNPJ_MISMATCH. `anchors` are PEM or DER (base64) roots.
85
+ * Shared steps: parse the CMS, require attached content to equal the text, verify the signature, pick the signer
86
+ * certificate, check its dates, run `identify` (which throws for the wrong kind of certificate), then the chain.
69
87
  */
70
- export async function verifyIcpSignature(p) {
88
+ async function checkCms(p, identify) {
71
89
  ensureEngine();
72
90
  const now = p.now ?? new Date();
73
91
  let cms;
@@ -88,7 +106,7 @@ export async function verifyIcpSignature(p) {
88
106
  const trusted = p.anchors.map((a) => pkijs.Certificate.fromBER(unb64(a.replace(/-----[^-]+-----/g, ""))));
89
107
  const data = new TextEncoder().encode(p.challenge);
90
108
  // An attached CMS (the .p7s carries the signed text) is verified over its own content by pkijs, whatever
91
- // `data` says: it MUST carry exactly the challenge, or any document the company ever signed would pass.
109
+ // `data` says: it MUST carry exactly the challenge, or any document ever signed would pass.
92
110
  const embedded = signed.encapContentInfo.eContent;
93
111
  if (embedded) {
94
112
  const content = new Uint8Array(embedded.getValue());
@@ -104,7 +122,7 @@ export async function verifyIcpSignature(p) {
104
122
  throw new KyaError("CHALLENGE_FAILED", `signature verification failed: ${e.message}`);
105
123
  }
106
124
  if (!ok)
107
- throw new KyaError("CHALLENGE_FAILED", "e-CNPJ signature does not match the challenge");
125
+ throw new KyaError("CHALLENGE_FAILED", "the ICP-Brasil signature does not match the challenge");
108
126
  // The signer: match SignerInfo's issuerAndSerialNumber against the carried certificates.
109
127
  const si = signed.signerInfos[0];
110
128
  const sid = si.sid;
@@ -113,11 +131,7 @@ export async function verifyIcpSignature(p) {
113
131
  const x = new X509Certificate(der);
114
132
  if (x.notBefore > now || x.notAfter < now)
115
133
  throw new KyaError("CERTIFICATE_INVALID", `certificate valid from ${x.notBefore.toISOString()} to ${x.notAfter.toISOString()}`);
116
- const cnpj = cnpjFromCertificate(x);
117
- if (!cnpj)
118
- throw new KyaError("CERTIFICATE_INVALID", "certificate has no ICP-Brasil CNPJ (not an e-CNPJ)");
119
- if (cnpj !== normalizeCnpj(p.expectedCnpj))
120
- throw new KyaError("CNPJ_MISMATCH", `certificate belongs to CNPJ ${cnpj}, not ${normalizeCnpj(p.expectedCnpj)}`);
134
+ identify(x);
121
135
  if (p.requireChain !== false) {
122
136
  if (!trusted.length)
123
137
  throw new KyaError("CERTIFICATE_INVALID", "no ICP-Brasil trust anchors configured");
@@ -126,18 +140,60 @@ export async function verifyIcpSignature(p) {
126
140
  if (!chain.result)
127
141
  throw new KyaError("CERTIFICATE_INVALID", `certificate chain not trusted: ${chain.resultMessage}`);
128
142
  }
129
- // The issuing AC: carried in the CMS or among the anchors (the bundle includes the intermediate ACs).
130
- const issuerCert = [...certs, ...trusted].map((c) => new X509Certificate(c.toSchema().toBER(false))).find((c) => c.subject === x.issuer) ?? null;
143
+ return { x, der, certs, trusted, now };
144
+ }
145
+ /** The issuing AC: carried in the CMS or among the anchors (the bundle includes the intermediate ACs). */
146
+ function issuerCertificateOf(c) {
147
+ const issuerCert = [...c.certs, ...c.trusted].map((k) => new X509Certificate(k.toSchema().toBER(false))).find((k) => k.subject === c.x.issuer) ?? null;
148
+ return issuerCert ? b64(issuerCert.rawData) : null;
149
+ }
150
+ /**
151
+ * Verifies a CMS SignedData (base64 DER) over `challenge` by an e-CNPJ and returns the proof. Throws KyaError
152
+ * CERTIFICATE_INVALID / CHALLENGE_FAILED / CNPJ_MISMATCH. `anchors` are PEM or DER (base64) roots.
153
+ */
154
+ export async function verifyIcpSignature(p) {
155
+ let cnpj = "";
156
+ const c = await checkCms(p, (x) => {
157
+ const found = cnpjFromCertificate(x);
158
+ if (!found)
159
+ throw new KyaError("CERTIFICATE_INVALID", cpfFromCertificate(x) ? "this is a natural person's certificate (e-CPF); sign with the company's e-CNPJ" : "certificate has no ICP-Brasil CNPJ (not an e-CNPJ)");
160
+ if (found !== normalizeCnpj(p.expectedCnpj))
161
+ throw new KyaError("CNPJ_MISMATCH", `certificate belongs to CNPJ ${found}, not ${normalizeCnpj(p.expectedCnpj)}`);
162
+ cnpj = found;
163
+ });
131
164
  return {
132
165
  cnpj,
133
- certificate_sha256: await sha256Hex(new Uint8Array(der)),
134
- certificate_subject: x.subject,
135
- issuer: x.issuer,
136
- not_before: x.notBefore.toISOString(),
137
- not_after: x.notAfter.toISOString(),
138
- signed_at: now.toISOString(),
139
- serial_number: normalizeSerial(x.serialNumber),
140
- crl_urls: crlUrlsOf(x),
141
- issuer_certificate: issuerCert ? b64(issuerCert.rawData) : null
166
+ certificate_sha256: await sha256Hex(new Uint8Array(c.der)),
167
+ certificate_subject: c.x.subject,
168
+ issuer: c.x.issuer,
169
+ not_before: c.x.notBefore.toISOString(),
170
+ not_after: c.x.notAfter.toISOString(),
171
+ signed_at: c.now.toISOString(),
172
+ serial_number: normalizeSerial(c.x.serialNumber),
173
+ crl_urls: crlUrlsOf(c.x),
174
+ issuer_certificate: issuerCertificateOf(c)
175
+ };
176
+ }
177
+ /** Verifies a CMS over `challenge` by a natural person's e-CPF (D-29). Refuses an e-CNPJ. */
178
+ export async function verifyIcpPersonSignature(p) {
179
+ let cpf = "";
180
+ const c = await checkCms(p, (x) => {
181
+ if (cnpjFromCertificate(x))
182
+ throw new KyaError("CERTIFICATE_INVALID", "this is a company certificate (e-CNPJ); a person signs with their e-CPF");
183
+ const found = cpfFromCertificate(x);
184
+ if (!found)
185
+ throw new KyaError("CERTIFICATE_INVALID", "certificate has no ICP-Brasil CPF (not an e-CPF)");
186
+ cpf = found;
187
+ });
188
+ return {
189
+ cpf,
190
+ certificate_sha256: await sha256Hex(new Uint8Array(c.der)),
191
+ issuer: c.x.issuer,
192
+ not_before: c.x.notBefore.toISOString(),
193
+ not_after: c.x.notAfter.toISOString(),
194
+ signed_at: c.now.toISOString(),
195
+ serial_number: normalizeSerial(c.x.serialNumber),
196
+ crl_urls: crlUrlsOf(c.x),
197
+ issuer_certificate: issuerCertificateOf(c)
142
198
  };
143
199
  }
package/dist/index.d.ts CHANGED
@@ -9,3 +9,4 @@ export * from "./revocation.js";
9
9
  export * from "./erc8004.js";
10
10
  export * from "./ecnpj-sign.js";
11
11
  export * from "./cloud-sign.js";
12
+ export * from "./person.js";
package/dist/index.js CHANGED
@@ -9,3 +9,4 @@ export * from "./revocation.js";
9
9
  export * from "./erc8004.js";
10
10
  export * from "./ecnpj-sign.js";
11
11
  export * from "./cloud-sign.js";
12
+ export * from "./person.js";
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Natural persons as principals (D-29): "this agent key acts for a person who signed with an ICP-Brasil e-CPF".
3
+ * Privacy rules: the name is never stored or published; the CPF is never stored, published or revealed. The
4
+ * issuer keeps only HMAC(key, CPF) while a credential is alive, answers paying verifiers whether a CPF they
5
+ * already know matches, and gives each verifier its own pseudonymous subject reference for that person.
6
+ */
7
+ import { type AgentKey } from "./types.js";
8
+ import { type Challenge } from "./challenge.js";
9
+ /** 11 digits; throws INVALID_INPUT on anything that is not a valid CPF (check digits included). */
10
+ export declare function normalizeCpf(input: string): string;
11
+ export declare function isValidCpf(input: string): boolean;
12
+ /** Keyed hash of a CPF: what the issuer stores. Without the key, a CPF cannot be tested against it. */
13
+ export declare const personHash: (keyB64: string, cpf: string) => Promise<string>;
14
+ /**
15
+ * Pairwise subject reference: the same person gets a different, stable value for each verifier, so a verifier
16
+ * can track that person's agents on its side without two verifiers being able to link them.
17
+ */
18
+ export declare const subjectRef: (keyB64: string, hash: string, verifier: string) => Promise<string>;
19
+ /** Versioned keys from a JSON object {"1": "<base64 32 bytes>", "2": ...}; the highest version is current. */
20
+ export declare function personKeys(json: string | undefined): {
21
+ current: {
22
+ version: number;
23
+ key: string;
24
+ };
25
+ all: Map<number, string>;
26
+ } | null;
27
+ /** Consent the person signs with the e-CPF; part of the challenge text, so the signature records it. */
28
+ export declare const PERSON_CONSENT = "I authorize this issuer to keep a keyed hash of my CPF while this credential is valid and to answer, to verifiers who pay for each check, only whether a CPF they already have matches. My name and CPF are never stored in clear, published or revealed. / Autorizo este emissor a guardar um hash com chave do meu CPF enquanto esta credencial valer e a responder, a verificadores que pagam por consulta, apenas se um CPF que eles ja tem confere. Meu nome e meu CPF nunca sao guardados em claro, publicados ou revelados.";
29
+ /** The text a person and the agent sign (no CPF: it comes from the e-CPF certificate). */
30
+ export declare function personChallengeText(p: {
31
+ issuer: string;
32
+ challenge_id: string;
33
+ agent: AgentKey;
34
+ expires_at: string;
35
+ }): string;
36
+ /** The text a person signs with the e-CPF to see their own agents (accepted for 10 minutes after `at`). */
37
+ export declare const personAgentsText: (at?: Date) => string;
38
+ export declare function newPersonChallenge(p: {
39
+ issuer: string;
40
+ agent: AgentKey;
41
+ now?: Date;
42
+ }): Challenge;
package/dist/person.js ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Natural persons as principals (D-29): "this agent key acts for a person who signed with an ICP-Brasil e-CPF".
3
+ * Privacy rules: the name is never stored or published; the CPF is never stored, published or revealed. The
4
+ * issuer keeps only HMAC(key, CPF) while a credential is alive, answers paying verifiers whether a CPF they
5
+ * already know matches, and gives each verifier its own pseudonymous subject reference for that person.
6
+ */
7
+ import { KyaError } from "./types.js";
8
+ import { CHALLENGE_TTL_SECONDS } from "./challenge.js";
9
+ /** 11 digits; throws INVALID_INPUT on anything that is not a valid CPF (check digits included). */
10
+ export function normalizeCpf(input) {
11
+ const d = String(input).replace(/\D/g, "");
12
+ if (!isValidCpf(d))
13
+ throw new KyaError("INVALID_INPUT", "cpf must be a valid Brazilian CPF");
14
+ return d;
15
+ }
16
+ export function isValidCpf(input) {
17
+ const d = String(input).replace(/\D/g, "");
18
+ if (d.length !== 11 || /^(\d)\1{10}$/.test(d))
19
+ return false;
20
+ const check = (len) => {
21
+ let sum = 0;
22
+ for (let i = 0; i < len; i++)
23
+ sum += Number(d[i]) * (len + 1 - i);
24
+ const r = (sum * 10) % 11;
25
+ return r === 10 ? 0 : r;
26
+ };
27
+ return check(9) === Number(d[9]) && check(10) === Number(d[10]);
28
+ }
29
+ const b64url = (u) => {
30
+ let s = "";
31
+ for (const b of u)
32
+ s += String.fromCharCode(b);
33
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
34
+ };
35
+ const unb64 = (s) => Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
36
+ async function hmac(keyB64, message) {
37
+ const raw = unb64(keyB64);
38
+ if (raw.length < 32)
39
+ throw new KyaError("UPSTREAM_ERROR", "person key must be at least 32 bytes");
40
+ const key = await crypto.subtle.importKey("raw", raw.buffer, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
41
+ return b64url(new Uint8Array(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message))));
42
+ }
43
+ /** Keyed hash of a CPF: what the issuer stores. Without the key, a CPF cannot be tested against it. */
44
+ export const personHash = (keyB64, cpf) => hmac(keyB64, `kya:cpf:${normalizeCpf(cpf)}`);
45
+ /**
46
+ * Pairwise subject reference: the same person gets a different, stable value for each verifier, so a verifier
47
+ * can track that person's agents on its side without two verifiers being able to link them.
48
+ */
49
+ export const subjectRef = (keyB64, hash, verifier) => hmac(keyB64, `kya:subject:${hash}:${verifier.toLowerCase()}`);
50
+ /** Versioned keys from a JSON object {"1": "<base64 32 bytes>", "2": ...}; the highest version is current. */
51
+ export function personKeys(json) {
52
+ if (!json)
53
+ return null;
54
+ let parsed;
55
+ try {
56
+ parsed = JSON.parse(json);
57
+ }
58
+ catch {
59
+ throw new KyaError("UPSTREAM_ERROR", "PERSON_HMAC_KEYS is not valid JSON");
60
+ }
61
+ const all = new Map(Object.entries(parsed).map(([v, k]) => [Number(v), k]).filter(([v, k]) => Number.isInteger(v) && v > 0 && typeof k === "string"));
62
+ if (!all.size)
63
+ return null;
64
+ const version = Math.max(...all.keys());
65
+ return { current: { version, key: all.get(version) }, all };
66
+ }
67
+ /** Consent the person signs with the e-CPF; part of the challenge text, so the signature records it. */
68
+ export const PERSON_CONSENT = "I authorize this issuer to keep a keyed hash of my CPF while this credential is valid and to answer, to verifiers who pay for each check, only whether a CPF they already have matches. My name and CPF are never stored in clear, published or revealed. / Autorizo este emissor a guardar um hash com chave do meu CPF enquanto esta credencial valer e a responder, a verificadores que pagam por consulta, apenas se um CPF que eles ja tem confere. Meu nome e meu CPF nunca sao guardados em claro, publicados ou revelados.";
69
+ /** The text a person and the agent sign (no CPF: it comes from the e-CPF certificate). */
70
+ export function personChallengeText(p) {
71
+ return [
72
+ "KYA Brasil: prove this agent key acts for me, a natural person",
73
+ `issuer: ${p.issuer}`,
74
+ `challenge: ${p.challenge_id}`,
75
+ `agent: ${p.agent.kind}:${p.agent.id}`,
76
+ "principal: person (e-CPF)",
77
+ `consent: ${PERSON_CONSENT}`,
78
+ `expires: ${p.expires_at}`
79
+ ].join("\n");
80
+ }
81
+ /** The text a person signs with the e-CPF to see their own agents (accepted for 10 minutes after `at`). */
82
+ export const personAgentsText = (at = new Date()) => `KYA Brasil: my agents\nat: ${at.toISOString()}`;
83
+ export function newPersonChallenge(p) {
84
+ const now = p.now ?? new Date();
85
+ const challenge_id = crypto.randomUUID();
86
+ const expires_at = new Date(now.getTime() + CHALLENGE_TTL_SECONDS * 1000).toISOString();
87
+ return { challenge_id, expires_at, text: personChallengeText({ issuer: p.issuer, challenge_id, agent: p.agent, expires_at }) };
88
+ }
package/dist/types.d.ts CHANGED
@@ -67,22 +67,31 @@ export interface KybSnapshot {
67
67
  red_flags: string[];
68
68
  checked_at: string;
69
69
  }
70
+ export interface CompanyPrincipal {
71
+ kind?: "company";
72
+ cnpj: string;
73
+ cnpj_formatted: string;
74
+ legal_name: string;
75
+ registration_status: string;
76
+ }
77
+ /** D-29: a person who signed with an e-CPF. Never a name, a CPF or a hash of it; verifiers check a CPF they know through the issuer. */
78
+ export interface PersonPrincipal {
79
+ kind: "person";
80
+ verified_by: "icp-brasil-e-cpf";
81
+ }
82
+ export declare const isPersonPrincipal: (p: CompanyPrincipal | PersonPrincipal) => p is PersonPrincipal;
70
83
  /** The credential: "this agent key acts for this company, within this mandate, verified on this date". */
71
84
  export interface Credential {
72
85
  credential_id: string;
73
86
  version: 1;
74
87
  agent: AgentKey;
75
- principal: {
76
- cnpj: string;
77
- cnpj_formatted: string;
78
- legal_name: string;
79
- registration_status: string;
80
- };
81
- /** How the company authorized it: hash of the e-CNPJ certificate that signed the challenge (never the CPF of the signer). */
88
+ /** A company (e-CNPJ) or, since D-29, a natural person (e-CPF) with no personal data: tell them apart by `kind`. */
89
+ principal: CompanyPrincipal | PersonPrincipal;
90
+ /** How the principal authorized it: hash of the ICP-Brasil certificate that signed the challenge. No subject for persons. */
82
91
  authorized_by: {
83
- method: "icp-brasil-e-cnpj";
92
+ method: "icp-brasil-e-cnpj" | "icp-brasil-e-cpf";
84
93
  certificate_sha256: string;
85
- certificate_subject: string;
94
+ certificate_subject?: string;
86
95
  issuer: string;
87
96
  signed_at: string;
88
97
  };
package/dist/types.js CHANGED
@@ -1,4 +1,5 @@
1
1
  /** Shared types of KYA Brasil. Within /v1 fields are only added, never renamed or removed. */
2
+ export const isPersonPrincipal = (p) => p.kind === "person";
2
3
  export class KyaError extends Error {
3
4
  code;
4
5
  constructor(code, message) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kya-brasil/core",
3
- "version": "0.9.0",
3
+ "version": "0.10.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",