File: certificate.rb

package info (click to toggle)
ruby-certificate-authority 0.2.0~434c15cd-1
  • links: PTS, VCS
  • area: main
  • in suites: bullseye, buster
  • size: 424 kB
  • sloc: ruby: 2,645; makefile: 6
file content (256 lines) | stat: -rw-r--r-- 8,761 bytes parent folder | download | duplicates (2)
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
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
module CertificateAuthority
  class Certificate
    include Validations
    include Revocable

    attr_accessor :distinguished_name
    attr_accessor :serial_number
    attr_accessor :key_material
    attr_accessor :not_before
    attr_accessor :not_after
    attr_accessor :extensions
    attr_accessor :openssl_body

    alias :subject :distinguished_name #Same thing as the DN

    attr_accessor :parent

    def validate
      errors.add :base, "Distinguished name must be valid" unless distinguished_name.valid?
      errors.add :base, "Key material must be valid" unless key_material.valid?
      errors.add :base, "Serial number must be valid" unless serial_number.valid?
      errors.add :base, "Extensions must be valid" unless extensions.each do |item|
        unless item.respond_to?(:valid?)
          true
        else
          item.valid?
        end
      end
    end

    def initialize
      self.distinguished_name = DistinguishedName.new
      self.serial_number = SerialNumber.new
      self.key_material = MemoryKeyMaterial.new
      self.not_before = Date.today.utc
      self.not_after = Date.today.advance(:years => 1).utc
      self.parent = self
      self.extensions = load_extensions()

      self.signing_entity = false

    end

=begin
    def self.from_openssl openssl_cert
      unless openssl_cert.is_a? OpenSSL::X509::Certificate
        raise "Can only construct from an OpenSSL::X509::Certificate"
      end

      certificate = Certificate.new
      # Only subject, key_material, and body are used for signing
      certificate.distinguished_name = DistinguishedName.from_openssl openssl_cert.subject
      certificate.key_material.public_key = openssl_cert.public_key
      certificate.openssl_body = openssl_cert
      certificate.serial_number.number = openssl_cert.serial.to_i
      certificate.not_before = openssl_cert.not_before
      certificate.not_after = openssl_cert.not_after
      # TODO extensions
      certificate
    end
=end

    def sign!(signing_profile={})
      raise "Invalid certificate #{self.errors.full_messages}" unless valid?
      merge_profile_with_extensions(signing_profile)

      openssl_cert = OpenSSL::X509::Certificate.new
      openssl_cert.version = 2
      openssl_cert.not_before = self.not_before
      openssl_cert.not_after = self.not_after
      openssl_cert.public_key = self.key_material.public_key

      openssl_cert.serial = self.serial_number.number

      openssl_cert.subject = self.distinguished_name.to_x509_name
      openssl_cert.issuer = parent.distinguished_name.to_x509_name

      require 'tempfile'
      t = Tempfile.new("bullshit_conf")
      ## The config requires a file even though we won't use it
      openssl_config = OpenSSL::Config.new(t.path)

      factory = OpenSSL::X509::ExtensionFactory.new
      factory.subject_certificate = openssl_cert

      #NB: If the parent doesn't have an SSL body we're making this a self-signed cert
      if parent.openssl_body.nil?
        factory.issuer_certificate = openssl_cert
      else
        factory.issuer_certificate = parent.openssl_body
      end

      self.extensions.keys.each do |k|
        config_extensions = extensions[k].config_extensions
        openssl_config = merge_options(openssl_config,config_extensions)
      end

      # p openssl_config.sections

      factory.config = openssl_config

      # Order matters: e.g. for self-signed, subjectKeyIdentifier must come before authorityKeyIdentifier
      self.extensions.keys.sort{|a,b| b<=>a}.each do |k|
        e = extensions[k]
        next if e.to_s.nil? or e.to_s == "" ## If the extension returns an empty string we won't include it
        ext = factory.create_ext(e.openssl_identifier, e.to_s, e.critical)
        openssl_cert.add_extension(ext)
      end

      if signing_profile["digest"].nil?
        digest = OpenSSL::Digest.new("SHA512")
      else
        digest = OpenSSL::Digest.new(signing_profile["digest"])
      end

      self.openssl_body = openssl_cert.sign(parent.key_material.private_key, digest)
    ensure
      t.close! if t # We can get rid of the ridiculous temp file
    end

    def is_signing_entity?
      self.extensions["basicConstraints"].ca
    end

    def signing_entity=(signing)
      self.extensions["basicConstraints"].ca = signing
    end

    def revoked?
      !self.revoked_at.nil?
    end

    def to_pem
      raise "Certificate has no signed body" if self.openssl_body.nil?
      self.openssl_body.to_pem
    end

    def to_csr
      csr = SigningRequest.new
      csr.distinguished_name = self.distinguished_name
      csr.key_material = self.key_material
      factory = OpenSSL::X509::ExtensionFactory.new
      exts = []
      self.extensions.keys.each do |k|
        ## Don't copy over key identifiers for CSRs
        next if k == "subjectKeyIdentifier" || k == "authorityKeyIdentifier"
        e = extensions[k]
        ## If the extension returns an empty string we won't include it
        next if e.to_s.nil? or e.to_s == ""
        exts << factory.create_ext(e.openssl_identifier, e.to_s, e.critical)
      end
      attrval = OpenSSL::ASN1::Set([OpenSSL::ASN1::Sequence(exts)])
      attrs = [
        OpenSSL::X509::Attribute.new("extReq", attrval),
        OpenSSL::X509::Attribute.new("msExtReq", attrval)
      ]
      csr.attributes = attrs
      csr
    end

    def self.from_x509_cert(raw_cert)
      openssl_cert = OpenSSL::X509::Certificate.new(raw_cert)
      Certificate.from_openssl(openssl_cert)
    end

    def is_root_entity?
      self.parent == self && is_signing_entity?
    end

    def is_intermediate_entity?
      (self.parent != self) && is_signing_entity?
    end

    private

    def merge_profile_with_extensions(signing_profile={})
      return self.extensions if signing_profile["extensions"].nil?
      signing_config = signing_profile["extensions"]
      signing_config.keys.each do |k|
        extension = self.extensions[k]
        items = signing_config[k]
        items.keys.each do |profile_item_key|
          if extension.respond_to?("#{profile_item_key}=".to_sym)
            if k == 'subjectAltName' && profile_item_key == 'emails'
              items[profile_item_key].map do |email|
                if email == 'email:copy'
                  fail "no email address provided for subject: #{subject.to_x509_name}" unless subject.email_address
                  "email:#{subject.email_address}"
                else
                  email
                end
              end
            end
            extension.send("#{profile_item_key}=".to_sym, items[profile_item_key] )
          else
            p "Tried applying '#{profile_item_key}' to #{extension.class} but it doesn't respond!"
          end
        end
      end
    end

    # Enumeration of the extensions. Not the worst option since
    # the likelihood of these needing to be updated is low at best.
    EXTENSIONS = [
        CertificateAuthority::Extensions::BasicConstraints,
        CertificateAuthority::Extensions::CrlDistributionPoints,
        CertificateAuthority::Extensions::SubjectKeyIdentifier,
        CertificateAuthority::Extensions::AuthorityKeyIdentifier,
        CertificateAuthority::Extensions::AuthorityInfoAccess,
        CertificateAuthority::Extensions::KeyUsage,
        CertificateAuthority::Extensions::ExtendedKeyUsage,
        CertificateAuthority::Extensions::SubjectAlternativeName,
        CertificateAuthority::Extensions::CertificatePolicies
    ]

    def load_extensions
      extension_hash = {}

      EXTENSIONS.each do |klass|
        extension = klass.new
        extension_hash[extension.openssl_identifier] = extension
      end

      extension_hash
    end

    def merge_options(config,hash)
      hash.keys.each do |k|
        config[k] = hash[k]
      end
      config
    end

    def self.from_openssl openssl_cert
      unless openssl_cert.is_a? OpenSSL::X509::Certificate
        raise "Can only construct from an OpenSSL::X509::Certificate"
      end

      certificate = Certificate.new
      # Only subject, key_material, and body are used for signing
      certificate.distinguished_name = DistinguishedName.from_openssl openssl_cert.subject
      certificate.key_material.public_key = openssl_cert.public_key
      certificate.openssl_body = openssl_cert
      certificate.serial_number.number = openssl_cert.serial.to_i
      certificate.not_before = openssl_cert.not_before
      certificate.not_after = openssl_cert.not_after
      EXTENSIONS.each do |klass|
        _,v,c = (openssl_cert.extensions.detect { |e| e.to_a.first == klass::OPENSSL_IDENTIFIER } || []).to_a
        certificate.extensions[klass::OPENSSL_IDENTIFIER] = klass.parse(v, c) if v
      end

      certificate
    end

  end
end