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.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/anchor-pki/auto_cert.rb +134 -0
  3. metadata +19 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c204ca34e433d268fe98808738b1c0250ad3b2035e178cb6fd15add6f8a05a3
4
- data.tar.gz: 621128f1434720eaeeb0c44f54b6c29b779c12bc5faf3321192a766fe7da6b1d
3
+ metadata.gz: cc0b9b7fe9893fc5d4f23157bbb2a18c4a15066e929024c197b610ef5f6ffd80
4
+ data.tar.gz: 5cc3e63d7acbc16531c9abf60d04d205fe5ed0b638e595653a8aed74d8abc49a
5
5
  SHA512:
6
- metadata.gz: e51132aa31a45a11a2153b6e118d6864905d6f3b2b2fb656ad16d11c0944a8e6625fb239ac515ab4b93aec58231bc98ffc0323eb6907e7c75d49b2f4537b1395
7
- data.tar.gz: 66e37bb731e25e4abe95a020a7660cc94284960181752b6dd50eef474656ac9492e9f2d5a83c05ea5158e3fb436bb1a14230423b534ef9945ceaab453397434d
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.1.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: 2021-11-05 00:00:00.000000000 Z
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.2.30
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.