Skip to content

Commit

Permalink
Merge pull request #5 from eclipxe13/master
Browse files Browse the repository at this point in the history
Version 1.1.0
  • Loading branch information
eclipxe13 committed Nov 19, 2019
2 parents fc920c5 + d7561fa commit fcb7add
Show file tree
Hide file tree
Showing 22 changed files with 256 additions and 82 deletions.
5 changes: 1 addition & 4 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,14 @@

# Do not put this files on a distribution package (by .gitignore)
/vendor export-ignore
/build export-ignore
/composer.lock export-ignore
.phpunit.result.cache export-ignore

# Do not put this files on a distribution package
/docs/ export-ignore
/build/ export-ignore
/tests/ export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/.php_cs.dist export-ignore
/.phplint.yml export-ignore
/.scrutinizer.yml export-ignore
/.travis.yml export-ignore
/phpcs.xml.dist export-ignore
Expand Down
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# do not include this files on git
/vendor
/build
/composer.lock
.phpunit.result.cache
11 changes: 0 additions & 11 deletions .phplint.yml

This file was deleted.

9 changes: 4 additions & 5 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ matrix:
allow_failures:
- php: "7.4snapshot"

# This triggers builds to run on the new TravisCI infrastructure.
# See: http://docs.travis-ci.com/user/workers/container-based-infrastructure/
sudo: false
env:
global:
- PHP_CS_FIXER_FUTURE_MODE=1
- PHP_CS_FIXER_IGNORE_ENV=1

cache:
- directories:
Expand All @@ -18,10 +19,8 @@ cache:
before_script:
- phpenv config-rm xdebug.ini || true
- travis_retry composer install --no-interaction --prefer-dist
- mkdir -p build

script:
- vendor/bin/phplint
- vendor/bin/php-cs-fixer fix --verbose
- vendor/bin/phpcbf --colors -sp src/ tests/
- vendor/bin/phpunit --testdox --verbose
Expand Down
1 change: 0 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ If any of these do not pass, it will result in a complete build failure.
Before you can run these, be sure to `composer install` or `composer update`.

```shell
vendor/bin/parallel-lint src/ tests/
vendor/bin/phpcs -sp src/ tests/
vendor/bin/php-cs-fixer fix -v --dry-run
vendor/bin/phpunit --coverage-text
Expand Down
70 changes: 63 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ Esta librería ha sido creada para poder trabajar con los archivos CSD y FIEL de
se simplifica el proceso de firmar, verificar firma y obtener datos particulares del archivo de certificado
así como de la llave pública.

* El CSD (Certificado de Sello Digital) es utilizado para firmar Comprobantes Fiscales Digitales.
- El CSD (Certificado de Sello Digital) es utilizado para firmar Comprobantes Fiscales Digitales.

* La FIEL (o eFirma) es utilizada para firmar electrónicamente documentos (generalmente usando XML-SEC) y
está reconocida por el gobierno mexicano como una manera de firma legal de una persona física o moral.
- La FIEL (o eFirma) es utilizada para firmar electrónicamente documentos (generalmente usando XML-SEC) y
está reconocida por el gobierno mexicano como una manera de firma legal de una persona física o moral.


## Instalación
Expand All @@ -38,8 +38,8 @@ composer require phpcfdi/credentials
```php
<?php declare(strict_types=1);

$cerFile = 'fiel/certificado.cer'; // PEM o DER
$pemKeyFile = 'fiel/privatekey.pem'; // en formato PEM
$cerFile = 'fiel/certificado.cer';
$pemKeyFile = 'fiel/privatekey.key';
$passPhrase = '12345678a'; // contraseña para abrir la llave privada

$fiel = PhpCfdi\Credentials\Credential::openFiles($cerFile, $pemKeyFile, $passPhrase);
Expand All @@ -63,10 +63,66 @@ echo $certificado->serialNumber()->bytes(), PHP_EOL; // número de serie del cer
```


## Acerca de los archivos de certificado y llave privada

Los archivos de certificado vienen en formato `X.509 DER` y los de llave privada en formato `PKCS#8 DER`.
Ambos formatos no se pueden interpretar directamente en PHP (con `ext-openssl`), sin embargo sí lo pueden hacer
en el formatos compatible [`PEM`](https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail).

Esta librería tiene la capacidad de hacer esta conversión internamente (sin `openssl`), pues solo consiste en codificar
a `base64`, en renglones de 64 caracteres y con cabeceras específicas para certificado y llave privada.

De esta forma, para usar el certificado `AAA010101AAA.cer` o la llave privada `AAA010101AAA.key` provistos por
el SAT, no es necesario convertirlos con `openssl` y la librería los detectará correctamente.


### Crear un objeto de certificado `Certificate`

El objeto `Certificate` no se creará si contiene datos no válidos.

El SAT entrega el certificado en formato `X.509 DER`, por lo que internamente se puede convertir a `X.509 PEM`.
También es frecuente usar el formato `X.509 DER base64`, por ejemplo, en el atributo `Comprobante@Certificado`
o en las firmas XML, por este motivo, los formatos soportados para crear un objeto `Certificate` son
`X.509 DER`, `X.509 DER base64` y `X.509 PEM`.

- Para abrir usando un archivo local: `$certificate = Certificate::openFile($filename);`
- Para abrir usando una cadena de caracteres: `$certificate = new Certificate($content);`
- Si `$content` es un certificado en formato `X.509 PEM` con cabeceras ese se utiliza.
- Si `$content` está totalmente en `base64`, se interpreta como `X.509 DER base64` y se formatea a `X.509 PEM`
- En otro caso, se interpreta como formato `X.509 DER`, por lo que se formatea a `X.509 PEM`.


### Crear un objeto de llave privada `PrivateKey`

El objeto `PrivateKey` no se creará si contiene datos no válidos.

En SAT entrega la llave en formato `PKCS#8 DER`, por lo que internamente se puede convertir a `PKCS#8 PEM`
(con la misma contraseña) y usarla desde PHP.

Una vez abierta la llave también se puede cambiar o eliminar la contraseña, creando así un nuevo objeto `PrivateKey`.

- Para abrir usando un archivo local: `$key = PrivateKey::openFile($filename, $passPhrase);`
- Para abrir usando una cadena de caracteres: `$key = new PrivateKey($content, $passPhrase);`
- Si `$content` es una llave privada en formato `PEM` (`PKCS#8` o `PKCS#5`) se utiliza.
- En otro caso, se interpreta como formato `PKCS#8 DER`, por lo que se formatea a `PKCS#8 PEM`.

Notas de tratamiento de archivos `DER`:

- Al convertir `PKCS#8 DER` a `PKCS#8 PEM` se determina si es una llave encriptada si se estableció
una contraseña, si no se estableció se tratará como una llave plana (no encriptada).
- No se sabe reconocer de forma automática si se trata de un archivo `PKCS#5 DER` por lo que este
tipo de llave se deben convertir *manualmente* antes de intentar abrirlos, su cabecera es `RSA PRIVATE KEY`.
- A diferencia de los certificados que pueden interpretar un formato `DER base64`, la lectura de llave
privada no hace esta distinción, si desea trabajar con un formato sin caracteres especiales use `PEM`.

Para entender más de los formatos de llaves privadas se puede consultar la siguiente liga:
<https://github.com/kjur/jsrsasign/wiki/Tutorial-for-PKCS5-and-PKCS8-PEM-private-key-formats-differences>


## Compatilibilidad

Esta librería se mantendrá compatible con al menos la versión con
[soporte activo de PHP](http://php.net/supported-versions.php) más reciente.
[soporte activo de PHP](https://www.php.net/supported-versions.php) más reciente.

También utilizamos [Versionado Semántico 2.0.0](https://semver.org/lang/es/) por lo que puedes usar esta librería
sin temor a romper tu aplicación.
Expand Down Expand Up @@ -95,7 +151,7 @@ and licensed for use under the MIT License (MIT). Please see [LICENSE][] for mor
[coverage]: https://scrutinizer-ci.com/g/phpcfdi/credentials/code-structure/master/code-coverage/src/
[downloads]: https://packagist.org/packages/phpcfdi/credentials

[badge-source]: http://img.shields.io/badge/source-phpcfdi/credentials-blue?style=flat-square
[badge-source]: https://img.shields.io/badge/source-phpcfdi/credentials-blue?style=flat-square
[badge-release]: https://img.shields.io/github/release/phpcfdi/credentials?style=flat-square
[badge-license]: https://img.shields.io/github/license/phpcfdi/credentials?style=flat-square
[badge-build]: https://img.shields.io/travis/phpcfdi/credentials/master?style=flat-square
Expand Down
2 changes: 2 additions & 0 deletions build/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*
!.gitignore
7 changes: 3 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@
},
"require-dev": {
"ext-json": "*",
"phpunit/phpunit": "^8.0",
"overtrue/phplint": "^1.0",
"phpunit/phpunit": "^8.4",
"squizlabs/php_codesniffer": "^3.0",
"friendsofphp/php-cs-fixer": "^2.4",
"phpstan/phpstan-shim": "^0.11"
Expand All @@ -50,7 +49,7 @@
"vendor/bin/phpcbf --colors -sp src/ tests/"
],
"dev:test": [
"vendor/bin/phplint",
"@dev:check-style",
"vendor/bin/phpunit --testdox --verbose --stop-on-failure",
"vendor/bin/phpstan analyse --no-progress --verbose --level max src/ tests/"
],
Expand All @@ -62,7 +61,7 @@
"dev:build": "DEV: run dev:fix-style dev:tests and dev:docs, run before pull request",
"dev:check-style": "DEV: search for code style errors using php-cs-fixer and phpcs",
"dev:fix-style": "DEV: fix code style errors using php-cs-fixer and phpcbf",
"dev:test": "DEV: run phplint, phpunit and phpstan",
"dev:test": "DEV: dev:check-style, phpunit and phpstan",
"dev:coverage": "DEV: run phpunit with xdebug and storage coverage in build/coverage/html/"
}
}
10 changes: 10 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@

Nos apegamos a [SEMVER](SEMVER.md), revisa la información para entender mejor el control de versiones.

## Version 1.1.0 2019-11-19

- Se puede crear una llave privada en formato `PKCS#8 DER` encriptada o desprotegida.
Con este cambio se pueden leer las llaves tal y como las envía el SAT. Gracias @eislasq.
- Si la llave privada no estaba en formato `PEM` se hace una conversión de `PKCS#8 DER` a `PKCS#8 PEM`.
- Se agrega el método `PrivateKey::changePassPhrase` que devuelve una llave privada con la nueva contraseña.
- Se documenta la apertida de certificados y llaves privadas en diferentes formatos.
- Se limpia el entorno de desarrollo y se publica en el paquete distribuible la carpeta de documentación.
- Se hacen refactorizaciones menores para un mejor uso de memoria y rendimiento.

## Version 1.0.1 2019-09-18

- Agregar métodos a `PrivateKey` para poder exponer la llave privada en formato PEM y la frase de paso.
Expand Down
2 changes: 2 additions & 0 deletions docs/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@

- [ ] Verificar si un certificado fue realmente emitido por el SAT
Ver [VerificacionCertificadosSAT](VerificacionCertificadosSAT.md)

- [ ] Usar excepciones específicas en lugar de genéricas
4 changes: 2 additions & 2 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.0/phpunit.xsd"
bootstrap="./tests/bootstrap.php" colors="true" verbose="true" cacheResultFile="./build/phpunit.result.cache">
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.4/phpunit.xsd"
bootstrap="./tests/bootstrap.php" colors="true" verbose="true" cacheResult="false">
<testsuites>
<testsuite name="Default">
<directory>./tests/</directory>
Expand Down
40 changes: 31 additions & 9 deletions src/Certificate.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,19 @@ class Certificate
/** @var PublicKey|null Parsed public key */
private $publicKey;

/**
* Certificate constructor
*
* @param string $contents can be a X.509 PEM, X.509 DER or X.509 DER base64
*/
public function __construct(string $contents)
{
if ('' === $contents) {
throw new UnexpectedValueException('Create certificate from empty contents');
}
$pem = (new PemExtractor($contents))->extractCertificate();
if ('' === $pem) { // there is no pem certificate, convert from DER
/** @noinspection RegExpRedundantEscape phpstorm claims "\/" ... you are drunk, go home */
if (boolval(preg_match('/^[a-zA-Z0-9+\/]+={0,2}$/', $contents))) {
// if contents are base64 encoded, then decode it
$contents = base64_decode($contents, true) ?: '';
}
$pem = '-----BEGIN CERTIFICATE-----' . PHP_EOL
. chunk_split(base64_encode($contents), 64, PHP_EOL)
. '-----END CERTIFICATE-----';
if ('' === $pem) { // it could be a DER content, convert to PEM
$pem = static::convertDerToPem($contents);
}

/** @var array|false $parsed */
Expand All @@ -58,6 +56,30 @@ public function __construct(string $contents)
$this->legalName = strval($parsed['subject']['name'] ?? '');
}

/**
* Convert X.509 DER base64 or X.509 DER to X.509 PEM
*
* @param string $contents can be a X.509 DER or X.509 DER base64
* @return string
*/
public static function convertDerToPem(string $contents): string
{
// effectivelly compare that all the content is base64, if it isn't then encode it
if ($contents !== base64_encode(base64_decode($contents, true) ?: '')) {
$contents = base64_encode($contents);
}
return '-----BEGIN CERTIFICATE-----' . PHP_EOL
. chunk_split($contents, 64, PHP_EOL)
. '-----END CERTIFICATE-----';
}

/**
* Create a Certificate object by opening a local file
* The content file can be a X.509 PEM, X.509 DER or X.509 DER base64
*
* @param string $filename must be a local file (without scheme or file:// scheme)
* @return Certificate
*/
public static function openFile(string $filename)
{
return new self(static::localFileOpen($filename));
Expand Down
30 changes: 30 additions & 0 deletions src/Credential.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ class Credential
/** @var PrivateKey */
private $privateKey;

/**
* Credential constructor
*
* @param Certificate $certificate
* @param PrivateKey $privateKey
* @throws UnexpectedValueException Certificate does not belong to private key
*/
public function __construct(Certificate $certificate, PrivateKey $privateKey)
{
if (! $privateKey->belongsTo($certificate)) {
Expand All @@ -23,13 +30,36 @@ public function __construct(Certificate $certificate, PrivateKey $privateKey)
$this->privateKey = $privateKey;
}

/**
* Create a Credential object based on string contents
*
* The certificate content can be a X.509 PEM, X.509 DER or X.509 DER base64
* The private key content can be a PKCS#8 DER, PKCS#8 PEM or PKCS#5 PEM
*
* @param string $certificateContents
* @param string $privateKeyContents
* @param string $passPhrase
* @return static
*/
public static function create(string $certificateContents, string $privateKeyContents, string $passPhrase): self
{
$certificate = new Certificate($certificateContents);
$privateKey = new PrivateKey($privateKeyContents, $passPhrase);
return new self($certificate, $privateKey);
}

/**
* Create a Credential object based on local files
*
* File paths must be local, can have no schema or file:// schema
* The certificate file content can be a X.509 PEM, X.509 DER or X.509 DER base64
* The private key file content can be a PKCS#8 DER, PKCS#8 PEM or PKCS#5 PEM
*
* @param string $certificateFile
* @param string $privateKeyFile
* @param string $passPhrase
* @return static
*/
public static function openFiles(string $certificateFile, string $privateKeyFile, string $passPhrase): self
{
$certificate = Certificate::openFile($certificateFile);
Expand Down
2 changes: 1 addition & 1 deletion src/Internal/LocalFileOpenTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ trait LocalFileOpenTrait
{
private static function localFileOpen(string $filename): string
{
if (0 === strpos($filename, 'file://')) {
if ('file://' === substr($filename, 0, 7)) {
$filename = substr($filename, 7);
}
$scheme = strval(parse_url($filename, PHP_URL_SCHEME));
Expand Down
5 changes: 5 additions & 0 deletions src/PemExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,20 @@ public function extractPublicKey(): string

public function extractPrivateKey(): string
{
// see https://github.com/kjur/jsrsasign/wiki/Tutorial-for-PKCS5-and-PKCS8-PEM-private-key-formats-differences
// PKCS#8 plain private key
if ('' !== $extracted = $this->extractBase64('PRIVATE KEY')) {
return $extracted;
}
// PKCS#5 plain private key
if ('' !== $extracted = $this->extractBase64('RSA PRIVATE KEY')) {
return $extracted;
}
// PKCS#5 encrypted private key
if ('' !== $extracted = $this->extractRsaProtected()) {
return $extracted;
}
// PKCS#8 encrypted private key
return $this->extractBase64('ENCRYPTED PRIVATE KEY');
}

Expand Down
Loading

0 comments on commit fcb7add

Please sign in to comment.