The security envelope of the RSFN (Rede do Sistema Financeiro Nacional), the network that connects Brazilian financial institutions, clearing houses and Banco Central do Brasil, for Elixir.

It builds and opens the 588-byte security header defined in chapter 5 of the public Manual de Segurança do SFN (Volume I, version 6.00; chapter 5 is the same as in version 5.07), with Security Protocol v3 (AES-256-GCM) and v2 (3DES, legacy): signing, encryption, compression and the C03 error table. Newer versions of the manual are published on the Comunicação Eletrônica de Dados page of Banco Central.

Leia em português.

Status: v0.1, pre-release. The library follows the manual and is tested against an independent implementation, but a few points where the manual is silent follow observed market practice and have not been confirmed against real traffic yet. They are listed in docs/SPEC.md. Version 1.0 waits for that confirmation.

Sinetex is an independent open source project. It is not affiliated with or endorsed by Banco Central do Brasil.

Scope

In: the header (C01–C15), sending and receiving in v2 and v3, every C04 value, the error table with the "lowest code wins" rule, GZIP and ZIP.

Out: transport (MQ, Connect:Direct, SFTP), the XML of the messages, certificate registration and activation, revocation lists, key storage. The library works on bytes: you give it the content and the keys, it gives you the envelope, and the other way round. It has no runtime dependencies besides :crypto and :public_key.

Installation

def deps do
  [{:sinetex, "~> 0.1"}]
end

Elixir 1.16 or later, OTP 26 or later. Version 2 of the protocol needs 3DES in the OpenSSL that OTP is linked to.

Usage

Keys and certificates

# Your certificate and private key (PEM or DER; PKCS#1, PKCS#8 or encrypted PKCS#8)
{:ok, me} = Sinetex.Identity.load(File.read!("me.crt"), File.read!("me.key"))

# The counterparty's certificate
{:ok, peer} = Sinetex.Certificate.load(File.read!("peer.crt"))

C10 and C12 carry the code of the certification authority (PC). It is read from OU=CSPB-n in the certificate issuer. For certificates without it, give it explicitly: Sinetex.Certificate.load(pem, pc: 4).

Certificates must be RSA-2048 with a serial number of at most 16 bytes, as the header requires.

Sending

{:ok, envelope} = Sinetex.seal(xml, from: me, to: peer)

# A compressed file (C04 8). The format is required: see below.
{:ok, envelope} = Sinetex.seal(file, from: me, to: peer, c04: 8, compression: :gzip)

# Version 2
{:ok, envelope} = Sinetex.seal(xml, from: me, to: peer, version: 2)

Receiving

case Sinetex.open(envelope, as: me, from: peer) do
  {:ok, %Sinetex.Opened{content: content, header: header}} ->
    # header.c03 != 0 means the counterparty is reporting an error
    ...

  {:error, %Sinetex.Error{c03: c03, egen: egen} = error} ->
    # c03 is the code of the manual's table, egen the GEN0004 code (EGEN99xx)
    Logger.warning(Exception.message(error))
end

:from accepts a list of certificates, for a sender that is changing certificates. Other options:

OptionDefault
:known[]Other certificates. A message signed with one of them fails with 0x0C (certificate does not belong to the sender) instead of 0x0A
:certificate_checknonefn cert, header -> :ok | {:error, c03} end, for what depends on your certificate registry: not activated (0x10), expired or revoked (0x11), invalid (0x14)
:c14_errors:manual:uniform reports a failure to decrypt C14 as 0x0E instead of 0x0D. See SECURITY.md
:trim_paddingfalsev2 keeps its 0x00 padding, as the manual says. true removes up to 7 trailing zeros
:allow_clearfalseAccept the "empty" header (all fields zero, e.g. a GEN0001 echo), whose body is neither signed nor encrypted
:max_output_bytes512 MiBLimit for decompressed content
:max_input_bytes:infinityLimit for the envelope

Reporting an error

The library never answers anyone. When open/2 fails, you decide whether to send a GEN0004 back. error_envelope/2 builds its envelope: signed, not encrypted (C04 3), with C03 set. The GEN0004 XML is yours to build.

with {:error, %Sinetex.Error{} = error} <- Sinetex.open(envelope, as: me, from: peer) do
  Sinetex.error_envelope(gen0004_xml(error.egen), from: me, to: peer, c03: error.c03)
end

Compression

With C04 8 and 10, :compression is required when sending. The library has no default because the right format depends on the destination: clearing houses use GZIP, while the manual and the Bacen SFTP servers use ZIP with DEFLATE.

When receiving, the format is detected by its magic bytes, not by C04, and %Sinetex.Opened{compression: :gzip | :zip | :none} tells which one was found. Content marked as compressed that is not GZIP or ZIP is returned as is, which has been observed in practice.

Inspecting real traffic

tools/header_dump.exs prints the header of an envelope without decrypting anything and without keys. It does not need the library:

elixir tools/header_dump.exs envelope.bin

Documentation

  • API documentation: HexDocs.
  • docs/SPEC.md: what the library does with each byte, with the source of every rule (manual, market practice or library decision).
  • guides/manual.md (Portuguese): each function next to the section of the manual it implements.
  • SECURITY.md: security notes and how to report a vulnerability.

Development

mix test

The test certificates and keys are generated by test/support/gen_fixtures.sh (needs openssl) on the first run. No key or sample from any third party is in the repository.

The interoperability tests run against an independent implementation in Python, written from the specification only. They need Python 3 with the cryptography package and are skipped without it:

python3 -m venv .venv && .venv/bin/pip install cryptography
SINETEX_PYTHON=.venv/bin/python mix test

License

Apache-2.0: see the LICENSE and NOTICE files.