Class: Portless::Certs

Inherits:
Object
  • Object
show all
Defined in:
lib/portless/certs.rb

Overview

Local CA + per-host leaf certs, all in native Ruby OpenSSL (portless shells out to the openssl binary; we don't have to). Because *.localhost wildcard certs aren't honoured at the reserved-TLD boundary, every SNI hostname gets its own leaf, minted on demand and cached on disk + in memory.

Constant Summary collapse

CA_SUBJECT =
"/CN=rb-portless Local CA"
CA_DAYS =
3650
LEAF_DAYS =
365
CURVE =
"prime256v1"

Instance Method Summary collapse

Constructor Details

#initializeCerts

Returns a new instance of Certs.



17
18
19
# File 'lib/portless/certs.rb', line 17

def initialize
  @leaves = {} # hostname => [cert, key]
end

Instance Method Details

#ca_certificateObject

The CA certificate (PEM-loaded), generating + persisting one on first use.



22
23
24
25
26
27
# File 'lib/portless/certs.rb', line 22

def ca_certificate
  @ca_certificate ||= begin
    ensure_ca!
    OpenSSL::X509::Certificate.new(File.read(State.ca_cert))
  end
end

#ca_fingerprintObject

SHA-256 fingerprint — used by the trust marker + OS trust check.



37
38
39
# File 'lib/portless/certs.rb', line 37

def ca_fingerprint
  OpenSSL::Digest::SHA256.hexdigest(ca_certificate.to_der)
end

#ca_keyObject



29
30
31
32
33
34
# File 'lib/portless/certs.rb', line 29

def ca_key
  @ca_key ||= begin
    ensure_ca!
    OpenSSL::PKey.read(File.read(State.ca_key))
  end
end

#ensure_ca!Object



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/portless/certs.rb', line 48

def ensure_ca!
  return if File.exist?(State.ca_cert) && File.exist?(State.ca_key)

  State.ensure_dir!
  key = OpenSSL::PKey::EC.generate(CURVE)
  cert = OpenSSL::X509::Certificate.new
  name = OpenSSL::X509::Name.parse(CA_SUBJECT)
  cert.version = 2
  cert.serial = random_serial
  cert.subject = name
  cert.issuer = name
  cert.public_key = ec_public(key)
  cert.not_before = Time.now - 60
  cert.not_after = Time.now + CA_DAYS * 86_400

  ef = OpenSSL::X509::ExtensionFactory.new
  ef.subject_certificate = cert
  ef.issuer_certificate = cert
  cert.add_extension(ef.create_extension("basicConstraints", "CA:TRUE", true))
  cert.add_extension(ef.create_extension("keyUsage", "keyCertSign,cRLSign", true))
  cert.add_extension(ef.create_extension("subjectKeyIdentifier", "hash"))
  cert.sign(key, OpenSSL::Digest.new("SHA256"))

  write_secret(State.ca_key, key.to_pem)
  File.write(State.ca_cert, cert.to_pem)
  State.fix_ownership
end

#leaf_for(hostname) ⇒ Object

[cert, key] for an SNI hostname. Cached in memory; persisted under host-certs/ so a proxy restart doesn't re-mint everything.



43
44
45
46
# File 'lib/portless/certs.rb', line 43

def leaf_for(hostname)
  hostname = hostname.to_s.downcase
  @leaves[hostname] ||= load_leaf(hostname) || generate_leaf(hostname)
end