File: csr.rb

package info (click to toggle)
ruby-flores 0.0.8-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 244 kB
  • sloc: ruby: 952; makefile: 36
file content (232 lines) | stat: -rw-r--r-- 8,434 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
require "flores/namespace"

module Flores::PKI
  # A certificate signing request.
  #
  # From here, you can configure a certificate to be created based on your
  # desired configuration.
  #
  # Example making a root CA:
  #
  #     key = OpenSSL::PKey::RSA.generate(4096, 65537)
  #     csr = Flores::PKI::CertificateSigningRequest.new
  #     csr.subject = "OU=Fancy Pants Inc."
  #     certificate = csr.create_root(key)
  #
  # Example making an intermediate CA:
  #     
  #     root_key = OpenSSL::PKey::RSA.generate(4096, 65537)
  #     root_csr = Flores::PKI::CertificateSigningRequest.new
  #     root_csr.subject = "OU=Fancy Pants Inc."
  #     root_csr.public_key = root_key.public
  #     root_certificate = csr.create_root(root_key)
  #
  #     intermediate_key = OpenSSL::PKey::RSA.generate(4096, 65537)
  #     intermediate_csr = Flores::PKI::CertificateSigningRequest.new
  #     intermediate_csr.public_key = intermediate_key.public
  #     intermediate_csr.subject = "OU=Fancy Pants Inc. Intermediate 1"
  #     intermediate_certificate = csr.create_intermediate(root_certificate, root_key)
  class CertificateSigningRequest
    # raised when an invalid signing configuration is given
    class InvalidRequest < StandardError; end

    # raised when invalid data is present in a certificate request
    class InvalidData < StandardError; end

    # raised when an invalid subject (format, or whatever) is given in a certificate request
    class InvalidSubject < InvalidData; end

    # raised when an invalid time value is given for a certificate request
    class InvalidTime < InvalidData; end

    def initialize
      self.serial = Flores::PKI.random_serial
      self.digest_method = default_digest_method
    end

    private

    def validate_subject(value)
      OpenSSL::X509::Name.parse(value)
    rescue OpenSSL::X509::NameError => e
      raise InvalidSubject, "Invalid subject '#{value}'. (#{e})"
    rescue TypeError => e
      # Bug(?) in MRI 2.1.6(?)
      raise InvalidSubject, "Invalid subject '#{value}'. (#{e})"
    end

    def subject=(value)
      @subject = validate_subject(value)
    end

    attr_reader :subject

    def subject_alternates=(values)
      @subject_alternates = values
    end

    attr_reader :subject_alternates

    def public_key=(value)
      @public_key = validate_public_key(value)
    end

    def validate_public_key(value)
      raise InvalidData, "public key must be a OpenSSL::PKey::PKey" unless value.is_a? OpenSSL::PKey::PKey
      value
    end

    attr_reader :public_key

    def start_time=(value)
      @start_time = validate_time(value)
    end

    attr_reader :start_time

    def expire_time=(value)
      @expire_time = validate_time(value)
    end

    attr_reader :expire_time

    def validate_time(value)
      raise InvalidTime, "#{value.inspect} (class #{value.class.name})" unless value.is_a?(Time)
      value
    end

    def certificate
      return @certificate  if @certificate
      @certificate = OpenSSL::X509::Certificate.new

      # RFC5280
      # > 4.1.2.1.  Version
      # > version MUST be 3 (value is 2).
      #
      # Version value of '2' means a v3 certificate.
      @certificate.version = 2

      @certificate.subject = subject
      @certificate.not_before = start_time
      @certificate.not_after = expire_time
      @certificate.public_key = public_key
      @certificate
    end

    def default_digest_method
      OpenSSL::Digest::SHA256.new
    end

    def self_signed?
      @signing_certificate.nil?
    end

    def validate!
      if self_signed?
        if @signing_key.nil?
          raise InvalidRequest, "No signing_key given. Cannot sign key."
        end
      elsif @signing_certificate.nil? && @signing_key
        raise InvalidRequest, "signing_key given, but no signing_certificate is set"
      elsif @signing_certificate && @signing_key.nil?
        raise InvalidRequest, "signing_certificate given, but no signing_key is set"
      end
    end

    def create
      validate!
      extensions = OpenSSL::X509::ExtensionFactory.new
      extensions.subject_certificate = certificate
      extensions.issuer_certificate = self_signed? ? certificate : signing_certificate

      certificate.issuer = extensions.issuer_certificate.subject
      certificate.add_extension(extensions.create_extension("subjectKeyIdentifier", "hash", false))

      # RFC 5280 4.2.1.1. Authority Key Identifier
      # This is "who signed this key"
      certificate.add_extension(extensions.create_extension("authorityKeyIdentifier", "keyid:always", false))
      #certificate.add_extension(extensions.create_extension("authorityKeyIdentifier", "keyid:always,issuer:always", false))

      if want_signature_ability?
        # Create a CA.
        certificate.add_extension(extensions.create_extension("basicConstraints", "CA:TRUE", true))
        # Rough googling seems to indicate at least keyCertSign is required for CA and intermediate certs.
        certificate.add_extension(extensions.create_extension("keyUsage", "keyCertSign, cRLSign, digitalSignature", true))
      else
        # Create a client+server certificate
        #
        # It feels weird to create a certificate that's valid as both server and client, but a brief inspection of major
        # web properties (apple.com, google.com, yahoo.com, github.com, fastly.com, mozilla.com, amazon.com) reveals that
        # major web properties have certificates with both clientAuth and serverAuth extended key usages. Further,
        # these major server certificates all have digitalSignature and keyEncipherment for key usage.
        #
        # Here's the command I used to check this:
        #    echo mozilla.com apple.com github.com google.com yahoo.com fastly.com elastic.co amazon.com \
        #    | xargs -n1 sh -c 'openssl s_client -connect $1:443 \
        #    | sed -ne "/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/p" \
        #    | openssl x509 -text -noout | sed -ne "/X509v3 extensions/,/Signature Algorithm/p" | sed -e "s/^/$1 /"' - \
        #    | grep -A2 'Key Usage'
        certificate.add_extension(extensions.create_extension("keyUsage", "digitalSignature, keyEncipherment", true))
        certificate.add_extension(extensions.create_extension("extendedKeyUsage", "clientAuth, serverAuth", false))
      end

      if @subject_alternates
        certificate.add_extension(extensions.create_extension("subjectAltName", @subject_alternates.join(",")))
      end
        
      certificate.serial = OpenSSL::BN.new(serial)
      certificate.sign(signing_key, digest_method)
      certificate
    end

    # Set the certificate which is going to be signing this request.
    def signing_certificate=(certificate)
      raise InvalidData, "signing_certificate must be an OpenSSL::X509::Certificate" unless certificate.is_a?(OpenSSL::X509::Certificate)
      @signing_certificate = certificate
    end
    attr_reader :signing_certificate

    attr_reader :signing_key
    def signing_key=(private_key)
      raise InvalidData, "signing_key must be an OpenSSL::PKey::PKey (or a subclass)" unless private_key.is_a?(OpenSSL::PKey::PKey)
      @signing_key = private_key
    end

    def want_signature_ability=(value)
      raise InvalidData, "want_signature_ability must be a boolean" unless value == true || value == false
      @want_signature_ability = value
    end

    def want_signature_ability?
      @want_signature_ability == true
    end

    attr_reader :digest_method
    def digest_method=(value)
      raise InvalidData, "digest_method must be a OpenSSL::Digest (or a subclass)" unless value.is_a?(OpenSSL::Digest)
      @digest_method = value
    end

    attr_reader :serial
    def serial=(value)
      begin
        Integer(value)
      rescue
        raise InvalidData, "Invalid serial value. Must be a number (or a String containing only nubers)"
      end
      @serial = value
    end

    public(:serial, :serial=)
    public(:subject, :subject=)
    public(:subject_alternates, :subject_alternates=)
    public(:public_key, :public_key=)
    public(:start_time, :start_time=)
    public(:expire_time, :expire_time=)
    public(:digest_method, :digest_method=)
    public(:want_signature_ability?, :want_signature_ability=)
    public(:signing_key, :signing_key=)
    public(:signing_certificate, :signing_certificate=)
    public(:create)
  end # class CertificateSigningRequest
end