damnx509 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c930cca3a5c52c5260e694f4971c4c5c7426089b
4
+ data.tar.gz: 818f5492500718a1f5cdad67fc759fd61189b58b
5
+ SHA512:
6
+ metadata.gz: ec8b834cca14409ef2154c539a2e4cb9a91a4453a443abb7a03dff705ac5261ce282a0b8e5f7b925a0a2e1e152730457646e049e247137703422ca1cd9a35d8a
7
+ data.tar.gz: a201a8760c74a4aeb2d3f3a355a3406933849aca8ab628cd6b5277f1f8a230b70b43d9759a177c93ef5de2b23685aa64f2d43953c15d715cbcd1f686e28c49a3
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /damnx509.yaml
11
+ /test_ca
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project owner at greg@unrelenting.technology. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project owner is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in damnx509.gemspec
4
+ gemspec
@@ -0,0 +1,45 @@
1
+ # damnx509 [![Unlicense](https://img.shields.io/badge/un-license-green.svg?style=flat)](http://unlicense.org)
2
+
3
+ A simple CLI for managing a small X.509 Certificate Authority!
4
+
5
+ - Screw the `openssl` binary, shell scripts, searching your command history for `openssl` invocations, this is just much cleaner.
6
+ - damnx509 offers a nice interactive `issue` subcommand that lets you set:
7
+ - the extended usage thing (e.g. some WPA2 EAP-TLS clients absolutely require it to be set to `clientAuth`, now you don't have to worry about that)
8
+ - Subject Alternative Names (the `openssl` binary only sets that *from the openssl config file*, what the hell)
9
+ - the signature algorithm (RSA 2048/4096 and EC)
10
+ - the URI of the CRL
11
+ - It also automatically offers default values from the CA (e.g. you want to default to the same country, city and CRL URI, right?)
12
+ - And automatically builds a PKCS12 (`.p12`) key+cert bundle (useful for browser client certs and WPA2 EAP-TLS).
13
+ - There's also a `revoke` subcommand to update the CRL (don't forget to upload it to the URI mentioned in the certificates).
14
+ - DON'T FORGET TO [REMOVE](https://en.wikipedia.org/wiki/Srm_(Unix)) UNENCRYPTED KEYS IF YOU WRITE THEM
15
+
16
+ You can use damnx509 to manage a personal CA to sign things like:
17
+
18
+ - Your [home router](https://lede-project.org/start)'s admin interface (LuCI)
19
+ - Your home router's [WPA2 EAP-TLS network](http://www.blog.10deam.com/2015/01/08/install-freeradius2-on-a-openwrt-router-for-eap-authentication/)
20
+ - Your home NAS's web interface
21
+ - Your personal OpenVPN network
22
+ - Your home server's HTTPS services
23
+ - Client certificates for accessing admin/monitoring/etc. interfaces on your servers
24
+ - An [IndieCert](https://indiecert.net/faq) client certificate for [signing in with your domain](https://indieweb.org/Web_sign-in)
25
+
26
+ ## Installation
27
+
28
+ You need Ruby [older than 2.4 for now](https://github.com/r509/r509/issues/122).
29
+
30
+ ```bash
31
+ $ gem install damnx509
32
+ ```
33
+
34
+ Run the command to see how to use it.
35
+
36
+ ## Contributing
37
+
38
+ Please feel free to submit pull requests!
39
+
40
+ By participating in this project you agree to follow the [Contributor Code of Conduct](http://contributor-covenant.org/version/1/4/).
41
+
42
+ ## License
43
+
44
+ This is free and unencumbered software released into the public domain.
45
+ For more information, please refer to the `UNLICENSE` file or [unlicense.org](http://unlicense.org).
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -0,0 +1,24 @@
1
+ This is free and unencumbered software released into the public domain.
2
+
3
+ Anyone is free to copy, modify, publish, use, compile, sell, or
4
+ distribute this software, either in source code form or as a compiled
5
+ binary, for any purpose, commercial or non-commercial, and by any
6
+ means.
7
+
8
+ In jurisdictions that recognize copyright laws, the author or authors
9
+ of this software dedicate any and all copyright interest in the
10
+ software to the public domain. We make this dedication for the benefit
11
+ of the public at large and to the detriment of our heirs and
12
+ successors. We intend this dedication to be an overt act of
13
+ relinquishment in perpetuity of all present and future rights to this
14
+ software under copyright law.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ For more information, please refer to <http://unlicense.org/>
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'damnx509/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "damnx509"
8
+ spec.version = Damnx509::VERSION
9
+ spec.authors = ["Greg V"]
10
+ spec.email = ["greg@unrelenting.technology"]
11
+
12
+ spec.summary = %q{Easy interactive CLI for managing a small X.509 (TLS) Certificate Authority}
13
+ spec.homepage = "https://github.com/myfreeweb/damnx509"
14
+ spec.license = "Unlicense"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_dependency "thor", "~> 0.19"
24
+ spec.add_dependency "highline", "~> 1.7"
25
+ spec.add_dependency "chronic_duration", "~> 0.10"
26
+ spec.add_dependency "r509", "~> 1.0"
27
+ spec.add_development_dependency "bundler", "~> 1.14"
28
+ spec.add_development_dependency "rake", "~> 10.0"
29
+ end
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require 'damnx509'
3
+ Damnx509::CLI.start(ARGV)
@@ -0,0 +1,202 @@
1
+ require 'thor'
2
+ require 'highline'
3
+ require 'chronic_duration'
4
+ require 'r509'
5
+ #require 'damnx509/version'
6
+
7
+ module Damnx509
8
+ class CLI < Thor
9
+ YAML_FILE = 'damnx509.yaml'
10
+ CAS = 'certificate_authorities'
11
+ CLI = HighLine.new
12
+
13
+ desc 'init CA', 'Create a new certificate authority named CA in the current directory'
14
+ def init(name)
15
+ if File.exist?(name)
16
+ puts "#{name} already exists in the current directory."
17
+ return false
18
+ end
19
+ Dir.mkdir(name)
20
+ subject = _ask_subject
21
+ csr = R509::CSR.new(
22
+ type: 'RSA',
23
+ bit_length: 4096,
24
+ subject: subject,
25
+ san_names: _to_san(subject) + _ask_san
26
+ )
27
+ key_filename = "#{name}/root.key.pem"
28
+ _write_with_password(key_filename, csr.key)
29
+ cert_filename = "#{name}/root.cert.pem"
30
+ ext = []
31
+ crl_uri = CLI.ask('CRL URI?')
32
+ ext << R509::Cert::Extensions::CRLDistributionPoints.new(value: [{type: 'URI', value: crl_uri}]) unless crl_uri.empty?
33
+ cert = R509::CertificateAuthority::Signer.selfsign(
34
+ csr: csr,
35
+ extensions: ext,
36
+ not_before: Time.now.to_i,
37
+ not_after: Time.now.to_i + _ask_duration
38
+ )
39
+ File.write(cert_filename, cert.to_pem)
40
+ crl_list_filename = "#{name}/crl.list.txt"
41
+ File.write(crl_list_filename, '')
42
+ crl_number_filename = "#{name}/crl.number.txt"
43
+ File.write(crl_number_filename , '')
44
+ conf = File.exist?(YAML_FILE) ? YAML.load_file(YAML_FILE) : {}
45
+ conf['default_ca'] ||= name
46
+ conf[CAS] ||= {}
47
+ conf[CAS][name] ||= {}
48
+ conf[CAS][name]['ca_cert'] = { 'cert' => cert_filename, 'key' => key_filename }
49
+ conf[CAS][name]['crl_md'] = 'SHA256'
50
+ conf[CAS][name]['crl_validity_hours'] = 24 * 365
51
+ conf[CAS][name]['crl_start_skew_seconds'] = 30
52
+ conf[CAS][name]['crl_list_file'] = crl_list_filename
53
+ conf[CAS][name]['crl_number_file'] = crl_number_filename
54
+ File.write(YAML_FILE, conf.to_yaml)
55
+ end
56
+
57
+ desc 'issue NAME [CA]', 'Issue a new certificate (interactively), saving to CA/issued/NAME.* files'
58
+ def issue(name, ca=nil)
59
+ ca ||= ca || _conf['default_ca']
60
+ ca_config = _ca_config(ca)
61
+ if !ca_config
62
+ puts "CA #{ca} not found."
63
+ return false
64
+ end
65
+ subj_defaults = Hash[ca_config.ca_cert.cert.subject.to_a.map { |e| [e[0], e[1]] }]
66
+ ext = []
67
+
68
+ ext << R509::Cert::Extensions::BasicConstraints.new(:ca => false)
69
+ CLI.choose do |menu|
70
+ menu.prompt = 'Certificate usage?'
71
+ menu.choice('TLS (HTTPS/SMTPS/IMAPS/OpenVPN/WPA2 EAP-TLS/etc.) Server') {
72
+ ext << R509::Cert::Extensions::ExtendedKeyUsage.new(value: ['serverAuth'])
73
+ }
74
+ menu.choice('TLS (HTTPS/SMTPS/IMAPS/OpenVPN/WPA2 EAP-TLS/etc.) Client') {
75
+ ext << R509::Cert::Extensions::ExtendedKeyUsage.new(value: ['clientAuth'])
76
+ }
77
+ menu.choice('Code Signing') {
78
+ ext << R509::Cert::Extensions::ExtendedKeyUsage.new(value: ['codeSigning'])
79
+ }
80
+ menu.choice('E-mail Protection') {
81
+ ext << R509::Cert::Extensions::ExtendedKeyUsage.new(value: ['emailProtection'])
82
+ }
83
+ end
84
+
85
+ cert_type = 'RSA'
86
+ CLI.choose do |menu|
87
+ menu.prompt = 'Signature algorithm?'
88
+ menu.choice('RSA') {}
89
+ menu.choice('EC') { cert_type = 'EC' }
90
+ end
91
+ bit_length = nil
92
+ CLI.choose do |menu|
93
+ menu.prompt = 'Key length?'
94
+ menu.choice('2048') { bit_length = 2048 }
95
+ menu.choice('4096') { bit_length = 4096 }
96
+ end if cert_type == 'RSA'
97
+
98
+ crl_ext_p = (ca_config.ca_cert.cert.extensions || []).find { |e| e.oid == 'crlDistributionPoints' }
99
+ crl_uri = CLI.ask('CRL URI?') { |q| q.default = crl_ext_p && crl_ext_p.value.gsub(/\n[^:]+:/, '').strip }
100
+ ext << R509::Cert::Extensions::CRLDistributionPoints.new(value: [{type: 'URI', value: crl_uri}]) unless crl_uri.empty?
101
+
102
+ subject = _ask_subject(subj_defaults)
103
+ csr = R509::CSR.new(
104
+ type: cert_type,
105
+ bit_length: bit_length,
106
+ subject: subject,
107
+ san_names: _to_san(subject) + _ask_san
108
+ )
109
+ ext << R509::Cert::Extensions::SubjectAlternativeName.new(value: csr.san)
110
+ Dir.mkdir("#{ca}/issued") unless File.directory?("#{ca}/issued")
111
+ key_filename = "#{ca}/issued/#{name}.key.pem"
112
+ password = _write_with_password(key_filename, csr.key)
113
+ signer = R509::CertificateAuthority::Signer.new(ca_config)
114
+ cert = signer.sign(
115
+ csr: csr,
116
+ extensions: ext,
117
+ not_before: Time.now.to_i,
118
+ not_after: Time.now.to_i + _ask_duration
119
+ )
120
+ cert_filename = "#{ca}/issued/#{name}.cert.pem"
121
+ File.write(cert_filename, cert.to_pem)
122
+ unless password.empty?
123
+ p12_filename = "#{ca}/issued/#{name}.p12"
124
+ cert.write_pkcs12(p12_filename, password, "#{name} cert+key signed by #{ca}")
125
+ puts "Wrote #{cert_filename}, #{key_filename}, #{p12_filename}."
126
+ else
127
+ puts "Wrote #{cert_filename}, #{key_filename}."
128
+ end
129
+ end
130
+
131
+ desc 'revoke SERIAL [CA]', 'Revoke a certificate with a given SERIAL'
132
+ def revoke(serial, ca=nil)
133
+ # TODO: revoke from file
134
+ ca ||= ca || _conf['default_ca']
135
+ ca_config = _ca_config(ca)
136
+ if !ca_config
137
+ puts "CA #{ca} not found."
138
+ return false
139
+ end
140
+ admin = R509::CRL::Administrator.new(ca_config)
141
+ ser = serial.gsub(':', '').to_i(16)
142
+ admin.revoke_cert(ser)
143
+ crl = admin.generate_crl
144
+ crl_filename = "#{ca}/crl.pem"
145
+ crl.write_pem(crl_filename)
146
+ puts "Wrote #{crl_filename}."
147
+ end
148
+
149
+ private
150
+ def _conf
151
+ @conf ||= YAML.load_file(YAML_FILE)
152
+ end
153
+
154
+ def _ca_config(ca_name)
155
+ @ca_config ||= R509::Config::CAConfig.load_from_hash(_conf[CAS][ca_name])
156
+ rescue ArgumentError
157
+ nil
158
+ end
159
+
160
+ def _ask_subject(defaults=nil)
161
+ [
162
+ ['C', CLI.ask('C - Country (2 letter code):') { |q| q.default = defaults && defaults['C'] }],
163
+ ['ST', CLI.ask('ST - State or Province (full name):') { |q| q.default = defaults && defaults['ST'] }],
164
+ ['L', CLI.ask('L - Locality (e.g. city):') { |q| q.default = defaults && defaults['L'] }],
165
+ ['O', CLI.ask('O - Organization (e.g. company):') { |q| q.default = defaults && defaults['O'] }],
166
+ ['OU', CLI.ask('OU - Organizational Unit (e.g. section):') { |q| q.default = defaults && defaults['OU'] }],
167
+ ['CN', CLI.ask('CN - Common Name (e.g. fully qualified host name):')]
168
+ ]
169
+ end
170
+
171
+ def _ask_san
172
+ result = []
173
+ while cur = CLI.ask("SAN - Subject Alternative Name (enter one; type is automatically recognized, don't write 'DNS' etc.; empty to #{result.empty? ? 'skip' : 'stop'}):")
174
+ break if cur.empty?
175
+ result << cur
176
+ end
177
+ result
178
+ end
179
+
180
+ def _to_san(subject)
181
+ [Hash[subject]['CN']]
182
+ end
183
+
184
+ def _ask_duration
185
+ ChronicDuration.parse(CLI.ask('Expires in (natural input):') { |q| q.default = '365d'}, keep_zero: true)
186
+ end
187
+
188
+ def _ask_password
189
+ CLI.ask('Private key password (empty to skip key encryption):') { |q| q.echo = '*' }
190
+ end
191
+
192
+ def _write_with_password(key_filename, key)
193
+ password = _ask_password
194
+ if password.empty?
195
+ key.write_pem(key_filename)
196
+ else
197
+ key.write_encrypted_pem(key_filename, 'aes256', password)
198
+ end
199
+ password
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,3 @@
1
+ module Damnx509
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,139 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: damnx509
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Greg V
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-03-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.19'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.19'
27
+ - !ruby/object:Gem::Dependency
28
+ name: highline
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.7'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.7'
41
+ - !ruby/object:Gem::Dependency
42
+ name: chronic_duration
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.10'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.10'
55
+ - !ruby/object:Gem::Dependency
56
+ name: r509
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.14'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.14'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '10.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '10.0'
97
+ description:
98
+ email:
99
+ - greg@unrelenting.technology
100
+ executables:
101
+ - damnx509
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - ".gitignore"
106
+ - CODE_OF_CONDUCT.md
107
+ - Gemfile
108
+ - README.md
109
+ - Rakefile
110
+ - UNLICENSE
111
+ - damnx509.gemspec
112
+ - exe/damnx509
113
+ - lib/damnx509.rb
114
+ - lib/damnx509/version.rb
115
+ homepage: https://github.com/myfreeweb/damnx509
116
+ licenses:
117
+ - Unlicense
118
+ metadata: {}
119
+ post_install_message:
120
+ rdoc_options: []
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ requirements: []
134
+ rubyforge_project:
135
+ rubygems_version: 2.6.10
136
+ signing_key:
137
+ specification_version: 4
138
+ summary: Easy interactive CLI for managing a small X.509 (TLS) Certificate Authority
139
+ test_files: []