certificate_authority 0.1.1
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.
- data/Gemfile +9 -0
- data/Gemfile.lock +35 -0
- data/README.rdoc +68 -0
- data/Rakefile +52 -0
- data/VERSION.yml +5 -0
- data/certificate_authority.gemspec +90 -0
- data/lib/certificate_authority/certificate.rb +176 -0
- data/lib/certificate_authority/certificate_revocation_list.rb +59 -0
- data/lib/certificate_authority/distinguished_name.rb +39 -0
- data/lib/certificate_authority/extensions.rb +251 -0
- data/lib/certificate_authority/key_material.rb +62 -0
- data/lib/certificate_authority/ocsp_handler.rb +77 -0
- data/lib/certificate_authority/pkcs11_key_material.rb +65 -0
- data/lib/certificate_authority/serial_number.rb +9 -0
- data/lib/certificate_authority/signing_entity.rb +18 -0
- data/lib/certificate_authority.rb +19 -0
- data/lib/tasks/certificate_authority.rake +23 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/units/certificate_authority_spec.rb +4 -0
- data/spec/units/certificate_revocation_list_spec.rb +68 -0
- data/spec/units/certificate_spec.rb +351 -0
- data/spec/units/distinguished_name_spec.rb +38 -0
- data/spec/units/extensions_spec.rb +53 -0
- data/spec/units/key_material_spec.rb +96 -0
- data/spec/units/ocsp_handler_spec.rb +104 -0
- data/spec/units/serial_number_spec.rb +20 -0
- data/spec/units/signing_entity_spec.rb +4 -0
- data/spec/units/units_helper.rb +1 -0
- metadata +148 -0
@@ -0,0 +1,39 @@
|
|
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", common_name) unless organizational_unit.blank?
|
33
|
+
name.add_entry("S", common_name) unless state.blank?
|
34
|
+
name.add_entry("L", common_name) unless locality.blank?
|
35
|
+
|
36
|
+
name
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,251 @@
|
|
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
|
+
"URI:#{self.uri}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class SubjectKeyIdentifier
|
78
|
+
include ExtensionAPI
|
79
|
+
def openssl_identifier
|
80
|
+
"subjectKeyIdentifier"
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_s
|
84
|
+
"hash"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class AuthorityKeyIdentifier
|
89
|
+
include ExtensionAPI
|
90
|
+
|
91
|
+
def openssl_identifier
|
92
|
+
"authorityKeyIdentifier"
|
93
|
+
end
|
94
|
+
|
95
|
+
def to_s
|
96
|
+
"keyid,issuer"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class AuthorityInfoAccess
|
101
|
+
include ExtensionAPI
|
102
|
+
|
103
|
+
attr_accessor :ocsp
|
104
|
+
|
105
|
+
def initialize
|
106
|
+
self.ocsp = []
|
107
|
+
end
|
108
|
+
|
109
|
+
def openssl_identifier
|
110
|
+
"authorityInfoAccess"
|
111
|
+
end
|
112
|
+
|
113
|
+
def to_s
|
114
|
+
"OCSP;URI:#{self.ocsp}"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
class KeyUsage
|
119
|
+
include ExtensionAPI
|
120
|
+
|
121
|
+
attr_accessor :usage
|
122
|
+
|
123
|
+
def initialize
|
124
|
+
self.usage = ["digitalSignature", "nonRepudiation"]
|
125
|
+
end
|
126
|
+
|
127
|
+
def openssl_identifier
|
128
|
+
"keyUsage"
|
129
|
+
end
|
130
|
+
|
131
|
+
def to_s
|
132
|
+
"#{self.usage.join(',')}"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
class ExtendedKeyUsage
|
137
|
+
include ExtensionAPI
|
138
|
+
|
139
|
+
attr_accessor :usage
|
140
|
+
|
141
|
+
def initialize
|
142
|
+
self.usage = ["serverAuth","clientAuth"]
|
143
|
+
end
|
144
|
+
|
145
|
+
def openssl_identifier
|
146
|
+
"extendedKeyUsage"
|
147
|
+
end
|
148
|
+
|
149
|
+
def to_s
|
150
|
+
"#{self.usage.join(',')}"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
class SubjectAlternativeName
|
155
|
+
include ExtensionAPI
|
156
|
+
|
157
|
+
attr_accessor :uris
|
158
|
+
|
159
|
+
def initialize
|
160
|
+
self.uris = []
|
161
|
+
end
|
162
|
+
|
163
|
+
def uris=(value)
|
164
|
+
raise "URIs must be an array" unless value.is_a?(Array)
|
165
|
+
@uris = value
|
166
|
+
end
|
167
|
+
|
168
|
+
def openssl_identifier
|
169
|
+
"subjectAltName"
|
170
|
+
end
|
171
|
+
|
172
|
+
def to_s
|
173
|
+
if self.uris.empty?
|
174
|
+
return ""
|
175
|
+
end
|
176
|
+
"URI:#{self.uris.join(',URI:')}"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
class CertificatePolicies
|
181
|
+
include ExtensionAPI
|
182
|
+
|
183
|
+
attr_accessor :policy_identifier
|
184
|
+
attr_accessor :cps_uris
|
185
|
+
##User notice
|
186
|
+
attr_accessor :explicit_text
|
187
|
+
attr_accessor :organization
|
188
|
+
attr_accessor :notice_numbers
|
189
|
+
|
190
|
+
def initialize
|
191
|
+
@contains_data = false
|
192
|
+
end
|
193
|
+
|
194
|
+
|
195
|
+
def openssl_identifier
|
196
|
+
"certificatePolicies"
|
197
|
+
end
|
198
|
+
|
199
|
+
def user_notice=(value={})
|
200
|
+
value.keys.each do |key|
|
201
|
+
self.send("#{key}=".to_sym, value[key])
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def config_extensions
|
206
|
+
config_extension = {}
|
207
|
+
custom_policies = {}
|
208
|
+
notice = {}
|
209
|
+
unless self.policy_identifier.nil?
|
210
|
+
custom_policies["policyIdentifier"] = self.policy_identifier
|
211
|
+
end
|
212
|
+
|
213
|
+
if !self.cps_uris.nil? and self.cps_uris.is_a?(Array)
|
214
|
+
self.cps_uris.each_with_index do |cps_uri,i|
|
215
|
+
custom_policies["CPS.#{i}"] = cps_uri
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
unless self.explicit_text.nil?
|
220
|
+
notice["explicitText"] = self.explicit_text
|
221
|
+
end
|
222
|
+
|
223
|
+
unless self.organization.nil?
|
224
|
+
notice["organization"] = self.organization
|
225
|
+
end
|
226
|
+
|
227
|
+
unless self.notice_numbers.nil?
|
228
|
+
notice["noticeNumbers"] = self.notice_numbers
|
229
|
+
end
|
230
|
+
|
231
|
+
if notice.keys.size > 0
|
232
|
+
custom_policies["userNotice.1"] = "@notice"
|
233
|
+
config_extension["notice"] = notice
|
234
|
+
end
|
235
|
+
|
236
|
+
if custom_policies.keys.size > 0
|
237
|
+
config_extension["custom_policies"] = custom_policies
|
238
|
+
@contains_data = true
|
239
|
+
end
|
240
|
+
|
241
|
+
config_extension
|
242
|
+
end
|
243
|
+
|
244
|
+
def to_s
|
245
|
+
return "" unless @contains_data
|
246
|
+
"ia5org,@custom_policies"
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
end
|
251
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module CertificateAuthority
|
2
|
+
module KeyMaterial
|
3
|
+
def public_key
|
4
|
+
raise "Required implementation"
|
5
|
+
end
|
6
|
+
|
7
|
+
def private_key
|
8
|
+
raise "Required implementation"
|
9
|
+
end
|
10
|
+
|
11
|
+
def is_in_hardware?
|
12
|
+
raise "Required implementation"
|
13
|
+
end
|
14
|
+
|
15
|
+
def is_in_memory?
|
16
|
+
raise "Required implementation"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class MemoryKeyMaterial
|
21
|
+
include KeyMaterial
|
22
|
+
include ActiveModel::Validations
|
23
|
+
|
24
|
+
attr_accessor :keypair
|
25
|
+
attr_accessor :private_key
|
26
|
+
attr_accessor :public_key
|
27
|
+
|
28
|
+
def initialize
|
29
|
+
end
|
30
|
+
|
31
|
+
validates_each :private_key do |record, attr, value|
|
32
|
+
record.errors.add :private_key, "cannot be blank" if record.private_key.nil?
|
33
|
+
end
|
34
|
+
validates_each :public_key do |record, attr, value|
|
35
|
+
record.errors.add :public_key, "cannot be blank" if record.public_key.nil?
|
36
|
+
end
|
37
|
+
|
38
|
+
def is_in_hardware?
|
39
|
+
false
|
40
|
+
end
|
41
|
+
|
42
|
+
def is_in_memory?
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
def generate_key(modulus_bits=1024)
|
47
|
+
self.keypair = OpenSSL::PKey::RSA.new(modulus_bits)
|
48
|
+
self.private_key = keypair
|
49
|
+
self.public_key = keypair.public_key
|
50
|
+
self.keypair
|
51
|
+
end
|
52
|
+
|
53
|
+
def private_key
|
54
|
+
@private_key
|
55
|
+
end
|
56
|
+
|
57
|
+
def public_key
|
58
|
+
@public_key
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module CertificateAuthority
|
2
|
+
class OCSPHandler
|
3
|
+
include ActiveModel::Validations
|
4
|
+
|
5
|
+
attr_accessor :ocsp_request
|
6
|
+
attr_accessor :certificate_ids
|
7
|
+
|
8
|
+
attr_accessor :certificates
|
9
|
+
attr_accessor :parent
|
10
|
+
|
11
|
+
attr_accessor :ocsp_response_body
|
12
|
+
|
13
|
+
validate do |crl|
|
14
|
+
errors.add :parent, "A parent entity must be set" if parent.nil?
|
15
|
+
end
|
16
|
+
validate :all_certificates_available
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
self.certificates = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def <<(cert)
|
23
|
+
self.certificates[cert.serial_number.number.to_s] = cert
|
24
|
+
end
|
25
|
+
|
26
|
+
def extract_certificate_serials
|
27
|
+
raise "No valid OCSP request was supplied" if self.ocsp_request.nil?
|
28
|
+
openssl_request = OpenSSL::OCSP::Request.new(self.ocsp_request)
|
29
|
+
|
30
|
+
self.certificate_ids = openssl_request.certid.collect do |cert_id|
|
31
|
+
cert_id.serial
|
32
|
+
end
|
33
|
+
|
34
|
+
self.certificate_ids
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
def response
|
39
|
+
raise "Invalid response" unless valid?
|
40
|
+
|
41
|
+
openssl_ocsp_response = OpenSSL::OCSP::BasicResponse.new
|
42
|
+
openssl_ocsp_request = OpenSSL::OCSP::Request.new(self.ocsp_request)
|
43
|
+
openssl_ocsp_response.copy_nonce(openssl_ocsp_request)
|
44
|
+
|
45
|
+
openssl_ocsp_request.certid.each do |cert_id|
|
46
|
+
certificate = self.certificates[cert_id.serial.to_s]
|
47
|
+
|
48
|
+
openssl_ocsp_response.add_status(cert_id,
|
49
|
+
OpenSSL::OCSP::V_CERTSTATUS_GOOD, 0,
|
50
|
+
0, 0, 30, nil)
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
openssl_ocsp_response.sign(OpenSSL::X509::Certificate.new(self.parent.to_pem), self.parent.key_material.private_key, nil, nil)
|
55
|
+
final_response = OpenSSL::OCSP::Response.create(OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL, openssl_ocsp_response)
|
56
|
+
self.ocsp_response_body = final_response
|
57
|
+
self.ocsp_response_body
|
58
|
+
end
|
59
|
+
|
60
|
+
def to_der
|
61
|
+
raise "No signed OCSP response body available" if self.ocsp_response_body.nil?
|
62
|
+
self.ocsp_response_body.to_der
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def all_certificates_available
|
68
|
+
openssl_ocsp_request = OpenSSL::OCSP::Request.new(self.ocsp_request)
|
69
|
+
|
70
|
+
openssl_ocsp_request.certid.each do |cert_id|
|
71
|
+
certificate = self.certificates[cert_id.serial.to_s]
|
72
|
+
errors.add(:base, "Certificate #{cert_id.serial} has not been added yet") if certificate.nil?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module CertificateAuthority
|
2
|
+
class Pkcs11KeyMaterial
|
3
|
+
include KeyMaterial
|
4
|
+
include ActiveModel::Validations
|
5
|
+
include ActiveModel::Serialization
|
6
|
+
|
7
|
+
attr_accessor :engine
|
8
|
+
attr_accessor :token_id
|
9
|
+
attr_accessor :pkcs11_lib
|
10
|
+
attr_accessor :openssl_pkcs11_engine_lib
|
11
|
+
attr_accessor :pin
|
12
|
+
|
13
|
+
def initialize(attributes = {})
|
14
|
+
@attributes = attributes
|
15
|
+
initialize_engine
|
16
|
+
end
|
17
|
+
|
18
|
+
def is_in_hardware?
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
22
|
+
def is_in_memory?
|
23
|
+
false
|
24
|
+
end
|
25
|
+
|
26
|
+
def generate_key(modulus_bits=1024)
|
27
|
+
puts "Key generation is not currently supported in hardware"
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def private_key
|
32
|
+
initialize_engine
|
33
|
+
self.engine.load_private_key(self.token_id)
|
34
|
+
end
|
35
|
+
|
36
|
+
def public_key
|
37
|
+
initialize_engine
|
38
|
+
self.engine.load_public_key(self.token_id)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def initialize_engine
|
44
|
+
## We're going to return early and try again later if params weren't passed in
|
45
|
+
## at initialization. Any attempt at getting a public/private key will try
|
46
|
+
## again.
|
47
|
+
return false if self.openssl_pkcs11_engine_lib.nil? or self.pkcs11_lib.nil?
|
48
|
+
return self.engine unless self.engine.nil?
|
49
|
+
OpenSSL::Engine.load
|
50
|
+
|
51
|
+
pkcs11 = OpenSSL::Engine.by_id("dynamic") do |e|
|
52
|
+
e.ctrl_cmd("SO_PATH",self.openssl_pkcs11_engine_lib)
|
53
|
+
e.ctrl_cmd("ID","pkcs11")
|
54
|
+
e.ctrl_cmd("LIST_ADD","1")
|
55
|
+
e.ctrl_cmd("LOAD")
|
56
|
+
e.ctrl_cmd("PIN",self.pin) unless self.pin.nil? or self.pin == ""
|
57
|
+
e.ctrl_cmd("MODULE_PATH",self.pkcs11_lib)
|
58
|
+
end
|
59
|
+
|
60
|
+
self.engine = pkcs11
|
61
|
+
pkcs11
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|