yamine 0.9.3 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +45 -0
- data/lib/yamine/certs.rb +10 -18
- data/lib/yamine/cli/system.rb +55 -8
- data/lib/yamine/trust.rb +121 -59
- data/lib/yamine/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 73e497200efd0723559dff2f57e920e3d126d3caecf302770c37bec613d7998e
|
|
4
|
+
data.tar.gz: 2203cabc149df919703fb1182fb9ad76e9c6729d4ed105455c749dbfa7611593
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d30e26aec61c7036e349a84f6f241291756c7ee3207f6e1de210cb47465ccc94279619cc9657d3ecbc477b8983be8027ae2303d43456b4891762f79dec7bd885
|
|
7
|
+
data.tar.gz: 1a6a7e1b4e402b822a3d6f70ed6b19881c8927563c131875b322a7b750c9ec9368ba8a7f45ff99badfe55d23c54a964f163a80b8d10d6c45aa6a5a3ae5d9abc9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.10.1] — 2026-09-10
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **The CA is regenerated before trust is judged, not after.** Both
|
|
8
|
+
trust paths asked `Certs.trusted?` first, which compares the trust
|
|
9
|
+
marker against the certificate ON DISK. A proxy regenerates the CA at
|
|
10
|
+
boot when the on-disk one is missing, expiring, or renamed — so after
|
|
11
|
+
a rename the marker still matched the OLD certificate, trust was
|
|
12
|
+
skipped as "already done", and the proxy then came up serving a CA the
|
|
13
|
+
keychain did not trust: TLS verification fails for every route. The
|
|
14
|
+
check now brings the CA up to date first (`ca_current_and_trusted?`,
|
|
15
|
+
used by both paths).
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
## [0.10.0] — 2026-09-10
|
|
19
|
+
|
|
20
|
+
The ask-local rename is finished: the two user-visible names it left
|
|
21
|
+
behind are now yamine's own, and both migrate existing machines.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **The CA is named "Yamine CA"** (was "Ask Local CA", inherited from
|
|
26
|
+
the rename). The old name stays in `Certs::LEGACY_CA_COMMON_NAMES`, so
|
|
27
|
+
a machine carrying a pre-rename CA can still shed it. Because
|
|
28
|
+
`valid_pair?` requires the on-disk CA to carry the current name, the
|
|
29
|
+
first run after upgrading generates a new CA and re-trusts it — once,
|
|
30
|
+
automatically, as part of the normal trust path.
|
|
31
|
+
- **The launchd service is `dev.yamine`** (was `dev.ask.local`). The old
|
|
32
|
+
plist is booted out and deleted during install and uninstall: a label
|
|
33
|
+
is how launchctl addresses a service, so leaving the old one loaded
|
|
34
|
+
would put two root proxies in a race for port 443, the loser
|
|
35
|
+
crash-looping under `KeepAlive`.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- `prune_stale` protects the CA a running proxy signs with, identified
|
|
40
|
+
by signature rather than by skipping the prune entirely. A proxy loads
|
|
41
|
+
its CA once at startup and mints host certs from that in-memory copy,
|
|
42
|
+
so it keeps signing with its boot-time CA whatever is on disk —
|
|
43
|
+
removing that certificate would break TLS for every live route until a
|
|
44
|
+
restart. Since superseded CAs share a name with the live one, only a
|
|
45
|
+
signature check can tell them apart.
|
|
46
|
+
|
|
47
|
+
|
|
3
48
|
## [0.9.3] — 2026-09-10
|
|
4
49
|
|
|
5
50
|
### Fixed
|
data/lib/yamine/certs.rb
CHANGED
|
@@ -11,23 +11,15 @@ module Yamine
|
|
|
11
11
|
# through the SNI callback and held in an in-memory LRU
|
|
12
12
|
# (puma-dev ssl.go does the same; portless caches on disk instead).
|
|
13
13
|
module Certs
|
|
14
|
-
# The name is
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
|
|
23
|
-
# longer decide what gets removed.
|
|
24
|
-
#
|
|
25
|
-
# CA_COMMON_NAMES is the list of names we have ever generated. Add to
|
|
26
|
-
# it on any future rename so old certificates stay prunable —
|
|
27
|
-
# Trust.prune_stale only ever removes certificates whose subject is
|
|
28
|
-
# one of these.
|
|
29
|
-
CA_COMMON_NAME = "Ask Local CA"
|
|
30
|
-
CA_COMMON_NAMES = [CA_COMMON_NAME].freeze
|
|
14
|
+
# The CA's name is user-visible in the trust store and is what a
|
|
15
|
+
# cleanup keys off, so it belongs to this gem alone. It was inherited
|
|
16
|
+
# from the ask-local rename and had said "Ask Local CA" ever since.
|
|
17
|
+
CA_COMMON_NAME = "Yamine CA"
|
|
18
|
+
# Every name we have ever generated a CA under. Stale certificates
|
|
19
|
+
# under these are prunable (Trust.prune_stale); anything else in the
|
|
20
|
+
# trust store is none of our business. The pre-rename name stays here
|
|
21
|
+
# so machines that trusted an ask-local CA can still shed it.
|
|
22
|
+
LEGACY_CA_COMMON_NAMES = ["Ask Local CA"].freeze
|
|
31
23
|
CA_VALIDITY_DAYS = 3650
|
|
32
24
|
HOST_VALIDITY_DAYS = 825
|
|
33
25
|
CACHE_SIZE = 1024
|
|
@@ -35,7 +27,7 @@ module Yamine
|
|
|
35
27
|
module_function
|
|
36
28
|
|
|
37
29
|
def ca_common_names
|
|
38
|
-
|
|
30
|
+
[CA_COMMON_NAME, *LEGACY_CA_COMMON_NAMES].uniq
|
|
39
31
|
end
|
|
40
32
|
|
|
41
33
|
def state_dir
|
data/lib/yamine/cli/system.rb
CHANGED
|
@@ -4,6 +4,13 @@ module Yamine
|
|
|
4
4
|
class CLI
|
|
5
5
|
# System commands: proxy, service, hosts, trust, clean, doctor, kamal.
|
|
6
6
|
module SystemCommand
|
|
7
|
+
# The launchd label: the service's identity, and what `launchctl
|
|
8
|
+
# enable/kickstart/bootout` address it by. Renamed from "dev.ask.local"
|
|
9
|
+
# (an ask-local leftover); the old label is cleared on install so the
|
|
10
|
+
# two never coexist and fight over port 443.
|
|
11
|
+
LAUNCHD_LABEL = "dev.yamine"
|
|
12
|
+
LEGACY_LAUNCHD_LABELS = %w[dev.ask.local].freeze
|
|
13
|
+
|
|
7
14
|
module_function
|
|
8
15
|
|
|
9
16
|
def doctor(ctx, args)
|
|
@@ -291,7 +298,7 @@ module Yamine
|
|
|
291
298
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
292
299
|
<plist version="1.0">
|
|
293
300
|
<dict>
|
|
294
|
-
<key>Label</key><string
|
|
301
|
+
<key>Label</key><string>#{LAUNCHD_LABEL}</string>
|
|
295
302
|
<key>ProgramArguments</key>
|
|
296
303
|
<array>
|
|
297
304
|
<string>#{RbConfig.ruby}</string>
|
|
@@ -309,7 +316,8 @@ module Yamine
|
|
|
309
316
|
</dict>
|
|
310
317
|
</plist>
|
|
311
318
|
PLIST
|
|
312
|
-
|
|
319
|
+
remove_legacy_launchd(dir)
|
|
320
|
+
path = File.join(dir, "#{LAUNCHD_LABEL}.plist")
|
|
313
321
|
File.write(path, plist)
|
|
314
322
|
File.chmod(0o644, path)
|
|
315
323
|
# launchd requires /Library/LaunchDaemons plists to be
|
|
@@ -326,13 +334,29 @@ module Yamine
|
|
|
326
334
|
puts "Installed root LaunchDaemon on port 443 (state: #{state_dir})."
|
|
327
335
|
end
|
|
328
336
|
|
|
337
|
+
# True when the trust store covers the CA this machine should be
|
|
338
|
+
# serving with — regenerating the CA first if it is missing,
|
|
339
|
+
# expiring, or renamed.
|
|
340
|
+
#
|
|
341
|
+
# The ordering is the whole point, so it lives in one place rather
|
|
342
|
+
# than at each call site. `trusted?` compares a marker against the
|
|
343
|
+
# fingerprint of the certificate ON DISK, and a proxy regenerates
|
|
344
|
+
# the CA at boot when the name changes. Checking the marker before
|
|
345
|
+
# the CA is current therefore reads a stale match, skips trust, and
|
|
346
|
+
# lets the proxy come up serving a CA the keychain does not trust —
|
|
347
|
+
# breaking TLS for every route.
|
|
348
|
+
def ca_current_and_trusted?(dir = Certs.state_dir)
|
|
349
|
+
Certs.ensure_ca(dir)
|
|
350
|
+
Certs.trusted?(dir)
|
|
351
|
+
end
|
|
352
|
+
|
|
329
353
|
# Root-only CA trust: the System keychain (all users, no prompt).
|
|
330
354
|
# Safe to call repeatedly — once the marker is set it is a no-op.
|
|
331
355
|
# Prints before doing work: the System keychain add can take a few
|
|
332
356
|
# seconds, and this runs in the root half of setup where silence
|
|
333
357
|
# reads as a hang.
|
|
334
358
|
def ensure_system_ca_trust
|
|
335
|
-
return if
|
|
359
|
+
return if ca_current_and_trusted?(Certs.state_dir)
|
|
336
360
|
|
|
337
361
|
puts " Trusting the CA into the System keychain..."
|
|
338
362
|
result = Trust.trust
|
|
@@ -354,14 +378,34 @@ module Yamine
|
|
|
354
378
|
unless Command.run("launchctl", "bootstrap", "system", path)
|
|
355
379
|
raise Error, "launchctl bootstrap failed — check the plist at #{path}"
|
|
356
380
|
end
|
|
357
|
-
Command.run("launchctl", "enable", "system
|
|
358
|
-
Command.run("launchctl", "kickstart", "-k", "system
|
|
381
|
+
Command.run("launchctl", "enable", "system/#{LAUNCHD_LABEL}")
|
|
382
|
+
Command.run("launchctl", "kickstart", "-k", "system/#{LAUNCHD_LABEL}")
|
|
359
383
|
end
|
|
360
384
|
|
|
361
385
|
def launchctl_bootout(path)
|
|
362
386
|
Command.run("launchctl", "bootout", "system", path)
|
|
363
387
|
end
|
|
364
388
|
|
|
389
|
+
# The launchd label, and the pre-rename one that must be cleared.
|
|
390
|
+
# A label is the service's identity: installing the new one while
|
|
391
|
+
# the old plist is still loaded would leave TWO root proxies
|
|
392
|
+
# fighting over port 443 (whichever booted last wins, and the loser
|
|
393
|
+
# crash-loops under KeepAlive).
|
|
394
|
+
#
|
|
395
|
+
# Boot out and delete any plist from a previous label. Idempotent:
|
|
396
|
+
# bootout on a service that was never loaded prints an error and
|
|
397
|
+
# exits non-zero, which is expected and quiet.
|
|
398
|
+
def remove_legacy_launchd(dir = "/Library/LaunchDaemons")
|
|
399
|
+
LEGACY_LAUNCHD_LABELS.each do |label|
|
|
400
|
+
path = File.join(dir, "#{label}.plist")
|
|
401
|
+
next unless File.file?(path)
|
|
402
|
+
|
|
403
|
+
Command.run("launchctl", "bootout", "system", path, out: File::NULL, err: File::NULL)
|
|
404
|
+
FileUtils.rm_f(path)
|
|
405
|
+
puts " Removed legacy launchd service #{label}."
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
365
409
|
# Pure unit-file builder (testable without root). Binds 80/443 at
|
|
366
410
|
# boot; the proxy runs with the invoking user's state dir.
|
|
367
411
|
def systemd_unit
|
|
@@ -406,9 +450,12 @@ module Yamine
|
|
|
406
450
|
end
|
|
407
451
|
case RUBY_PLATFORM
|
|
408
452
|
when /darwin/
|
|
409
|
-
path = "/Library/LaunchDaemons
|
|
410
|
-
launchctl_bootout(path)
|
|
453
|
+
path = "/Library/LaunchDaemons/#{LAUNCHD_LABEL}.plist"
|
|
454
|
+
launchctl_bootout(path) if File.file?(path)
|
|
411
455
|
FileUtils.rm_f(path)
|
|
456
|
+
# Also clear any pre-rename service, so uninstall leaves no root
|
|
457
|
+
# proxy behind on a machine upgraded from an older yamine.
|
|
458
|
+
remove_legacy_launchd
|
|
412
459
|
puts "Removed root LaunchDaemon."
|
|
413
460
|
when /linux/
|
|
414
461
|
Command.run("systemctl", "disable", "--now", "yamine")
|
|
@@ -729,7 +776,7 @@ module Yamine
|
|
|
729
776
|
# those users had already chosen to avoid.
|
|
730
777
|
def ensure_workstation!(ctx)
|
|
731
778
|
# 1. CA
|
|
732
|
-
unless
|
|
779
|
+
unless ca_current_and_trusted?(ctx.store.dir)
|
|
733
780
|
result = Yamine::Trust.trust
|
|
734
781
|
unless result[:trusted]
|
|
735
782
|
abort_setup("CA trust failed: #{result[:error]}",
|
data/lib/yamine/trust.rb
CHANGED
|
@@ -32,8 +32,8 @@ module Yamine
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
# The fingerprint `security -Z` prints: uppercase hex, no colons.
|
|
35
|
-
# Certificates are identified by THIS, never by common name
|
|
36
|
-
#
|
|
35
|
+
# Certificates are identified by THIS, never by common name: every CA
|
|
36
|
+
# this project has generated shares a name with the others, so a
|
|
37
37
|
# by-name lookup or delete cannot tell the current CA from a stale
|
|
38
38
|
# one and may remove the wrong certificate.
|
|
39
39
|
def fingerprint_of(cert_path)
|
|
@@ -44,93 +44,155 @@ module Yamine
|
|
|
44
44
|
nil
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
47
|
+
# Certificates in a keychain as { fingerprint:, subject:, cert: }.
|
|
48
|
+
# Parsed in ONE call; empty when the keychain (or `security`) is
|
|
49
|
+
# unavailable, which callers treat as "cannot tell".
|
|
50
|
+
#
|
|
51
|
+
# -a is required: without it `security find-certificate` returns a
|
|
52
|
+
# single certificate, so any scan silently sees one entry and every
|
|
53
|
+
# comparison against it is wrong.
|
|
54
|
+
def keychain_certs(keychain)
|
|
55
|
+
out, status = Command.capture2("security", "find-certificate", "-a", "-Z", "-p", keychain)
|
|
55
56
|
return [] unless status.success?
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
require "digest"
|
|
59
|
+
out.split("-----BEGIN CERTIFICATE-----").filter_map do |block|
|
|
60
|
+
next unless block.include?("-----END CERTIFICATE-----")
|
|
61
|
+
|
|
62
|
+
cert = begin
|
|
63
|
+
OpenSSL::X509::Certificate.new("-----BEGIN CERTIFICATE-----" + block)
|
|
64
|
+
rescue OpenSSL::OpenSSLError
|
|
65
|
+
next
|
|
66
|
+
end
|
|
67
|
+
{ fingerprint: Digest::SHA1.hexdigest(cert.to_der).upcase,
|
|
68
|
+
subject: cert.subject.to_s,
|
|
69
|
+
cert: cert }
|
|
70
|
+
end
|
|
58
71
|
rescue SystemCallError
|
|
59
72
|
[]
|
|
60
73
|
end
|
|
61
74
|
|
|
75
|
+
# Fingerprints of certificates in a keychain, optionally limited to
|
|
76
|
+
# one common name.
|
|
77
|
+
def keychain_fingerprints(keychain, common_name: nil)
|
|
78
|
+
certs = keychain_certs(keychain)
|
|
79
|
+
certs = certs.select { |c| c[:subject].include?(common_name) } if common_name
|
|
80
|
+
certs.map { |c| c[:fingerprint] }
|
|
81
|
+
end
|
|
82
|
+
|
|
62
83
|
def keychains
|
|
63
84
|
[login_keychain, "/Library/Keychains/System.keychain"]
|
|
64
85
|
end
|
|
65
86
|
|
|
66
87
|
# Remove trusted certificates that carry one of our CA names but are
|
|
67
|
-
#
|
|
88
|
+
# neither the CA on disk nor the one a live proxy is signing with.
|
|
68
89
|
#
|
|
69
|
-
# Regeneration is normal (the CA is rebuilt when it is missing,
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
# liability.
|
|
90
|
+
# Regeneration is normal (the CA is rebuilt when it is missing,
|
|
91
|
+
# expiring, or renamed), and each rebuild produced a new certificate
|
|
92
|
+
# that was trusted and NEVER removed: one machine accumulated 14
|
|
93
|
+
# distinct trusted roots, all named "Ask Local CA". Apple's keychain
|
|
94
|
+
# keeps its own copy, so deleting the state dir does not untrust
|
|
95
|
+
# anything, and a trusted root whose superseded private key is still
|
|
96
|
+
# on disk (or in a backup) is a real liability.
|
|
77
97
|
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
98
|
+
# The running proxy's CA is protected by SIGNATURE, not by name:
|
|
99
|
+
# `server_context` loads the CA once at startup and the SNI callback
|
|
100
|
+
# mints host certs from that in-memory copy, so a live proxy keeps
|
|
101
|
+
# using its boot-time CA whatever is on disk. Deleting that
|
|
102
|
+
# certificate would break TLS for every live route until a restart,
|
|
103
|
+
# and since superseded CAs share a name with the live one, only a
|
|
104
|
+
# signature check can tell them apart.
|
|
105
|
+
def prune_stale(dir = Certs.state_dir, keychains: nil)
|
|
87
106
|
current = fingerprint_of(Certs.ca_paths(dir)[:cert])
|
|
88
107
|
return 0 unless current
|
|
89
108
|
|
|
90
109
|
names = Certs.ca_common_names
|
|
110
|
+
leaf = live_proxy_cert
|
|
91
111
|
(keychains || self.keychains).sum do |keychain|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
112
|
+
keychain_certs(keychain).count do |entry|
|
|
113
|
+
next false if entry[:fingerprint] == current
|
|
114
|
+
next false unless names.any? { |n| entry[:subject].include?(n) }
|
|
115
|
+
next false if leaf && signed_by?(leaf, entry[:cert])
|
|
116
|
+
|
|
117
|
+
Command.capture2("security", "delete-certificate", "-Z", entry[:fingerprint], keychain)
|
|
118
|
+
true
|
|
119
|
+
end
|
|
98
120
|
end
|
|
99
121
|
rescue SystemCallError
|
|
100
122
|
0
|
|
101
123
|
end
|
|
102
124
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
125
|
+
# True when `cert` is the issuer of `leaf` — i.e. that CA signed it.
|
|
126
|
+
# This is what identifies the live proxy's CA exactly.
|
|
127
|
+
def signed_by?(leaf, cert)
|
|
128
|
+
leaf.verify(cert.public_key)
|
|
107
129
|
rescue StandardError
|
|
108
130
|
false
|
|
109
131
|
end
|
|
110
132
|
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
# -a is required: without it `security` returns a single certificate,
|
|
116
|
-
# so the subject check silently answered "not ours" for every
|
|
117
|
-
# certificate but one.
|
|
118
|
-
def our_cert?(keychain, fingerprint, names)
|
|
119
|
-
out, status = Command.capture2("security", "find-certificate", "-a", "-Z", "-p", keychain)
|
|
120
|
-
return false unless status.success?
|
|
133
|
+
# A certificate minted by the running proxy, whose issuer is the CA it
|
|
134
|
+
# is signing with, or nil when no proxy is serving.
|
|
135
|
+
def live_proxy_cert(store = nil)
|
|
136
|
+
return nil unless defined?(ProxyControl)
|
|
121
137
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
138
|
+
store ||= RouteStore.new(Certs.state_dir)
|
|
139
|
+
port = ProxyControl.serving_port(store)
|
|
140
|
+
return nil unless port
|
|
125
141
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
end
|
|
131
|
-
next false unless Digest::SHA1.hexdigest(cert.to_der).upcase == fingerprint
|
|
142
|
+
proxy_peer_cert(port)
|
|
143
|
+
rescue StandardError
|
|
144
|
+
nil
|
|
145
|
+
end
|
|
132
146
|
|
|
133
|
-
|
|
147
|
+
def proxy_peer_cert(port, host = "yamine-ca-probe.localhost")
|
|
148
|
+
sock = TCPSocket.new("127.0.0.1", port)
|
|
149
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
|
150
|
+
ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
151
|
+
ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx)
|
|
152
|
+
ssl.hostname = host if ssl.respond_to?(:hostname=)
|
|
153
|
+
ssl.connect
|
|
154
|
+
ssl.peer_cert
|
|
155
|
+
ensure
|
|
156
|
+
begin
|
|
157
|
+
ssl&.close
|
|
158
|
+
rescue StandardError
|
|
159
|
+
nil
|
|
160
|
+
end
|
|
161
|
+
begin
|
|
162
|
+
sock&.close unless sock.nil? || sock.closed?
|
|
163
|
+
rescue StandardError
|
|
164
|
+
nil
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# The common name of the CA a running proxy actually signs with,
|
|
169
|
+
# read from the issuer of a certificate it mints, or nil when no
|
|
170
|
+
# proxy is serving or the name cannot be determined.
|
|
171
|
+
def serving_ca_common_name(store = nil)
|
|
172
|
+
leaf = live_proxy_cert(store)
|
|
173
|
+
return nil unless leaf
|
|
174
|
+
|
|
175
|
+
leaf.issuer.to_s[/CN=([^\/,]+)/, 1]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def proxy_peer_cert(port, host = "yamine-ca-probe.localhost")
|
|
179
|
+
sock = TCPSocket.new("127.0.0.1", port)
|
|
180
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
|
181
|
+
ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
|
182
|
+
ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx)
|
|
183
|
+
ssl.hostname = host if ssl.respond_to?(:hostname=)
|
|
184
|
+
ssl.connect
|
|
185
|
+
ssl.peer_cert
|
|
186
|
+
ensure
|
|
187
|
+
begin
|
|
188
|
+
ssl&.close
|
|
189
|
+
rescue StandardError
|
|
190
|
+
nil
|
|
191
|
+
end
|
|
192
|
+
begin
|
|
193
|
+
sock&.close unless sock.nil? || sock.closed?
|
|
194
|
+
rescue StandardError
|
|
195
|
+
nil
|
|
134
196
|
end
|
|
135
197
|
end
|
|
136
198
|
|
data/lib/yamine/version.rb
CHANGED