@kya-brasil/core 0.15.0 → 0.17.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 +8 -5
- package/dist/credential.d.ts +20 -2
- package/dist/credential.js +21 -7
- package/dist/revocation.d.ts +29 -6
- package/dist/revocation.js +37 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -186,25 +186,28 @@ const ecnpj_signature = await signer.sign(challengeText);
|
|
|
186
186
|
```ts
|
|
187
187
|
import {
|
|
188
188
|
newChallenge, verifyAgentSignature, verifyIcpSignature, checkRevocation,
|
|
189
|
-
validateMandate, buildCredential, signCredential, importPrivateKey, generateAttestationKey
|
|
189
|
+
validateMandate, mandateSha256, buildCredential, signCredential, importPrivateKey, generateAttestationKey
|
|
190
190
|
} from "@kya-brasil/core";
|
|
191
191
|
|
|
192
192
|
const key = await generateAttestationKey(); // once; keep privateKeyPkcs8 secret, publish publicJwk
|
|
193
|
-
const
|
|
193
|
+
const m = validateMandate(mandate);
|
|
194
|
+
const mandate_sha256 = await mandateSha256(m); // the e-CNPJ signs this hash with the challenge (D-33)
|
|
195
|
+
const ch = newChallenge({ issuer: "https://kya.acquirer.example", agent, cnpj, mandate_sha256 });
|
|
194
196
|
// The agent signs ch.text with its key; the company signs ch.text with its e-CNPJ (detached CMS).
|
|
195
197
|
if (!(await verifyAgentSignature(agent, ch.text, agentSignature))) throw new Error("agent signature");
|
|
196
198
|
const icp = await verifyIcpSignature({ cmsBase64: ecnpjSignature, challenge: ch.text, expectedCnpj: cnpj, anchors });
|
|
197
199
|
// anchors: the ICP-Brasil roots as PEM strings (bundle in the repository: anchors/icp-brasil.pem).
|
|
198
200
|
const crlDer = await fetchCrl(icp.crl_urls); // yours: HTTP + cache
|
|
199
|
-
const rev = await checkRevocation({ crlDer, serial: icp.serial_number, issuerCertificate: icp.issuer_certificate
|
|
201
|
+
const rev = await checkRevocation({ crlDer, serial: icp.serial_number, issuerCertificate: icp.issuer_certificate!, pastNextUpdate: "refuse" });
|
|
200
202
|
if (rev.revoked) throw new Error("certificate revoked");
|
|
201
203
|
|
|
202
204
|
const credential = await signCredential(
|
|
203
205
|
buildCredential({
|
|
204
206
|
credential_id: crypto.randomUUID(), agent, cnpj,
|
|
205
207
|
legal_name, registration_status, // from your registration source; "UNVERIFIED" if none answered
|
|
206
|
-
certificate: { sha256: icp.certificate_sha256, subject: icp.certificate_subject, issuer: icp.issuer, signed_at: icp.signed_at },
|
|
207
|
-
mandate:
|
|
208
|
+
certificate: { sha256: icp.certificate_sha256, subject: icp.certificate_subject, issuer: icp.issuer, signed_at: icp.signed_at, not_after: icp.not_after },
|
|
209
|
+
mandate: m, mandate_sha256,
|
|
210
|
+
ttl_seconds: 30 * 86400, // your risk decision; capped by the certificate and the mandate (D-35)
|
|
208
211
|
issuer: "https://kya.acquirer.example"
|
|
209
212
|
}),
|
|
210
213
|
await importPrivateKey(key.privateKeyPkcs8), key.keyId
|
package/dist/credential.d.ts
CHANGED
|
@@ -1,18 +1,32 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
1
|
+
import { type Credential, type CredentialStatus, type KybSnapshot, type Mandate, type AgentKey } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* D-35: when a credential expires. The lifetime is the caller's (`ttl_seconds`, no default: it is a risk decision),
|
|
4
|
+
* and the credential never outlives the mandate nor the certificate that signed it, so a proof checked against
|
|
5
|
+
* ICP-Brasil (D-34) never fails while the credential still reads as active.
|
|
6
|
+
*/
|
|
7
|
+
export declare function credentialExpiry(p: {
|
|
8
|
+
now: Date;
|
|
9
|
+
ttl_seconds: number;
|
|
10
|
+
mandate: Pick<Mandate, "valid_until">;
|
|
11
|
+
certificate_not_after: string;
|
|
12
|
+
}): Date;
|
|
3
13
|
export declare function buildCredential(p: {
|
|
4
14
|
credential_id: string;
|
|
5
15
|
agent: AgentKey;
|
|
6
16
|
cnpj: string;
|
|
7
17
|
legal_name: string;
|
|
8
18
|
registration_status: string;
|
|
19
|
+
/** not_after: the signing certificate's end of validity (IcpProof.not_after); the credential never outlives it. */
|
|
9
20
|
certificate: {
|
|
10
21
|
sha256: string;
|
|
11
22
|
subject: string;
|
|
12
23
|
issuer: string;
|
|
13
24
|
signed_at: string;
|
|
25
|
+
not_after: string;
|
|
14
26
|
};
|
|
15
27
|
mandate: Mandate;
|
|
28
|
+
/** D-35: the credential lifetime in seconds. Required, no default: how long a credential lives is the caller's risk. */
|
|
29
|
+
ttl_seconds: number;
|
|
16
30
|
/** D-33: the mandate SHA-256 the signed challenge carried (mandateSha256OfChallenge). */
|
|
17
31
|
mandate_sha256?: string;
|
|
18
32
|
/** /v1 compatibility only; leave out (null). */
|
|
@@ -26,12 +40,16 @@ export declare function buildCredential(p: {
|
|
|
26
40
|
export declare function buildPersonCredential(p: {
|
|
27
41
|
credential_id: string;
|
|
28
42
|
agent: AgentKey;
|
|
43
|
+
/** not_after: the signing certificate's end of validity (IcpPersonProof.not_after); the credential never outlives it. */
|
|
29
44
|
certificate: {
|
|
30
45
|
sha256: string;
|
|
31
46
|
issuer: string;
|
|
32
47
|
signed_at: string;
|
|
48
|
+
not_after: string;
|
|
33
49
|
};
|
|
34
50
|
mandate: Mandate;
|
|
51
|
+
/** D-35: the credential lifetime in seconds. Required, no default. */
|
|
52
|
+
ttl_seconds: number;
|
|
35
53
|
/** D-33: the mandate SHA-256 the signed challenge carried (mandateSha256OfChallenge). */
|
|
36
54
|
mandate_sha256?: string;
|
|
37
55
|
issuer: string;
|
package/dist/credential.js
CHANGED
|
@@ -5,13 +5,29 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { attestedPayload, canonicalize, signAttestation, verifyAttestation } from "./attestation.js";
|
|
7
7
|
import { formatCnpj, normalizeCnpj } from "./cnpj.js";
|
|
8
|
-
|
|
8
|
+
import { KyaError } from "./types.js";
|
|
9
|
+
/**
|
|
10
|
+
* D-35: when a credential expires. The lifetime is the caller's (`ttl_seconds`, no default: it is a risk decision),
|
|
11
|
+
* and the credential never outlives the mandate nor the certificate that signed it, so a proof checked against
|
|
12
|
+
* ICP-Brasil (D-34) never fails while the credential still reads as active.
|
|
13
|
+
*/
|
|
14
|
+
export function credentialExpiry(p) {
|
|
15
|
+
if (typeof p.ttl_seconds !== "number" || !Number.isFinite(p.ttl_seconds) || p.ttl_seconds <= 0)
|
|
16
|
+
throw new KyaError("INVALID_INPUT", "ttl_seconds must be a positive number of seconds (the credential lifetime is the caller's decision)");
|
|
17
|
+
const notAfter = new Date(p.certificate_not_after);
|
|
18
|
+
if (Number.isNaN(notAfter.getTime()))
|
|
19
|
+
throw new KyaError("INVALID_INPUT", "certificate.not_after must be an ISO 8601 date");
|
|
20
|
+
if (notAfter <= p.now)
|
|
21
|
+
throw new KyaError("CERTIFICATE_INVALID", `certificate expired on ${notAfter.toISOString()}`);
|
|
22
|
+
const candidates = [new Date(p.now.getTime() + p.ttl_seconds * 1000), notAfter];
|
|
23
|
+
if (p.mandate.valid_until)
|
|
24
|
+
candidates.push(new Date(p.mandate.valid_until));
|
|
25
|
+
return new Date(Math.min(...candidates.map((d) => d.getTime())));
|
|
26
|
+
}
|
|
9
27
|
export function buildCredential(p) {
|
|
10
28
|
const now = p.now ?? new Date();
|
|
11
29
|
const cnpj = normalizeCnpj(p.cnpj);
|
|
12
|
-
const
|
|
13
|
-
const mandateEnd = p.mandate.valid_until ? new Date(p.mandate.valid_until) : null;
|
|
14
|
-
const expires = mandateEnd && mandateEnd < ttl ? mandateEnd : ttl;
|
|
30
|
+
const expires = credentialExpiry({ now, ttl_seconds: p.ttl_seconds, mandate: p.mandate, certificate_not_after: p.certificate.not_after });
|
|
15
31
|
return {
|
|
16
32
|
credential_id: p.credential_id,
|
|
17
33
|
version: 1,
|
|
@@ -32,9 +48,7 @@ export function buildCredential(p) {
|
|
|
32
48
|
/** D-29: credential of a natural person. Nothing personal: no name, no CPF, no certificate subject. */
|
|
33
49
|
export function buildPersonCredential(p) {
|
|
34
50
|
const now = p.now ?? new Date();
|
|
35
|
-
const
|
|
36
|
-
const mandateEnd = p.mandate.valid_until ? new Date(p.mandate.valid_until) : null;
|
|
37
|
-
const expires = mandateEnd && mandateEnd < ttl ? mandateEnd : ttl;
|
|
51
|
+
const expires = credentialExpiry({ now, ttl_seconds: p.ttl_seconds, mandate: p.mandate, certificate_not_after: p.certificate.not_after });
|
|
38
52
|
return {
|
|
39
53
|
credential_id: p.credential_id,
|
|
40
54
|
version: 1,
|
package/dist/revocation.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface RevocationStatus {
|
|
|
15
15
|
revoked: boolean;
|
|
16
16
|
revoked_at: string | null;
|
|
17
17
|
reason: string | null;
|
|
18
|
+
/** RFC 5280 invalidityDate: when the key is known or suspected to have been compromised, if the AC says so. */
|
|
19
|
+
invalidity_date?: string | null;
|
|
18
20
|
crl_this_update: string;
|
|
19
21
|
crl_next_update: string | null;
|
|
20
22
|
}
|
|
@@ -46,12 +48,33 @@ export declare function checkRevocation(p: {
|
|
|
46
48
|
pastNextUpdate: "refuse" | "accept";
|
|
47
49
|
now?: Date;
|
|
48
50
|
}): Promise<RevocationStatus>;
|
|
51
|
+
/** RFC 5280 CRL reason codes (section 5.3.1), by name. */
|
|
52
|
+
export declare const CRL_REASONS: readonly ["unspecified", "keyCompromise", "cACompromise", "affiliationChanged", "superseded", "cessationOfOperation", "certificateHold", "removeFromCRL", "privilegeWithdrawn", "aACompromise"];
|
|
53
|
+
export type CrlReason = (typeof CRL_REASONS)[number];
|
|
49
54
|
/**
|
|
50
|
-
*
|
|
51
|
-
* -
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* -
|
|
55
|
+
* D-36: what a revoked certificate does to what it already authorized, decided by whoever relies on it.
|
|
56
|
+
* - `by_reason`: the effect of each CRL reason the caller has an opinion on: "revoke" (the authorization ends),
|
|
57
|
+
* "suspend" (it stops while the entry stays, e.g. certificateHold) or "keep" (it stands: the certificate was
|
|
58
|
+
* replaced or retired without compromise);
|
|
59
|
+
* - `other`: the effect of any reason not listed, including an entry without a reason. Never "keep": an
|
|
60
|
+
* unexplained revocation is not ignored.
|
|
61
|
+
* No default: which reasons end a mandate is a legal and risk decision (Código Civil art. 682; SPEC section 11).
|
|
62
|
+
*/
|
|
63
|
+
export interface CertificateRevocationPolicy {
|
|
64
|
+
by_reason: Partial<Record<CrlReason, "revoke" | "suspend" | "keep">>;
|
|
65
|
+
other: "revoke" | "suspend";
|
|
66
|
+
}
|
|
67
|
+
/** Throws INVALID_INPUT unless `p` is a complete CertificateRevocationPolicy. */
|
|
68
|
+
export declare function certificateRevocationPolicy(p: CertificateRevocationPolicy): CertificateRevocationPolicy;
|
|
69
|
+
/**
|
|
70
|
+
* What a CRL entry does to an authorization the certificate already signed (D-16, D-36), under the caller's policy.
|
|
71
|
+
* Two rules hold whatever the policy says, because they are about the signature, not about the mandate:
|
|
72
|
+
* - "keep" applies only when the revocation, and the suspected compromise (invalidityDate) when the AC gives
|
|
73
|
+
* one, come after the authorization; otherwise the authorization was signed with a certificate already
|
|
74
|
+
* revoked or already compromised, and it is revoked (MP 2.200-2 art. 10 presumes only a valid certificate);
|
|
75
|
+
* - without the authorization's date there is nothing to compare: "keep" becomes "revoke".
|
|
55
76
|
* At issuance any entry refuses: a new authorization must be signed with a certificate in force.
|
|
56
77
|
*/
|
|
57
|
-
export declare function revocationEffect(r: Pick<RevocationStatus, "revoked" | "revoked_at" | "reason"
|
|
78
|
+
export declare function revocationEffect(r: Pick<RevocationStatus, "revoked" | "revoked_at" | "reason"> & {
|
|
79
|
+
invalidity_date?: string | null;
|
|
80
|
+
}, authorizedAt: string | null, policy: CertificateRevocationPolicy): "none" | "suspend" | "revoke";
|
package/dist/revocation.js
CHANGED
|
@@ -64,6 +64,7 @@ export async function verifyCrl(p) {
|
|
|
64
64
|
revoked: entry !== null,
|
|
65
65
|
revoked_at: entry ? entry.revocationDate.toISOString() : null,
|
|
66
66
|
reason: entry && entry.reason !== undefined ? (X509CrlReason[entry.reason] ?? String(entry.reason)) : null,
|
|
67
|
+
invalidity_date: entry?.invalidity ? entry.invalidity.toISOString() : null,
|
|
67
68
|
crl_this_update: this_update,
|
|
68
69
|
crl_next_update: next_update
|
|
69
70
|
};
|
|
@@ -74,21 +75,45 @@ export async function verifyCrl(p) {
|
|
|
74
75
|
export async function checkRevocation(p) {
|
|
75
76
|
return (await verifyCrl(p)).lookup(p.serial);
|
|
76
77
|
}
|
|
78
|
+
/** RFC 5280 CRL reason codes (section 5.3.1), by name. */
|
|
79
|
+
export const CRL_REASONS = ["unspecified", "keyCompromise", "cACompromise", "affiliationChanged", "superseded", "cessationOfOperation", "certificateHold", "removeFromCRL", "privilegeWithdrawn", "aACompromise"];
|
|
80
|
+
/** Throws INVALID_INPUT unless `p` is a complete CertificateRevocationPolicy. */
|
|
81
|
+
export function certificateRevocationPolicy(p) {
|
|
82
|
+
if (!p || typeof p !== "object")
|
|
83
|
+
throw new KyaError("INVALID_INPUT", "a certificate revocation policy is required: { by_reason, other }");
|
|
84
|
+
if (p.other !== "revoke" && p.other !== "suspend")
|
|
85
|
+
throw new KyaError("INVALID_INPUT", 'policy.other is required: "revoke" or "suspend" (an unexplained revocation is never ignored)');
|
|
86
|
+
if (!p.by_reason || typeof p.by_reason !== "object")
|
|
87
|
+
throw new KyaError("INVALID_INPUT", "policy.by_reason is required (it may be empty)");
|
|
88
|
+
for (const [k, v] of Object.entries(p.by_reason)) {
|
|
89
|
+
if (!CRL_REASONS.includes(k))
|
|
90
|
+
throw new KyaError("INVALID_INPUT", `policy.by_reason.${k} is not an RFC 5280 reason`);
|
|
91
|
+
if (v !== "revoke" && v !== "suspend" && v !== "keep")
|
|
92
|
+
throw new KyaError("INVALID_INPUT", `policy.by_reason.${k} must be "revoke", "suspend" or "keep"`);
|
|
93
|
+
}
|
|
94
|
+
return { by_reason: { ...p.by_reason }, other: p.other };
|
|
95
|
+
}
|
|
96
|
+
const isAfter = (a, b) => a !== null && Date.parse(a) > Date.parse(b);
|
|
77
97
|
/**
|
|
78
|
-
* What a CRL entry does to
|
|
79
|
-
*
|
|
80
|
-
* -
|
|
81
|
-
*
|
|
82
|
-
*
|
|
98
|
+
* What a CRL entry does to an authorization the certificate already signed (D-16, D-36), under the caller's policy.
|
|
99
|
+
* Two rules hold whatever the policy says, because they are about the signature, not about the mandate:
|
|
100
|
+
* - "keep" applies only when the revocation, and the suspected compromise (invalidityDate) when the AC gives
|
|
101
|
+
* one, come after the authorization; otherwise the authorization was signed with a certificate already
|
|
102
|
+
* revoked or already compromised, and it is revoked (MP 2.200-2 art. 10 presumes only a valid certificate);
|
|
103
|
+
* - without the authorization's date there is nothing to compare: "keep" becomes "revoke".
|
|
83
104
|
* At issuance any entry refuses: a new authorization must be signed with a certificate in force.
|
|
84
105
|
*/
|
|
85
|
-
export function revocationEffect(r, authorizedAt) {
|
|
106
|
+
export function revocationEffect(r, authorizedAt, policy) {
|
|
107
|
+
const pol = certificateRevocationPolicy(policy);
|
|
86
108
|
if (!r.revoked)
|
|
87
109
|
return "none";
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
110
|
+
const reason = r.reason && CRL_REASONS.includes(r.reason) ? r.reason : null;
|
|
111
|
+
const effect = (reason && pol.by_reason[reason]) ?? pol.other;
|
|
112
|
+
if (effect !== "keep")
|
|
113
|
+
return effect;
|
|
114
|
+
if (!authorizedAt || Number.isNaN(Date.parse(authorizedAt)))
|
|
115
|
+
return "revoke";
|
|
116
|
+
const revokedAfter = isAfter(r.revoked_at, authorizedAt);
|
|
117
|
+
const compromiseAfter = r.invalidity_date ? isAfter(r.invalidity_date, authorizedAt) : true;
|
|
118
|
+
return revokedAfter && compromiseAfter ? "none" : "revoke";
|
|
94
119
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kya-brasil/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.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",
|