Skip to content

Commit

Permalink
Merge pull request #51 from eclipxe13/development
Browse files Browse the repository at this point in the history
Create certificate using PEM contents (version 2.12.3)
  • Loading branch information
eclipxe13 committed Sep 26, 2019
2 parents 62a7ad3 + cde31ed commit bd1eb71
Show file tree
Hide file tree
Showing 7 changed files with 66 additions and 30 deletions.
8 changes: 8 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@
- Change visibility of `CfdiUtils\Cleaner\Cleaner#removeIncompleteSchemaLocation()` to private.


## Version 2.12.3 2019-09-26

- `CfdiUtils\Certificado\Certificado` can be created using PEM contents and not only a certificate path.
- `CfdiUtils\Creator33` can use a certificate without associated filename.
- `CfdiUtils\RetencionesCreator10` can use a certificate without associated filename.
- `CfdiUtils\Certificado\NodeCertificado` can obtain the certificate without creating a temporary file.


## Version 2.12.2 2019-09-24

- When cannot load an Xml string include `LibXMLError` information into exception, like:
Expand Down
50 changes: 38 additions & 12 deletions src/CfdiUtils/Certificado/Certificado.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,27 +38,36 @@ class Certificado
/**
* Certificado constructor.
*
* @param string $filename
* @param string $filename Allows filename or certificate contents (PEM or DER)
* @param OpenSSL $openSSL
* @throws \UnexpectedValueException when the file does not exists or is not readable
* @throws \UnexpectedValueException when cannot read the certificate file or is empty
* @throws \RuntimeException when cannot parse the certificate file or is empty
* @throws \UnexpectedValueException when the certificate does not exists or is not readable
* @throws \UnexpectedValueException when cannot read the certificate or is empty
* @throws \RuntimeException when cannot parse the certificate or is empty
* @throws \RuntimeException when cannot get serialNumberHex or serialNumber from certificate
*/
public function __construct(string $filename, OpenSSL $openSSL = null)
{
$this->assertFileExists($filename);
$contents = strval(file_get_contents($filename));
$this->setOpenSSL($openSSL ?: new OpenSSL());
$contents = $this->extractPemCertificate($filename);
// using $filename as PEM content did not retrieve any result, so, use it as path
if ('' === $contents) {
throw new \UnexpectedValueException("File $filename is empty");
$sourceName = 'file ' . $filename;
$this->assertFileExists($filename);
$contents = file_get_contents($filename) ?: '';
if ('' === $contents) {
throw new \UnexpectedValueException("File $filename is empty");
}
// this will take PEM contents or perform a PHP conversion from DER to PEM
$contents = $this->obtainPemCertificate($contents);
} else {
$filename = '';
$sourceName = '(contents)';
}
$this->setOpenSSL($openSSL ?: new OpenSSL());
$contents = $this->obtainPemCertificate($contents);

// get the certificate data
$data = openssl_x509_parse($contents, true);
if (! is_array($data)) {
throw new \RuntimeException("Cannot parse the certificate file $filename");
throw new \RuntimeException("Cannot parse the certificate $sourceName");
}

// get the public key
Expand All @@ -74,7 +83,7 @@ public function __construct(string $filename, OpenSSL $openSSL = null)
} elseif (isset($data['serialNumber'])) {
$serial->loadDecimal($data['serialNumber']);
} else {
throw new \RuntimeException("Cannot get serialNumberHex or serialNumber from certificate file $filename");
throw new \RuntimeException("Cannot get serialNumberHex or serialNumber from certificate $sourceName");
}
$this->serial = $serial;
$this->validFrom = $data['validFrom_time_t'] ?? 0;
Expand All @@ -84,12 +93,29 @@ public function __construct(string $filename, OpenSSL $openSSL = null)
$this->filename = $filename;
}

private function extractPemCertificate(string $contents): string
{
if (strlen($contents) < 2000) {
return ''; // is too short to be a PEM certificate
}
$openssl = $this->getOpenSSL();
$decoded = @base64_decode($contents, true) ?: '';
if ($contents === base64_encode($decoded)) { // is a one liner certificate
$doubleEncoded = $openssl->readPemContents($decoded)->certificate();
if ($doubleEncoded !== '') {
return $doubleEncoded;
}
$contents = $this->getOpenSSL()->derCerConvertPhp($decoded);
}
return $openssl->readPemContents($contents)->certificate();
}

private function obtainPemCertificate(string $contents): string
{
$openssl = $this->getOpenSSL();
$extracted = $openssl->readPemContents($contents)->certificate();
if ('' === $extracted) { // cannot extract, could be on DER format
$extracted = $openssl->derCerConvertPhp($contents);
$extracted = $this->getOpenSSL()->derCerConvertPhp($contents);
}
return $extracted;
}
Expand Down
13 changes: 4 additions & 9 deletions src/CfdiUtils/Certificado/NodeCertificado.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<?php
namespace CfdiUtils\Certificado;

use CfdiUtils\Internals\TemporaryFile;
use CfdiUtils\Nodes\NodeInterface;

class NodeCertificado
Expand Down Expand Up @@ -99,14 +98,10 @@ public function save(string $filename)
*/
public function obtain(): Certificado
{
$temporaryFile = TemporaryFile::create();
// the temporary name was created
try {
$this->save($temporaryFile->getPath());
$certificado = new Certificado($temporaryFile->getPath());
return $certificado;
} finally {
$temporaryFile->remove();
$certificado = $this->extract();
if ('' === $certificado) {
throw new \RuntimeException('The certificado attribute is empty');
}
return new Certificado(base64_encode($certificado));
}
}
6 changes: 2 additions & 4 deletions src/CfdiUtils/CfdiCreator33.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,9 @@ public function comprobante(): Comprobante
public function putCertificado(Certificado $certificado, bool $putEmisorRfcNombre = true)
{
$this->setCertificado($certificado);
$cerfile = $certificado->getFilename();
$this->comprobante['NoCertificado'] = $certificado->getSerial();
if (file_exists($cerfile)) {
$this->comprobante['Certificado'] = base64_encode(strval(file_get_contents($cerfile)));
}
$pemContents = implode('', preg_grep('/^((?!-).)*$/', explode(PHP_EOL, $certificado->getPemContents())));
$this->comprobante['Certificado'] = $pemContents;
if ($putEmisorRfcNombre) {
$emisor = $this->comprobante->searchNode('cfdi:Emisor');
if (null === $emisor) {
Expand Down
6 changes: 2 additions & 4 deletions src/CfdiUtils/Retenciones/RetencionesCreator10.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,9 @@ public function retenciones(): Retenciones
public function putCertificado(Certificado $certificado)
{
$this->setCertificado($certificado);
$cerfile = $certificado->getFilename();
$this->retenciones['NumCert'] = $certificado->getSerial();
if (file_exists($cerfile)) {
$this->retenciones['Cert'] = base64_encode(strval(file_get_contents($cerfile)));
}
$pemContents = implode('', preg_grep('/^((?!-).)*$/', explode(PHP_EOL, $certificado->getPemContents())));
$this->retenciones['Cert'] = $pemContents;
}

public function buildCadenaDeOrigen(): string
Expand Down
11 changes: 11 additions & 0 deletions tests/CfdiUtilsTests/Certificado/CertificadoTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ public function testVerifyWithKnownData()
$this->assertTrue($verify);
}

public function testConstructUsingPemContents()
{
$pemfile = $this->utilAsset('certs/CSD01_AAA010101AAA.cer.pem');
$contents = file_get_contents($pemfile) ?: '';

$fromFile = new Certificado($pemfile);
$fromContents = new Certificado($contents);

$this->assertSame($fromFile->getPemContents(), $fromContents->getPemContents());
}

public function testVerifyWithInvalidData()
{
$dataFile = $this->utilAsset('certs/data-to-sign.txt');
Expand Down
2 changes: 1 addition & 1 deletion tests/CfdiUtilsTests/Certificado/NodeCertificadoTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ public function testObtain()
$nodeCertificado = $this->createNodeCertificado(strval(file_get_contents($cfdiSample)));

$certificate = $nodeCertificado->obtain();
$this->assertFileNotExists($certificate->getFilename());
$this->assertEmpty($certificate->getFilename());
$this->assertEquals('CTO021007DZ8', $certificate->getRfc());
}
}

0 comments on commit bd1eb71

Please sign in to comment.