anchor-pki 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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.