<p align="center">
  <a href="https://www.npmjs.com/package/@cfdi/csd">
    <img src="https://raw.githubusercontent.com/MisaelMa/cards/main/packages/cfdi-csd.png" alt="@cfdi/csd" width="400" />
  </a>
</p>

<h3 align="center">Certificados de Sello Digital (CSD) - lectura de archivos .cer y .key para CFDI</h3>

<p align="center">
  <a href="https://www.npmjs.com/package/@cfdi/csd">
    <img src="https://img.shields.io/npm/v/@cfdi/csd?style=flat-square&color=cb3837&label=npm" alt="npm version" />
  </a>
  <a href="https://www.npmjs.com/package/@cfdi/csd">
    <img src="https://img.shields.io/npm/dm/@cfdi/csd?style=flat-square&color=cb3837&label=downloads" alt="npm downloads" />
  </a>
  <a href="https://github.com/MisaelMa/node-cfdi/blob/main/LICENSE">
    <img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="license" />
  </a>
  <img src="https://img.shields.io/badge/node-%3E%3D22-brightgreen?style=flat-square&logo=node.js&logoColor=white" alt="node" />
  <img src="https://img.shields.io/badge/TypeScript-strict-3178c6?style=flat-square&logo=typescript&logoColor=white" alt="typescript" />
</p>

<p align="center">
  <a href="https://cfdi.recreando.dev">
    <img src="https://raw.githubusercontent.com/MisaelMa/cards/main/cfdi-documentacion.png" alt="Documentacion" width="300" />
  </a>
</p>

---

## Ecosistema CFDI

<p align="center">
  <img src="https://raw.githubusercontent.com/MisaelMa/cards/main/cfdi-ecosystem.png" alt="CFDI Ecosystem" width="600" />
</p>

<table>
  <tr>
    <td align="center" width="33%">
      <a href="https://www.npmjs.com/package/@cfdi/xml">
        <img src="https://raw.githubusercontent.com/MisaelMa/cards/main/packages/cfdi-xml.png" alt="@cfdi/xml" width="100%" />
      </a>
    </td>
    <td align="center" width="33%">
      <a href="https://www.npmjs.com/package/@cfdi/complementos">
        <img src="https://raw.githubusercontent.com/MisaelMa/cards/main/packages/cfdi-complementos.png" alt="@cfdi/complementos" width="100%" />
      </a>
    </td>
    <td align="center" width="33%">
      <a href="https://www.npmjs.com/package/@cfdi/xsd">
        <img src="https://raw.githubusercontent.com/MisaelMa/cards/main/packages/cfdi-xsd.png" alt="@cfdi/xsd" width="100%" />
      </a>
    </td>
  </tr>
  <tr>
    <td align="center" width="33%">
      <a href="https://www.npmjs.com/package/@cfdi/csd">
        <img src="https://raw.githubusercontent.com/MisaelMa/cards/main/packages/cfdi-csd.png" alt="@cfdi/csd" width="100%" />
      </a>
    </td>
    <td align="center" width="33%">
      <a href="https://www.npmjs.com/package/@cfdi/csf">
        <img src="https://raw.githubusercontent.com/MisaelMa/cards/main/packages/cfdi-csf.png" alt="@cfdi/csf" width="100%" />
      </a>
    </td>
    <td align="center" width="33%">
      <a href="https://www.npmjs.com/package/@cfdi/catalogos">
        <img src="https://raw.githubusercontent.com/MisaelMa/cards/main/packages/cfdi-catalogos.png" alt="@cfdi/catalogos" width="100%" />
      </a>
    </td>
  </tr>
  <tr>
    <td align="center" width="33%">
      <a href="https://www.npmjs.com/package/@cfdi/transform">
        <img src="https://raw.githubusercontent.com/MisaelMa/cards/main/packages/cfdi-transform.png" alt="@cfdi/transform" width="100%" />
      </a>
    </td>
    <td align="center" width="33%">
      <a href="https://www.npmjs.com/package/@cfdi/elements">
        <img src="https://raw.githubusercontent.com/MisaelMa/cards/main/packages/cfdi-elements.png" alt="@cfdi/elements" width="100%" />
      </a>
    </td>
    <td align="center" width="33%">
      <a href="https://www.npmjs.com/package/@cfdi/types">
        <img src="https://raw.githubusercontent.com/MisaelMa/cards/main/packages/cfdi-types.png" alt="@cfdi/types" width="100%" />
      </a>
    </td>
  </tr>
  <tr>
    <td align="center" width="33%">
      <a href="https://www.npmjs.com/package/@cfdi/expresiones">
        <img src="https://raw.githubusercontent.com/MisaelMa/cards/main/packages/cfdi-expresiones.png" alt="@cfdi/expresiones" width="100%" />
      </a>
    </td>
    <td align="center" width="33%">
      <a href="https://www.npmjs.com/package/@cfdi/xml2json">
        <img src="https://raw.githubusercontent.com/MisaelMa/cards/main/packages/cfdi-2json.png" alt="@cfdi/xml2json" width="100%" />
      </a>
    </td>
    <td align="center" width="33%">
      <a href="https://www.npmjs.com/package/@cfdi/rfc">
        <img src="https://raw.githubusercontent.com/MisaelMa/cards/main/packages/cfdi-rfc.png" alt="@cfdi/rfc" width="100%" />
      </a>
    </td>
  </tr>
  <tr>
    <td align="center" width="33%">
      <a href="https://www.npmjs.com/package/@cfdi/utils">
        <img src="https://raw.githubusercontent.com/MisaelMa/cards/main/packages/cfdi-utils.png" alt="@cfdi/utils" width="100%" />
      </a>
    </td>
    <td align="center" width="33%">
      <a href="https://www.npmjs.com/package/@clir/openssl">
        <img src="https://raw.githubusercontent.com/MisaelMa/cards/main/packages/clir-openssl.png" alt="@clir/openssl" width="100%" />
      </a>
    </td>
    <td align="center" width="33%">
      <a href="https://www.npmjs.com/package/@saxon-he/cli">
        <img src="https://raw.githubusercontent.com/MisaelMa/cards/main/packages/saxon-he-cli.png" alt="@saxon-he/cli" width="100%" />
      </a>
    </td>
  </tr>
</table>

---

## Instalacion

```bash
npm install @cfdi/csd
```

---

## API

`@cfdi/csd` lee y opera certificados X.509 (`.cer`) y llaves privadas RSA (`.key`)
del SAT mexicano. Cubre CSD (Certificado de Sello Digital) y FIEL (e.firma).

### Clases

| Clase | Responsabilidad |
|---|---|
| `Certificate` | Wrapper del `.cer` X.509 (CSD o FIEL) |
| `PrivateKey` | Wrapper del `.key` RSA (PKCS#8 cifrado, PKCS#8 plano o PKCS#1) |
| `Credential` | Une `Certificate` + `PrivateKey` para firmar/verificar |
| `Ocsp` | Validación de revocación contra el responder OCSP del SAT |

### `Certificate`

```ts
import { Certificate } from '@cfdi/csd';

const cert = await Certificate.fromFile('mi.cer');         // DER o PEM auto
// también: Certificate.fromDer(buf), Certificate.fromPem(str), fromFileSync
```

#### Identidad / metadata

| Método | Retorna | Descripción |
|---|---|---|
| `serialNumber()` | `string` (hex) | Serial del cert |
| `noCertificado()` | `string` (20 dig) | Número de certificado SAT |
| `rfc()` | `string` | RFC del titular (subject OID 2.5.4.45 / 2.5.4.5 / UID) |
| `legalName()` | `string` | Razón social / nombre del CN |
| `subject()` | `Record<string, string>` | Subject como mapa atributo→valor |
| `issuer()` | `Record<string, string>` | Issuer como mapa atributo→valor |
| `validFrom()` | `Date` | Inicio de vigencia |
| `validTo()` | `Date` | Fin de vigencia |
| `isExpired()` | `boolean` | `now > validTo` |
| `isValid()` | `boolean` | `validFrom ≤ now ≤ validTo` (vigente) |
| `acVersion()` | `number \| null` | Versión de AC del SAT (4 o 5) — desde dígito 12 del `noCertificado` |
| `subjectType()` | `'MORAL' \| 'FISICA' \| 'UNKNOWN'` | Persona moral o física, derivado del OID 2.5.4.45 |

#### Tipo de certificado

| Método | Retorna |
|---|---|
| `certificateType()` | `'CSD' \| 'FIEL' \| 'UNKNOWN'` — por extensiones X.509 (extKeyUsage / keyUsage), fallback a OU |
| `isCsd()` | `boolean` — equivalente a `certificateType() === 'CSD'` |
| `isFiel()` | `boolean` — equivalente a `certificateType() === 'FIEL'` |

> "FIEL" y "e.firma" son la misma cosa: nomenclatura legal vs marca comercial del SAT.

#### Conversiones / hashing

| Método | Retorna |
|---|---|
| `toPem()` | `string` PEM con cabeceras |
| `toDer()` | `Buffer` DER |
| `toBase64()` | `string` base64 sin cabeceras ni saltos |
| `publicKey()` | `string` PEM `-----BEGIN PUBLIC KEY-----` |
| `fingerprint()` | `string` SHA-1 con `:` (`AA:BB:...`) |
| `fingerprintSha256()` | `string` SHA-256 hex |

#### Verificación

| Método | Retorna |
|---|---|
| `verify(data, signatureBase64, alg='SHA256')` | `boolean` — verifica firma contra `data` con la pública del cert |
| `verifyIssuedBy(issuer)` | `boolean` — `true` si fue firmado por `issuer` (cadena AC) |
| `verifyIntegrity(issuer)` | alias de `verifyIssuedBy` (compat con `e.firma`) |
| `rsaEncrypt(message)` | `string` base64 — encripta con la pública del cert |

### `PrivateKey`

```ts
import { PrivateKey } from '@cfdi/csd';

// .key del SAT (PKCS#8 cifrado)
const key = await PrivateKey.fromFile('mi.key', '12345678a');

// strict: solo acepta PKCS#8 cifrado
const sat = await PrivateKey.fromFile('mi.key', '12345678a', { strict: true });
```

| Método | Retorna |
|---|---|
| `fromDer(buf, password, opts?)` | acepta PKCS#8 cifrado, PKCS#8 plano o PKCS#1 |
| `fromPem(pem)` | PEM sin cifrado |
| `fromFile(path, password, opts?)` | DER o PEM auto |
| `fromFileSync(path, password, opts?)` | versión síncrona |
| `toPem()` | PEM PKCS#8 sin cifrar |
| `sign(data, alg='SHA256')` | firma base64 |
| `rsaDecrypt(encryptedBase64)` | desencripta lo que `Certificate.rsaEncrypt` produjo |
| `belongsToCertificate(cert)` | `true` si la pública de la llave == pública del cert |

### `Credential`

```ts
import { Credential } from '@cfdi/csd';

const cred = await Credential.create('mi.cer', 'mi.key', '12345678a');
const sello = cred.sign('||cadena|original||');
cred.verify('||cadena|original||', sello); // true
```

| Método | Retorna |
|---|---|
| `create(cerPath, keyPath, password)` | desde rutas |
| `fromPem(cerPem, keyPem)` | desde PEM strings |
| `sign(data, alg='SHA256')` | firma base64 |
| `verify(data, sig, alg='SHA256')` | bool |
| `rfc()`, `legalName()`, `noCertificado()`, `serialNumber()` | proxy al cert |
| `isCsd()`, `isFiel()`, `isValid()`, `belongsTo(rfc)` | predicados |
| `keyMatchesCertificate()` | bool |

### `Ocsp` — validación de revocación SAT

El SAT expone `https://cfdi.sat.gob.mx/edofiel` para consultar el estado
(`GOOD` / `REVOKED`) de un certificado en línea. Se necesitan **tres** certificados:

```ts
import { Ocsp, Certificate } from '@cfdi/csd';

const subject = await Certificate.fromFile('contribuyente.cer');
const issuer = await Certificate.fromFile('AC5_SAT.cer');
const ocspCert = await Certificate.fromFile('ocsp.ac5_sat.cer');

const ocsp = new Ocsp('https://cfdi.sat.gob.mx/edofiel', issuer, subject, ocspCert);
const res = await ocsp.verify();

if (res.status === 'REVOKED') {
  console.log('Certificado revocado el', res.revocationTime);
}
```

| Método | Retorna |
|---|---|
| `verify()` | `{ status, revocationTime?, ocspRequestBase64, ocspResponseBase64 }` |
| `parseResponseStatus(asn1)` | `OcspResponseStatus` |
| `parseCertificateStatus(asn1Basic)` | `{ status, revocationTime? }` |
| `verifyResponseSignature(asn1Basic)` | `boolean` |

---

## Compatibilidad con el paquete `e.firma`

Si vienes del paquete [`e.firma`](https://www.npmjs.com/package/e.firma):

| `e.firma` | `@cfdi/csd` |
|---|---|
| `new x509Certificate(bin)` | `await Certificate.fromFile(path)` o `Certificate.fromDer(buf)` |
| `cert.certificateType` (`'CSD' \| 'EFIRMA'`) | `cert.certificateType()` (`'CSD' \| 'FIEL'`) |
| `cert.acVersion` | `cert.acVersion()` |
| `cert.subjectType` | `cert.subjectType()` |
| `cert.valid` | `cert.isValid()` |
| `cert.serialNumber` | `cert.serialNumber()` |
| `cert.getPEM()` | `cert.toPem()` |
| `cert.getBinary()` | `cert.toDer()` |
| `cert.verifyIntegrity(issuerBin)` | `cert.verifyIssuedBy(issuer)` (alias `verifyIntegrity` disponible) |
| `cert.rsaEncrypt(msg)` | `cert.rsaEncrypt(msg)` |
| `cert.rsaVerifySignature(msg, sig)` | `cert.verify(msg, sig)` |
| `new PrivateKey(bin)` (cifrada solamente) | `await PrivateKey.fromFile(path, pwd, { strict: true })` |
| `key.rsaDecrypt(text, pwd)` | `key.rsaDecrypt(text)` (la pwd va en el `from*`) |
| `key.rsaSign(msg, pwd)` | `key.sign(msg)` |
| `new Ocsp(url, issuer, subject, ocsp)` | `new Ocsp(url, issuer, subject, ocsp)` |

---

## Soporte

<p>
  <a href="https://github.com/MisaelMa/node-cfdi/issues">
    <img src="https://img.shields.io/badge/GitHub-Issues-181717?style=for-the-badge&logo=github" alt="issues" />
  </a>
  <a href="https://github.com/MisaelMa/node-cfdi/discussions">
    <img src="https://img.shields.io/badge/GitHub-Discussions-181717?style=for-the-badge&logo=github" alt="discussions" />
  </a>
  <a href="https://www.npmjs.com/package/@cfdi/csd">
    <img src="https://img.shields.io/badge/npm-@cfdi/csd-cb3837?style=for-the-badge&logo=npm" alt="npm" />
  </a>
</p>

---

## Autor

<p align="center">
  <a href="https://github.com/MisaelMa">
    <img src="https://raw.githubusercontent.com/MisaelMa/cards/main/author.png" alt="Amir Misael Marin Coh" width="100%" />
  </a>
</p>

## Licencia

[MIT](../../LICENSE)
