r509 0.8.1 → 0.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (203) hide show
  1. data/README.md +343 -151
  2. data/Rakefile +26 -23
  3. data/bin/r509 +126 -112
  4. data/bin/r509-parse +24 -24
  5. data/doc/R509.html +169 -7
  6. data/doc/R509/ASN1.html +370 -0
  7. data/doc/R509/ASN1/GeneralName.html +1121 -0
  8. data/doc/R509/ASN1/GeneralNames.html +843 -0
  9. data/doc/R509/ASN1/NoticeReference.html +392 -0
  10. data/doc/R509/ASN1/PolicyInformation.html +387 -0
  11. data/doc/R509/ASN1/PolicyQualifiers.html +455 -0
  12. data/doc/R509/ASN1/UserNotice.html +386 -0
  13. data/doc/R509/{Crl.html → CRL.html} +7 -7
  14. data/doc/R509/CRL/Administrator.html +1559 -0
  15. data/doc/R509/{Crl/Parser.html → CRL/SignedList.html} +501 -210
  16. data/doc/R509/{Csr.html → CSR.html} +444 -314
  17. data/doc/R509/Cert.html +866 -617
  18. data/doc/R509/Cert/Extensions.html +52 -41
  19. data/doc/R509/Cert/Extensions/AuthorityInfoAccess.html +70 -35
  20. data/doc/R509/Cert/Extensions/AuthorityKeyIdentifier.html +387 -4
  21. data/doc/R509/Cert/Extensions/BasicConstraints.html +61 -25
  22. data/doc/R509/Cert/Extensions/CRLDistributionPoints.html +354 -0
  23. data/doc/R509/Cert/Extensions/CertificatePolicies.html +340 -0
  24. data/doc/R509/Cert/Extensions/ExtendedKeyUsage.html +440 -49
  25. data/doc/R509/Cert/Extensions/{CrlDistributionPoints.html → InhibitAnyPolicy.html} +52 -35
  26. data/doc/R509/Cert/Extensions/KeyUsage.html +247 -121
  27. data/doc/R509/Cert/Extensions/NameConstraints.html +445 -0
  28. data/doc/R509/Cert/Extensions/OCSPNoCheck.html +239 -0
  29. data/doc/R509/Cert/Extensions/PolicyConstraints.html +424 -0
  30. data/doc/R509/Cert/Extensions/SubjectAlternativeName.html +437 -62
  31. data/doc/R509/Cert/Extensions/SubjectKeyIdentifier.html +52 -10
  32. data/doc/R509/CertificateAuthority.html +4 -4
  33. data/doc/R509/CertificateAuthority/Signer.html +154 -187
  34. data/doc/R509/Config.html +6 -6
  35. data/doc/R509/Config/{CaConfig.html → CAConfig.html} +451 -348
  36. data/doc/R509/Config/{CaConfigPool.html → CAConfigPool.html} +47 -47
  37. data/doc/R509/Config/CAProfile.html +1015 -0
  38. data/doc/R509/Config/SubjectItemPolicy.html +86 -86
  39. data/doc/R509/IOHelpers.html +22 -22
  40. data/doc/R509/MessageDigest.html +14 -14
  41. data/doc/R509/NameSanitizer.html +53 -53
  42. data/doc/R509/{Ocsp.html → OCSP.html} +9 -9
  43. data/doc/R509/{Ocsp → OCSP}/Request.html +7 -7
  44. data/doc/R509/{Ocsp → OCSP}/Request/Nonce.html +56 -11
  45. data/doc/R509/{Ocsp → OCSP}/Response.html +44 -44
  46. data/doc/R509/{OidMapper.html → OIDMapper.html} +23 -39
  47. data/doc/R509/PrivateKey.html +415 -168
  48. data/doc/R509/R509Error.html +3 -3
  49. data/doc/R509/{Spki.html → SPKI.html} +354 -192
  50. data/doc/R509/Subject.html +224 -113
  51. data/doc/R509/Validity.html +27 -5
  52. data/doc/R509/Validity/Checker.html +13 -13
  53. data/doc/R509/Validity/DefaultChecker.html +13 -13
  54. data/doc/R509/Validity/DefaultWriter.html +14 -14
  55. data/doc/R509/Validity/Status.html +39 -39
  56. data/doc/R509/Validity/Writer.html +18 -18
  57. data/doc/_index.html +138 -35
  58. data/doc/class_list.html +1 -1
  59. data/doc/css/style.css +10 -0
  60. data/doc/file.README.html +368 -171
  61. data/doc/file.r509.html +92 -69
  62. data/doc/frames.html +1 -1
  63. data/doc/index.html +368 -171
  64. data/doc/method_list.html +910 -390
  65. data/doc/top-level-namespace.html +3 -3
  66. data/lib/r509.rb +32 -16
  67. data/lib/r509/asn1.rb +375 -0
  68. data/lib/r509/cert.rb +381 -364
  69. data/lib/r509/cert/extensions.rb +443 -76
  70. data/lib/r509/certificate_authority.rb +407 -0
  71. data/lib/r509/config.rb +547 -351
  72. data/lib/r509/crl.rb +336 -366
  73. data/lib/r509/csr.rb +278 -289
  74. data/lib/r509/ec-hack.rb +37 -0
  75. data/lib/r509/exceptions.rb +3 -3
  76. data/lib/r509/io_helpers.rb +44 -44
  77. data/lib/r509/message_digest.rb +53 -0
  78. data/lib/r509/ocsp.rb +80 -70
  79. data/lib/r509/oid_mapper.rb +32 -0
  80. data/lib/r509/private_key.rb +228 -0
  81. data/lib/r509/spki.rb +145 -93
  82. data/lib/r509/subject.rb +203 -110
  83. data/lib/r509/validity.rb +70 -68
  84. data/lib/r509/version.rb +2 -2
  85. data/r509.yaml +92 -69
  86. data/spec/asn1_spec.rb +402 -0
  87. data/spec/cert/extensions_spec.rb +957 -494
  88. data/spec/cert_spec.rb +382 -307
  89. data/spec/certificate_authority_spec.rb +668 -250
  90. data/spec/config_spec.rb +515 -302
  91. data/spec/crl_spec.rb +197 -198
  92. data/spec/csr_spec.rb +334 -289
  93. data/spec/fixtures.rb +247 -171
  94. data/spec/fixtures/cert1.der +0 -0
  95. data/spec/fixtures/cert1.pem +0 -0
  96. data/spec/fixtures/cert1_public_key_modulus.txt +0 -0
  97. data/spec/fixtures/cert3.p12 +0 -0
  98. data/spec/fixtures/cert3.pem +0 -0
  99. data/spec/fixtures/cert3_key.pem +0 -0
  100. data/spec/fixtures/cert3_key_des3.pem +0 -0
  101. data/spec/fixtures/cert4.pem +0 -0
  102. data/spec/fixtures/cert5.pem +0 -0
  103. data/spec/fixtures/cert6.pem +0 -0
  104. data/spec/fixtures/cert_expired.pem +0 -0
  105. data/spec/fixtures/cert_inhibit.pem +24 -0
  106. data/spec/fixtures/cert_name_constraints.pem +29 -0
  107. data/spec/fixtures/cert_not_yet_valid.pem +0 -0
  108. data/spec/fixtures/cert_ocsp_no_check.pem +18 -0
  109. data/spec/fixtures/cert_policy_constraints.pem +31 -0
  110. data/spec/fixtures/cert_san.pem +0 -0
  111. data/spec/fixtures/cert_san2.pem +0 -0
  112. data/spec/fixtures/cert_unknown_extension.pem +28 -0
  113. data/spec/fixtures/config_pool_test_minimal.yaml +11 -11
  114. data/spec/fixtures/config_test.yaml +54 -36
  115. data/spec/fixtures/config_test_dsa.yaml +35 -0
  116. data/spec/fixtures/config_test_ec.yaml +35 -0
  117. data/spec/fixtures/config_test_engine_key.yaml +5 -5
  118. data/spec/fixtures/config_test_engine_no_key_name.yaml +4 -4
  119. data/spec/fixtures/config_test_minimal.yaml +4 -4
  120. data/spec/fixtures/config_test_password.yaml +5 -5
  121. data/spec/fixtures/config_test_various.yaml +111 -74
  122. data/spec/fixtures/crl_list_file.txt +0 -0
  123. data/spec/fixtures/crl_with_reason.pem +0 -0
  124. data/spec/fixtures/csr1.der +0 -0
  125. data/spec/fixtures/csr1.pem +0 -0
  126. data/spec/fixtures/csr1_key.der +0 -0
  127. data/spec/fixtures/csr1_key.pem +0 -0
  128. data/spec/fixtures/csr1_key_encrypted_des3.pem +0 -0
  129. data/spec/fixtures/csr1_newlines.pem +0 -0
  130. data/spec/fixtures/csr1_no_begin_end.pem +0 -0
  131. data/spec/fixtures/csr1_public_key_modulus.txt +0 -0
  132. data/spec/fixtures/csr2.pem +0 -0
  133. data/spec/fixtures/csr2_key.pem +0 -0
  134. data/spec/fixtures/csr3.pem +0 -0
  135. data/spec/fixtures/csr4.pem +0 -0
  136. data/spec/fixtures/csr_dsa.pem +0 -0
  137. data/spec/fixtures/csr_invalid_signature.pem +0 -0
  138. data/spec/fixtures/dsa_key.pem +0 -0
  139. data/spec/fixtures/dsa_root.cer +28 -0
  140. data/spec/fixtures/dsa_root.key +20 -0
  141. data/spec/fixtures/ec_csr2.der +0 -0
  142. data/spec/fixtures/ec_csr2.pem +8 -0
  143. data/spec/fixtures/ec_key1.der +0 -0
  144. data/spec/fixtures/ec_key1.pem +6 -0
  145. data/spec/fixtures/ec_key1_encrypted.pem +9 -0
  146. data/spec/fixtures/ec_key2.pem +6 -0
  147. data/spec/fixtures/hmacsha1.sig +1 -0
  148. data/spec/fixtures/hmacsha512.sig +1 -0
  149. data/spec/fixtures/key4.pem +0 -0
  150. data/spec/fixtures/key4_encrypted_des3.pem +0 -0
  151. data/spec/fixtures/missing_key_identifier_ca.cer +0 -0
  152. data/spec/fixtures/missing_key_identifier_ca.key +0 -0
  153. data/spec/fixtures/ocsptest.r509.local.pem +0 -0
  154. data/spec/fixtures/ocsptest.r509.local_ocsp_request.der +0 -0
  155. data/spec/fixtures/ocsptest2.r509.local.pem +0 -0
  156. data/spec/fixtures/second_ca.cer +0 -0
  157. data/spec/fixtures/second_ca.key +0 -0
  158. data/spec/fixtures/spkac.der +0 -0
  159. data/spec/fixtures/spkac.txt +0 -0
  160. data/spec/fixtures/spkac_dsa.txt +1 -1
  161. data/spec/fixtures/spkac_dsa_no_verify.txt +1 -0
  162. data/spec/fixtures/spkac_ec.txt +1 -0
  163. data/spec/fixtures/spkac_rsa_newlines.txt +13 -0
  164. data/spec/fixtures/stca.pem +0 -0
  165. data/spec/fixtures/stca_ocsp_request.der +0 -0
  166. data/spec/fixtures/stca_ocsp_response.der +0 -0
  167. data/spec/fixtures/test1.csr +0 -0
  168. data/spec/fixtures/test_ca.cer +0 -0
  169. data/spec/fixtures/test_ca.key +0 -0
  170. data/spec/fixtures/test_ca.p12 +0 -0
  171. data/spec/fixtures/test_ca_des3.key +0 -0
  172. data/spec/fixtures/test_ca_ec.cer +14 -0
  173. data/spec/fixtures/test_ca_ec.key +6 -0
  174. data/spec/fixtures/test_ca_ec_ee.cer +22 -0
  175. data/spec/fixtures/test_ca_ec_ee.key +6 -0
  176. data/spec/fixtures/test_ca_ocsp.cer +0 -0
  177. data/spec/fixtures/test_ca_ocsp.key +0 -0
  178. data/spec/fixtures/test_ca_ocsp.p12 +0 -0
  179. data/spec/fixtures/test_ca_ocsp_chain.txt +0 -0
  180. data/spec/fixtures/test_ca_ocsp_response.der +0 -0
  181. data/spec/fixtures/test_ca_subroot.cer +0 -0
  182. data/spec/fixtures/test_ca_subroot.key +0 -0
  183. data/spec/fixtures/test_ca_subroot_ocsp.cer +0 -0
  184. data/spec/fixtures/test_ca_subroot_ocsp.key +0 -0
  185. data/spec/fixtures/test_ca_subroot_ocsp_response.der +0 -0
  186. data/spec/fixtures/unknown_oid.csr +0 -0
  187. data/spec/message_digest_spec.rb +104 -84
  188. data/spec/ocsp_spec.rb +105 -105
  189. data/spec/oid_mapper_spec.rb +21 -21
  190. data/spec/private_key_spec.rb +275 -0
  191. data/spec/r509_spec.rb +35 -0
  192. data/spec/spec_helper.rb +15 -6
  193. data/spec/spki_spec.rb +221 -142
  194. data/spec/subject_spec.rb +232 -164
  195. data/spec/validity_spec.rb +91 -91
  196. metadata +79 -25
  197. data/doc/R509/Config/CaProfile.html +0 -651
  198. data/doc/R509/Crl/Administrator.html +0 -2073
  199. data/lib/r509/certificateauthority.rb +0 -290
  200. data/lib/r509/messagedigest.rb +0 -49
  201. data/lib/r509/oidmapper.rb +0 -32
  202. data/lib/r509/privatekey.rb +0 -185
  203. data/spec/privatekey_spec.rb +0 -198
@@ -0,0 +1,407 @@
1
+ require 'openssl'
2
+ require 'r509/config'
3
+ require 'r509/cert'
4
+ require 'r509/exceptions'
5
+ require 'r509/ec-hack'
6
+
7
+ # CertificateAuthority related classes
8
+ module R509::CertificateAuthority
9
+ # Contains the certification authority signing operation methods
10
+ class Signer
11
+ # @param [R509::Config] config
12
+ def initialize(config=nil)
13
+ @config = config
14
+
15
+ if not @config.nil? and not @config.kind_of?(R509::Config::CAConfig)
16
+ raise R509::R509Error, "config must be a kind of R509::Config::CAConfig or nil (for self-sign only)"
17
+ end
18
+ if not @config.nil? and not @config.ca_cert.has_private_key?
19
+ raise R509::R509Error, "You must have a private key associated with your CA certificate to issue"
20
+ end
21
+ end
22
+
23
+ # Signs a CSR
24
+ # @option options :csr [R509::CSR]
25
+ # @option options :spki [R509::SPKI]
26
+ # @option options :profile_name [String] The CA profile you want to use (eg "server" in your config)
27
+ # @option options :subject [R509::Subject,OpenSSL::X509::Subject,Array] (optional for R509::CSR, required for R509::SPKI)
28
+ # @option options :san_names [Array,R509::ASN1::GeneralNames] optional either an array of names that will be automatically parsed to determine their type, or an explicit R509::ASN1::GeneralNames object
29
+ # @option options :message_digest [String] the message digest to use for this certificate instead of the config's default
30
+ # @option options :serial [String] the serial number you want to issue the certificate with
31
+ # @option options :not_before [Time] the notBefore for the certificate
32
+ # @option options :not_after [Time] the notAfter for the certificate
33
+ # @return [R509::Cert] the signed cert object
34
+ def sign(options)
35
+ if @config.nil?
36
+ raise R509::R509Error, "When instantiating the signer without a config you can only call #selfsign"
37
+ elsif @config.num_profiles == 0
38
+ raise R509::R509Error, "You must have at least one CAProfile on your CAConfig to issue"
39
+ end
40
+
41
+ check_options(options)
42
+
43
+ subject, san_names, public_key = extract_public_key_subject_san(options)
44
+
45
+
46
+ if options.has_key?(:csr) and not options[:csr].verify_signature
47
+ raise R509::R509Error, "Certificate request signature is invalid."
48
+ end
49
+
50
+ # prior to OpenSSL 1.0 DSA could only use DSS1 (aka SHA1) signatures. post-1.0 anything
51
+ # goes but at the moment we don't enforce this restriction so an OpenSSL error could
52
+ # bubble up if they do it wrong.
53
+ message_digest = (options.has_key?(:message_digest))? R509::MessageDigest.new(options[:message_digest]) : R509::MessageDigest.new(@config.message_digest)
54
+
55
+ profile = @config.profile(options[:profile_name])
56
+
57
+ validated_subject = validate_subject(subject,profile)
58
+
59
+ cert = build_cert(
60
+ :subject => validated_subject.name,
61
+ :issuer => @config.ca_cert.subject.name,
62
+ :not_before => options[:not_before],
63
+ :not_after => options[:not_after],
64
+ :public_key => public_key,
65
+ :serial => options[:serial]
66
+ )
67
+
68
+ basic_constraints = profile.basic_constraints
69
+ key_usage = profile.key_usage
70
+ extended_key_usage = profile.extended_key_usage
71
+ certificate_policies = profile.certificate_policies
72
+ ocsp_no_check = profile.ocsp_no_check
73
+
74
+ build_extensions(
75
+ :subject_certificate => cert,
76
+ :issuer_certificate => @config.ca_cert.cert,
77
+ :basic_constraints => basic_constraints,
78
+ :key_usage => key_usage,
79
+ :extended_key_usage => extended_key_usage,
80
+ :ocsp_no_check => ocsp_no_check,
81
+ :certificate_policies => certificate_policies,
82
+ :san_names => san_names,
83
+ :inhibit_any_policy => profile.inhibit_any_policy,
84
+ :policy_constraints => profile.policy_constraints,
85
+ :name_constraints => profile.name_constraints
86
+ )
87
+
88
+
89
+ #@config.ca_cert.key.key ... ugly. ca_cert returns R509::Cert
90
+ # #key returns R509::PrivateKey and #key on that returns OpenSSL object we need
91
+ cert.sign( @config.ca_cert.key.key, message_digest.digest )
92
+ R509::Cert.new(:cert => cert)
93
+ end
94
+
95
+ # Self-signs a CSR
96
+ # @option options :csr [R509::CSR]
97
+ # @option options :message_digest [String] the message digest to use for this certificate (defaults to sha1)
98
+ # @option options :serial [String] the serial number you want to issue the certificate with (defaults to random)
99
+ # @option options :not_before [Time] the notBefore for the certificate (defaults to now)
100
+ # @option options :not_after [Time] the notAfter for the certificate (defaults to 1 year)
101
+ # @option options :san_names [Array,R509::ASN1::GeneralNames] optional either an array of names that will be automatically parsed to determine their type, or an explicit R509::ASN1::GeneralNames object
102
+ # @return [R509::Cert] the signed cert object
103
+ def selfsign(options)
104
+ if not options.kind_of?(Hash)
105
+ raise ArgumentError, "You must pass a hash of options consisting of at minimum :csr"
106
+ end
107
+ csr = options[:csr]
108
+ if csr.key.nil?
109
+ raise ArgumentError, 'CSR must also have a private key to self sign'
110
+ end
111
+ cert = build_cert(
112
+ :subject => csr.subject.name,
113
+ :issuer => csr.subject.name,
114
+ :not_before => options[:not_before],
115
+ :not_after => options[:not_after],
116
+ :public_key => csr.public_key,
117
+ :serial => options[:serial]
118
+ )
119
+
120
+ sans = (options.has_key?(:san_names))? options[:san_names] : csr.san
121
+ san_names = parse_san_names(sans)
122
+
123
+ build_extensions(
124
+ :subject_certificate => cert,
125
+ :issuer_certificate => cert,
126
+ :basic_constraints => {"ca" => true },
127
+ :san_names => san_names
128
+ )
129
+
130
+
131
+ if options.has_key?(:message_digest)
132
+ message_digest = R509::MessageDigest.new(options[:message_digest])
133
+ else
134
+ message_digest = R509::MessageDigest.new('sha1')
135
+ end
136
+
137
+ # CSR#key returns R509::PrivateKey and #key on that returns OpenSSL object we need
138
+ cert.sign( csr.key.key, message_digest.digest )
139
+ R509::Cert.new(:cert => cert)
140
+ end
141
+
142
+ private
143
+
144
+ def check_options(options)
145
+ if options.has_key?(:csr) and options.has_key?(:spki)
146
+ raise ArgumentError, "You can't pass both :csr and :spki"
147
+ elsif not options.has_key?(:csr) and not options.has_key?(:spki)
148
+ raise ArgumentError, "You must supply either :csr or :spki"
149
+ elsif options.has_key?(:csr)
150
+ if not options[:csr].kind_of?(R509::CSR)
151
+ raise ArgumentError, "You must pass an R509::CSR object for :csr"
152
+ end
153
+ elsif not options.has_key?(:csr) and options.has_key?(:spki)
154
+ if not options[:spki].kind_of?(R509::SPKI)
155
+ raise ArgumentError, "You must pass an R509::SPKI object for :spki"
156
+ end
157
+ end
158
+ end
159
+
160
+ def extract_public_key_subject_san(options)
161
+ if options.has_key?(:csr)
162
+ subject = (options.has_key?(:subject))? R509::Subject.new(options[:subject]) : options[:csr].subject
163
+ sans = (options.has_key?(:san_names))? options[:san_names] : options[:csr].san
164
+ san_names = parse_san_names(sans)
165
+ public_key = options[:csr].public_key
166
+ else
167
+ # spki
168
+ if not options.has_key?(:subject)
169
+ raise ArgumentError, "You must supply :subject when passing :spki"
170
+ end
171
+ public_key = options[:spki].public_key
172
+ subject = R509::Subject.new(options[:subject])
173
+ san_names = parse_san_names(options[:san_names]) # optional
174
+ end
175
+
176
+ [subject,san_names,public_key]
177
+ end
178
+
179
+ def parse_san_names(sans)
180
+ case sans
181
+ when nil then nil
182
+ when R509::ASN1::GeneralNames then sans
183
+ when Array then R509::ASN1.general_name_parser(sans)
184
+ else
185
+ raise ArgumentError, "When passing SAN names it must be provided as either an array of strings or an R509::ASN1::GeneralNames object"
186
+ end
187
+ end
188
+
189
+ def build_conf(section,hash,index)
190
+ conf = ["[#{section}]"]
191
+ conf.push "policyIdentifier=#{hash["policy_identifier"]}" unless hash["policy_identifier"].nil?
192
+ hash["cps_uris"].each_with_index do |cps,idx|
193
+ conf.push "CPS.#{idx+1}=\"#{cps}\""
194
+ end if hash["cps_uris"].respond_to?(:each_with_index)
195
+
196
+ user_notice_confs = []
197
+ hash["user_notices"].each_with_index do |un,k|
198
+ conf.push "userNotice.#{k+1}=@user_notice#{k+1}#{index}"
199
+ user_notice_confs.push "[user_notice#{k+1}#{index}]"
200
+ user_notice_confs.push "explicitText=\"#{un["explicit_text"]}\"" unless un["explicit_text"].nil?
201
+ # if org is supplied notice numbers is also required (and vice versa). enforced in CAProfile
202
+ user_notice_confs.push "organization=\"#{un["organization"]}\"" unless un["organization"].nil?
203
+ user_notice_confs.push "noticeNumbers=\"#{un["notice_numbers"]}\"" unless un["notice_numbers"].nil?
204
+ end unless not hash["user_notices"].kind_of?(Array)
205
+
206
+ conf.concat(user_notice_confs)
207
+ conf.join "\n"
208
+ end
209
+
210
+ def validate_subject(subject,profile)
211
+ if profile.subject_item_policy.nil? then
212
+ subject
213
+ else
214
+ profile.subject_item_policy.validate_subject(subject)
215
+ end
216
+ end
217
+
218
+ def build_cert(options)
219
+
220
+ cert = OpenSSL::X509::Certificate.new
221
+
222
+ cert.subject = options[:subject]
223
+ cert.issuer = options[:issuer]
224
+ cert.not_before = calculate_not_before(options[:not_before])
225
+ cert.not_after = calculate_not_after(options[:not_after],cert.not_before)
226
+ cert.public_key = options[:public_key]
227
+ cert.serial = create_serial(options[:serial])
228
+ cert.version = 2 #2 means v3
229
+ cert
230
+ end
231
+
232
+ def create_serial(serial)
233
+ if not serial.nil?
234
+ serial = OpenSSL::BN.new(serial.to_s)
235
+ else
236
+ # generate random serial in accordance with best practices
237
+ # guidelines state 20-bits of entropy, but we can cram more in
238
+ # per rfc5280 conforming CAs can make the serial field up to 20 octets
239
+ # to prevent even the incredibly remote possibility of collision we'll
240
+ # concatenate current time (to the microsecond) with a random num
241
+ rand = OpenSSL::BN.rand(96,0) # 96 bits is 12 bytes (octets).
242
+ serial = OpenSSL::BN.new((Time.now.to_f*1000000).to_i.to_s + rand.to_s)
243
+ # since second param is 0 the most significant bit must always be 1
244
+ # this theoretically gives us 95 bits of entropy + microtime, which
245
+ # adds a non-zero quantity of entropy. depending upon how predictable
246
+ # your issuance is, this could range from a reasonably large quantity
247
+ # of entropy to very little
248
+ end
249
+ serial
250
+ end
251
+
252
+ def build_extensions(options)
253
+ ef = OpenSSL::X509::ExtensionFactory.new
254
+
255
+ ef.subject_certificate = options[:subject_certificate]
256
+
257
+ ef.issuer_certificate = options[:issuer_certificate]
258
+
259
+ ext = []
260
+ if not options[:basic_constraints].nil?
261
+ bc = options[:basic_constraints]
262
+ if bc["ca"] == true
263
+ bc_value = "CA:TRUE"
264
+ if not bc["path_length"].nil?
265
+ bc_value += ",pathlen:#{bc["path_length"]}"
266
+ end
267
+ else
268
+ bc_value = "CA:FALSE"
269
+ end
270
+
271
+ ext << ef.create_extension("basicConstraints", bc_value, true)
272
+ end
273
+ if not options[:key_usage].nil? and not options[:key_usage].empty?
274
+ ext << ef.create_extension("keyUsage", options[:key_usage].join(","))
275
+ end
276
+ if not options[:extended_key_usage].nil? and not options[:extended_key_usage].empty?
277
+ ext << ef.create_extension("extendedKeyUsage", options[:extended_key_usage].join(","))
278
+ end
279
+ ext << ef.create_extension("subjectKeyIdentifier", "hash")
280
+
281
+ #attach the key identifier if it's not a self-sign
282
+ if not ef.subject_certificate == ef.issuer_certificate and not R509::Cert.new(:cert=>options[:issuer_certificate]).authority_key_identifier.nil?
283
+ ext << ef.create_extension("authorityKeyIdentifier", "keyid:always") # this could also be keyid:always,issuer:always
284
+ end
285
+
286
+ if not options[:certificate_policies].nil? and options[:certificate_policies].respond_to?(:each)
287
+ conf = []
288
+ policy_names = ["ia5org"]
289
+ options[:certificate_policies].each_with_index do |policy,i|
290
+ conf << build_conf("certPolicies#{i}",policy,i)
291
+ policy_names << "@certPolicies#{i}"
292
+ end
293
+ ef.config = OpenSSL::Config.parse(conf.join("\n"))
294
+ ext << ef.create_extension("certificatePolicies", policy_names.join(","))
295
+ end
296
+
297
+ if not options[:san_names].nil? and not options[:san_names].names.empty?
298
+ serialize = options[:san_names].serialize_names
299
+ ef.config = OpenSSL::Config.parse(serialize[:conf])
300
+ ext << ef.create_extension("subjectAltName", serialize[:extension_string])
301
+ end
302
+
303
+ if not @config.nil? and not @config.cdp_location.nil? and not @config.cdp_location.empty?
304
+ gns = R509::ASN1.general_name_parser(@config.cdp_location)
305
+ serialize = gns.serialize_names
306
+ ef.config = OpenSSL::Config.parse(serialize[:conf])
307
+ ext << ef.create_extension("crlDistributionPoints", serialize[:extension_string])
308
+ end
309
+
310
+ #authorityInfoAccess processing
311
+ if not @config.nil?
312
+ aia = []
313
+ aia_conf = []
314
+
315
+ if not @config.ocsp_location.nil? and not @config.ocsp_location.empty?
316
+ gns = R509::ASN1.general_name_parser(@config.ocsp_location)
317
+ gns.names.each do |ocsp|
318
+ serialize = ocsp.serialize_name
319
+ aia.push "OCSP;#{serialize[:extension_string]}"
320
+ aia_conf.push serialize[:conf]
321
+ end
322
+ end
323
+
324
+ if not @config.nil? and not @config.ca_issuers_location.nil? and not @config.ca_issuers_location.empty?
325
+ gns = R509::ASN1.general_name_parser(@config.ca_issuers_location)
326
+ gns.names.each do |ca_issuers|
327
+ serialize = ca_issuers.serialize_name
328
+ aia.push "caIssuers;#{serialize[:extension_string]}"
329
+ aia_conf.push serialize[:conf]
330
+ end
331
+ end
332
+
333
+ if not aia.empty?
334
+ ef.config = OpenSSL::Config.parse(aia_conf.join("\n"))
335
+ ext << ef.create_extension("authorityInfoAccess",aia.join(","))
336
+ end
337
+ end
338
+
339
+ if options[:inhibit_any_policy]
340
+ ext << ef.create_extension("inhibitAnyPolicy",options[:inhibit_any_policy].to_s,true) # must be set critical per RFC 5280
341
+ end
342
+
343
+ if options[:policy_constraints]
344
+ pc = options[:policy_constraints]
345
+ constraints = []
346
+ constraints << "requireExplicitPolicy:#{pc["require_explicit_policy"]}" unless pc["require_explicit_policy"].nil?
347
+ constraints << "inhibitPolicyMapping:#{pc["inhibit_policy_mapping"]}" unless pc["inhibit_policy_mapping"].nil?
348
+ ext << ef.create_extension("policyConstraints",constraints.join(","),true) # must be set critical per RFC 5280
349
+ end
350
+
351
+ if options[:name_constraints]
352
+ nc = options[:name_constraints]
353
+ nc_data = []
354
+ nc_conf = []
355
+ if not nc["permitted"].nil?
356
+ gns = R509::ASN1::GeneralNames.new
357
+ nc["permitted"].each do |p|
358
+ gns.create_item(:type => p["type"], :value => p["value"])
359
+ end
360
+ gns.names.each do |permitted|
361
+ serialize = permitted.serialize_name
362
+ nc_data.push "permitted;#{serialize[:extension_string]}"
363
+ nc_conf.push serialize[:conf]
364
+ end
365
+ end
366
+ if not nc["excluded"].nil?
367
+ gns = R509::ASN1::GeneralNames.new
368
+ nc["excluded"].each do |p|
369
+ gns.create_item(:type => p["type"], :value => p["value"])
370
+ end
371
+ gns.names.each do |excluded|
372
+ serialize = excluded.serialize_name
373
+ nc_data.push "excluded;#{serialize[:extension_string]}"
374
+ nc_conf.push serialize[:conf]
375
+ end
376
+ end
377
+
378
+ ef.config = OpenSSL::Config.parse nc_conf.join("\n")
379
+ ext << ef.create_extension("nameConstraints",nc_data.join(","))
380
+ end
381
+
382
+ if options[:ocsp_no_check]
383
+ # the value of this extension is not encoded. presence is all that matters
384
+ ext << ef.create_extension("noCheck","yes")
385
+ end
386
+
387
+ options[:subject_certificate].extensions = ext
388
+ nil
389
+ end
390
+
391
+ def calculate_not_before(not_before)
392
+ if not_before.nil?
393
+ #not_before will be set to 6 hours before now to prevent issues with bad system clocks (clients don't sync)
394
+ not_before = Time.now - 6 * 60 * 60
395
+ end
396
+ not_before
397
+ end
398
+
399
+ def calculate_not_after(not_after,not_before)
400
+ if not_after.nil?
401
+ not_after = not_before + 365 * 24 * 60 * 60
402
+ end
403
+ not_after
404
+ end
405
+
406
+ end
407
+ end
data/lib/r509/config.rb CHANGED
@@ -3,405 +3,601 @@ require 'openssl'
3
3
  require 'r509/exceptions'
4
4
  require 'r509/io_helpers'
5
5
  require 'r509/subject'
6
- require 'r509/privatekey'
6
+ require 'r509/private_key'
7
7
  require 'fileutils'
8
8
  require 'pathname'
9
9
 
10
10
  module R509
11
- # Module to contain all configuration related classes (e.g. CaConfig, CaProfile, SubjectItemPolicy)
12
- module Config
13
- # Provides access to configuration profiles
14
- class CaProfile
15
- attr_reader :basic_constraints, :key_usage, :extended_key_usage,
16
- :certificate_policies, :subject_item_policy
17
-
18
- # @option [String] :basic_constraints
19
- # @option [Array] :key_usage
20
- # @option [Array] :extended_key_usage
21
- # @option [Array] :certificate_policies
22
- # @option [R509::Config::SubjectItemPolicy] :subject_item_policy optional
23
- def initialize(opts = {})
24
- @basic_constraints = opts[:basic_constraints]
25
- @key_usage = opts[:key_usage]
26
- @extended_key_usage = opts[:extended_key_usage]
27
- @certificate_policies = opts[:certificate_policies]
28
- if opts.has_key?(:subject_item_policy) and not opts[:subject_item_policy].kind_of?(R509::Config::SubjectItemPolicy)
29
- end
30
- @subject_item_policy = opts[:subject_item_policy] || nil
31
- end
11
+ # Module to contain all configuration related classes (e.g. CAConfig, CAProfile, SubjectItemPolicy)
12
+ module Config
13
+ # Provides access to configuration profiles
14
+ class CAProfile
15
+ attr_reader :basic_constraints, :key_usage, :extended_key_usage,
16
+ :certificate_policies, :subject_item_policy, :ocsp_no_check,
17
+ :inhibit_any_policy, :policy_constraints, :name_constraints
18
+
19
+ # All hash options for CAProfile are optional.
20
+ # @option opts [String] :basic_constraints
21
+ # @option opts [Array] :key_usage
22
+ # @option opts [Array] :extended_key_usage
23
+ # @option opts [Array] :certificate_policies
24
+ # @option opts [Boolean] :ocsp_no_check Sets OCSP No Check extension in the certificate if true
25
+ # @option opts [Integer] :inhibit_any_policy Sets the value of the inhibitAnyPolicy extension
26
+ # @option opts [Hash] :policy_constraints Sets the value of the policyConstriants extension
27
+ # @option opts [Hash] :name_constraints Sets the value of the nameConstraints extension
28
+ # @option opts [R509::Config::SubjectItemPolicy] :subject_item_policy
29
+ def initialize(opts = {})
30
+ validate_basic_constraints opts[:basic_constraints]
31
+ validate_key_usage opts[:key_usage]
32
+ validate_extended_key_usage opts[:extended_key_usage]
33
+ validate_certificate_policies opts[:certificate_policies]
34
+ validate_inhibit_any_policy opts[:inhibit_any_policy]
35
+ validate_policy_constraints opts[:policy_constraints]
36
+ validate_name_constraints opts[:name_constraints]
37
+ @ocsp_no_check = (opts[:ocsp_no_check] == true or opts[:ocsp_no_check] == "true")?true:false
38
+ validate_subject_item_policy opts[:subject_item_policy]
39
+ end
40
+
41
+ private
42
+ # @private
43
+ # validates subject item policy
44
+ def validate_subject_item_policy(sip)
45
+ if not sip.nil? and not sip.kind_of?(R509::Config::SubjectItemPolicy)
46
+ raise ArgumentError, "subject_item_policy must be of type R509::Config::SubjectItemPolicy"
32
47
  end
33
-
34
- # returns information about the subject item policy for a profile
35
- class SubjectItemPolicy
36
- attr_reader :required, :optional
37
-
38
- # @param [Hash] hash of required/optional subject items. These must be in OpenSSL shortname format.
39
- # @example sample hash
40
- # {"CN" => "required",
41
- # "O" => "required",
42
- # "OU" => "optional",
43
- # "ST" => "required",
44
- # "C" => "required",
45
- # "L" => "required",
46
- # "emailAddress" => "optional"}
47
- def initialize(hash={})
48
- if not hash.kind_of?(Hash)
49
- raise ArgumentError, "Must supply a hash in form 'shortname'=>'required/optional'"
48
+ @subject_item_policy = sip
49
+ end
50
+
51
+ # @private
52
+ # validates key usage array
53
+ def validate_key_usage(ku)
54
+ if not ku.nil? and not ku.kind_of?(Array)
55
+ raise ArgumentError, "key_usage must be an array of strings (see README)"
56
+ end
57
+ @key_usage = ku
58
+ end
59
+
60
+ # @private
61
+ # validates inhibit any policy
62
+ def validate_inhibit_any_policy(iap)
63
+ if not iap.nil?
64
+ validate_non_negative_integer("Inhibit any policy",iap)
65
+ end
66
+ @inhibit_any_policy = iap
67
+ end
68
+
69
+ # @private
70
+ def validate_policy_constraints(pc)
71
+ if not pc.nil?
72
+ if not pc.kind_of?(Hash)
73
+ raise ArgumentError, 'Policy constraints must be provided as a hash with at least one of the two allowed keys: "inhibit_policy_mapping" and "require_explicit_policy"'
74
+ end
75
+ if not pc["inhibit_policy_mapping"].nil?
76
+ ipm = validate_non_negative_integer("inhibit_policy_mapping",pc["inhibit_policy_mapping"])
77
+ end
78
+ if not pc["require_explicit_policy"].nil?
79
+ rep = validate_non_negative_integer("require_explicit_policy",pc["require_explicit_policy"])
80
+ end
81
+ if not ipm and not rep
82
+ raise ArgumentError, 'Policy constraints must have at least one of two keys: "inhibit_policy_mapping" and "require_explicit_policy" and the value must be non-negative'
83
+ end
84
+ end
85
+ @policy_constraints = pc
86
+ end
87
+
88
+ # @private
89
+ # used by iap and pc validation methods
90
+ def validate_non_negative_integer(source,value)
91
+ if not value.kind_of?(Integer) or value < 0
92
+ raise ArgumentError, "#{source} must be a non-negative integer"
93
+ end
94
+ value
95
+ end
96
+
97
+ # @private
98
+ # validates extended key usage array
99
+ def validate_extended_key_usage(eku)
100
+ if not eku.nil? and not eku.kind_of?(Array)
101
+ raise ArgumentError, "extended_key_usage must be an array of strings (see README)"
102
+ end
103
+ @extended_key_usage = eku
104
+ end
105
+
106
+
107
+ # @private
108
+ # validates the structure of the certificate policies array
109
+ def validate_certificate_policies(policies)
110
+ if not policies.nil?
111
+ if not policies.respond_to?(:each)
112
+ raise ArgumentError, "Not a valid certificate policy structure. Must be an array of hashes"
113
+ else
114
+ policies.each do |policy|
115
+ if policy["policy_identifier"].nil?
116
+ raise ArgumentError, "Each policy requires a policy identifier"
117
+ end
118
+ if not policy["cps_uris"].nil?
119
+ if not policy["cps_uris"].respond_to?(:each)
120
+ raise ArgumentError, "CPS URIs must be an array of strings"
50
121
  end
51
- @required = []
52
- @optional = []
53
- if not hash.empty?
54
- hash.each_pair do |key,value|
55
- if value == "required"
56
- @required.push(key)
57
- elsif value == "optional"
58
- @optional.push(key)
59
- else
60
- raise ArgumentError, "Unknown subject item policy value. Allowed values are required and optional"
61
- end
122
+ end
123
+ if not policy["user_notices"].nil?
124
+ if not policy["user_notices"].respond_to?(:each)
125
+ raise ArgumentError, "User notices must be an array of hashes"
126
+ else
127
+ policy["user_notices"].each do |un|
128
+ if not un["organization"].nil? and un["notice_numbers"].nil?
129
+ raise ArgumentError, "If you provide an organization you must provide notice numbers"
62
130
  end
131
+ if not un["notice_numbers"].nil? and un["organization"].nil?
132
+ raise ArgumentError, "If you provide notice numbers you must provide an organization"
133
+ end
134
+ end
63
135
  end
136
+ end
64
137
  end
65
-
66
- # @param [R509::Subject] subject
67
- # @return [R509::Subject] validated version of the subject or error
68
- def validate_subject(subject)
69
- # convert the subject components into an array of component names that match
70
- # those that are on the required list
71
- supplied = subject.to_a.each do |item|
72
- @required.include?(item[0])
73
- end.map do |item|
74
- item[0]
75
- end
76
- # so we can make sure they gave us everything that's required
77
- diff = @required - supplied
78
- if not diff.empty?
79
- raise R509::R509Error, "This profile requires you supply "+@required.join(", ")
80
- end
81
-
82
- # the validated subject contains only those subject components that are either
83
- # required or optional
84
- R509::Subject.new(subject.to_a.select do |item|
85
- @required.include?(item[0]) or @optional.include?(item[0])
86
- end)
87
- end
138
+ end
139
+ @certificate_policies = policies
88
140
  end
89
-
90
- # pool of configs, so we can support multiple CAs from a single config file
91
- class CaConfigPool
92
- # @option configs [Hash<String, R509::Config::CaConfig>] the configs to add to the pool
93
- def initialize(configs)
94
- @configs = configs
95
- end
96
-
97
- # get all the config names
98
- def names
99
- @configs.keys
100
- end
101
-
102
- # retrieve a particular config by its name
103
- def [](name)
104
- @configs[name]
141
+ end
142
+
143
+ # @private
144
+ def validate_name_constraints(nc)
145
+ if not nc.nil?
146
+ if not nc.kind_of?(Hash)
147
+ raise ArgumentError, "name_constraints must be provided as a hash"
148
+ end
149
+ ["permitted","excluded"].each do |key|
150
+ if not nc[key].nil?
151
+ validate_name_constraints_elements(key,nc[key])
105
152
  end
153
+ end
154
+ if (nc["permitted"].nil? or nc["permitted"].empty?) and (nc["excluded"].nil? or nc["excluded"].empty?)
155
+ raise ArgumentError, "If name_constraints are supplied you must have at least one valid permitted or excluded element"
156
+ end
157
+ end
158
+ @name_constraints = nc
159
+ end
106
160
 
107
- # @return a list of all the configs in this pool
108
- def all
109
- @configs.values
110
- end
161
+ # @private
162
+ def validate_name_constraints_elements(type,arr)
163
+ if not arr.kind_of?(Array)
164
+ raise ArgumentError, "#{type} must be an array"
165
+ end
166
+ arr.each do |el|
167
+ if not el.kind_of?(Hash) or not el.has_key?("type") or not el.has_key?("value")
168
+ raise ArgumentError, "Elements within the #{type} array must be hashes with both type and value"
169
+ end
170
+ if R509::ASN1::GeneralName.map_type_to_tag(el["type"]) == nil
171
+ raise ArgumentError, "#{el["type"]} is not an allowed type. Check R509::ASN1::GeneralName.map_type_to_tag to see a list of types"
172
+ end
173
+ end
174
+ end
175
+
176
+ # @private
177
+ # validates the structure of the certificate policies array
178
+ def validate_basic_constraints(constraints)
179
+ if not constraints.nil?
180
+ if not constraints.respond_to?(:has_key?) or not constraints.has_key?("ca")
181
+ raise ArgumentError, "You must supply a hash with a key named \"ca\" with a boolean value"
182
+ end
183
+ if constraints["ca"].nil? or (not constraints["ca"].kind_of?(TrueClass) and not constraints["ca"].kind_of?(FalseClass))
184
+ raise ArgumentError, "You must supply true/false for the ca key when specifying basic constraints"
185
+ end
186
+ if constraints["ca"] == false and not constraints["path_length"].nil?
187
+ raise ArgumentError, "path_length is not allowed when ca is false"
188
+ end
189
+ if constraints["ca"] == true and not constraints["path_length"].nil? and (constraints["path_length"] < 0 or not constraints["path_length"].kind_of?(Integer))
190
+ raise ArgumentError, "Path length must be a non-negative integer (>= 0)"
191
+ end
192
+ end
193
+ @basic_constraints = constraints
194
+ end
195
+ end
111
196
 
112
- # Loads the named configuration config from a yaml string.
113
- # @param [String] name The name of the config within the file. Note
114
- # that a single yaml file can contain more than one configuration.
115
- # @param [String] yaml_data The filename to load yaml config data from.
116
- def self.from_yaml(name, yaml_data, opts = {})
117
- conf = YAML.load(yaml_data)
118
- configs = {}
119
- conf[name].each_pair do |ca_name, data|
120
- configs[ca_name] = R509::Config::CaConfig.load_from_hash(data, opts)
121
- end
122
- R509::Config::CaConfigPool.new(configs)
197
+ # returns information about the subject item policy for a profile
198
+ class SubjectItemPolicy
199
+ attr_reader :required, :optional
200
+
201
+ # @param [Hash] hash of required/optional subject items. These must be in OpenSSL shortname format.
202
+ # @example sample hash
203
+ # {"CN" => "required",
204
+ # "O" => "required",
205
+ # "OU" => "optional",
206
+ # "ST" => "required",
207
+ # "C" => "required",
208
+ # "L" => "required",
209
+ # "emailAddress" => "optional"}
210
+ def initialize(hash={})
211
+ if not hash.kind_of?(Hash)
212
+ raise ArgumentError, "Must supply a hash in form 'shortname'=>'required/optional'"
213
+ end
214
+ @required = []
215
+ @optional = []
216
+ if not hash.empty?
217
+ hash.each_pair do |key,value|
218
+ if value == "required"
219
+ @required.push(key)
220
+ elsif value == "optional"
221
+ @optional.push(key)
222
+ else
223
+ raise ArgumentError, "Unknown subject item policy value. Allowed values are required and optional"
123
224
  end
225
+ end
226
+ end
227
+ end
228
+
229
+ # @param [R509::Subject] subject
230
+ # @return [R509::Subject] validated version of the subject or error
231
+ def validate_subject(subject)
232
+ # convert the subject components into an array of component names that match
233
+ # those that are on the required list
234
+ supplied = subject.to_a.each do |item|
235
+ @required.include?(item[0])
236
+ end.map do |item|
237
+ item[0]
238
+ end
239
+ # so we can make sure they gave us everything that's required
240
+ diff = @required - supplied
241
+ if not diff.empty?
242
+ raise R509::R509Error, "This profile requires you supply "+@required.join(", ")
124
243
  end
125
244
 
126
- # Stores a configuration for our CA.
127
- class CaConfig
128
- include R509::IOHelpers
129
- extend R509::IOHelpers
130
- attr_accessor :ca_cert, :crl_validity_hours, :message_digest,
131
- :cdp_location, :crl_start_skew_seconds, :ocsp_location, :ocsp_chain,
132
- :ocsp_start_skew_seconds, :ocsp_validity_hours, :crl_number_file, :crl_list_file
133
-
134
- # @option opts [R509::Cert] :ca_cert Cert+Key pair
135
- # @option opts [Integer] :crl_validity_hours (168) The number of hours that
136
- # a CRL will be valid. Defaults to 7 days.
137
- # @option opts [Hash<String, R509::Config::CaProfile>] :profiles
138
- # @option opts [String] :message_digest (SHA1) The hashing algorithm to use.
139
- # @option opts [String] :cdp_location
140
- # @option opts [String] :ocsp_location
141
- # @option opts [String] :crl_number_file The file that we will save
142
- # the CRL numbers to. defaults to a StringIO object if not provided
143
- # @option opts [String] :crl_list_file The file that we will save
144
- # the CRL list data to. defaults to a StringIO object if not provided
145
- # @option opts [R509::Cert] :ocsp_cert An optional cert+key pair
146
- # OCSP signing delegate
147
- # @option opts [Array<OpenSSL::X509::Certificate>] :ocsp_chain An optional array
148
- # that constitutes the chain to attach to an OCSP response
149
- #
150
- def initialize(opts = {} )
151
- if not opts.has_key?(:ca_cert) then
152
- raise ArgumentError, 'Config object requires that you pass :ca_cert'
153
- end
154
-
155
- @ca_cert = opts[:ca_cert]
156
-
157
- if not @ca_cert.kind_of?(R509::Cert) then
158
- raise ArgumentError, ':ca_cert must be of type R509::Cert'
159
- end
160
-
161
- #ocsp data
162
- if opts.has_key?(:ocsp_cert) and not opts[:ocsp_cert].kind_of?(R509::Cert) and not opts[:ocsp_cert].nil?
163
- raise ArgumentError, ':ocsp_cert, if provided, must be of type R509::Cert'
164
- end
165
- if opts.has_key?(:ocsp_cert) and not opts[:ocsp_cert].nil? and not opts[:ocsp_cert].has_private_key?
166
- raise ArgumentError, ':ocsp_cert must contain a private key, not just a certificate'
167
- end
168
- @ocsp_cert = opts[:ocsp_cert] unless opts[:ocsp_cert].nil?
169
- @ocsp_location = opts[:ocsp_location]
170
- @ocsp_chain = opts[:ocsp_chain] if opts[:ocsp_chain].kind_of?(Array)
171
- @ocsp_validity_hours = opts[:ocsp_validity_hours] || 168
172
- @ocsp_start_skew_seconds = opts[:ocsp_start_skew_seconds] || 3600
173
-
174
- @crl_validity_hours = opts[:crl_validity_hours] || 168
175
- @crl_start_skew_seconds = opts[:crl_start_skew_seconds] || 3600
176
- @crl_number_file = opts[:crl_number_file] || nil
177
- @crl_list_file = opts[:crl_list_file] || nil
178
- @cdp_location = opts[:cdp_location]
179
- @message_digest = opts[:message_digest] || "SHA1"
180
-
181
-
245
+ # the validated subject contains only those subject components that are either
246
+ # required or optional
247
+ R509::Subject.new(subject.to_a.select do |item|
248
+ @required.include?(item[0]) or @optional.include?(item[0])
249
+ end)
250
+ end
251
+ end
182
252
 
183
- @profiles = {}
184
- if opts[:profiles]
185
- opts[:profiles].each_pair do |name, prof|
186
- set_profile(name, prof)
187
- end
188
- end
253
+ # pool of configs, so we can support multiple CAs from a single config file
254
+ class CAConfigPool
255
+ # @option configs [Hash<String, R509::Config::CAConfig>] the configs to add to the pool
256
+ def initialize(configs)
257
+ @configs = configs
258
+ end
259
+
260
+ # get all the config names
261
+ def names
262
+ @configs.keys
263
+ end
264
+
265
+ # retrieve a particular config by its name
266
+ def [](name)
267
+ @configs[name]
268
+ end
269
+
270
+ # @return a list of all the configs in this pool
271
+ def all
272
+ @configs.values
273
+ end
274
+
275
+ # Loads the named configuration config from a yaml string.
276
+ # @param [String] name The name of the config within the file. Note
277
+ # that a single yaml file can contain more than one configuration.
278
+ # @param [String] yaml_data The filename to load yaml config data from.
279
+ def self.from_yaml(name, yaml_data, opts = {})
280
+ conf = YAML.load(yaml_data)
281
+ configs = {}
282
+ conf[name].each_pair do |ca_name, data|
283
+ configs[ca_name] = R509::Config::CAConfig.load_from_hash(data, opts)
284
+ end
285
+ R509::Config::CAConfigPool.new(configs)
286
+ end
287
+ end
189
288
 
190
- end
289
+ # Stores a configuration for our CA.
290
+ class CAConfig
291
+ include R509::IOHelpers
292
+ extend R509::IOHelpers
293
+ attr_accessor :ca_cert, :crl_validity_hours, :message_digest,
294
+ :cdp_location, :crl_start_skew_seconds, :ocsp_location, :ocsp_chain,
295
+ :ocsp_start_skew_seconds, :ocsp_validity_hours, :crl_number_file, :crl_list_file,
296
+ :ca_issuers_location
297
+
298
+ # @option opts [R509::Cert] :ca_cert Cert+Key pair
299
+ # @option opts [Integer] :crl_validity_hours (168) The number of hours that
300
+ # a CRL will be valid. Defaults to 7 days.
301
+ # @option opts [Hash<String, R509::Config::CAProfile>] :profiles
302
+ # @option opts [String] :message_digest (SHA1) The hashing algorithm to use.
303
+ # @option opts [Array] :cdp_location array of strings (URLs)
304
+ # @option opts [Array] :ocsp_location array of strings (URLs)
305
+ # @option opts [Array] :ca_issuers_location array of strings (URLs)
306
+ # @option opts [String] :crl_number_file The file that we will save
307
+ # the CRL numbers to. defaults to a StringIO object if not provided
308
+ # @option opts [String] :crl_list_file The file that we will save
309
+ # the CRL list data to. defaults to a StringIO object if not provided
310
+ # @option opts [R509::Cert] :ocsp_cert An optional cert+key pair
311
+ # OCSP signing delegate
312
+ # @option opts [Array<OpenSSL::X509::Certificate>] :ocsp_chain An optional array
313
+ # that constitutes the chain to attach to an OCSP response
314
+ #
315
+ def initialize(opts = {} )
316
+ if not opts.has_key?(:ca_cert) then
317
+ raise ArgumentError, 'Config object requires that you pass :ca_cert'
318
+ end
191
319
 
192
- # @return [R509::Cert] either a custom OCSP cert or the ca_cert
193
- def ocsp_cert
194
- if @ocsp_cert.nil? then @ca_cert else @ocsp_cert end
195
- end
320
+ @ca_cert = opts[:ca_cert]
196
321
 
197
- # @param [String] name The name of the profile
198
- # @param [R509::Config::CaProfile] prof The profile configuration
199
- def set_profile(name, prof)
200
- unless prof.is_a?(R509::Config::CaProfile)
201
- raise TypeError, "profile is supposed to be a R509::Config::CaProfile"
202
- end
203
- @profiles[name] = prof
204
- end
322
+ if not @ca_cert.kind_of?(R509::Cert) then
323
+ raise ArgumentError, ':ca_cert must be of type R509::Cert'
324
+ end
205
325
 
206
- # @param [String] prof
207
- # @return [R509::Config::CaProfile] The config profile.
208
- def profile(prof)
209
- if !@profiles.has_key?(prof)
210
- raise R509::R509Error, "unknown profile '#{prof}'"
211
- end
212
- @profiles[prof]
213
- end
326
+ #ocsp data
327
+ if opts.has_key?(:ocsp_cert) and not opts[:ocsp_cert].kind_of?(R509::Cert) and not opts[:ocsp_cert].nil?
328
+ raise ArgumentError, ':ocsp_cert, if provided, must be of type R509::Cert'
329
+ end
330
+ if opts.has_key?(:ocsp_cert) and not opts[:ocsp_cert].nil? and not opts[:ocsp_cert].has_private_key?
331
+ raise ArgumentError, ':ocsp_cert must contain a private key, not just a certificate'
332
+ end
333
+ @ocsp_cert = opts[:ocsp_cert] unless opts[:ocsp_cert].nil?
334
+ validate_ocsp_location opts[:ocsp_location]
335
+ validate_ca_issuers_location opts[:ca_issuers_location]
336
+ @ocsp_chain = opts[:ocsp_chain] if opts[:ocsp_chain].kind_of?(Array)
337
+ @ocsp_validity_hours = opts[:ocsp_validity_hours] || 168
338
+ @ocsp_start_skew_seconds = opts[:ocsp_start_skew_seconds] || 3600
339
+
340
+ @crl_validity_hours = opts[:crl_validity_hours] || 168
341
+ @crl_start_skew_seconds = opts[:crl_start_skew_seconds] || 3600
342
+ @crl_number_file = opts[:crl_number_file] || nil
343
+ @crl_list_file = opts[:crl_list_file] || nil
344
+ validate_cdp_location opts[:cdp_location]
345
+ @message_digest = opts[:message_digest] || "SHA1"
346
+
347
+
348
+
349
+ @profiles = {}
350
+ if opts[:profiles]
351
+ opts[:profiles].each_pair do |name, prof|
352
+ set_profile(name, prof)
353
+ end
354
+ end
214
355
 
215
- # @return [Integer] The number of profiles
216
- def num_profiles
217
- @profiles.count
218
- end
356
+ end
219
357
 
358
+ # @return [R509::Cert] either a custom OCSP cert or the ca_cert
359
+ def ocsp_cert
360
+ if @ocsp_cert.nil? then @ca_cert else @ocsp_cert end
361
+ end
220
362
 
221
- ######### Class Methods ##########
363
+ # @param [String] name The name of the profile
364
+ # @param [R509::Config::CAProfile] prof The profile configuration
365
+ def set_profile(name, prof)
366
+ unless prof.is_a?(R509::Config::CAProfile)
367
+ raise TypeError, "profile is supposed to be a R509::Config::CAProfile"
368
+ end
369
+ @profiles[name] = prof
370
+ end
371
+
372
+ # @param [String] prof
373
+ # @return [R509::Config::CAProfile] The config profile.
374
+ def profile(prof)
375
+ if !@profiles.has_key?(prof)
376
+ raise R509::R509Error, "unknown profile '#{prof}'"
377
+ end
378
+ @profiles[prof]
379
+ end
222
380
 
223
- # Load the configuration from a data hash. The same type that might be
224
- # used when loading from a YAML file.
225
- # @param [Hash] conf A hash containing all the configuration options
226
- # @option opts [String] :ca_root_path The root path for the CA. Defaults to
227
- # the current working directory.
228
- def self.load_from_hash(conf, opts = {})
229
- if conf.nil?
230
- raise ArgumentError, "conf not found"
231
- end
232
- unless conf.kind_of?(Hash)
233
- raise ArgumentError, "conf must be a Hash"
234
- end
381
+ # @return [Integer] The number of profiles
382
+ def num_profiles
383
+ @profiles.count
384
+ end
235
385
 
236
- ca_root_path = Pathname.new(opts[:ca_root_path] || FileUtils.getwd)
237
386
 
238
- unless File.directory?(ca_root_path)
239
- raise R509Error, "ca_root_path is not a directory: #{ca_root_path}"
240
- end
387
+ ######### Class Methods ##########
241
388
 
242
- ca_cert_hash = conf['ca_cert']
389
+ # Load the configuration from a data hash. The same type that might be
390
+ # used when loading from a YAML file.
391
+ # @param [Hash] conf A hash containing all the configuration options
392
+ # @option opts [String] :ca_root_path The root path for the CA. Defaults to
393
+ # the current working directory.
394
+ def self.load_from_hash(conf, opts = {})
395
+ if conf.nil?
396
+ raise ArgumentError, "conf not found"
397
+ end
398
+ unless conf.kind_of?(Hash)
399
+ raise ArgumentError, "conf must be a Hash"
400
+ end
243
401
 
244
- if ca_cert_hash.has_key?('engine')
245
- ca_cert = self.load_with_engine(ca_cert_hash,ca_root_path)
246
- end
402
+ ca_root_path = Pathname.new(opts[:ca_root_path] || FileUtils.getwd)
247
403
 
248
- if ca_cert.nil? and ca_cert_hash.has_key?('pkcs12')
249
- ca_cert = self.load_with_pkcs12(ca_cert_hash,ca_root_path)
250
- end
404
+ unless File.directory?(ca_root_path)
405
+ raise R509Error, "ca_root_path is not a directory: #{ca_root_path}"
406
+ end
251
407
 
252
- if ca_cert.nil? and ca_cert_hash.has_key?('cert')
253
- ca_cert = self.load_with_key(ca_cert_hash,ca_root_path)
254
- end
408
+ ca_cert_hash = conf['ca_cert']
255
409
 
256
- if conf.has_key?("ocsp_cert")
257
- if conf["ocsp_cert"].has_key?('engine')
258
- ocsp_cert = self.load_with_engine(conf["ocsp_cert"],ca_root_path)
259
- end
410
+ if ca_cert_hash.has_key?('engine')
411
+ ca_cert = self.load_with_engine(ca_cert_hash,ca_root_path)
412
+ end
260
413
 
261
- if ocsp_cert.nil? and conf["ocsp_cert"].has_key?('pkcs12')
262
- ocsp_cert = self.load_with_pkcs12(conf["ocsp_cert"],ca_root_path)
263
- end
414
+ if ca_cert.nil? and ca_cert_hash.has_key?('pkcs12')
415
+ ca_cert = self.load_with_pkcs12(ca_cert_hash,ca_root_path)
416
+ end
264
417
 
265
- if ocsp_cert.nil? and conf["ocsp_cert"].has_key?('cert')
266
- ocsp_cert = self.load_with_key(conf["ocsp_cert"],ca_root_path)
267
- end
268
- end
418
+ if ca_cert.nil? and ca_cert_hash.has_key?('cert')
419
+ ca_cert = self.load_with_key(ca_cert_hash,ca_root_path)
420
+ end
269
421
 
270
- ocsp_chain = []
271
- if conf.has_key?("ocsp_chain")
272
- ocsp_chain_data = read_data(ca_root_path+conf["ocsp_chain"])
273
- cert_regex = /-----BEGIN CERTIFICATE-----.+?-----END CERTIFICATE-----/m
274
- ocsp_chain_data.scan(cert_regex) do |cert|
275
- ocsp_chain.push(OpenSSL::X509::Certificate.new(cert))
276
- end
277
- end
422
+ if conf.has_key?("ocsp_cert")
423
+ if conf["ocsp_cert"].has_key?('engine')
424
+ ocsp_cert = self.load_with_engine(conf["ocsp_cert"],ca_root_path)
425
+ end
278
426
 
279
- opts = {
280
- :ca_cert => ca_cert,
281
- :ocsp_cert => ocsp_cert,
282
- :ocsp_chain => ocsp_chain,
283
- :crl_validity_hours => conf['crl_validity_hours'],
284
- :ocsp_validity_hours => conf['ocsp_validity_hours'],
285
- :ocsp_start_skew_seconds => conf['ocsp_start_skew_seconds'],
286
- :ocsp_location => conf['ocsp_location'],
287
- :cdp_location => conf['cdp_location'],
288
- :message_digest => conf['message_digest'],
289
- }
290
-
291
- if conf.has_key?("crl_list")
292
- opts[:crl_list_file] = (ca_root_path + conf['crl_list']).to_s
293
- end
427
+ if ocsp_cert.nil? and conf["ocsp_cert"].has_key?('pkcs12')
428
+ ocsp_cert = self.load_with_pkcs12(conf["ocsp_cert"],ca_root_path)
429
+ end
294
430
 
295
- if conf.has_key?("crl_number")
296
- opts[:crl_number_file] = (ca_root_path + conf['crl_number']).to_s
297
- end
431
+ if ocsp_cert.nil? and conf["ocsp_cert"].has_key?('cert')
432
+ ocsp_cert = self.load_with_key(conf["ocsp_cert"],ca_root_path)
433
+ end
434
+ end
298
435
 
436
+ ocsp_chain = []
437
+ if conf.has_key?("ocsp_chain")
438
+ ocsp_chain_data = read_data(ca_root_path+conf["ocsp_chain"])
439
+ cert_regex = /-----BEGIN CERTIFICATE-----.+?-----END CERTIFICATE-----/m
440
+ ocsp_chain_data.scan(cert_regex) do |cert|
441
+ ocsp_chain.push(OpenSSL::X509::Certificate.new(cert))
442
+ end
443
+ end
299
444
 
300
- profs = {}
301
- conf['profiles'].keys.each do |profile|
302
- data = conf['profiles'][profile]
303
- if not data["subject_item_policy"].nil?
304
- subject_item_policy = R509::Config::SubjectItemPolicy.new(data["subject_item_policy"])
305
- end
306
- profs[profile] = R509::Config::CaProfile.new(:key_usage => data["key_usage"],
307
- :extended_key_usage => data["extended_key_usage"],
308
- :basic_constraints => data["basic_constraints"],
309
- :certificate_policies => data["certificate_policies"],
310
- :subject_item_policy => subject_item_policy)
311
- end unless conf['profiles'].nil?
312
- opts[:profiles] = profs
313
-
314
- # Create the instance.
315
- self.new(opts)
316
- end
445
+ opts = {
446
+ :ca_cert => ca_cert,
447
+ :ocsp_cert => ocsp_cert,
448
+ :ocsp_chain => ocsp_chain,
449
+ :crl_validity_hours => conf['crl_validity_hours'],
450
+ :ocsp_validity_hours => conf['ocsp_validity_hours'],
451
+ :ocsp_start_skew_seconds => conf['ocsp_start_skew_seconds'],
452
+ :ocsp_location => conf['ocsp_location'],
453
+ :ca_issuers_location => conf['ca_issuers_location'],
454
+ :cdp_location => conf['cdp_location'],
455
+ :message_digest => conf['message_digest'],
456
+ }
457
+
458
+ if conf.has_key?("crl_list")
459
+ opts[:crl_list_file] = (ca_root_path + conf['crl_list']).to_s
460
+ end
317
461
 
318
- # Loads the named configuration config from a yaml file.
319
- # @param [String] conf_name The name of the config within the file. Note
320
- # that a single yaml file can contain more than one configuration.
321
- # @param [String] yaml_file The filename to load yaml config data from.
322
- def self.load_yaml(conf_name, yaml_file, opts = {})
323
- conf = YAML.load_file(yaml_file)
324
- self.load_from_hash(conf[conf_name], opts)
325
- end
462
+ if conf.has_key?("crl_number")
463
+ opts[:crl_number_file] = (ca_root_path + conf['crl_number']).to_s
464
+ end
326
465
 
327
- # Loads the named configuration config from a yaml string.
328
- # @param [String] conf_name The name of the config within the file. Note
329
- # that a single yaml file can contain more than one configuration.
330
- # @param [String] yaml_data The filename to load yaml config data from.
331
- def self.from_yaml(conf_name, yaml_data, opts = {})
332
- conf = YAML.load(yaml_data)
333
- self.load_from_hash(conf[conf_name], opts)
334
- end
335
466
 
336
- private
467
+ profs = {}
468
+ conf['profiles'].keys.each do |profile|
469
+ data = conf['profiles'][profile]
470
+ if not data["subject_item_policy"].nil?
471
+ subject_item_policy = R509::Config::SubjectItemPolicy.new(data["subject_item_policy"])
472
+ end
473
+ profs[profile] = R509::Config::CAProfile.new(:key_usage => data["key_usage"],
474
+ :extended_key_usage => data["extended_key_usage"],
475
+ :basic_constraints => data["basic_constraints"],
476
+ :certificate_policies => data["certificate_policies"],
477
+ :ocsp_no_check => data["ocsp_no_check"],
478
+ :inhibit_any_policy => data["inhibit_any_policy"],
479
+ :policy_constraints => data["policy_constraints"],
480
+ :name_constraints => data["name_constraints"],
481
+ :subject_item_policy => subject_item_policy)
482
+ end unless conf['profiles'].nil?
483
+ opts[:profiles] = profs
484
+
485
+ # Create the instance.
486
+ self.new(opts)
487
+ end
488
+
489
+ # Loads the named configuration config from a yaml file.
490
+ # @param [String] conf_name The name of the config within the file. Note
491
+ # that a single yaml file can contain more than one configuration.
492
+ # @param [String] yaml_file The filename to load yaml config data from.
493
+ def self.load_yaml(conf_name, yaml_file, opts = {})
494
+ conf = YAML.load_file(yaml_file)
495
+ self.load_from_hash(conf[conf_name], opts)
496
+ end
497
+
498
+ # Loads the named configuration config from a yaml string.
499
+ # @param [String] conf_name The name of the config within the file. Note
500
+ # that a single yaml file can contain more than one configuration.
501
+ # @param [String] yaml_data The filename to load yaml config data from.
502
+ def self.from_yaml(conf_name, yaml_data, opts = {})
503
+ conf = YAML.load(yaml_data)
504
+ self.load_from_hash(conf[conf_name], opts)
505
+ end
506
+
507
+ private
508
+
509
+ def self.load_with_engine(ca_cert_hash,ca_root_path)
510
+ if ca_cert_hash.has_key?('key')
511
+ raise ArgumentError, "You can't specify both key and engine"
512
+ end
513
+ if ca_cert_hash.has_key?('pkcs12')
514
+ raise ArgumentError, "You can't specify both engine and pkcs12"
515
+ end
516
+ if not ca_cert_hash.has_key?('key_name')
517
+ raise ArgumentError, "You must supply a key_name with an engine"
518
+ end
337
519
 
338
- def self.load_with_engine(ca_cert_hash,ca_root_path)
339
- if ca_cert_hash.has_key?('key')
340
- raise R509Error, "You can't specify both key and engine"
341
- end
342
- if ca_cert_hash.has_key?('pkcs12')
343
- raise R509Error, "You can't specify both engine and pkcs12"
344
- end
345
- if not ca_cert_hash.has_key?('key_name')
346
- raise R509Error, "You must supply a key_name with an engine"
347
- end
520
+ if ca_cert_hash['engine'].respond_to?(:load_private_key)
521
+ #this path is only for testing...ugh
522
+ engine = ca_cert_hash['engine']
523
+ else
524
+ #this path can't be tested by unit tests. bah!
525
+ engine = OpenSSL::Engine.by_id(ca_cert_hash['engine'])
526
+ end
527
+ ca_key = R509::PrivateKey.new(
528
+ :engine => engine,
529
+ :key_name => ca_cert_hash['key_name']
530
+ )
531
+ ca_cert_file = ca_root_path + ca_cert_hash['cert']
532
+ ca_cert = R509::Cert.new(
533
+ :cert => read_data(ca_cert_file),
534
+ :key => ca_key
535
+ )
536
+ ca_cert
537
+ end
538
+
539
+ def self.load_with_pkcs12(ca_cert_hash,ca_root_path)
540
+ if ca_cert_hash.has_key?('cert')
541
+ raise ArgumentError, "You can't specify both pkcs12 and cert"
542
+ end
543
+ if ca_cert_hash.has_key?('key')
544
+ raise ArgumentError, "You can't specify both pkcs12 and key"
545
+ end
348
546
 
349
- if ca_cert_hash['engine'].respond_to?(:load_private_key)
350
- #this path is only for testing...ugh
351
- engine = ca_cert_hash['engine']
352
- else
353
- #this path can't be tested by unit tests. bah!
354
- engine = OpenSSL::Engine.by_id(ca_cert_hash['engine'])
355
- end
356
- ca_key = R509::PrivateKey.new(
357
- :engine => engine,
358
- :key_name => ca_cert_hash['key_name']
359
- )
360
- ca_cert_file = ca_root_path + ca_cert_hash['cert']
361
- ca_cert = R509::Cert.new(
362
- :cert => read_data(ca_cert_file),
363
- :key => ca_key
364
- )
365
- ca_cert
366
- end
547
+ pkcs12_file = ca_root_path + ca_cert_hash['pkcs12']
548
+ ca_cert = R509::Cert.new(
549
+ :pkcs12 => read_data(pkcs12_file),
550
+ :password => ca_cert_hash['password']
551
+ )
552
+ ca_cert
553
+ end
554
+
555
+ def self.load_with_key(ca_cert_hash,ca_root_path)
556
+ ca_cert_file = ca_root_path + ca_cert_hash['cert']
557
+
558
+ if ca_cert_hash.has_key?('key')
559
+ ca_key_file = ca_root_path + ca_cert_hash['key']
560
+ ca_key = R509::PrivateKey.new(
561
+ :key => read_data(ca_key_file),
562
+ :password => ca_cert_hash['password']
563
+ )
564
+ ca_cert = R509::Cert.new(
565
+ :cert => read_data(ca_cert_file),
566
+ :key => ca_key
567
+ )
568
+ else
569
+ # in certain cases (OCSP responders for example) we may want
570
+ # to load a ca_cert with no private key
571
+ ca_cert = R509::Cert.new(:cert => read_data(ca_cert_file))
572
+ end
573
+ ca_cert
574
+ end
367
575
 
368
- def self.load_with_pkcs12(ca_cert_hash,ca_root_path)
369
- if ca_cert_hash.has_key?('cert')
370
- raise R509Error, "You can't specify both pkcs12 and cert"
371
- end
372
- if ca_cert_hash.has_key?('key')
373
- raise R509Error, "You can't specify both pkcs12 and key"
374
- end
576
+ private
375
577
 
376
- pkcs12_file = ca_root_path + ca_cert_hash['pkcs12']
377
- ca_cert = R509::Cert.new(
378
- :pkcs12 => read_data(pkcs12_file),
379
- :password => ca_cert_hash['password']
380
- )
381
- ca_cert
382
- end
578
+ # @private
579
+ def validate_cdp_location(location)
580
+ if not location.nil? and not location.kind_of?(Array)
581
+ raise ArgumentError, "cdp_location must be an array if provided"
582
+ end
583
+ @cdp_location = location
584
+ end
383
585
 
384
- def self.load_with_key(ca_cert_hash,ca_root_path)
385
- ca_cert_file = ca_root_path + ca_cert_hash['cert']
386
-
387
- if ca_cert_hash.has_key?('key')
388
- ca_key_file = ca_root_path + ca_cert_hash['key']
389
- ca_key = R509::PrivateKey.new(
390
- :key => read_data(ca_key_file),
391
- :password => ca_cert_hash['password']
392
- )
393
- ca_cert = R509::Cert.new(
394
- :cert => read_data(ca_cert_file),
395
- :key => ca_key
396
- )
397
- else
398
- # in certain cases (OCSP responders for example) we may want
399
- # to load a ca_cert with no private key
400
- ca_cert = R509::Cert.new(:cert => read_data(ca_cert_file))
401
- end
402
- ca_cert
403
- end
586
+ # @private
587
+ def validate_ocsp_location(location)
588
+ if not location.nil? and not location.kind_of?(Array)
589
+ raise ArgumentError, "ocsp_location must be an array if provided"
590
+ end
591
+ @ocsp_location = location
592
+ end
404
593
 
594
+ # @private
595
+ def validate_ca_issuers_location(location)
596
+ if not location.nil? and not location.kind_of?(Array)
597
+ raise ArgumentError, "ca_issuers_location must be an array if provided"
405
598
  end
599
+ @ca_issuers_location = location
600
+ end
406
601
  end
602
+ end
407
603
  end