mini_ca 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ba8cda5d5c7a9ebbd7baa7803916e988ffc7520c
4
+ data.tar.gz: fdee1aafd4a2e43fb1bb4b98dd271cb4a7ef0654
5
+ SHA512:
6
+ metadata.gz: cd647947568fd6e7081284b3b34a4935c10326b95c0dbeddcc439845d76435d14c17b498e773dffb5eeee3fea9481b82ed5d62a08dab6dc932c1516e12f3f98f
7
+ data.tar.gz: 7ed91bff6a7617c18415eb5f9e652511f403d1fa2cb897932d26ec94375534d16bebeb78092dca0364cc2c5070b288cee58078ff37b131fc42b5575b98c79ee2
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/.travis.yml ADDED
@@ -0,0 +1,12 @@
1
+ sudo: false
2
+
3
+ cache: bundler
4
+
5
+ before_install:
6
+ gem update bundler
7
+
8
+ rvm:
9
+ - 2.0.0
10
+ - 2.1.0
11
+ - 2.2.0
12
+ - 2.3.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in mini_ca.gemspec
4
+ gemspec
data/LICENSE.md ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2017 Aptible, Inc.
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
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
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # ![](https://raw.github.com/aptible/straptible/master/lib/straptible/rails/templates/public.api/icon-60px.png) MiniCa
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/mini_ca.png)](https://rubygems.org/gems/mini_ca)
4
+ [![Build Status](https://travis-ci.org/aptible/mini_ca.png?branch=master)](https://travis-ci.org/aptible/mini_ca)
5
+ [![Dependency Status](https://gemnasium.com/aptible/mini_ca.png)](https://gemnasium.com/aptible/mini_ca)
6
+
7
+ A Gem to generate custom X509 certificates in specs.
8
+
9
+ ## Installation
10
+
11
+ Add the following line to your application's Gemfile.
12
+
13
+ gem 'mini_ca'
14
+
15
+ And then run `bundle install`.
16
+
17
+ ## Usage
18
+
19
+ ```
20
+ # Instantiate a CA
21
+ ca = MiniCa::Certificate.new('My Test CA', ca: true)
22
+
23
+ # Create an intermediate
24
+ intermediate = ca.issue('My Intermediate', ca: true)
25
+
26
+ # Create a certificate
27
+ certificate = intermediate.issue('My Certificate')
28
+
29
+ # Get the certificate chain as PEM
30
+ certificate.chain_pem
31
+
32
+ # Get the certificate bundle (i.e. including the leaf certificate) as PEM
33
+ certificate.bundle_pem
34
+
35
+ # Verify a certificate
36
+ ca.store.verify(certificate.x509, [intermediate.x509])
37
+ ```
38
+
39
+ See the specs for more examples.
40
+
41
+ ## Contributing
42
+
43
+ 1. Fork the project.
44
+ 1. Commit your changes, with specs.
45
+ 1. Ensure that your code passes specs (`rake spec`) and meets Aptible's Ruby style guide (`rake rubocop`).
46
+ 1. Create a new pull request on GitHub.
47
+
48
+ ## Copyright and License
49
+
50
+ MIT License, see [LICENSE](LICENSE.md) for details.
51
+
52
+ Copyright (c) 2017 [Aptible](https://www.aptible.com), Thomas Orozco, and contributors.
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'aptible/tasks'
4
+ Aptible::Tasks.load_tasks
@@ -0,0 +1,129 @@
1
+ module MiniCa
2
+ class Certificate
3
+ DIGEST = OpenSSL::Digest::SHA256
4
+
5
+ attr_reader :key, :x509, :issuer, :ca
6
+
7
+ # rubocop:disable ParameterLists
8
+ def initialize(
9
+ cn,
10
+ sans: nil,
11
+ issuer: nil,
12
+ ca: false,
13
+ serial: nil,
14
+ not_before: nil,
15
+ not_after: nil,
16
+ country: nil,
17
+ state: nil,
18
+ location: nil,
19
+ organization: nil
20
+ )
21
+ @key = OpenSSL::PKey::RSA.new(2048)
22
+ @x509 = OpenSSL::X509::Certificate.new
23
+ @issuer = issuer
24
+ @ca = ca
25
+ @counter = 0
26
+
27
+ x509.version = 0x2
28
+ x509.serial = serial || 0
29
+
30
+ x509.public_key = key.public_key
31
+
32
+ x509.subject = OpenSSL::X509::Name.new
33
+
34
+ [
35
+ ['CN', cn],
36
+ ['C', country],
37
+ ['ST', state],
38
+ ['L', location],
39
+ ['O', organization]
40
+ ].each do |prop, value|
41
+ next if value.nil?
42
+ x509.subject = x509.subject.add_entry(prop, value)
43
+ end
44
+
45
+ x509.issuer = issuer ? issuer.x509.subject : x509.subject
46
+
47
+ if issuer
48
+ not_before ||= issuer.x509.not_before
49
+ not_after ||= issuer.x509.not_after
50
+
51
+ if issuer.x509.not_before > not_before
52
+ raise Error, 'Certificate cannot become valid before issuer'
53
+ end
54
+
55
+ if issuer.x509.not_after < not_after
56
+ raise Error, 'Certificate cannot expire after issuer'
57
+ end
58
+ else
59
+ not_before ||= Time.now - 3600 * 24
60
+ not_after ||= Time.now + 3600 + 24
61
+ end
62
+
63
+ x509.not_before = not_before
64
+ x509.not_after = not_after
65
+
66
+ ef = OpenSSL::X509::ExtensionFactory.new
67
+ ef.subject_certificate = x509
68
+
69
+ sans = (sans || []) + ["DNS:#{cn}"]
70
+
71
+ exts = if ca
72
+ [
73
+ ef.create_extension('basicConstraints', 'CA:TRUE', true)
74
+ ]
75
+ else
76
+ [
77
+ ef.create_extension('basicConstraints', 'CA:FALSE', true),
78
+ ef.create_extension('subjectAltName', sans.join(','), false)
79
+ ]
80
+ end
81
+
82
+ exts.each { |e| x509.add_extension(e) }
83
+
84
+ signing_key = issuer ? issuer.key : key
85
+ x509.sign signing_key, DIGEST.new
86
+ end
87
+ # rubocop:enable ParameterLists
88
+
89
+ def issue(cn, **opts)
90
+ raise 'CA must be set to use #issue' unless ca
91
+ @counter += 1
92
+ Certificate.new(cn, issuer: self, serial: @counter, **opts)
93
+ end
94
+
95
+ def store
96
+ raise 'CA must be set to use #store' unless ca
97
+ OpenSSL::X509::Store.new.tap { |store| store.add_cert(x509) }
98
+ end
99
+
100
+ def chain
101
+ bits = []
102
+ this_cert = self
103
+ until (this_cert = this_cert.issuer).nil?
104
+ bits << this_cert
105
+ end
106
+ bits[0...-1]
107
+ end
108
+
109
+ def bundle
110
+ [self] + chain
111
+ end
112
+
113
+ def x509_pem
114
+ x509.to_s
115
+ end
116
+
117
+ def chain_pem
118
+ chain.map(&:x509).map(&:to_s).join('')
119
+ end
120
+
121
+ def bundle_pem
122
+ bundle.map(&:x509).map(&:to_s).join('')
123
+ end
124
+
125
+ def key_pem
126
+ key.to_s
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,4 @@
1
+ module MiniCa
2
+ class Error < StandardError
3
+ end
4
+ end
@@ -0,0 +1,3 @@
1
+ module MiniCa
2
+ VERSION = '0.1.0'.freeze
3
+ end
data/lib/mini_ca.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'openssl'
2
+
3
+ require 'mini_ca/version'
4
+ require 'mini_ca/error'
5
+ require 'mini_ca/certificate'
6
+
7
+ module MiniCa
8
+ end
data/mini_ca.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # encoding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ require 'English'
6
+ require 'mini_ca/version'
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = 'mini_ca'
10
+ spec.version = MiniCa::VERSION
11
+ spec.authors = ['Thomas Orozco']
12
+ spec.email = ['thomas@orozco.fr']
13
+ spec.description = 'A minimal Certification Authority, for use in specs'
14
+ spec.summary = spec.description
15
+ spec.homepage = 'https://github.com/aptible/mini_ca'
16
+ spec.license = 'MIT'
17
+
18
+ spec.files = `git ls-files`.split($RS)
19
+ spec.test_files = spec.files.grep(%r{^spec/})
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.add_development_dependency 'bundler'
23
+ spec.add_development_dependency 'aptible-tasks'
24
+ spec.add_development_dependency 'rake'
25
+ spec.add_development_dependency 'rspec'
26
+ end
@@ -0,0 +1,133 @@
1
+ require 'spec_helper'
2
+
3
+ describe MiniCa::Certificate do
4
+ describe '#initialize' do
5
+ it 'initializes a self-signed certificate' do
6
+ c = described_class.new('name')
7
+ expect(c.x509.subject.to_s).to eq('/CN=name')
8
+ expect(c.x509.issuer.to_s).to eq('/CN=name')
9
+ end
10
+
11
+ it 'initializes a CA certificate' do
12
+ c1 = described_class.new('foo', ca: true)
13
+ c2 = c1.issue('bar')
14
+ expect(c1.store.verify(c2.x509)).to be_truthy
15
+ end
16
+
17
+ it 'initializes a certificate with a serial' do
18
+ c1 = described_class.new('foo', serial: 10)
19
+ expect(c1.x509.serial).to eq(10)
20
+ end
21
+
22
+ it 'initializes a certificate with not_before' do
23
+ t = Time.at((Time.now - 100).to_i)
24
+ c1 = described_class.new('foo', not_before: t)
25
+ expect(c1.x509.not_before).to eq(t)
26
+ end
27
+
28
+ it 'initializes a certificate with not_after' do
29
+ t = Time.at((Time.now + 100).to_i)
30
+ c1 = described_class.new('foo', not_after: t)
31
+ expect(c1.x509.not_after).to eq(t)
32
+ end
33
+
34
+ context 'subject fields' do
35
+ it 'sets country' do
36
+ expect(described_class.new('x', country: 'bar').x509.subject.to_s)
37
+ .to eq('/CN=x/C=bar')
38
+ end
39
+
40
+ it 'sets state' do
41
+ expect(described_class.new('x', state: 'bar').x509.subject.to_s)
42
+ .to eq('/CN=x/ST=bar')
43
+ end
44
+
45
+ it 'sets location' do
46
+ expect(described_class.new('x', location: 'bar').x509.subject.to_s)
47
+ .to eq('/CN=x/L=bar')
48
+ end
49
+
50
+ it 'sets organization' do
51
+ expect(described_class.new('x', organization: 'bar').x509.subject.to_s)
52
+ .to eq('/CN=x/O=bar')
53
+ end
54
+ end
55
+ end
56
+
57
+ context 'CA' do
58
+ subject { described_class.new('MyCA', ca: true) }
59
+
60
+ describe '#issue' do
61
+ it 'issues signed certificates with a valid serial' do
62
+ c1 = subject.issue('c1')
63
+ c2 = subject.issue('c2')
64
+ expect(c1.x509.serial).not_to eq(c2.x509.serial)
65
+ end
66
+
67
+ it 'fails if the CA becomes valid after the certificate' do
68
+ t = subject.x509.not_before - 100
69
+ expect { subject.issue('c', not_before: t) }
70
+ .to raise_error(/cannot become valid before issuer/i)
71
+ end
72
+
73
+ it 'fails if the CA expires before the certificate' do
74
+ t = subject.x509.not_after + 100
75
+ expect { subject.issue('c', not_after: t) }
76
+ .to raise_error(/cannot expire after issuer/i)
77
+ end
78
+ end
79
+
80
+ describe '#store' do
81
+ it 'returns a store trusting the CA' do
82
+ alt = described_class.new('OtherCA', ca: true)
83
+ cert = subject.issue('Client')
84
+
85
+ expect(subject.store.verify(cert.x509)).to be_truthy
86
+ expect(alt.store.verify(cert.x509)).to be_falsey
87
+ end
88
+ end
89
+ end
90
+
91
+ describe '#chain / #bundle' do
92
+ it 'returns nothing for a self-signed certificate' do
93
+ c = described_class.new('c')
94
+ expect(c.chain).to be_empty
95
+ end
96
+
97
+ it 'returns nothing for a certificate issued by a CA' do
98
+ ca = described_class.new('ca', ca: true)
99
+ c = ca.issue('c')
100
+ expect(c.chain).to be_empty
101
+ expect(c.bundle).to eq([c])
102
+ end
103
+
104
+ it 'returns 1 leaf certificate for 1 intermediate' do
105
+ ca = described_class.new('ca', ca: true)
106
+ i1 = ca.issue('i1', ca: true)
107
+ c = i1.issue('c')
108
+ expect(c.chain).to eq([i1])
109
+ expect(c.bundle).to eq([c, i1])
110
+ end
111
+
112
+ it 'returns 2 leaf certificates for 2 intermediates' do
113
+ ca = described_class.new('ca', ca: true)
114
+ i1 = ca.issue('i1', ca: true)
115
+ i2 = i1.issue('i2', ca: true)
116
+ c = i2.issue('c')
117
+ expect(c.chain).to eq([i2, i1])
118
+ expect(c.bundle).to eq([c, i2, i1])
119
+ end
120
+ end
121
+
122
+ describe '#chain_pem / #bundle_pem' do
123
+ it 'returns the certificate chain' do
124
+ ca = described_class.new('ca', ca: true)
125
+ i1 = ca.issue('i1', ca: true)
126
+ c = i1.issue('c')
127
+ expect(c.chain_pem.split("\n").grep(/BEGIN CERTIFICATE/).size).to eq(1)
128
+ expect(c.bundle_pem.split("\n").grep(/BEGIN CERTIFICATE/).size).to eq(2)
129
+ expect(c.bundle_pem).to start_with(c.x509_pem)
130
+ expect(c.bundle_pem).to end_with(i1.x509_pem)
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,10 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+
4
+ # Load shared spec files
5
+ Dir["#{File.dirname(__FILE__)}/shared/**/*.rb"].each do |file|
6
+ require file
7
+ end
8
+
9
+ # Require library up front
10
+ require 'mini_ca'
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mini_ca
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Thomas Orozco
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-11-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: aptible-tasks
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: A minimal Certification Authority, for use in specs
70
+ email:
71
+ - thomas@orozco.fr
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - ".rspec"
78
+ - ".travis.yml"
79
+ - Gemfile
80
+ - LICENSE.md
81
+ - README.md
82
+ - Rakefile
83
+ - lib/mini_ca.rb
84
+ - lib/mini_ca/certificate.rb
85
+ - lib/mini_ca/error.rb
86
+ - lib/mini_ca/version.rb
87
+ - mini_ca.gemspec
88
+ - spec/mini_ca/certificate_spec.rb
89
+ - spec/spec_helper.rb
90
+ homepage: https://github.com/aptible/mini_ca
91
+ licenses:
92
+ - MIT
93
+ metadata: {}
94
+ post_install_message:
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubyforge_project:
110
+ rubygems_version: 2.6.13
111
+ signing_key:
112
+ specification_version: 4
113
+ summary: A minimal Certification Authority, for use in specs
114
+ test_files:
115
+ - spec/mini_ca/certificate_spec.rb
116
+ - spec/spec_helper.rb