Nfse.Certificate (nfse v0.1.0)

Copy Markdown View Source

Certificado A1 (um .pfx PKCS#12) aberto nas peças em DER que :ssl e :public_key pedem, mais o que a biblioteca precisa saber dele: CNPJ ou CPF, validade e clientAuth.

O OTP não tem leitor de PKCS#12 (:public_key não exporta um, e PFX é um tipo indefinido em OTP-PUB-KEY e PKCS-FRAME), então from_pfx/3 chama openssl pkcs12. Duas regras tornam isso seguro, e nenhuma é opcional:

  • o bundle entra pelo stdin do processo filho, nunca por arquivo temporário, porque o arquivo guardaria a chave privada sem cifra;
  • a senha entra por variável de ambiente do filho, nunca por argv, porque o argv é legível por qualquer usuário no ps.

A maior parte dos A1 da ICP-Brasil ainda é exportada com PBE-SHA1-RC2-40, que o OpenSSL 3 moveu para o provider legacy. A primeira tentativa vai sem -legacy e só então repete com ele: um export moderno custa uma chamada e um antigo custa duas. O motivo da ordem é que o openssl se recusa a rodar quando não consegue carregar um provider pedido na linha de comando, então começar por -legacy perderia os bundles modernos em todo host sem o pacote.

Em produção esse provider vem no pacote openssl-provider-legacy. Sem ele, todo certificado real falha ao abrir enquanto os fixtures no Mac do desenvolvedor continuam funcionando, por isso :legacy_provider_missing é um erro próprio e não :invalid_file. Um subprocesso que nunca responde recebe o mesmo tratamento e volta como :timeout, e um host sem o executável openssl no PATH volta como :openssl_missing: nenhum dos três é problema do arquivo, e mandar exportar o certificado de novo levaria a pessoa atrás da coisa errada.

Os erros são %Nfse.Error{kind: :certificate} e details é um destes:

detailsQuando
:invalid_passworda senha não abre o bundle
:invalid_fileo arquivo não é um PKCS#12 completo, ou falta chave ou folha
:legacy_provider_missingo bundle usa cifra legada e o openssl não tem o provider
:openssl_missingo executável openssl não está no PATH
:timeouto openssl não respondeu dentro do prazo
:unsupported_keya chave não é RSA, e a assinatura da DPS exige RSA

Summary

Functions

Diz se now já passou do fim da validade.

Monta o certificado a partir de peças em DER que o chamador já tem, sem passar pelo openssl.

Abre bundle com password.

Types

t()

@type t() :: %Nfse.Certificate{
  certificate: binary(),
  chain: [binary()],
  client_auth: boolean(),
  cnpj: String.t() | nil,
  cpf: String.t() | nil,
  key: {:RSAPrivateKey, binary()},
  not_after: DateTime.t(),
  not_before: DateTime.t()
}

Functions

expired?(certificate, now)

@spec expired?(t(), DateTime.t()) :: boolean()

Diz se now já passou do fim da validade.

from_der(certificate, key, chain)

@spec from_der(binary(), {:RSAPrivateKey, binary()}, [binary()]) ::
  {:ok, t()} | {:error, Nfse.Error.t()}

Monta o certificado a partir de peças em DER que o chamador já tem, sem passar pelo openssl.

from_pfx(bundle, password, opts \\ [])

@spec from_pfx(binary(), String.t(), [{:timeout, pos_integer()}]) ::
  {:ok, t()} | {:error, Nfse.Error.t()}

Abre bundle com password.

A folha é escolhida casando a chave pública de cada certificado com a chave privada: a ordem dentro de um .pfx não é especificada, e pegar o primeiro certificado devolve uma intermediária com frequência suficiente para importar.

Opções

  • :timeout - prazo em milissegundos para a chamada inteira, incluindo a repetição com -legacy. Padrão: 15_000.