@f-o-t/digital-certificate 1.0.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.
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Digital Certificate Types
3
+ *
4
+ * Types for Brazilian A1 digital certificate handling,
5
+ * XML-DSig signing, and mTLS configuration.
6
+ */
7
+ interface CertificateSubject {
8
+ commonName: string | null;
9
+ organization: string | null;
10
+ organizationalUnit: string | null;
11
+ country: string | null;
12
+ state: string | null;
13
+ locality: string | null;
14
+ /** Full raw subject string */
15
+ raw: string;
16
+ }
17
+ interface CertificateIssuer {
18
+ commonName: string | null;
19
+ organization: string | null;
20
+ country: string | null;
21
+ raw: string;
22
+ }
23
+ interface CertificateValidity {
24
+ notBefore: Date;
25
+ notAfter: Date;
26
+ }
27
+ interface BrazilianFields {
28
+ /** CNPJ extracted from certificate (14 digits) */
29
+ cnpj: string | null;
30
+ /** CPF extracted from certificate (11 digits) */
31
+ cpf: string | null;
32
+ }
33
+ interface CertificateInfo {
34
+ /** Certificate serial number */
35
+ serialNumber: string;
36
+ /** Subject (who the certificate was issued to) */
37
+ subject: CertificateSubject;
38
+ /** Issuer (who issued the certificate) */
39
+ issuer: CertificateIssuer;
40
+ /** Validity period */
41
+ validity: CertificateValidity;
42
+ /** SHA-256 fingerprint of the certificate */
43
+ fingerprint: string;
44
+ /** Whether the certificate is currently valid */
45
+ isValid: boolean;
46
+ /** Brazilian-specific fields (CNPJ, CPF) */
47
+ brazilian: BrazilianFields;
48
+ /** PEM-encoded certificate */
49
+ certPem: string;
50
+ /** PEM-encoded private key */
51
+ keyPem: string;
52
+ /** Raw PFX buffer */
53
+ pfxBuffer: Buffer;
54
+ /** PFX password */
55
+ pfxPassword: string;
56
+ }
57
+ type SignatureAlgorithm = "sha1" | "sha256";
58
+ interface SignOptions {
59
+ /** The parsed certificate to use for signing */
60
+ certificate: CertificateInfo;
61
+ /** The URI of the element to sign (e.g., "#NFe123") */
62
+ referenceUri: string;
63
+ /** Hash algorithm for digest and signature (default: "sha256") */
64
+ algorithm?: SignatureAlgorithm;
65
+ /** Tag name of the element where Signature will be inserted.
66
+ * If not specified, the signature is appended to the root element. */
67
+ signatureParent?: string;
68
+ /** Whether to include the XML declaration in output (default: true) */
69
+ includeDeclaration?: boolean;
70
+ }
71
+ interface PemPair {
72
+ cert: string;
73
+ key: string;
74
+ }
75
+ interface MtlsOptions {
76
+ /** The parsed certificate to use */
77
+ certificate: CertificateInfo;
78
+ /** Additional CA certificates (PEM-encoded) */
79
+ caCerts?: string[];
80
+ /** Whether to reject unauthorized connections (default: true) */
81
+ rejectUnauthorized?: boolean;
82
+ }
83
+ /**
84
+ * Parse a .pfx/.p12 certificate file and extract all relevant information
85
+ *
86
+ * @param pfx - The PFX/P12 file contents as a Buffer
87
+ * @param password - The password to decrypt the PFX file
88
+ * @returns Parsed certificate information
89
+ * @throws {Error} If the PFX cannot be parsed or the password is wrong
90
+ */
91
+ declare function parseCertificate(pfx: Buffer, password: string): CertificateInfo;
92
+ /**
93
+ * Check if a certificate is currently valid (not expired)
94
+ */
95
+ declare function isCertificateValid(cert: CertificateInfo): boolean;
96
+ /**
97
+ * Get the number of days until certificate expiry
98
+ * Returns negative if already expired
99
+ */
100
+ declare function daysUntilExpiry(cert: CertificateInfo): number;
101
+ /**
102
+ * Get PEM pair (certificate + private key) from certificate info
103
+ */
104
+ declare function getPemPair(cert: CertificateInfo): {
105
+ cert: string;
106
+ key: string;
107
+ };
108
+ import { z } from "zod";
109
+ declare const signatureAlgorithmSchema: z.ZodEnum<{
110
+ sha1: "sha1";
111
+ sha256: "sha256";
112
+ }>;
113
+ declare const signOptionsSchema: z.ZodObject<{
114
+ referenceUri: z.ZodString;
115
+ algorithm: z.ZodDefault<z.ZodEnum<{
116
+ sha1: "sha1";
117
+ sha256: "sha256";
118
+ }>>;
119
+ signatureParent: z.ZodOptional<z.ZodString>;
120
+ includeDeclaration: z.ZodDefault<z.ZodBoolean>;
121
+ }, z.core.$strip>;
122
+ declare const mtlsOptionsSchema: z.ZodObject<{
123
+ caCerts: z.ZodOptional<z.ZodArray<z.ZodString>>;
124
+ rejectUnauthorized: z.ZodDefault<z.ZodBoolean>;
125
+ }, z.core.$strip>;
126
+ /**
127
+ * Utility functions for digital certificate handling
128
+ *
129
+ * PEM encoding, OID maps for Brazilian certificate fields,
130
+ * and ASN.1 helpers.
131
+ */
132
+ /**
133
+ * OIDs used in Brazilian ICP-Brasil certificates
134
+ * to encode CNPJ and CPF in subject fields
135
+ */
136
+ declare const BRAZILIAN_OIDS: Record<string, string>;
137
+ /**
138
+ * Parse an X.509 subject/issuer string into key-value pairs
139
+ *
140
+ * Handles formats like:
141
+ * - "CN=Name, O=Org, C=BR"
142
+ * - "/CN=Name/O=Org/C=BR"
143
+ */
144
+ declare function parseDistinguishedName(dn: string): Record<string, string>;
145
+ /**
146
+ * Extract CNPJ from certificate subject string
147
+ * Looks for patterns like:
148
+ * - OU with CNPJ: "OU=CNPJ:12345678000190"
149
+ * - Direct in CN or subject alternative names
150
+ */
151
+ declare function extractCnpj(subjectRaw: string): string | null;
152
+ /**
153
+ * Extract CPF from certificate subject string
154
+ */
155
+ declare function extractCpf(subjectRaw: string): string | null;
156
+ /** Extract base64 content from PEM string (strips headers/whitespace) */
157
+ declare function pemToBase64(pem: string): string;
158
+ declare const XMLDSIG_NS = "http://www.w3.org/2000/09/xmldsig#";
159
+ declare const EXC_C14N_NS = "http://www.w3.org/2001/10/xml-exc-c14n#";
160
+ declare const DIGEST_ALGORITHMS: {
161
+ readonly sha1: {
162
+ readonly uri: "http://www.w3.org/2000/09/xmldsig#sha1";
163
+ readonly nodeAlgo: "sha1";
164
+ };
165
+ readonly sha256: {
166
+ readonly uri: "http://www.w3.org/2001/04/xmlenc#sha256";
167
+ readonly nodeAlgo: "sha256";
168
+ };
169
+ };
170
+ declare const SIGNATURE_ALGORITHMS: {
171
+ readonly sha1: {
172
+ readonly uri: "http://www.w3.org/2000/09/xmldsig#rsa-sha1";
173
+ readonly nodeAlgo: "sha1";
174
+ };
175
+ readonly sha256: {
176
+ readonly uri: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
177
+ readonly nodeAlgo: "sha256";
178
+ };
179
+ };
180
+ declare const TRANSFORM_ALGORITHMS: {
181
+ readonly envelopedSignature: "http://www.w3.org/2000/09/xmldsig#enveloped-signature";
182
+ readonly excC14n: "http://www.w3.org/2001/10/xml-exc-c14n#";
183
+ };
184
+ export { signatureAlgorithmSchema, signOptionsSchema, pemToBase64, parseDistinguishedName, parseCertificate, mtlsOptionsSchema, isCertificateValid, getPemPair, extractCpf, extractCnpj, daysUntilExpiry, XMLDSIG_NS, TRANSFORM_ALGORITHMS, SignatureAlgorithm, SignOptions, SIGNATURE_ALGORITHMS, PemPair, MtlsOptions, EXC_C14N_NS, DIGEST_ALGORITHMS, CertificateValidity, CertificateSubject, CertificateIssuer, CertificateInfo, BrazilianFields, BRAZILIAN_OIDS };
package/dist/index.js ADDED
@@ -0,0 +1,149 @@
1
+ import {
2
+ BRAZILIAN_OIDS,
3
+ DIGEST_ALGORITHMS,
4
+ EXC_C14N_NS,
5
+ SIGNATURE_ALGORITHMS,
6
+ TRANSFORM_ALGORITHMS,
7
+ XMLDSIG_NS,
8
+ extractCnpj,
9
+ extractCpf,
10
+ parseDistinguishedName,
11
+ pemToBase64
12
+ } from "./shared/chunk-rkg27a9a.js";
13
+ import {
14
+ mtlsOptionsSchema,
15
+ signOptionsSchema,
16
+ signatureAlgorithmSchema
17
+ } from "./shared/chunk-ft0pdf3s.js";
18
+
19
+ // src/certificate.ts
20
+ import { execSync } from "node:child_process";
21
+ import crypto from "node:crypto";
22
+ function parseCertificate(pfx, password) {
23
+ const { certPem, keyPem } = extractPemFromPfx(pfx, password);
24
+ const x509 = new crypto.X509Certificate(certPem);
25
+ const subject = parseSubject(x509.subject);
26
+ const issuer = parseIssuer(x509.issuer);
27
+ const validity = parseValidity(x509);
28
+ const fingerprint = x509.fingerprint256.replace(/:/g, "").toLowerCase();
29
+ const isValid = checkValidity(validity);
30
+ const brazilian = extractBrazilianFields(x509.subject, x509.subjectAltName);
31
+ return {
32
+ serialNumber: x509.serialNumber,
33
+ subject,
34
+ issuer,
35
+ validity,
36
+ fingerprint,
37
+ isValid,
38
+ brazilian,
39
+ certPem,
40
+ keyPem,
41
+ pfxBuffer: pfx,
42
+ pfxPassword: password
43
+ };
44
+ }
45
+ function isCertificateValid(cert) {
46
+ return checkValidity(cert.validity);
47
+ }
48
+ function daysUntilExpiry(cert) {
49
+ const now = new Date;
50
+ const diff = cert.validity.notAfter.getTime() - now.getTime();
51
+ return Math.floor(diff / (1000 * 60 * 60 * 24));
52
+ }
53
+ function getPemPair(cert) {
54
+ return { cert: cert.certPem, key: cert.keyPem };
55
+ }
56
+ function extractPemFromPfx(pfx, password) {
57
+ const escapedPassword = escapeShellArg(password);
58
+ const certPem = opensslExtract(pfx, `-nokeys -clcerts -passin pass:${escapedPassword}`);
59
+ if (!certPem.includes("-----BEGIN CERTIFICATE-----")) {
60
+ throw new Error("Failed to extract certificate from PFX. Ensure the file is a valid PKCS#12 archive and the password is correct.");
61
+ }
62
+ const keyPem = opensslExtract(pfx, `-nocerts -passin pass:${escapedPassword} -passout pass: -nodes`);
63
+ if (!keyPem.includes("-----BEGIN PRIVATE KEY-----") && !keyPem.includes("-----BEGIN RSA PRIVATE KEY-----")) {
64
+ throw new Error("Failed to extract private key from PFX. Ensure the file is a valid PKCS#12 archive and the password is correct.");
65
+ }
66
+ return { certPem: certPem.trim(), keyPem: keyPem.trim() };
67
+ }
68
+ function opensslExtract(pfx, args) {
69
+ try {
70
+ return execSync(`openssl pkcs12 -in /dev/stdin ${args} -legacy`, {
71
+ input: pfx,
72
+ stdio: ["pipe", "pipe", "pipe"]
73
+ }).toString();
74
+ } catch {
75
+ try {
76
+ return execSync(`openssl pkcs12 -in /dev/stdin ${args}`, {
77
+ input: pfx,
78
+ stdio: ["pipe", "pipe", "pipe"]
79
+ }).toString();
80
+ } catch (e) {
81
+ const message = e instanceof Error ? e.message : String(e);
82
+ throw new Error(`OpenSSL PKCS12 extraction failed: ${message}`);
83
+ }
84
+ }
85
+ }
86
+ function escapeShellArg(arg) {
87
+ return arg.replace(/'/g, "'\\''");
88
+ }
89
+ function parseSubject(subjectStr) {
90
+ const fields = parseDistinguishedName(subjectStr);
91
+ return {
92
+ commonName: fields.CN ?? null,
93
+ organization: fields.O ?? null,
94
+ organizationalUnit: fields.OU ?? null,
95
+ country: fields.C ?? null,
96
+ state: fields.ST ?? null,
97
+ locality: fields.L ?? null,
98
+ raw: subjectStr
99
+ };
100
+ }
101
+ function parseIssuer(issuerStr) {
102
+ const fields = parseDistinguishedName(issuerStr);
103
+ return {
104
+ commonName: fields.CN ?? null,
105
+ organization: fields.O ?? null,
106
+ country: fields.C ?? null,
107
+ raw: issuerStr
108
+ };
109
+ }
110
+ function parseValidity(x509) {
111
+ return {
112
+ notBefore: new Date(x509.validFrom),
113
+ notAfter: new Date(x509.validTo)
114
+ };
115
+ }
116
+ function checkValidity(validity) {
117
+ const now = new Date;
118
+ return now >= validity.notBefore && now <= validity.notAfter;
119
+ }
120
+ function extractBrazilianFields(subject, subjectAltName) {
121
+ let cnpj = extractCnpj(subject);
122
+ let cpf = extractCpf(subject);
123
+ if (subjectAltName) {
124
+ if (!cnpj)
125
+ cnpj = extractCnpj(subjectAltName);
126
+ if (!cpf)
127
+ cpf = extractCpf(subjectAltName);
128
+ }
129
+ return { cnpj, cpf };
130
+ }
131
+ export {
132
+ signatureAlgorithmSchema,
133
+ signOptionsSchema,
134
+ pemToBase64,
135
+ parseDistinguishedName,
136
+ parseCertificate,
137
+ mtlsOptionsSchema,
138
+ isCertificateValid,
139
+ getPemPair,
140
+ extractCpf,
141
+ extractCnpj,
142
+ daysUntilExpiry,
143
+ XMLDSIG_NS,
144
+ TRANSFORM_ALGORITHMS,
145
+ SIGNATURE_ALGORITHMS,
146
+ EXC_C14N_NS,
147
+ DIGEST_ALGORITHMS,
148
+ BRAZILIAN_OIDS
149
+ };
package/dist/mtls.d.ts ADDED
@@ -0,0 +1,100 @@
1
+ import https from "node:https";
2
+ import tls from "node:tls";
3
+ /**
4
+ * Digital Certificate Types
5
+ *
6
+ * Types for Brazilian A1 digital certificate handling,
7
+ * XML-DSig signing, and mTLS configuration.
8
+ */
9
+ interface CertificateSubject {
10
+ commonName: string | null;
11
+ organization: string | null;
12
+ organizationalUnit: string | null;
13
+ country: string | null;
14
+ state: string | null;
15
+ locality: string | null;
16
+ /** Full raw subject string */
17
+ raw: string;
18
+ }
19
+ interface CertificateIssuer {
20
+ commonName: string | null;
21
+ organization: string | null;
22
+ country: string | null;
23
+ raw: string;
24
+ }
25
+ interface CertificateValidity {
26
+ notBefore: Date;
27
+ notAfter: Date;
28
+ }
29
+ interface BrazilianFields {
30
+ /** CNPJ extracted from certificate (14 digits) */
31
+ cnpj: string | null;
32
+ /** CPF extracted from certificate (11 digits) */
33
+ cpf: string | null;
34
+ }
35
+ interface CertificateInfo {
36
+ /** Certificate serial number */
37
+ serialNumber: string;
38
+ /** Subject (who the certificate was issued to) */
39
+ subject: CertificateSubject;
40
+ /** Issuer (who issued the certificate) */
41
+ issuer: CertificateIssuer;
42
+ /** Validity period */
43
+ validity: CertificateValidity;
44
+ /** SHA-256 fingerprint of the certificate */
45
+ fingerprint: string;
46
+ /** Whether the certificate is currently valid */
47
+ isValid: boolean;
48
+ /** Brazilian-specific fields (CNPJ, CPF) */
49
+ brazilian: BrazilianFields;
50
+ /** PEM-encoded certificate */
51
+ certPem: string;
52
+ /** PEM-encoded private key */
53
+ keyPem: string;
54
+ /** Raw PFX buffer */
55
+ pfxBuffer: Buffer;
56
+ /** PFX password */
57
+ pfxPassword: string;
58
+ }
59
+ interface PemPair {
60
+ cert: string;
61
+ key: string;
62
+ }
63
+ interface MtlsOptions {
64
+ /** The parsed certificate to use */
65
+ certificate: CertificateInfo;
66
+ /** Additional CA certificates (PEM-encoded) */
67
+ caCerts?: string[];
68
+ /** Whether to reject unauthorized connections (default: true) */
69
+ rejectUnauthorized?: boolean;
70
+ }
71
+ /**
72
+ * Create a TLS SecureContext for use with mTLS connections
73
+ *
74
+ * @param certificate - The parsed certificate
75
+ * @param options - Additional mTLS options
76
+ * @returns A tls.SecureContext configured for mTLS
77
+ */
78
+ declare function createTlsContext(certificate: CertificateInfo, options?: Omit<MtlsOptions, "certificate">): tls.SecureContext;
79
+ /**
80
+ * Create an HTTPS Agent configured for mTLS
81
+ *
82
+ * Use this agent with Node.js http/https requests or compatible
83
+ * HTTP clients (e.g., axios, node-fetch with agent option).
84
+ *
85
+ * @param certificate - The parsed certificate
86
+ * @param options - Additional mTLS options
87
+ * @returns An https.Agent configured for mTLS
88
+ */
89
+ declare function createHttpsAgent(certificate: CertificateInfo, options?: Omit<MtlsOptions, "certificate">): https.Agent;
90
+ /**
91
+ * Get PEM pair (certificate + private key) for use with custom HTTP clients
92
+ *
93
+ * Useful when you need to pass cert/key as strings to HTTP libraries
94
+ * that don't accept PFX directly (e.g., Bun.fetch with tls option).
95
+ *
96
+ * @param certificate - The parsed certificate
97
+ * @returns Object with cert and key PEM strings
98
+ */
99
+ declare function getMtlsPemPair(certificate: CertificateInfo): PemPair;
100
+ export { getMtlsPemPair, createTlsContext, createHttpsAgent };
package/dist/mtls.js ADDED
@@ -0,0 +1,41 @@
1
+ import {
2
+ mtlsOptionsSchema
3
+ } from "./shared/chunk-ft0pdf3s.js";
4
+
5
+ // src/mtls.ts
6
+ import https from "node:https";
7
+ import tls from "node:tls";
8
+ function createTlsContext(certificate, options) {
9
+ const opts = mtlsOptionsSchema.parse(options ?? {});
10
+ const contextOptions = {
11
+ pfx: certificate.pfxBuffer,
12
+ passphrase: certificate.pfxPassword
13
+ };
14
+ if (opts.caCerts && opts.caCerts.length > 0) {
15
+ contextOptions.ca = opts.caCerts;
16
+ }
17
+ return tls.createSecureContext(contextOptions);
18
+ }
19
+ function createHttpsAgent(certificate, options) {
20
+ const opts = mtlsOptionsSchema.parse(options ?? {});
21
+ const agentOptions = {
22
+ pfx: certificate.pfxBuffer,
23
+ passphrase: certificate.pfxPassword,
24
+ rejectUnauthorized: opts.rejectUnauthorized
25
+ };
26
+ if (opts.caCerts && opts.caCerts.length > 0) {
27
+ agentOptions.ca = opts.caCerts;
28
+ }
29
+ return new https.Agent(agentOptions);
30
+ }
31
+ function getMtlsPemPair(certificate) {
32
+ return {
33
+ cert: certificate.certPem,
34
+ key: certificate.keyPem
35
+ };
36
+ }
37
+ export {
38
+ getMtlsPemPair,
39
+ createTlsContext,
40
+ createHttpsAgent
41
+ };
@@ -0,0 +1,15 @@
1
+ // src/schemas.ts
2
+ import { z } from "zod";
3
+ var signatureAlgorithmSchema = z.enum(["sha1", "sha256"]);
4
+ var signOptionsSchema = z.object({
5
+ referenceUri: z.string(),
6
+ algorithm: signatureAlgorithmSchema.default("sha256"),
7
+ signatureParent: z.string().optional(),
8
+ includeDeclaration: z.boolean().default(true)
9
+ });
10
+ var mtlsOptionsSchema = z.object({
11
+ caCerts: z.array(z.string()).optional(),
12
+ rejectUnauthorized: z.boolean().default(true)
13
+ });
14
+
15
+ export { signatureAlgorithmSchema, signOptionsSchema, mtlsOptionsSchema };
@@ -0,0 +1,101 @@
1
+ // src/utils.ts
2
+ var BRAZILIAN_OIDS = {
3
+ "2.16.76.1.3.1": "otherName-CPF",
4
+ "2.16.76.1.3.2": "otherName-ICP-Brasil-Name",
5
+ "2.16.76.1.3.3": "otherName-CNPJ",
6
+ "2.16.76.1.3.4": "otherName-ICP-Brasil-Responsible",
7
+ "2.16.76.1.3.5": "otherName-ICP-Brasil-Voter",
8
+ "2.16.76.1.3.6": "otherName-ICP-Brasil-INSS",
9
+ "2.16.76.1.3.7": "otherName-ICP-Brasil-CEI",
10
+ "2.16.76.1.3.8": "otherName-ICP-Brasil-OAB"
11
+ };
12
+ function parseDistinguishedName(dn) {
13
+ const result = {};
14
+ if (!dn)
15
+ return result;
16
+ let normalized = dn;
17
+ if (dn.startsWith("/")) {
18
+ normalized = dn.slice(1).split("/").join(", ");
19
+ } else if (dn.includes(`
20
+ `)) {
21
+ normalized = dn.split(`
22
+ `).join(", ");
23
+ }
24
+ const parts = splitDnParts(normalized);
25
+ for (const part of parts) {
26
+ const eqIdx = part.indexOf("=");
27
+ if (eqIdx !== -1) {
28
+ const key = part.slice(0, eqIdx).trim();
29
+ const value = part.slice(eqIdx + 1).trim();
30
+ result[key] = value;
31
+ }
32
+ }
33
+ return result;
34
+ }
35
+ function splitDnParts(dn) {
36
+ const parts = [];
37
+ let current = "";
38
+ let inQuotes = false;
39
+ for (let i = 0;i < dn.length; i++) {
40
+ const ch = dn[i];
41
+ if (ch === '"') {
42
+ inQuotes = !inQuotes;
43
+ current += ch;
44
+ } else if (ch === "," && !inQuotes) {
45
+ parts.push(current.trim());
46
+ current = "";
47
+ } else {
48
+ current += ch;
49
+ }
50
+ }
51
+ if (current.trim()) {
52
+ parts.push(current.trim());
53
+ }
54
+ return parts;
55
+ }
56
+ function extractCnpj(subjectRaw) {
57
+ const cnpjMatch = subjectRaw.match(/CNPJ[:\s=]+(\d{14})/i);
58
+ if (cnpjMatch?.[1])
59
+ return cnpjMatch[1];
60
+ const ouMatch = subjectRaw.match(/OU\s*=\s*[^,]*?(\d{14})/);
61
+ if (ouMatch?.[1])
62
+ return ouMatch[1];
63
+ return null;
64
+ }
65
+ function extractCpf(subjectRaw) {
66
+ const cpfMatch = subjectRaw.match(/CPF[:\s=]+(\d{11})/i);
67
+ if (cpfMatch?.[1])
68
+ return cpfMatch[1];
69
+ return null;
70
+ }
71
+ function pemToBase64(pem) {
72
+ return pem.replace(/-----BEGIN [^-]+-----/, "").replace(/-----END [^-]+-----/, "").replace(/\s+/g, "");
73
+ }
74
+ var XMLDSIG_NS = "http://www.w3.org/2000/09/xmldsig#";
75
+ var EXC_C14N_NS = "http://www.w3.org/2001/10/xml-exc-c14n#";
76
+ var DIGEST_ALGORITHMS = {
77
+ sha1: {
78
+ uri: "http://www.w3.org/2000/09/xmldsig#sha1",
79
+ nodeAlgo: "sha1"
80
+ },
81
+ sha256: {
82
+ uri: "http://www.w3.org/2001/04/xmlenc#sha256",
83
+ nodeAlgo: "sha256"
84
+ }
85
+ };
86
+ var SIGNATURE_ALGORITHMS = {
87
+ sha1: {
88
+ uri: "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
89
+ nodeAlgo: "sha1"
90
+ },
91
+ sha256: {
92
+ uri: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
93
+ nodeAlgo: "sha256"
94
+ }
95
+ };
96
+ var TRANSFORM_ALGORITHMS = {
97
+ envelopedSignature: "http://www.w3.org/2000/09/xmldsig#enveloped-signature",
98
+ excC14n: "http://www.w3.org/2001/10/xml-exc-c14n#"
99
+ };
100
+
101
+ export { BRAZILIAN_OIDS, parseDistinguishedName, extractCnpj, extractCpf, pemToBase64, XMLDSIG_NS, EXC_C14N_NS, DIGEST_ALGORITHMS, SIGNATURE_ALGORITHMS, TRANSFORM_ALGORITHMS };
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Digital Certificate Types
3
+ *
4
+ * Types for Brazilian A1 digital certificate handling,
5
+ * XML-DSig signing, and mTLS configuration.
6
+ */
7
+ interface CertificateSubject {
8
+ commonName: string | null;
9
+ organization: string | null;
10
+ organizationalUnit: string | null;
11
+ country: string | null;
12
+ state: string | null;
13
+ locality: string | null;
14
+ /** Full raw subject string */
15
+ raw: string;
16
+ }
17
+ interface CertificateIssuer {
18
+ commonName: string | null;
19
+ organization: string | null;
20
+ country: string | null;
21
+ raw: string;
22
+ }
23
+ interface CertificateValidity {
24
+ notBefore: Date;
25
+ notAfter: Date;
26
+ }
27
+ interface BrazilianFields {
28
+ /** CNPJ extracted from certificate (14 digits) */
29
+ cnpj: string | null;
30
+ /** CPF extracted from certificate (11 digits) */
31
+ cpf: string | null;
32
+ }
33
+ interface CertificateInfo {
34
+ /** Certificate serial number */
35
+ serialNumber: string;
36
+ /** Subject (who the certificate was issued to) */
37
+ subject: CertificateSubject;
38
+ /** Issuer (who issued the certificate) */
39
+ issuer: CertificateIssuer;
40
+ /** Validity period */
41
+ validity: CertificateValidity;
42
+ /** SHA-256 fingerprint of the certificate */
43
+ fingerprint: string;
44
+ /** Whether the certificate is currently valid */
45
+ isValid: boolean;
46
+ /** Brazilian-specific fields (CNPJ, CPF) */
47
+ brazilian: BrazilianFields;
48
+ /** PEM-encoded certificate */
49
+ certPem: string;
50
+ /** PEM-encoded private key */
51
+ keyPem: string;
52
+ /** Raw PFX buffer */
53
+ pfxBuffer: Buffer;
54
+ /** PFX password */
55
+ pfxPassword: string;
56
+ }
57
+ type SignatureAlgorithm = "sha1" | "sha256";
58
+ interface SignOptions {
59
+ /** The parsed certificate to use for signing */
60
+ certificate: CertificateInfo;
61
+ /** The URI of the element to sign (e.g., "#NFe123") */
62
+ referenceUri: string;
63
+ /** Hash algorithm for digest and signature (default: "sha256") */
64
+ algorithm?: SignatureAlgorithm;
65
+ /** Tag name of the element where Signature will be inserted.
66
+ * If not specified, the signature is appended to the root element. */
67
+ signatureParent?: string;
68
+ /** Whether to include the XML declaration in output (default: true) */
69
+ includeDeclaration?: boolean;
70
+ }
71
+ /**
72
+ * Sign an XML document with an enveloped XML-DSig signature
73
+ *
74
+ * @param xml - The XML string to sign
75
+ * @param options - Signing options (certificate, reference URI, algorithm)
76
+ * @returns The signed XML string
77
+ */
78
+ declare function signXml(xml: string, options: SignOptions): string;
79
+ export { signXml };
@@ -0,0 +1,119 @@
1
+ import {
2
+ DIGEST_ALGORITHMS,
3
+ EXC_C14N_NS,
4
+ SIGNATURE_ALGORITHMS,
5
+ TRANSFORM_ALGORITHMS,
6
+ XMLDSIG_NS,
7
+ pemToBase64
8
+ } from "./shared/chunk-rkg27a9a.js";
9
+ import {
10
+ signOptionsSchema
11
+ } from "./shared/chunk-ft0pdf3s.js";
12
+
13
+ // src/xml-signer.ts
14
+ import crypto from "node:crypto";
15
+ import {
16
+ getAttributeValue,
17
+ parseXml,
18
+ serializeXml,
19
+ XML_NODE_TYPES
20
+ } from "@f-o-t/xml";
21
+ import { canonicalize } from "@f-o-t/xml/canonicalize";
22
+ function signXml(xml, options) {
23
+ const opts = signOptionsSchema.parse(options);
24
+ const algorithm = opts.algorithm;
25
+ const doc = parseXml(xml);
26
+ if (!doc.root) {
27
+ throw new Error("XML document has no root element");
28
+ }
29
+ const targetElement = findSignTarget(doc.root, opts.referenceUri);
30
+ if (!targetElement) {
31
+ throw new Error(`Could not find element with URI reference: ${opts.referenceUri}`);
32
+ }
33
+ const canonicalXml = canonicalize(targetElement, {
34
+ exclusive: true,
35
+ withComments: false
36
+ });
37
+ const digestValue = computeDigest(canonicalXml, algorithm);
38
+ const signedInfoXml = buildSignedInfoXml(opts.referenceUri, algorithm, digestValue);
39
+ const signedInfoDoc = parseXml(signedInfoXml);
40
+ const canonicalSignedInfo = canonicalize(signedInfoDoc.root, {
41
+ exclusive: true,
42
+ withComments: false
43
+ });
44
+ const signatureValue = computeSignature(canonicalSignedInfo, options.certificate.keyPem, algorithm);
45
+ const signatureXml = buildSignatureXml(signedInfoXml, signatureValue, options.certificate.certPem);
46
+ const signatureDoc = parseXml(signatureXml);
47
+ const signatureElement = signatureDoc.root;
48
+ const signatureParent = opts.signatureParent ? findElementByName(doc.root, opts.signatureParent) : targetElement;
49
+ if (!signatureParent) {
50
+ throw new Error(`Could not find signature parent element: ${opts.signatureParent}`);
51
+ }
52
+ signatureElement.parent = signatureParent;
53
+ signatureParent.children.push(signatureElement);
54
+ return serializeXml(doc, {
55
+ declaration: opts.includeDeclaration,
56
+ indent: "",
57
+ selfClose: true
58
+ });
59
+ }
60
+ function findSignTarget(root, referenceUri) {
61
+ if (referenceUri === "") {
62
+ return root;
63
+ }
64
+ const id = referenceUri.startsWith("#") ? referenceUri.slice(1) : referenceUri;
65
+ return findElementById(root, id);
66
+ }
67
+ function findElementById(element, id) {
68
+ const idAttrs = ["Id", "id", "ID"];
69
+ for (const attrName of idAttrs) {
70
+ const val = getAttributeValue(element, attrName);
71
+ if (val === id)
72
+ return element;
73
+ }
74
+ for (const child of element.children) {
75
+ if (child.type === XML_NODE_TYPES.ELEMENT) {
76
+ const found = findElementById(child, id);
77
+ if (found)
78
+ return found;
79
+ }
80
+ }
81
+ return null;
82
+ }
83
+ function findElementByName(element, name) {
84
+ if (element.name === name || element.localName === name) {
85
+ return element;
86
+ }
87
+ for (const child of element.children) {
88
+ if (child.type === XML_NODE_TYPES.ELEMENT) {
89
+ const found = findElementByName(child, name);
90
+ if (found)
91
+ return found;
92
+ }
93
+ }
94
+ return null;
95
+ }
96
+ function computeDigest(data, algorithm) {
97
+ const algo = DIGEST_ALGORITHMS[algorithm];
98
+ const hash = crypto.createHash(algo.nodeAlgo);
99
+ hash.update(data, "utf8");
100
+ return hash.digest("base64");
101
+ }
102
+ function computeSignature(data, privateKeyPem, algorithm) {
103
+ const algo = SIGNATURE_ALGORITHMS[algorithm];
104
+ const sign = crypto.createSign(`RSA-${algo.nodeAlgo.toUpperCase()}`);
105
+ sign.update(data, "utf8");
106
+ return sign.sign(privateKeyPem, "base64");
107
+ }
108
+ function buildSignedInfoXml(referenceUri, algorithm, digestValue) {
109
+ const sigAlgo = SIGNATURE_ALGORITHMS[algorithm];
110
+ const digAlgo = DIGEST_ALGORITHMS[algorithm];
111
+ return `<SignedInfo xmlns="${XMLDSIG_NS}"><CanonicalizationMethod Algorithm="${EXC_C14N_NS}"/><SignatureMethod Algorithm="${sigAlgo.uri}"/><Reference URI="${referenceUri}"><Transforms><Transform Algorithm="${TRANSFORM_ALGORITHMS.envelopedSignature}"/><Transform Algorithm="${TRANSFORM_ALGORITHMS.excC14n}"/></Transforms><DigestMethod Algorithm="${digAlgo.uri}"/><DigestValue>${digestValue}</DigestValue></Reference></SignedInfo>`;
112
+ }
113
+ function buildSignatureXml(signedInfoXml, signatureValue, certPem) {
114
+ const certBase64 = pemToBase64(certPem);
115
+ return `<Signature xmlns="${XMLDSIG_NS}">${signedInfoXml}<SignatureValue>${signatureValue}</SignatureValue><KeyInfo><X509Data><X509Certificate>${certBase64}</X509Certificate></X509Data></KeyInfo></Signature>`;
116
+ }
117
+ export {
118
+ signXml
119
+ };
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@f-o-t/digital-certificate",
3
+ "version": "1.0.0",
4
+ "description": "Brazilian A1 digital certificate handling with .pfx/.p12 parsing, XML-DSig signing, certificate management, and mTLS support",
5
+ "type": "module",
6
+ "private": false,
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "bun": "./src/index.ts",
12
+ "import": {
13
+ "default": "./dist/index.js",
14
+ "types": "./dist/index.d.ts"
15
+ },
16
+ "types": "./src/index.ts"
17
+ },
18
+ "./xml-signer": {
19
+ "bun": "./src/xml-signer.ts",
20
+ "import": {
21
+ "default": "./dist/xml-signer.js",
22
+ "types": "./dist/xml-signer.d.ts"
23
+ },
24
+ "types": "./src/xml-signer.ts"
25
+ },
26
+ "./mtls": {
27
+ "bun": "./src/mtls.ts",
28
+ "import": {
29
+ "default": "./dist/mtls.js",
30
+ "types": "./dist/mtls.d.ts"
31
+ },
32
+ "types": "./src/mtls.ts"
33
+ },
34
+ "./package.json": "./package.json"
35
+ },
36
+ "files": [
37
+ "dist"
38
+ ],
39
+ "scripts": {
40
+ "build": "bunup",
41
+ "check": "biome check --write .",
42
+ "dev": "bunup --watch",
43
+ "release": "bumpp --commit --push --tag",
44
+ "test": "bun test",
45
+ "test:coverage": "bun test --coverage",
46
+ "test:watch": "bun test --watch",
47
+ "typecheck": "tsc"
48
+ },
49
+ "dependencies": {
50
+ "@f-o-t/xml": "1.0.0",
51
+ "zod": "4.3.6"
52
+ },
53
+ "devDependencies": {
54
+ "@biomejs/biome": "2.3.12",
55
+ "@types/bun": "1.3.6",
56
+ "bumpp": "10.4.0",
57
+ "bunup": "0.16.20",
58
+ "typescript": "5.9.3"
59
+ },
60
+ "peerDependencies": {
61
+ "typescript": ">=4.5.0"
62
+ },
63
+ "peerDependenciesMeta": {
64
+ "typescript": {
65
+ "optional": true
66
+ }
67
+ },
68
+ "license": "MIT",
69
+ "repository": {
70
+ "type": "git",
71
+ "url": "https://github.com/F-O-T/contentta-nx.git"
72
+ },
73
+ "homepage": "https://github.com/F-O-T/contentta-nx/blob/master/libraries/digital-certificate",
74
+ "bugs": {
75
+ "url": "https://github.com/F-O-T/contentta-nx/issues"
76
+ },
77
+ "publishConfig": {
78
+ "access": "public"
79
+ }
80
+ }