From a636fe767964fce9f6a3580ef4fd4753a8b62e33 Mon Sep 17 00:00:00 2001 From: Carlos C Soto Date: Thu, 14 Nov 2019 16:35:07 -0600 Subject: [PATCH 01/22] Remove obsolete configuration sudo: false --- .travis.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index ddad008..26da94b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,10 +7,6 @@ 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 - cache: - directories: - $HOME/.composer From f198c67ff90b39478bbe72b96aab243d15f3b02b Mon Sep 17 00:00:00 2001 From: Carlos C Soto Date: Thu, 14 Nov 2019 16:35:27 -0600 Subject: [PATCH 02/22] update https links --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fc90568..e7e182f 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ echo $certificado->serialNumber()->bytes(), PHP_EOL; // número de serie del cer ## 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. @@ -95,7 +95,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 From 61454574769f4bde78d941fc336d2123640dffb3 Mon Sep 17 00:00:00 2001 From: Carlos C Soto Date: Thu, 14 Nov 2019 16:35:49 -0600 Subject: [PATCH 03/22] extract Certificate::convertDerToPem from constructor --- src/Certificate.php | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/Certificate.php b/src/Certificate.php index c382bc4..10160ae 100644 --- a/src/Certificate.php +++ b/src/Certificate.php @@ -36,15 +36,8 @@ public function __construct(string $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 */ @@ -58,6 +51,17 @@ public function __construct(string $contents) $this->legalName = strval($parsed['subject']['name'] ?? ''); } + 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-----'; + } + public static function openFile(string $filename) { return new self(static::localFileOpen($filename)); From ac14b6dbf50e452b3dfd22f069924d2fba9358cd Mon Sep 17 00:00:00 2001 From: Carlos C Soto Date: Thu, 14 Nov 2019 16:36:27 -0600 Subject: [PATCH 04/22] replace strpos with substr --- src/Internal/LocalFileOpenTrait.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Internal/LocalFileOpenTrait.php b/src/Internal/LocalFileOpenTrait.php index cfa85ff..e11293d 100644 --- a/src/Internal/LocalFileOpenTrait.php +++ b/src/Internal/LocalFileOpenTrait.php @@ -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)); From 99128dfe6a74f325ef4cad075e026d55a5884a2a Mon Sep 17 00:00:00 2001 From: Carlos C Soto Date: Thu, 14 Nov 2019 16:37:19 -0600 Subject: [PATCH 05/22] Replace array_reduce with implode + array_map better memory management --- src/SerialNumber.php | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/SerialNumber.php b/src/SerialNumber.php index 8182657..61b271a 100644 --- a/src/SerialNumber.php +++ b/src/SerialNumber.php @@ -44,13 +44,9 @@ public static function createFromDecimal(string $decString): self public static function createFromBytes(string $input): self { - $hexadecimal = array_reduce( - str_split($input, 1), - function (string $carry, string $value): string { - return $carry . dechex(ord($value)); - }, - '' - ); + $hexadecimal = implode('', array_map(function (string $value): string { + return dechex(ord($value)); + }, str_split($input, 1))); return new self($hexadecimal); } @@ -61,13 +57,9 @@ public function hexadecimal(): string public function bytes(): string { - return array_reduce( - str_split($this->hexadecimal, 2) ?: [], - function (string $carry, string $value): string { - return $carry . chr(intval(hexdec($value))); - }, - '' - ); + return implode('', array_map(function (string $value): string { + return chr(intval(hexdec($value))); + }, str_split($this->hexadecimal, 2) ?: [])); } public function decimal(): string From 335115586c446ae2e34270f24dacd2a46309b219 Mon Sep 17 00:00:00 2001 From: Carlos C Soto Date: Thu, 14 Nov 2019 16:38:45 -0600 Subject: [PATCH 06/22] fix issue on phpunit 8 when generic test case is not abstract --- tests/TestCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestCase.php b/tests/TestCase.php index a253723..5fe6bfc 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -4,7 +4,7 @@ namespace PhpCfdi\Credentials\Tests; -class TestCase extends \PHPUnit\Framework\TestCase +abstract class TestCase extends \PHPUnit\Framework\TestCase { public static function filePath(string $filename): string { From d37f443eac0be9b4f061e4998ba49af48c4a9e8a Mon Sep 17 00:00:00 2001 From: Carlos C Soto Date: Thu, 14 Nov 2019 16:51:36 -0600 Subject: [PATCH 07/22] simplify variables usage --- src/PrivateKey.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/PrivateKey.php b/src/PrivateKey.php index fb2069a..c28fc10 100644 --- a/src/PrivateKey.php +++ b/src/PrivateKey.php @@ -29,8 +29,8 @@ public function __construct(string $source, string $passPhrase) throw new UnexpectedValueException('Private key is empty'); } $pemExtractor = new PemExtractor($source); - $private = $pemExtractor->extractPrivateKey(); - if ('' === $private) { + $pem = $pemExtractor->extractPrivateKey(); + if ('' === $pem) { if (boolval(preg_match('/^[a-zA-Z0-9+\/]+={0,2}$/', $source))) { // if contents are base64 encoded, then decode it $source = base64_decode($source, true) ?: ''; @@ -38,8 +38,6 @@ public function __construct(string $source, string $passPhrase) $pem = '-----BEGIN ENCRYPTED PRIVATE KEY-----' . PHP_EOL . chunk_split(base64_encode($source), 64, PHP_EOL) . '-----END ENCRYPTED PRIVATE KEY-----'; - } else { - $pem = $private; } $this->pem = $pem; $this->passPhrase = $passPhrase; From 08e24648da572933f8e4b0831ffdd9d45494bc31 Mon Sep 17 00:00:00 2001 From: Carlos C Soto Date: Thu, 14 Nov 2019 16:53:49 -0600 Subject: [PATCH 08/22] extract PrivateKey convertDerToPem from constructor --- src/PrivateKey.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/PrivateKey.php b/src/PrivateKey.php index c28fc10..ee6e491 100644 --- a/src/PrivateKey.php +++ b/src/PrivateKey.php @@ -35,9 +35,8 @@ public function __construct(string $source, string $passPhrase) // if contents are base64 encoded, then decode it $source = base64_decode($source, true) ?: ''; } - $pem = '-----BEGIN ENCRYPTED PRIVATE KEY-----' . PHP_EOL - . chunk_split(base64_encode($source), 64, PHP_EOL) - . '-----END ENCRYPTED PRIVATE KEY-----'; + // it could be a DER content, convert to PEM + $pem = static::convertDerToPem($source); } $this->pem = $pem; $this->passPhrase = $passPhrase; @@ -50,6 +49,13 @@ function ($privateKey): array { parent::__construct($dataArray); } + public static function convertDerToPem(string $contents): string + { + return '-----BEGIN ENCRYPTED PRIVATE KEY-----' . PHP_EOL + . chunk_split(base64_encode($contents), 64, PHP_EOL) + . '-----END ENCRYPTED PRIVATE KEY-----'; + } + public static function openFile(string $filename, string $passPhrase): self { return new self(static::localFileOpen($filename), $passPhrase); From 3cbbe976fe3851679e21485c21764d0d6e02b4f2 Mon Sep 17 00:00:00 2001 From: Carlos C Soto Date: Thu, 14 Nov 2019 16:56:06 -0600 Subject: [PATCH 09/22] Remove private key der content as base64 encoded This is useful for certificates since is a commonly used format, but not for private keys --- src/PrivateKey.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/PrivateKey.php b/src/PrivateKey.php index ee6e491..80b101a 100644 --- a/src/PrivateKey.php +++ b/src/PrivateKey.php @@ -31,10 +31,6 @@ public function __construct(string $source, string $passPhrase) $pemExtractor = new PemExtractor($source); $pem = $pemExtractor->extractPrivateKey(); if ('' === $pem) { - if (boolval(preg_match('/^[a-zA-Z0-9+\/]+={0,2}$/', $source))) { - // if contents are base64 encoded, then decode it - $source = base64_decode($source, true) ?: ''; - } // it could be a DER content, convert to PEM $pem = static::convertDerToPem($source); } From bd82343cf7086bd396f31873a603040403eab24f Mon Sep 17 00:00:00 2001 From: Carlos C Soto Date: Thu, 14 Nov 2019 22:00:56 -0600 Subject: [PATCH 10/22] Document certificate & private key formats on readme and phpdoc --- README.md | 51 ++++++++++++++++++++++++++++++++++++++++++-- src/Certificate.php | 18 ++++++++++++++++ src/Credential.php | 30 ++++++++++++++++++++++++++ src/PemExtractor.php | 5 +++++ src/PrivateKey.php | 20 +++++++++++++++++ 5 files changed, 122 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e7e182f..a1a4a38 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,8 @@ composer require phpcfdi/credentials ```php 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 contraseña) y usarla desde PHP. +También usando `openssl` se puede convertir a formato `PKCS#5 PEM ENCRYPTED` (con contraseña, `RSA PRIVATE KEY`) +y el formato `PKCS#5 PEM` (sin contraseña, `PRIVATE KEY`). + +- 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`. + +Para entender más de los formatos de llaves privadas se puede consultar la siguiente liga: + + + ## Compatilibilidad Esta librería se mantendrá compatible con al menos la versión con diff --git a/src/Certificate.php b/src/Certificate.php index 10160ae..f949968 100644 --- a/src/Certificate.php +++ b/src/Certificate.php @@ -30,6 +30,11 @@ 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) { @@ -51,6 +56,12 @@ 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 @@ -62,6 +73,13 @@ public static function convertDerToPem(string $contents): string . '-----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)); diff --git a/src/Credential.php b/src/Credential.php index 6b3c1c8..7483b1e 100644 --- a/src/Credential.php +++ b/src/Credential.php @@ -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)) { @@ -23,6 +30,17 @@ 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); @@ -30,6 +48,18 @@ public static function create(string $certificateContents, string $privateKeyCon 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); diff --git a/src/PemExtractor.php b/src/PemExtractor.php index 90dda4a..49d8230 100644 --- a/src/PemExtractor.php +++ b/src/PemExtractor.php @@ -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'); } diff --git a/src/PrivateKey.php b/src/PrivateKey.php index 80b101a..f5257d1 100644 --- a/src/PrivateKey.php +++ b/src/PrivateKey.php @@ -23,6 +23,12 @@ class PrivateKey extends Key /** @var PublicKey|null $public key extracted from private key */ private $publicKey; + /** + * PrivateKey constructor + * + * @param string $source can be a PKCS#8 DER, PKCS#8 PEM or PKCS#5 PEM + * @param string $passPhrase + */ public function __construct(string $source, string $passPhrase) { if ('' === $source) { @@ -45,6 +51,12 @@ function ($privateKey): array { parent::__construct($dataArray); } + /** + * Convert PKCS#8 DER to PKCS#8 PEM + * + * @param string $contents can be a PKCS#8 DER + * @return string + */ public static function convertDerToPem(string $contents): string { return '-----BEGIN ENCRYPTED PRIVATE KEY-----' . PHP_EOL @@ -52,6 +64,14 @@ public static function convertDerToPem(string $contents): string . '-----END ENCRYPTED PRIVATE KEY-----'; } + /** + * Create a PrivateKey object by opening a local file + * The content file can be a PKCS#8 DER, PKCS#8 PEM or PKCS#5 PEM + * + * @param string $filename must be a local file (without scheme or file:// scheme) + * @param string $passPhrase + * @return static + */ public static function openFile(string $filename, string $passPhrase): self { return new self(static::localFileOpen($filename), $passPhrase); From 325caaf35340028e690e3c2beb7cfc3e4bc1b6f3 Mon Sep 17 00:00:00 2001 From: Carlos C Soto Date: Thu, 14 Nov 2019 23:47:43 -0600 Subject: [PATCH 11/22] Add PrivateKey::changePaddPhrase method --- src/PrivateKey.php | 23 +++++++++++++++++++++++ tests/Unit/PrivateKeyTest.php | 26 ++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/PrivateKey.php b/src/PrivateKey.php index f5257d1..a8b2d35 100644 --- a/src/PrivateKey.php +++ b/src/PrivateKey.php @@ -155,4 +155,27 @@ public function callOnPrivateKey(Closure $function) openssl_free_key($privateKey); } } + + /** + * Export the current private key to a new private key with a different password + * + * @param string $newPassPhrase If empty the new private key will be unencrypted + * @return self + */ + public function changePassPhrase(string $newPassPhrase): self + { + $pem = $this->callOnPrivateKey( + function ($privateKey) use ($newPassPhrase): string { + $exportConfig = [ + 'private_key_bits' => $this->publicKey()->numberOfBits(), + 'encrypt_key' => ('' !== $newPassPhrase), // if empty then set that the key is not encrypted + ]; + if (! openssl_pkey_export($privateKey, $exported, $newPassPhrase, $exportConfig)) { + throw new RuntimeException('Cannot export the private KEY to change password'); + } + return $exported; + } + ); + return new self($pem, $newPassPhrase); + } } diff --git a/tests/Unit/PrivateKeyTest.php b/tests/Unit/PrivateKeyTest.php index 66e4dce..e0c3197 100644 --- a/tests/Unit/PrivateKeyTest.php +++ b/tests/Unit/PrivateKeyTest.php @@ -19,6 +19,13 @@ public function createPrivateKey(): PrivateKey return PrivateKey::openFile($filename, $password); } + public function createCertificate(): Certificate + { + // this certificate match with PrivateKey returned by createPrivateKey() + $filename = $this->filePath('FIEL_AAA010101AAA/certificate.cer'); + return Certificate::openFile($filename); + } + public function testPemAndPassPhraseProperties(): void { $passPhrase = trim($this->fileContents('FIEL_AAA010101AAA/password.txt')); @@ -107,4 +114,23 @@ public function testBelongsTo(string $filename, bool $expectBelongsTo): void $privateKey = $this->createPrivateKey(); $this->assertSame($expectBelongsTo, $privateKey->belongsTo($certificate)); } + + /** + * @param string $newPassword + * @param string $expectedHeaderName + * @testWith ["other password", "ENCRYPTED PRIVATE KEY"] + * ["", "PRIVATE KEY"] + */ + public function testChangePassPhrase(string $newPassword, string $expectedHeaderName): void + { + $certificate = $this->createCertificate(); + $baseKey = $this->createPrivateKey(); + + $changed = $baseKey->changePassPhrase($newPassword); + + $this->assertNotEquals($baseKey->pem(), $changed->pem(), 'Changed PK must be different than base PK'); + $this->assertTrue($changed->belongsTo($certificate), 'Changed PK must belong to certificate'); + $pkcs8Header = sprintf('-----BEGIN %s-----', $expectedHeaderName); + $this->assertStringStartsWith($pkcs8Header, $changed->pem(), 'Changed PK does not have expected header'); + } } From cfc3c47e5bc46f5be0b90efab5d67c8ab565867b Mon Sep 17 00:00:00 2001 From: Carlos C Soto Date: Fri, 15 Nov 2019 01:35:17 -0600 Subject: [PATCH 12/22] Allow open PKCS#8 unencrypted private key --- src/PrivateKey.php | 13 ++++++++----- tests/Unit/PrivateKeyConstructTest.php | 9 ++++++++- .../CSD01_AAA010101AAA/private_key_plain.key | Bin 0 -> 1218 bytes 3 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 tests/_files/CSD01_AAA010101AAA/private_key_plain.key diff --git a/src/PrivateKey.php b/src/PrivateKey.php index a8b2d35..83332b1 100644 --- a/src/PrivateKey.php +++ b/src/PrivateKey.php @@ -27,7 +27,7 @@ class PrivateKey extends Key * PrivateKey constructor * * @param string $source can be a PKCS#8 DER, PKCS#8 PEM or PKCS#5 PEM - * @param string $passPhrase + * @param string $passPhrase If empty asume unencrypted/plain private key */ public function __construct(string $source, string $passPhrase) { @@ -38,7 +38,8 @@ public function __construct(string $source, string $passPhrase) $pem = $pemExtractor->extractPrivateKey(); if ('' === $pem) { // it could be a DER content, convert to PEM - $pem = static::convertDerToPem($source); + $convertSourceIsEncrypted = ('' !== $passPhrase); + $pem = static::convertDerToPem($source, $convertSourceIsEncrypted); } $this->pem = $pem; $this->passPhrase = $passPhrase; @@ -55,13 +56,15 @@ function ($privateKey): array { * Convert PKCS#8 DER to PKCS#8 PEM * * @param string $contents can be a PKCS#8 DER + * @param bool $isEncrypted * @return string */ - public static function convertDerToPem(string $contents): string + public static function convertDerToPem(string $contents, bool $isEncrypted): string { - return '-----BEGIN ENCRYPTED PRIVATE KEY-----' . PHP_EOL + $privateKeyName = ($isEncrypted) ? 'ENCRYPTED PRIVATE KEY' : 'PRIVATE KEY'; + return "-----BEGIN $privateKeyName-----" . PHP_EOL . chunk_split(base64_encode($contents), 64, PHP_EOL) - . '-----END ENCRYPTED PRIVATE KEY-----'; + . "-----END $privateKeyName-----"; } /** diff --git a/tests/Unit/PrivateKeyConstructTest.php b/tests/Unit/PrivateKeyConstructTest.php index be9f1d4..8077b44 100644 --- a/tests/Unit/PrivateKeyConstructTest.php +++ b/tests/Unit/PrivateKeyConstructTest.php @@ -62,11 +62,18 @@ public function testConstructWithInvalidButBase64Content(): void new PrivateKey('INVALID+CONTENT', ''); } - public function testConstructWithPkcs8Content(): void + public function testConstructWithPkcs8Encrypted(): void { $content = $this->fileContents('CSD01_AAA010101AAA/private_key.key'); $password = trim($this->fileContents('CSD01_AAA010101AAA/password.txt')); $privateKey = new PrivateKey($content, $password); $this->assertGreaterThan(0, $privateKey->numberOfBits()); } + + public function testConstructWithPkcs8Unencrypted(): void + { + $content = $this->fileContents('CSD01_AAA010101AAA/private_key_plain.key'); + $privateKey = new PrivateKey($content, ''); + $this->assertGreaterThan(0, $privateKey->numberOfBits()); + } } diff --git a/tests/_files/CSD01_AAA010101AAA/private_key_plain.key b/tests/_files/CSD01_AAA010101AAA/private_key_plain.key new file mode 100644 index 0000000000000000000000000000000000000000..584c508624e619141d2ed356d669a9412c4d9954 GIT binary patch literal 1218 zcmV;z1U>sOf&{(-0RS)!1_>&LNQUrs4#*Aqyhl|0)hbn0GCv9!N?GZ zFpB%YYI{bVg-APpyU900*4YZ8mdpCml8q5))Jo5EVsSrsF;A1e03riHbABB!ZZTlk zouv_KREQVQjuaB6V{*bJmBSm`5_bFgprWXl`G?Pl2 zy6{QXG?7*QL5?yMZ0jv)3VHTUdcs%YY9RdRZ009Dm0RSy8 z8PuXz%~CNfS{88_bdU!zJi=un3omOvv8^=Pe`O+>z#}5_E?o9gZ6C}M;P^=|kE>iv zdW`&0UR*PsFOqH0E`>dxM6#^~Nki_c;M!&NEG{ymS5+_N)qw)* z_WcY0*QE)+MkxZMU{^m&3pbbg-{yj-1tZv@HZ zm^P(n%Bv4NzVSd!1Ecg0S`?qKs=M$5A@Cg&HD;N!hO{qiF*qN2Ug54a; zz2ZbM7SiXoVk(IEER?zouVRqzS0BL#4|4*6fdHh4(ygc=U;3*y>LRF|4x(ozetB_G z+O+yc@D!-Nuk!0i`i&_>(a5&X`3JGpo;rkr9)Hfm-da^Adu zyb%I{fL}#_Pf;Nx2_3Kjk*^=ZKP+?-mK$``Lu9Z+)i*EpKe~IeX%z+do$wVF0Wkrt z(mcx>l*N1nSpm(}Bp_qNZi?|~$xEY*_a_z7pjz2!k literal 0 HcmV?d00001 From 6413c5015875ae2d2c923d05978590c02162bc33 Mon Sep 17 00:00:00 2001 From: Carlos C Soto Date: Fri, 15 Nov 2019 01:36:20 -0600 Subject: [PATCH 13/22] Improve docs about private key open include markdown format fixes --- README.md | 33 +++++++++++++++++++++------------ tests/_files/README.md | 9 ++++++--- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index a1a4a38..cd98929 100644 --- a/README.md +++ b/README.md @@ -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 @@ -67,7 +67,7 @@ echo $certificado->serialNumber()->bytes(), PHP_EOL; // número de serie del cer 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]. +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. @@ -87,9 +87,9 @@ o en las firmas XML, por este motivo, los formatos soportados para crear un obje - 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`. + - 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` @@ -97,14 +97,23 @@ o en las firmas XML, por este motivo, los formatos soportados para crear un obje 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 contraseña) y usarla desde PHP. -También usando `openssl` se puede convertir a formato `PKCS#5 PEM ENCRYPTED` (con contraseña, `RSA PRIVATE KEY`) -y el formato `PKCS#5 PEM` (sin contraseña, `PRIVATE KEY`). +(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`. + - 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 simple use `PEM`. Para entender más de los formatos de llaves privadas se puede consultar la siguiente liga: diff --git a/tests/_files/README.md b/tests/_files/README.md index 3214614..41322d0 100644 --- a/tests/_files/README.md +++ b/tests/_files/README.md @@ -9,10 +9,13 @@ Commands: # get certificate information: openssl x509 -nameopt utf8,sep_multiline,lname -inform DER -noout -dates -serial -subject -fingerprint -pubkey -in CSD01_AAA010101AAA.cer -# convert private key from DER to PEM (password 12345678a): -openssl pkcs8 -inform DER -in CSD01_AAA010101AAA.key -out CSD01_AAA010101AAA.key.pem +# convert private key from DER to PEM (unprotected private key): +openssl pkcs8 -inform DER -in CSD01_AAA010101AAA.key -passin pass:12345678a -out CSD01_AAA010101AAA.key.pem -# protect with password the private key, not required but used for test suite: +# convert private key from PEM to DER: +openssl pkcs8 -topk8 -in CSD01_AAA010101AAA.key.pem -passin pass:12345678a -passout pass:12345678a -out CSD01_AAA010101AAA.key -outform DER + +# protect with password the private key (PKCS#5), not required but used for test suite: openssl rsa -in CSD01_AAA010101AAA.key.pem -des3 -out CSD01_AAA010101AAA_password.key.pem # convert public key from DER to PEM, not required but used for test suite: From fc198fb7e61c899e764ec1a8ee2db8fa6dfd99bf Mon Sep 17 00:00:00 2001 From: Carlos C Soto Date: Fri, 15 Nov 2019 01:41:12 -0600 Subject: [PATCH 14/22] run php-cs-fixer on php 7.4snapshot --- .travis.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.travis.yml b/.travis.yml index 26da94b..bbe5a14 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,11 @@ matrix: allow_failures: - php: "7.4snapshot" +env: + global: + - PHP_CS_FIXER_FUTURE_MODE=1 + - PHP_CS_FIXER_IGNORE_ENV=1 + cache: - directories: - $HOME/.composer From fe55ffab786528de4537ee6af161df1ebf21c31a Mon Sep 17 00:00:00 2001 From: Carlos C Soto Date: Fri, 15 Nov 2019 01:43:04 -0600 Subject: [PATCH 15/22] run @dev:check-style on @dev:build --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d33e38e..2b2a7e2 100644 --- a/composer.json +++ b/composer.json @@ -51,6 +51,7 @@ ], "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/" ], @@ -62,7 +63,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: run phplint, dev:check-style, phpunit and phpstan", "dev:coverage": "DEV: run phpunit with xdebug and storage coverage in build/coverage/html/" } } From 3664eaa8c7e123943c16d003d1fb0627ab23a368 Mon Sep 17 00:00:00 2001 From: Carlos C Soto Date: Fri, 15 Nov 2019 01:45:27 -0600 Subject: [PATCH 16/22] Remove overtrue/phplint dependence --- .gitattributes | 1 - .phplint.yml | 11 ----------- .travis.yml | 1 - CONTRIBUTING.md | 1 - composer.json | 4 +--- 5 files changed, 1 insertion(+), 17 deletions(-) delete mode 100644 .phplint.yml diff --git a/.gitattributes b/.gitattributes index 389ec24..374d5c4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,7 +13,6 @@ /.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 diff --git a/.phplint.yml b/.phplint.yml deleted file mode 100644 index 790809c..0000000 --- a/.phplint.yml +++ /dev/null @@ -1,11 +0,0 @@ -# config file for phplint -# see https://github.com/overtrue/phplint - -path: ./ -cache: build/phplint.cache -jobs: 10 -extensions: - - php -exclude: - - vendor - - build diff --git a/.travis.yml b/.travis.yml index bbe5a14..8068a55 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,6 @@ before_script: - 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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 99e832d..d3995db 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/composer.json b/composer.json index 2b2a7e2..045a23a 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,6 @@ "require-dev": { "ext-json": "*", "phpunit/phpunit": "^8.0", - "overtrue/phplint": "^1.0", "squizlabs/php_codesniffer": "^3.0", "friendsofphp/php-cs-fixer": "^2.4", "phpstan/phpstan-shim": "^0.11" @@ -50,7 +49,6 @@ "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/" @@ -63,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, dev:check-style, 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/" } } From ec9ccd5e5b9d4c6da272be531efe4d022dea5aa8 Mon Sep 17 00:00:00 2001 From: Carlos C Soto Date: Fri, 15 Nov 2019 01:50:03 -0600 Subject: [PATCH 17/22] build directory exists by default but is excluded from dist package --- .gitattributes | 2 +- .gitignore | 1 - .travis.yml | 1 - build/.gitignore | 2 ++ 4 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 build/.gitignore diff --git a/.gitattributes b/.gitattributes index 374d5c4..213f6b4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,11 +3,11 @@ # 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 +/build/ export-ignore /docs/ export-ignore /tests/ export-ignore /.gitattributes export-ignore diff --git a/.gitignore b/.gitignore index f5b87d8..840d101 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ # do not include this files on git /vendor -/build /composer.lock .phpunit.result.cache diff --git a/.travis.yml b/.travis.yml index 8068a55..889dc3e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,6 @@ cache: before_script: - phpenv config-rm xdebug.ini || true - travis_retry composer install --no-interaction --prefer-dist - - mkdir -p build script: - vendor/bin/php-cs-fixer fix --verbose diff --git a/build/.gitignore b/build/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/build/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore From 6169817a08a1e5fed9a03881de0f77aa5fa02ed9 Mon Sep 17 00:00:00 2001 From: Carlos C Soto Date: Fri, 15 Nov 2019 01:52:02 -0600 Subject: [PATCH 18/22] do not create phpunit.result.cache, cleanup --- .gitattributes | 1 - .gitignore | 1 - composer.json | 2 +- phpunit.xml.dist | 4 ++-- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.gitattributes b/.gitattributes index 213f6b4..c5811d5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,7 +4,6 @@ # Do not put this files on a distribution package (by .gitignore) /vendor export-ignore /composer.lock export-ignore -.phpunit.result.cache export-ignore # Do not put this files on a distribution package /build/ export-ignore diff --git a/.gitignore b/.gitignore index 840d101..946faf0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ # do not include this files on git /vendor /composer.lock -.phpunit.result.cache diff --git a/composer.json b/composer.json index 045a23a..3e9b3dd 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ }, "require-dev": { "ext-json": "*", - "phpunit/phpunit": "^8.0", + "phpunit/phpunit": "^8.4", "squizlabs/php_codesniffer": "^3.0", "friendsofphp/php-cs-fixer": "^2.4", "phpstan/phpstan-shim": "^0.11" diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 379678d..b3521fe 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,6 +1,6 @@ + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/8.4/phpunit.xsd" + bootstrap="./tests/bootstrap.php" colors="true" verbose="true" cacheResult="false"> ./tests/ From 318d38398d4b43d92b8841cefc903b0c257e7274 Mon Sep 17 00:00:00 2001 From: Carlos C Soto Date: Fri, 15 Nov 2019 01:53:03 -0600 Subject: [PATCH 19/22] include docs/ on dist package --- .gitattributes | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index c5811d5..64689ea 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,7 +7,6 @@ # Do not put this files on a distribution package /build/ export-ignore -/docs/ export-ignore /tests/ export-ignore /.gitattributes export-ignore /.gitignore export-ignore From 2ab4ec4168bb326194ef4c3520e592c9003952e0 Mon Sep 17 00:00:00 2001 From: Carlos C Soto Date: Fri, 15 Nov 2019 01:54:27 -0600 Subject: [PATCH 20/22] exceptions --- docs/TODO.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/TODO.md b/docs/TODO.md index 823bdfb..9716370 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -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 From 1d0b3413a084ce0b0b5d409950407b0974fc263c Mon Sep 17 00:00:00 2001 From: Carlos C Soto Date: Fri, 15 Nov 2019 02:04:22 -0600 Subject: [PATCH 21/22] Changes on version 1.1.0 --- docs/CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e2baaa6..0e55920 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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-15 + +- 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. From d7561fa91ff1e47b7648f58ed50cb7523a33ea8e Mon Sep 17 00:00:00 2001 From: Carlos C Soto Date: Tue, 19 Nov 2019 09:39:27 -0600 Subject: [PATCH 22/22] Review 1.1.0 --- README.md | 4 ++-- docs/CHANGELOG.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cd98929..e3c54a6 100644 --- a/README.md +++ b/README.md @@ -111,9 +111,9 @@ 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`. + 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 simple use `PEM`. + 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: diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0e55920..fb65411 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,7 +6,7 @@ Nos apegamos a [SEMVER](SEMVER.md), revisa la información para entender mejor el control de versiones. -## Version 1.1.0 2019-11-15 +## 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.