certificate_authority_sonian 0.1.7

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.
@@ -0,0 +1,215 @@
1
+ module CertificateAuthority
2
+ class Certificate
3
+ # include SigningEntity
4
+ include ActiveModel::Validations
5
+
6
+ attr_accessor :distinguished_name
7
+ attr_accessor :serial_number
8
+ attr_accessor :key_material
9
+ attr_accessor :not_before
10
+ attr_accessor :not_after
11
+ attr_accessor :revoked_at
12
+ attr_accessor :extensions
13
+ attr_accessor :openssl_body
14
+
15
+ alias :subject :distinguished_name #Same thing as the DN
16
+
17
+ attr_accessor :parent
18
+
19
+ validate do |certificate|
20
+ errors.add :base, "Distinguished name must be valid" unless distinguished_name.valid?
21
+ errors.add :base, "Key material must be valid" unless key_material.valid?
22
+ errors.add :base, "Serial number must be valid" unless serial_number.valid?
23
+ errors.add :base, "Extensions must be valid" unless extensions.each do |item|
24
+ unless item.respond_to?(:valid?)
25
+ true
26
+ else
27
+ item.valid?
28
+ end
29
+ end
30
+ end
31
+
32
+ def initialize
33
+ self.distinguished_name = DistinguishedName.new
34
+ self.serial_number = SerialNumber.new
35
+ self.key_material = MemoryKeyMaterial.new
36
+ self.not_before = Time.now
37
+ self.not_after = Time.now + 60 * 60 * 24 * 365 #One year
38
+ self.parent = self
39
+ self.extensions = load_extensions()
40
+
41
+ self.signing_entity = false
42
+
43
+ end
44
+
45
+ def sign!(signing_profile={})
46
+ raise "Invalid certificate #{self.errors.full_messages}" unless valid?
47
+ merge_profile_with_extensions(signing_profile)
48
+
49
+ openssl_cert = OpenSSL::X509::Certificate.new
50
+ openssl_cert.version = 2
51
+ openssl_cert.not_before = self.not_before
52
+ openssl_cert.not_after = self.not_after
53
+ openssl_cert.public_key = self.key_material.public_key
54
+
55
+ openssl_cert.serial = self.serial_number.number
56
+
57
+ openssl_cert.subject = self.distinguished_name.to_x509_name
58
+ openssl_cert.issuer = parent.distinguished_name.to_x509_name
59
+
60
+ require 'tempfile'
61
+ t = Tempfile.new("bullshit_conf")
62
+ # t = File.new("/tmp/openssl.cnf")
63
+ ## The config requires a file even though we won't use it
64
+ openssl_config = OpenSSL::Config.new(t.path)
65
+
66
+ factory = OpenSSL::X509::ExtensionFactory.new
67
+ factory.subject_certificate = openssl_cert
68
+
69
+ #NB: If the parent doesn't have an SSL body we're making this a self-signed cert
70
+ if parent.openssl_body.nil?
71
+ factory.issuer_certificate = openssl_cert
72
+ else
73
+ factory.issuer_certificate = parent.openssl_body
74
+ end
75
+
76
+ self.extensions.keys.each do |k|
77
+ config_extensions = extensions[k].config_extensions
78
+ openssl_config = merge_options(openssl_config,config_extensions)
79
+ end
80
+
81
+ # p openssl_config.sections
82
+
83
+ factory.config = openssl_config
84
+
85
+ # Order matters: e.g. for self-signed, subjectKeyIdentifier must come before authorityKeyIdentifier
86
+ self.extensions.keys.sort{|a,b| b<=>a}.each do |k|
87
+ e = extensions[k]
88
+ next if e.to_s.nil? or e.to_s == "" ## If the extension returns an empty string we won't include it
89
+ ext = factory.create_ext(e.openssl_identifier, e.to_s)
90
+ openssl_cert.add_extension(ext)
91
+ end
92
+
93
+ if signing_profile["digest"].nil?
94
+ digest = OpenSSL::Digest::Digest.new("SHA512")
95
+ else
96
+ digest = OpenSSL::Digest::Digest.new(signing_profile["digest"])
97
+ end
98
+ self.openssl_body = openssl_cert.sign(parent.key_material.private_key,digest)
99
+ t.close! if t.is_a?(Tempfile)# We can get rid of the ridiculous temp file
100
+ self.openssl_body
101
+ end
102
+
103
+ def is_signing_entity?
104
+ self.extensions["basicConstraints"].ca
105
+ end
106
+
107
+ def signing_entity=(signing)
108
+ self.extensions["basicConstraints"].ca = signing
109
+ end
110
+
111
+ def revoked?
112
+ !self.revoked_at.nil?
113
+ end
114
+
115
+ def to_pem
116
+ raise "Certificate has no signed body" if self.openssl_body.nil?
117
+ self.openssl_body.to_pem
118
+ end
119
+
120
+ def is_root_entity?
121
+ self.parent == self && is_signing_entity?
122
+ end
123
+
124
+ def is_intermediate_entity?
125
+ (self.parent != self) && is_signing_entity?
126
+ end
127
+
128
+ private
129
+
130
+ def merge_profile_with_extensions(signing_profile={})
131
+ return self.extensions if signing_profile["extensions"].nil?
132
+ signing_config = signing_profile["extensions"]
133
+ signing_config.keys.each do |k|
134
+ extension = self.extensions[k]
135
+ items = signing_config[k]
136
+ items.keys.each do |profile_item_key|
137
+ if extension.respond_to?("#{profile_item_key}=".to_sym)
138
+ extension.send("#{profile_item_key}=".to_sym, items[profile_item_key] )
139
+ else
140
+ p "Tried applying '#{profile_item_key}' to #{extension.class} but it doesn't respond!"
141
+ end
142
+ end
143
+ end
144
+ end
145
+
146
+ def load_extensions
147
+ extension_hash = {}
148
+
149
+ temp_extensions = []
150
+ basic_constraints = CertificateAuthority::Extensions::BasicContraints.new
151
+ temp_extensions << basic_constraints
152
+ crl_distribution_points = CertificateAuthority::Extensions::CrlDistributionPoints.new
153
+ temp_extensions << crl_distribution_points
154
+ subject_key_identifier = CertificateAuthority::Extensions::SubjectKeyIdentifier.new
155
+ temp_extensions << subject_key_identifier
156
+ authority_key_identifier = CertificateAuthority::Extensions::AuthorityKeyIdentifier.new
157
+ temp_extensions << authority_key_identifier
158
+ authority_info_access = CertificateAuthority::Extensions::AuthorityInfoAccess.new
159
+ temp_extensions << authority_info_access
160
+ key_usage = CertificateAuthority::Extensions::KeyUsage.new
161
+ temp_extensions << key_usage
162
+ extended_key_usage = CertificateAuthority::Extensions::ExtendedKeyUsage.new
163
+ temp_extensions << extended_key_usage
164
+ subject_alternative_name = CertificateAuthority::Extensions::SubjectAlternativeName.new
165
+ temp_extensions << subject_alternative_name
166
+ certificate_policies = CertificateAuthority::Extensions::CertificatePolicies.new
167
+ temp_extensions << certificate_policies
168
+
169
+ temp_extensions.each do |extension|
170
+ extension_hash[extension.openssl_identifier] = extension
171
+ end
172
+
173
+ extension_hash
174
+ end
175
+
176
+ def merge_options(config,hash)
177
+ hash.keys.each do |k|
178
+ config[k] = hash[k]
179
+ end
180
+ config
181
+ end
182
+
183
+ def self.from_pkcs12_file pkcs12_file, passphrase = nil
184
+ raise "#{pksc12_file} does not exist" unless File.exists?(pkcs12_file)
185
+ from_pkcs12 OpenSSL::PKCS12.new(File.read(pkcs12_file), passphrase)
186
+ end
187
+
188
+ def self.from_pkcs12 openssl_pkcs12
189
+ unless openssl_pkcs12.is_a? OpenSSL::PKCS12
190
+ raise "Can only construct from an OpenSSL::PKCS12"
191
+ end
192
+ certificate = from_openssl openssl_pkcs12.certificate
193
+ certificate.key_material.private_key = openssl_pkcs12.key
194
+ certificate
195
+ end
196
+
197
+ def self.from_openssl openssl_cert
198
+ unless openssl_cert.is_a? OpenSSL::X509::Certificate
199
+ raise "Can only construct from an OpenSSL::X509::Certificate"
200
+ end
201
+
202
+ certificate = Certificate.new
203
+ # Only subject, key_material, and body are used for signing
204
+ certificate.distinguished_name = DistinguishedName.from_openssl openssl_cert.subject
205
+ certificate.key_material.public_key = openssl_cert.public_key
206
+ certificate.openssl_body = openssl_cert
207
+ certificate.serial_number.number = openssl_cert.serial.to_i
208
+ certificate.not_before = openssl_cert.not_before
209
+ certificate.not_after = openssl_cert.not_after
210
+ # TODO extensions
211
+ certificate
212
+ end
213
+
214
+ end
215
+ end
@@ -0,0 +1,59 @@
1
+ module CertificateAuthority
2
+ class CertificateRevocationList
3
+ include ActiveModel::Validations
4
+
5
+ attr_accessor :certificates
6
+ attr_accessor :parent
7
+ attr_accessor :crl_body
8
+ attr_accessor :next_update
9
+
10
+ validate do |crl|
11
+ errors.add :next_update, "Next update must be a positive value" if crl.next_update < 0
12
+ errors.add :parent, "A parent entity must be set" if crl.parent.nil?
13
+ end
14
+
15
+ def initialize
16
+ self.certificates = []
17
+ self.next_update = 60 * 60 * 4 # 4 hour default
18
+ end
19
+
20
+ def <<(cert)
21
+ raise "Only revoked certificates can be added to a CRL" unless cert.revoked?
22
+ self.certificates << cert
23
+ end
24
+
25
+ def sign!
26
+ raise "No parent entity has been set!" if self.parent.nil?
27
+ raise "Invalid CRL" unless self.valid?
28
+
29
+ revocations = self.certificates.collect do |certificate|
30
+ revocation = OpenSSL::X509::Revoked.new
31
+ x509_cert = OpenSSL::X509::Certificate.new(certificate.to_pem)
32
+ revocation.serial = x509_cert.serial
33
+ revocation.time = certificate.revoked_at
34
+ revocation
35
+ end
36
+
37
+ crl = OpenSSL::X509::CRL.new
38
+ revocations.each do |revocation|
39
+ crl.add_revoked(revocation)
40
+ end
41
+
42
+ crl.version = 1
43
+ crl.last_update = Time.now
44
+ crl.next_update = Time.now + self.next_update
45
+
46
+ signing_cert = OpenSSL::X509::Certificate.new(self.parent.to_pem)
47
+ digest = OpenSSL::Digest::Digest.new("SHA512")
48
+ crl.issuer = signing_cert.subject
49
+ self.crl_body = crl.sign(self.parent.key_material.private_key, digest)
50
+
51
+ self.crl_body
52
+ end
53
+
54
+ def to_pem
55
+ raise "No signed CRL body" if self.crl_body.nil?
56
+ self.crl_body.to_pem
57
+ end
58
+ end#CertificateRevocationList
59
+ end
@@ -0,0 +1,58 @@
1
+ module CertificateAuthority
2
+ class DistinguishedName
3
+ include ActiveModel::Validations
4
+
5
+ validates_presence_of :common_name
6
+
7
+ attr_accessor :common_name
8
+ alias :cn :common_name
9
+
10
+ attr_accessor :locality
11
+ alias :l :locality
12
+
13
+ attr_accessor :state
14
+ alias :s :state
15
+
16
+ attr_accessor :country
17
+ alias :c :country
18
+
19
+ attr_accessor :organization
20
+ alias :o :organization
21
+
22
+ attr_accessor :organizational_unit
23
+ alias :ou :organizational_unit
24
+
25
+ def to_x509_name
26
+ raise "Invalid Distinguished Name" unless valid?
27
+
28
+ # NB: the capitalization in the strings counts
29
+ name = OpenSSL::X509::Name.new
30
+ name.add_entry("CN", common_name)
31
+ name.add_entry("O", organization) unless organization.blank?
32
+ name.add_entry("OU", organizational_unit) unless organizational_unit.blank?
33
+ name.add_entry("ST", state) unless state.blank?
34
+ name.add_entry("L", locality) unless locality.blank?
35
+ name.add_entry("C", country) unless country.blank?
36
+ name
37
+ end
38
+
39
+ def self.from_openssl openssl_name
40
+ unless openssl_name.is_a? OpenSSL::X509::Name
41
+ raise "Argument must be a OpenSSL::X509::Name"
42
+ end
43
+
44
+ name = DistinguishedName.new
45
+ openssl_name.to_a.each do |k,v|
46
+ case k
47
+ when "CN" then name.common_name = v
48
+ when "L" then name.locality = v
49
+ when "ST" then name.state = v
50
+ when "C" then name.country = v
51
+ when "O" then name.organization = v
52
+ when "OU" then name.organizational_unit = v
53
+ end
54
+ end
55
+ name
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,266 @@
1
+ module CertificateAuthority
2
+ module Extensions
3
+ module ExtensionAPI
4
+ def to_s
5
+ raise "Implementation required"
6
+ end
7
+
8
+ def config_extensions
9
+ {}
10
+ end
11
+
12
+ def openssl_identifier
13
+ raise "Implementation required"
14
+ end
15
+ end
16
+
17
+ class BasicContraints
18
+ include ExtensionAPI
19
+ include ActiveModel::Validations
20
+ attr_accessor :ca
21
+ attr_accessor :path_len
22
+ validates :ca, :inclusion => [true,false]
23
+
24
+ def initialize
25
+ self.ca = false
26
+ end
27
+
28
+ def is_ca?
29
+ self.ca
30
+ end
31
+
32
+ def path_len=(value)
33
+ raise "path_len must be a non-negative integer" if value < 0 or !value.is_a?(Fixnum)
34
+ @path_len = value
35
+ end
36
+
37
+ def openssl_identifier
38
+ "basicConstraints"
39
+ end
40
+
41
+ def to_s
42
+ result = ""
43
+ result += "CA:#{self.ca}"
44
+ result += ",pathlen:#{self.path_len}" unless self.path_len.nil?
45
+ result
46
+ end
47
+ end
48
+
49
+ class CrlDistributionPoints
50
+ include ExtensionAPI
51
+
52
+ attr_accessor :uri
53
+
54
+ def initialize
55
+ # self.uri = "http://moo.crlendPoint.example.com/something.crl"
56
+ end
57
+
58
+ def openssl_identifier
59
+ "crlDistributionPoints"
60
+ end
61
+
62
+ ## NB: At this time it seems OpenSSL's extension handlers don't support
63
+ ## any of the config options the docs claim to support... everything comes back
64
+ ## "missing value" on GENERAL NAME. Even if copied verbatim
65
+ def config_extensions
66
+ {
67
+ # "custom_crl_fields" => {"fullname" => "URI:#{fullname}"},
68
+ # "issuer_sect" => {"CN" => "crlissuer.com", "C" => "US", "O" => "shudder"}
69
+ }
70
+ end
71
+
72
+ def to_s
73
+ return "" if self.uri.nil?
74
+ "URI:#{self.uri}"
75
+ end
76
+ end
77
+
78
+ class SubjectKeyIdentifier
79
+ include ExtensionAPI
80
+ def openssl_identifier
81
+ "subjectKeyIdentifier"
82
+ end
83
+
84
+ def to_s
85
+ "hash"
86
+ end
87
+ end
88
+
89
+ class AuthorityKeyIdentifier
90
+ include ExtensionAPI
91
+
92
+ def openssl_identifier
93
+ "authorityKeyIdentifier"
94
+ end
95
+
96
+ def to_s
97
+ "keyid,issuer"
98
+ end
99
+ end
100
+
101
+ class AuthorityInfoAccess
102
+ include ExtensionAPI
103
+
104
+ attr_accessor :ocsp
105
+
106
+ def initialize
107
+ self.ocsp = []
108
+ end
109
+
110
+ def openssl_identifier
111
+ "authorityInfoAccess"
112
+ end
113
+
114
+ def to_s
115
+ return "" if self.ocsp.empty?
116
+ "OCSP;URI:#{self.ocsp}"
117
+ end
118
+ end
119
+
120
+ class KeyUsage
121
+ include ExtensionAPI
122
+
123
+ attr_accessor :usage
124
+
125
+ def initialize
126
+ self.usage = ["digitalSignature", "nonRepudiation"]
127
+ end
128
+
129
+ def openssl_identifier
130
+ "keyUsage"
131
+ end
132
+
133
+ def to_s
134
+ "#{self.usage.join(',')}"
135
+ end
136
+ end
137
+
138
+ class ExtendedKeyUsage
139
+ include ExtensionAPI
140
+
141
+ attr_accessor :usage
142
+
143
+ def initialize
144
+ self.usage = ["serverAuth","clientAuth"]
145
+ end
146
+
147
+ def openssl_identifier
148
+ "extendedKeyUsage"
149
+ end
150
+
151
+ def to_s
152
+ "#{self.usage.join(',')}"
153
+ end
154
+ end
155
+
156
+ class SubjectAlternativeName
157
+ include ExtensionAPI
158
+
159
+ attr_accessor :uris, :dns_names, :ips
160
+
161
+ def initialize
162
+ self.uris = []
163
+ self.dns_names = []
164
+ self.ips = []
165
+ end
166
+
167
+ def uris=(value)
168
+ raise "URIs must be an array" unless value.is_a?(Array)
169
+ @uris = value
170
+ end
171
+
172
+ def dns_names=(value)
173
+ raise "DNS names must be an array" unless value.is_a?(Array)
174
+ @dns_names = value
175
+ end
176
+
177
+ def ips=(value)
178
+ raise "IPs must be an array" unless value.is_a?(Array)
179
+ @ips = value
180
+ end
181
+
182
+ def openssl_identifier
183
+ "subjectAltName"
184
+ end
185
+
186
+ def to_s
187
+ res = self.uris.map {|u| "URI:#{u}" }
188
+ res += self.dns_names.map {|d| "DNS:#{d}" }
189
+ res += self.ips.map {|i| "IP:#{i}" }
190
+
191
+ return res.join(',')
192
+ end
193
+ end
194
+
195
+ class CertificatePolicies
196
+ include ExtensionAPI
197
+
198
+ attr_accessor :policy_identifier
199
+ attr_accessor :cps_uris
200
+ ##User notice
201
+ attr_accessor :explicit_text
202
+ attr_accessor :organization
203
+ attr_accessor :notice_numbers
204
+
205
+ def initialize
206
+ @contains_data = false
207
+ end
208
+
209
+
210
+ def openssl_identifier
211
+ "certificatePolicies"
212
+ end
213
+
214
+ def user_notice=(value={})
215
+ value.keys.each do |key|
216
+ self.send("#{key}=".to_sym, value[key])
217
+ end
218
+ end
219
+
220
+ def config_extensions
221
+ config_extension = {}
222
+ custom_policies = {}
223
+ notice = {}
224
+ unless self.policy_identifier.nil?
225
+ custom_policies["policyIdentifier"] = self.policy_identifier
226
+ end
227
+
228
+ if !self.cps_uris.nil? and self.cps_uris.is_a?(Array)
229
+ self.cps_uris.each_with_index do |cps_uri,i|
230
+ custom_policies["CPS.#{i}"] = cps_uri
231
+ end
232
+ end
233
+
234
+ unless self.explicit_text.nil?
235
+ notice["explicitText"] = self.explicit_text
236
+ end
237
+
238
+ unless self.organization.nil?
239
+ notice["organization"] = self.organization
240
+ end
241
+
242
+ unless self.notice_numbers.nil?
243
+ notice["noticeNumbers"] = self.notice_numbers
244
+ end
245
+
246
+ if notice.keys.size > 0
247
+ custom_policies["userNotice.1"] = "@notice"
248
+ config_extension["notice"] = notice
249
+ end
250
+
251
+ if custom_policies.keys.size > 0
252
+ config_extension["custom_policies"] = custom_policies
253
+ @contains_data = true
254
+ end
255
+
256
+ config_extension
257
+ end
258
+
259
+ def to_s
260
+ return "" unless @contains_data
261
+ "ia5org,@custom_policies"
262
+ end
263
+ end
264
+
265
+ end
266
+ end