Certificado digital

Copy Markdown View Source

A NFS-e Nacional autentica quem chama por mTLS, com o certificado A1 da ICP-Brasil do emitente, e a mesma chave assina a DPS. A biblioteca lê o arquivo .pfx em memória e guarda o resultado em %Nfse.Certificate{}.

Abrindo o .pfx

bundle = File.read!("certificado.pfx")

{:ok, certificate} = Nfse.Certificate.from_pfx(bundle, System.fetch_env!("NFSE_PFX_PASSWORD"))

certificate.cnpj
#=> "11222333000181"
certificate.not_after
#=> ~U[2027-03-01 12:00:00Z]
certificate.client_auth
#=> true

O CNPJ (ou o CPF, no certificado de pessoa física) sai do certificado. A senha vem do ambiente ou de um cofre de segredos, nunca do código.

Nfse.Certificate.from_pfx/3 chama o openssl pkcs12 do sistema, porque o OTP não lê PKCS#12. O arquivo e a senha seguem para o processo filho pelo stdin e por variável de ambiente, nunca por arquivo temporário nem por argumento de linha de comando.

Erros

Todo erro é um %Nfse.Error{kind: :certificate} e o motivo fica em details:

detailsQuando
:invalid_passworda senha não abre o arquivo
:invalid_fileo arquivo não é um PKCS#12 completo, ou falta a chave ou o certificado
:legacy_provider_missingo arquivo usa cifra antiga e o openssl do servidor não tem o provider legacy
:openssl_missingo executável openssl não está no PATH do servidor; o arquivo pode estar perfeito
:timeouto openssl não respondeu a tempo
:unsupported_keya chave não é RSA
:expiredo certificado venceu (devolvido por Nfse.Client.new/1)
:missing_client_autho certificado não serve para autenticar o cliente (devolvido por Nfse.Client.new/1)
case Nfse.Certificate.from_pfx(bundle, password) do
  {:ok, certificate} -> certificate
  {:error, %Nfse.Error{kind: :certificate, details: :invalid_password}} -> :senha_errada
  {:error, %Nfse.Error{kind: :certificate, details: :legacy_provider_missing}} -> :instalar_provider
  {:error, %Nfse.Error{kind: :certificate, details: other}} -> {:certificado_invalido, other}
end

Servidor sem o provider legacy

A maioria dos A1 ainda é exportada com a cifra PBE-SHA1-RC2-40, que o OpenSSL 3 trouxe para o provider legacy. Instale o pacote que o contém (openssl-provider-legacy). Sem ele, o certificado real falha com :legacy_provider_missing enquanto um arquivo de teste moderno continua abrindo, o que engana quem só testa no próprio computador.

Se o seu certificado já vem em cifra moderna, nada disso é preciso. Quem tem as peças em DER pode usar Nfse.Certificate.from_der/3 e dispensar o openssl.

Validade

Nfse.Client.new/1 recusa, antes de qualquer chamada de rede, o certificado vencido e o que não traz a extensão clientAuth. A Sefin responde 403 sem explicação a esse segundo caso, e por isso a verificação acontece cedo. Nfse.Certificate.expired?/2 ajuda a avisar o cliente da renovação antes do vencimento:

Nfse.Certificate.expired?(certificate, DateTime.add(DateTime.utc_now(), 30, :day))
#=> false