anchor-pki 0.1.0 → 0.2.0
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/lib/anchor-pki/auto_cert.rb +134 -0
- metadata +19 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cc0b9b7fe9893fc5d4f23157bbb2a18c4a15066e929024c197b610ef5f6ffd80
|
4
|
+
data.tar.gz: 5cc3e63d7acbc16531c9abf60d04d205fe5ed0b638e595653a8aed74d8abc49a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7d5d6bb50ee250800df59aa17ecca780cddc08293ff4bb609a4b944fcc611ef76ec916d5d6b75026694b0740addeabf592533c4117ea90cd3a1032c21c816f5e
|
7
|
+
data.tar.gz: 4e2c6f2a836c59319a6cab2a6f48204d75ae7e8fb1b721e6177c2ec75be57c1f8271a18fef02afc847dc822b1db8e70c228c4cbd90ec023fd54bd8ecd620d87c
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'acme/client'
|
4
|
+
require 'uri'
|
5
|
+
|
6
|
+
module Anchor
|
7
|
+
# AutoCert provides automatic access to certificates from Anchor, but
|
8
|
+
# theoretically it could work with Let's Encrypt and any other ACME-based CA.
|
9
|
+
class AutoCert
|
10
|
+
ACCEPT_ANY_TOS = [//].freeze
|
11
|
+
|
12
|
+
def initialize(name_policies, tos_acceptor, directory, account: {}, **opts)
|
13
|
+
@name_policies = name_policies
|
14
|
+
@tos_acceptor = tos_acceptor
|
15
|
+
@directory_url = URI.parse(directory)
|
16
|
+
|
17
|
+
@renew_before = opts[:renew_before]
|
18
|
+
@work_dir = Pathname(opts[:work_dir] || Dir.mktmpdir)
|
19
|
+
@cache = opts[:cache]
|
20
|
+
@client = opts[:client]
|
21
|
+
|
22
|
+
@client ||= new_client(**account)
|
23
|
+
@account_opts = account
|
24
|
+
end
|
25
|
+
|
26
|
+
def certificate(*names, algo: :ecdsa, common_name: names.first, **opts)
|
27
|
+
if (arr = unmatched_names(names)).size.positive?
|
28
|
+
raise StandardError, "unallowed names '#{arr.join(',')}'"
|
29
|
+
end
|
30
|
+
|
31
|
+
key_pem = @cache&.read("#{common_name}+#{algo}")
|
32
|
+
cert_pem = @cache&.read(common_name)
|
33
|
+
|
34
|
+
if key_pem.nil? || cert_pem.nil? || needs_renewal?(cert_pem)
|
35
|
+
cert_pem, key_pem = provision(names, algo, common_name, **opts)
|
36
|
+
|
37
|
+
@cache&.write("#{common_name}+#{algo}", key_pem)
|
38
|
+
@cache&.write(common_name, cert_pem)
|
39
|
+
end
|
40
|
+
|
41
|
+
cert = (@work_dir / common_name).open('w') { |f| f << cert_pem }.path
|
42
|
+
key = (@work_dir / "#{common_name}+#{algo}").open('w') { |f| f << key_pem }.path
|
43
|
+
|
44
|
+
[cert, key]
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def provision(names, algo, common_name, **opts)
|
50
|
+
load_or_build_account
|
51
|
+
|
52
|
+
order = @client.new_order(identifiers: names, **opts)
|
53
|
+
|
54
|
+
key_pem ||= new_key(algo).to_pem
|
55
|
+
csr = Acme::Client::CertificateRequest.new(private_key: parse_key_pem(key_pem), common_name: common_name, names: names)
|
56
|
+
|
57
|
+
order.finalize(csr: csr)
|
58
|
+
# TODO: loop over order.authorizations and process the challenges
|
59
|
+
|
60
|
+
while order.status == 'processing'
|
61
|
+
sleep(1)
|
62
|
+
order.reload
|
63
|
+
end
|
64
|
+
|
65
|
+
return order.certificate, key_pem
|
66
|
+
end
|
67
|
+
|
68
|
+
def load_or_build_account
|
69
|
+
@account ||= build_account(**@account_opts)
|
70
|
+
end
|
71
|
+
|
72
|
+
def build_account(contact: nil, external_account_binding: nil, terms_of_service_agreed: false, **)
|
73
|
+
terms_of_service_agreed ||= @tos_acceptor.any? { |a| a.match?(@client.terms_of_service) }
|
74
|
+
|
75
|
+
@client.new_account(contact: contact, terms_of_service_agreed: terms_of_service_agreed, external_account_binding: external_account_binding)
|
76
|
+
end
|
77
|
+
|
78
|
+
def needs_renewal?(cert_pem)
|
79
|
+
cert = OpenSSL::X509::Certificate.new(cert_pem)
|
80
|
+
|
81
|
+
(Time.zone.now + @renew_before.to_i) > cert.not_after
|
82
|
+
end
|
83
|
+
|
84
|
+
def new_client(account_key: nil, contact: nil, **)
|
85
|
+
account_key ||= fetch_account_key(contact) { new_key(:ecdsa) }
|
86
|
+
|
87
|
+
Acme::Client.new(private_key: account_key, directory: @directory_url)
|
88
|
+
end
|
89
|
+
|
90
|
+
def new_key(algo)
|
91
|
+
case algo
|
92
|
+
when :ecdsa then OpenSSL::PKey::EC.generate('prime256v1')
|
93
|
+
else
|
94
|
+
raise "unknown key algo '#{algo}'"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def fetch_account_key(contact)
|
99
|
+
id = "#{contact || 'default'}+#{@directory_url.host}+key"
|
100
|
+
parse_key_pem(@cache&.fetch(id) { parse_key_pem(yield.to_pem) } || yield.to_pem)
|
101
|
+
end
|
102
|
+
|
103
|
+
def parse_key_pem(data)
|
104
|
+
begin
|
105
|
+
OpenSSL::PKey::RSA.new(data)
|
106
|
+
rescue StandardError
|
107
|
+
nil
|
108
|
+
end ||
|
109
|
+
begin
|
110
|
+
OpenSSL::PKey::EC.new(data)
|
111
|
+
rescue StandardError
|
112
|
+
nil
|
113
|
+
end ||
|
114
|
+
(raise 'unknown key data format')
|
115
|
+
end
|
116
|
+
|
117
|
+
def unmatched_names(names)
|
118
|
+
names.reject do |name|
|
119
|
+
@name_policies.any? do |policy|
|
120
|
+
case policy
|
121
|
+
when String then name == policy
|
122
|
+
when Regexp then policy.match?(name)
|
123
|
+
when IPAddr
|
124
|
+
begin
|
125
|
+
policy.include?(name)
|
126
|
+
rescue IPAddr::Error
|
127
|
+
nil
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: anchor-pki
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Anchor
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
12
|
-
dependencies:
|
11
|
+
date: 2023-04-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: acme-client
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.0.13
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.0.13
|
13
27
|
description: Anchor is a hosted PKI platform for your internal organization.
|
14
28
|
email: support@anchor.dev
|
15
29
|
executables: []
|
@@ -17,6 +31,7 @@ extensions: []
|
|
17
31
|
extra_rdoc_files: []
|
18
32
|
files:
|
19
33
|
- lib/anchor-pki.rb
|
34
|
+
- lib/anchor-pki/auto_cert.rb
|
20
35
|
homepage: https://anchor.dev/
|
21
36
|
licenses:
|
22
37
|
- MIT
|
@@ -36,7 +51,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
36
51
|
- !ruby/object:Gem::Version
|
37
52
|
version: '0'
|
38
53
|
requirements: []
|
39
|
-
rubygems_version: 3.
|
54
|
+
rubygems_version: 3.3.26
|
40
55
|
signing_key:
|
41
56
|
specification_version: 4
|
42
57
|
summary: Ruby client for Anchor PKI. See https://anchor.dev/ for details.
|