Module: Portless::Trust

Defined in:
lib/portless/trust.rb

Overview

Install / remove our CA in the OS trust store so browsers accept the per-host certs. macOS uses the login keychain (GUI/Touch-ID auth — no sudo); Linux drops it in the distro anchor dir + runs update-ca-trust (sudo). Firefox and Chrome-on-Linux ignore the OS store and read their own NSS DBs, so we also install via certutil into every Firefox profile + ~/.pki/nssdb. A ca.trusted fingerprint marker short-circuits the check. Mirrors mkcert/ portless's trustCA paths.

Constant Summary collapse

NSS_NICKNAME =

The nickname our CA carries inside NSS DBs (Firefox/Chrome).

"rb-portless Local CA"

Class Method Summary collapse

Class Method Details

.certsObject



17
# File 'lib/portless/trust.rb', line 17

def certs = @certs ||= Certs.new

.certutilObject

certutil's path, or nil. Memoised; resolved off PATH so we never shell out just to probe for it.



140
141
142
143
144
145
# File 'lib/portless/trust.rb', line 140

def certutil
  return @certutil if defined?(@certutil)

  @certutil = ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).map { |dir| File.join(dir, "certutil") }
                 .find { |path| File.executable?(path) && !File.directory?(path) }
end

.elevateObject



85
86
87
# File 'lib/portless/trust.rb', line 85

def elevate
  Privilege.reexec_with_sudo([ "trust" ]) || raise(Error, "sudo required to trust the CA on Linux")
end

.firefox_profiles(home = Dir.home) ⇒ Object

Firefox keeps a profile dir (each with its own cert9.db) under a handful of roots depending on packaging — native, Snap, Flatpak, and macOS.



122
123
124
125
126
127
128
129
130
131
132
# File 'lib/portless/trust.rb', line 122

def firefox_profiles(home = Dir.home)
  [
    File.join(home, ".mozilla", "firefox"),
    File.join(home, "snap", "firefox", "common", ".mozilla", "firefox"),
    File.join(home, ".var", "app", "org.mozilla.firefox", ".mozilla", "firefox"),
    File.join(home, "Library", "Application Support", "Firefox", "Profiles")
  ].select { |root| File.directory?(root) }.flat_map do |root|
    Dir.children(root).map { |child| File.join(root, child) }
       .select { |dir| File.exist?(File.join(dir, "cert9.db")) }
  end
end

.install!Object



24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/portless/trust.rb', line 24

def install!
  certs.ensure_ca!
  if Constants::MACOS
    install_macos
  elsif linux?
    install_linux
  else
    raise Error, unsupported_message
  end
  install_nss # Firefox (every OS) + Chrome-on-Linux read their own NSS DBs.
  write_marker
end

.install_linuxObject

── Linux (distro anchors + update tool; needs root) ─────────────────



56
57
58
59
60
61
62
63
# File 'lib/portless/trust.rb', line 56

def install_linux
  dir, update = linux_anchor
  return elevate if !Privilege.root? && !File.writable?(dir)

  require "fileutils"
  FileUtils.cp(State.ca_cert, File.join(dir, "rb-portless.crt"))
  system(*update) || raise(Error, "failed to run #{update.first}")
end

.install_macosObject

── macOS ────────────────────────────────────────────────────────────

Raises:



50
51
52
53
# File 'lib/portless/trust.rb', line 50

def install_macos
  ok = system("security", "add-trusted-cert", "-r", "trustRoot", "-k", , State.ca_cert)
  raise Error, "failed to trust the CA via `security add-trusted-cert`" unless ok
end

.install_nssObject

── Firefox / Chrome NSS DBs (no sudo — they live under $HOME) ───────── Browsers that ship their own cert store ignore the OS trust anchor, so add the CA to each NSS DB directly. Best-effort: a missing certutil or a single failing profile must never abort the trust flow.



93
94
95
96
97
98
99
100
101
102
# File 'lib/portless/trust.rb', line 93

def install_nss
  dbs = nss_dbs
  return if dbs.empty?
  return warn_no_certutil unless certutil

  dbs.each do |db|
    system(certutil, "-A", "-d", "sql:#{db}", "-t", "C,,", "-n", NSS_NICKNAME, "-i", State.ca_cert,
           out: File::NULL, err: File::NULL)
  end
end

.linux?Boolean

Returns:

  • (Boolean)


147
# File 'lib/portless/trust.rb', line 147

def linux? = RbConfig::CONFIG["host_os"] =~ /linux/i

.linux_anchorObject

Map the distro to its CA anchor dir + refresh command.



73
74
75
76
77
78
79
80
81
82
83
# File 'lib/portless/trust.rb', line 73

def linux_anchor
  id = File.read("/etc/os-release")[/^ID=(\w+)/, 1] rescue nil
  case id
  when "fedora", "rhel", "centos", "rocky", "almalinux"
    [ "/etc/pki/ca-trust/source/anchors", %w[update-ca-trust] ]
  when "arch", "manjaro"
    [ "/etc/ca-certificates/trust-source/anchors", %w[update-ca-trust] ]
  else # debian/ubuntu and friends
    [ "/usr/local/share/ca-certificates", %w[update-ca-certificates] ]
  end
end

.login_keychainObject



161
162
163
164
# File 'lib/portless/trust.rb', line 161

def 
  out = `security login-keychain -d user 2>/dev/null`.strip.delete('"')
  out.empty? ? File.expand_path("~/Library/Keychains/login.keychain-db") : out
end

.marker_matches?Boolean

Returns:

  • (Boolean)


149
150
151
152
153
154
# File 'lib/portless/trust.rb', line 149

def marker_matches?
  File.exist?(State.ca_trusted_marker) &&
    File.read(State.ca_trusted_marker).strip == certs.ca_fingerprint
rescue StandardError
  false
end

.nss_dbs(home = Dir.home) ⇒ Object

Every NSS DB worth trusting into: Chrome's shared store plus each Firefox profile. An NSS sql DB is a directory holding a cert9.db.



114
115
116
117
118
# File 'lib/portless/trust.rb', line 114

def nss_dbs(home = Dir.home)
  ([ File.join(home, ".pki", "nssdb") ] + firefox_profiles(home))
    .select { |db| File.exist?(File.join(db, "cert9.db")) }
    .uniq
end

.trusted?Boolean

Returns:

  • (Boolean)


19
20
21
22
# File 'lib/portless/trust.rb', line 19

def trusted?
  certs.ensure_ca!
  marker_matches?
end

.uninstall!Object



37
38
39
40
41
42
43
44
45
46
47
# File 'lib/portless/trust.rb', line 37

def uninstall!
  return unless File.exist?(State.ca_cert)

  uninstall_nss
  if Constants::MACOS
    system("security", "remove-trusted-cert", State.ca_cert)
  elsif linux?
    uninstall_linux
  end
  File.delete(State.ca_trusted_marker) if File.exist?(State.ca_trusted_marker)
end

.uninstall_linuxObject



65
66
67
68
69
70
# File 'lib/portless/trust.rb', line 65

def uninstall_linux
  dir, update = linux_anchor
  crt = File.join(dir, "rb-portless.crt")
  File.delete(crt) if File.exist?(crt)
  system(*update)
end

.uninstall_nssObject



104
105
106
107
108
109
110
# File 'lib/portless/trust.rb', line 104

def uninstall_nss
  return unless certutil

  nss_dbs.each do |db|
    system(certutil, "-D", "-d", "sql:#{db}", "-n", NSS_NICKNAME, out: File::NULL, err: File::NULL)
  end
end

.unsupported_messageObject



166
167
168
# File 'lib/portless/trust.rb', line 166

def unsupported_message
  "automatic CA trust isn't wired for this OS yet — trust #{State.ca_cert} manually"
end

.warn_no_certutilObject



134
135
136
# File 'lib/portless/trust.rb', line 134

def warn_no_certutil
  warn "rb-portless: install `nss`/`libnss3-tools` (certutil) to trust the CA in Firefox/Chrome"
end

.write_markerObject



156
157
158
159
# File 'lib/portless/trust.rb', line 156

def write_marker
  File.write(State.ca_trusted_marker, certs.ca_fingerprint)
  State.fix_ownership
end