Module: AceMQ::AMQP::DevelopmentCertificates

Defined in:
lib/acemq/amqp/dev_certificates.rb

Overview

Generates a throwaway certificate authority and the certificates a local broker and client need, so that running with TLS on a developer's machine is one command rather than an afternoon with openssl.

AceMQ::AMQP::DevelopmentCertificates.generate(directory: "certs")

or from a checkout:

./scripts/acemq-certs.rb --out certs --broker localhost --days 30

The marker

Everything produced here carries ACEMQ DEVELOPMENT ONLY - DO NOT TRUST in its subject organisation, and Security refuses a certificate carrying it however trust is configured — unverified mode included — unless Security#allowing_development_certificates says otherwise. That is the point: a self-signed authority that drifts into production is worse than no encryption, because everything looks protected and nothing is verified. These fail closed instead.

The marker goes in the organisation rather than the organisational unit, which is where Go and .NET put it and where every tool that prints a subject shows it. The Java library puts it in OU, so a Java-generated certificate is caught here too — the check is on the whole subject and the whole issuer — but a certificate generated here matches what scripts/tls-broker.sh greps for.

What it writes

The six files the other libraries' generators write, under the same names, so this generator and Go's acemq-certs are interchangeable:

ca.crt      the authority, to trust
ca.key      its key, so more certificates can be signed later
server.crt  the broker's certificate
server.key  the broker's key
client.crt  a client certificate, for EXTERNAL authentication
client.key  its key

and, unless asked not to, a rabbitmq.conf that serves TLS from them.

Keys are written 0600. A development key is still a key, and one world-readable in a repository checkout is a habit worth not forming.

Certificates are short-lived by default. A development certificate that never expires is one that outlives the reason it was created.

Elliptic curve, not RSA

P-256, as the Go generator uses. RSA-4096 as the Java generator uses is equally fine and takes several seconds per key in Ruby, which is a long time to wait for something whose whole promise is that it is one command.

Defined Under Namespace

Classes: Result

Constant Summary collapse

MARKER =

Written into every subject, and refused by Security unless explicitly allowed. The same string in all five languages.

Security::DEVELOPMENT_MARKER
AUTHORITY_NAME =

The authority's common name, shared with Go and .NET.

"AceMQ development CA"
CLIENT_NAME =

The client certificate's common name, shared with Go and .NET.

"acemq-client"
DEFAULT_VALIDITY_DAYS =

Short on purpose.

30
DEFAULT_BROKER_CERTIFICATE_DIRECTORY =

Where rabbitmq.conf will look, which is where the certificates are mounted inside the broker's container rather than where they are here.

"/certs"
CURVE =

The curve. P-256 is what Go's generator uses and what every broker and every client in this decade supports.

"prime256v1"

Class Method Summary collapse

Class Method Details

.broker_configuration(certificate_directory = DEFAULT_BROKER_CERTIFICATE_DIRECTORY) ⇒ String

A rabbitmq.conf that serves TLS from what generate wrote.

verify_peer with fail_if_no_peer_cert off means the broker will use a client certificate if one is presented and will not insist on it, which is what suits a development broker that is also reached by password.

Parameters:

  • certificate_directory (String) (defaults to: DEFAULT_BROKER_CERTIFICATE_DIRECTORY) —

    the path inside the broker

Returns:

  • (String)


161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/acemq/amqp/dev_certificates.rb', line 161

def broker_configuration(certificate_directory = DEFAULT_BROKER_CERTIFICATE_DIRECTORY)
  <<~CONF
    # Written by AceMQ::AMQP::DevelopmentCertificates. Development only.
    listeners.ssl.default = 5671

    ssl_options.cacertfile = #{certificate_directory}/ca.crt
    ssl_options.certfile   = #{certificate_directory}/server.crt
    ssl_options.keyfile    = #{certificate_directory}/server.key
    ssl_options.verify     = verify_peer
    ssl_options.fail_if_no_peer_cert = false

    # The plaintext listener stays on so a laptop can use either.
    listeners.tcp.default = 5672
  CONF
end

.generate(directory: "certs", broker_host: "localhost", days: DEFAULT_VALIDITY_DAYS, broker_certificate_directory: DEFAULT_BROKER_CERTIFICATE_DIRECTORY, broker_config: true) ⇒ Result

Writes a certificate authority, a broker certificate and a client certificate.

Existing files are overwritten. These are development certificates with a short life, so regenerating them is the expected way to deal with expiry rather than something to guard against — but regenerating changes the authority, which invalidates anything already trusting it, including a broker somebody is still pointing at.

Parameters:

  • directory (String) (defaults to: "certs") —

    where to write them; created if absent

  • broker_host (String) (defaults to: "localhost") —

    the name the broker's certificate is issued for, and the name a client must connect by for verification to succeed

  • days (Integer) (defaults to: DEFAULT_VALIDITY_DAYS) —

    how long the certificates last

  • broker_certificate_directory (String) (defaults to: DEFAULT_BROKER_CERTIFICATE_DIRECTORY) —

    the path rabbitmq.conf points at

  • broker_config (Boolean) (defaults to: true) —

    whether to write rabbitmq.conf

Returns:



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/acemq/amqp/dev_certificates.rb', line 122

def generate(directory: "certs", broker_host: "localhost",
             days: DEFAULT_VALIDITY_DAYS,
             broker_certificate_directory: DEFAULT_BROKER_CERTIFICATE_DIRECTORY,
             broker_config: true)
  FileUtils.mkdir_p(directory)
  # Backdated an hour. A machine whose clock is a few minutes behind the
  # one that generated these would otherwise reject them as not yet
  # valid, which is a confusing way to spend a morning.
  life = ((Time.now - 3600)..(Time.now + (days * 24 * 60 * 60)))

  ca_key, authority = authority_pair(life)
  server_key, server = leaf_pair(broker_host, "serverAuth", ca_key, authority, life,
                                 alternative_names(broker_host))
  client_key, client = leaf_pair(CLIENT_NAME, "clientAuth", ca_key, authority, life,
                                 nil)

  written = write(directory,
                  "ca" => [authority, ca_key],
                  "server" => [server, server_key],
                  "client" => [client, client_key])
  config = File.join(directory, "rabbitmq.conf") if broker_config
  if config
    written << write_file(config, broker_configuration(broker_certificate_directory),
                          0o644)
  end

  Result.new(directory: directory, authority: authority, server: server, client: client,
             expiry: life.end, files: written, broker_config: config)
end