Skip to content

Latest commit

 

History

History
97 lines (70 loc) · 3.79 KB

mtls.md

File metadata and controls

97 lines (70 loc) · 3.79 KB

Certificado e mTLS

O padrão do BACEN para API Pix exige mTLS em toda requisição, a do token incluída: o PSP só conversa com quem apresentar o certificado que ele emitiu. Não é autenticação a mais — sem o certificado a conexão não se estabelece, e a credencial nem chega a ser lida.

Quem trata disso é PHPay\Http\Certificate, em Http e não dentro de um gateway, porque Efí, Inter, BB, Itaú, Sicoob e Sicredi seguem o mesmo esquema.

← voltar ao índice

Os PSPs entregam de duas formas

E as duas são aceitas, porque é o cURL que lê o arquivo e ele aceita as duas.

O PSP entrega Você usa
um arquivo com certificado e chave dentro (.p12, .pem) new Certificate($caminho, $senha)
dois arquivos, certificado e chave separados (.crt + .key) Certificate::pair($crt, $key, $senha)

A Efí entrega um .p12. O Inter entrega .crt e .key, e a documentação dele manda juntar os dois com openssl pkcs12 -export — o que funciona, mas não é necessário: o pair() entrega as duas metades ao cURL como elas vieram.

use PHPay\Http\Certificate;

/* arquivo único */
new Certificate('/caminho/certificado.p12');
new Certificate('/caminho/certificado.p12', 'senha-da-chave');

/* certificado e chave separados */
Certificate::pair('/caminho/certificado.crt', '/caminho/chave.key');
Certificate::pair('/caminho/certificado.crt', '/caminho/chave.key', 'senha-da-chave');

Dois gateways da biblioteca falam mTLS: a Efí, com arquivo único, e o Inter, com o par.

A senha é da chave, não do certificado

Num par, a senha protege a chave privada. O certificado é público — não tem o que decifrar. É por isso que pair() tem um parâmetro de senha só, e ele vai para a chave:

Certificate::pair($crt, $key, 'segredo')->guzzleOptions();
// ['cert' => '…/certificado.crt', 'ssl_key' => ['…/chave.key', 'segredo']]

Esse é o detalhe que falha em silêncio se você montar as opções na mão: o Guzzle transforma o segundo elemento de cert numa senha de certificado (CURLOPT_SSLCERTPASSWD), e o handshake quebra sem dizer por quê.

Em container, o certificado vem em variável de ambiente

Para os dois formatos, em base64:

Certificate::fromBase64(getenv('EFI_CERT_BASE64'), getenv('EFI_CERT_SENHA'));

Certificate::pairFromBase64(getenv('CERT_BASE64'), getenv('CHAVE_BASE64'));

Os dois gravam em arquivo temporário com permissão 0600, apagado quando o processo termina.

O fromBase64() aceita um terceiro parâmetro de formato ('p12' ou 'pem'), porque num arquivo único é a extensão que diz ao cURL se ele é um bundle PKCS#12 ou um PEM. O pairFromBase64() não aceita: as duas metades de um par são PEM, e a extensão não informa nada.

Extensões aceitas

arquivo único .p12, .pem
certificado de um par .crt, .cer, .pem
chave de um par .key, .pem

A extensão não é capricho: é por ela que o Guzzle diz ao cURL que o arquivo é PKCS#12. Um .pfx é o mesmo formato de um .p12, mas o Guzzle não reconhece essa extensão — a mensagem de erro manda renomear, e renomear resolve.

Um .crt sozinho é recusado, e a mensagem aponta o pair(): certificado sem chave privada não fecha handshake nenhum.

O que o Certificate não faz

  • Não lê nem valida o arquivo. Quem faz isso é o cURL, na hora da requisição. O Certificate confere que o arquivo existe, que dá para ler e que a extensão é uma que o cURL entende — senha errada ou arquivo corrompido aparecem como erro de conexão na primeira chamada.
  • Não guarda a senha onde se possa ler. __debugInfo() troca por ********, então var_dump() e print_r() não a mostram. Não existe getter para ela.