certificate_authority_sonian 0.1.7

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