summaryrefslogtreecommitdiffstats
path: root/lib/puppet/sslcertificates/ca.rb
blob: 9fec908d61b7ba003992720dcd4dab75fc7fbbf8 (plain)
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
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
class Puppet::SSLCertificates::CA
    Certificate = Puppet::SSLCertificates::Certificate
    attr_accessor :keyfile, :file, :config, :dir, :cert

    Puppet.setdefaults("ca",
        :certdir => ["$ssldir/certs", "The certificate directory."],
        :publickeydir => ["$ssldir/public_keys", "The public key directory."],
        :privatekeydir => ["$ssldir/private_keys", "The private key directory."],
        :cadir => ["$ssldir/ca",
            "The root directory for the certificate authority."],
        :cacert => ["$cadir/ca_crt.pem", "The CA certificate."],
        :cakey => ["$cadir/ca_key.pem", "The CA private key."],
        :capub => ["$cadir/ca_pub.pem", "The CA public key."],
        :caprivatedir => ["$cadir/private",
            "Where the CA stores private certificate information."],
        :csrdir => ["$cadir/requests",
            "Where the CA stores certificate requests"],
        :signeddir => ["$cadir/signed",
            "Where the CA stores signed certificates."],
        :capass => ["$caprivatedir/ca.pass",
            "Where the CA stores the password for the private key"],
        :serial => ["$cadir/serial",
            "Where the serial number for certificates is stored."],
        :privatedir => ["$ssldir/private",
            "Where the client stores private certificate information."],
        :passfile => ["$privatedir/password",
            "Where puppetd stores the password for its private key.  Generally
            unused."],
        :autosign => ["$confdir/autosign.conf",
            "Whether to enable autosign.  Valid values are true (which autosigns
            any key request, and is a very bad idea), false (which never autosigns
            any key request), and the path to a file, which uses that configuration
            file to determine which keys to sign."],
        :ca_days => [1825, "How long a certificate should be valid."],
        :ca_md => ["md5", "The type of hash used in certificates."],
        :req_bits => [2048, "The bit length of the certificates."],
        :keylength => [1024, "The bit length of keys."]
    )

    #@@params.each { |param|
    #    Puppet.setdefault(param,@@defaults[param])
    #}

    def certfile
        @config[:cacert]
    end

    def host2csrfile(hostname)
        File.join(Puppet[:csrdir], [hostname, "pem"].join("."))
    end

    # this stores signed certs in a directory unrelated to 
    # normal client certs
    def host2certfile(hostname)
        File.join(Puppet[:signeddir], [hostname, "pem"].join("."))
    end

    def thing2name(thing)
        thing.subject.to_a.find { |ary|
            ary[0] == "CN"
        }[1]
    end

    def initialize(hash = {})
        self.setconfig(hash)

        if Puppet[:capass]
            if FileTest.exists?(Puppet[:capass])
                #puts "Reading %s" % Puppet[:capass]
                #system "ls -al %s" % Puppet[:capass]
                #File.read Puppet[:capass]
                @config[:password] = self.getpass
            else
                # Don't create a password if the cert already exists
                unless FileTest.exists?(@config[:cacert])
                    @config[:password] = self.genpass
                end
            end
        end

        self.getcert
        unless FileTest.exists?(@config[:serial])
            File.open(@config[:serial], "w") { |f|
                f << "%04X" % 1
            }
        end
    end

    def genpass
        pass = ""
        20.times { pass += (rand(74) + 48).chr }

        Puppet.recmkdir(File.dirname(@config[:capass]))
        begin
            File.open(@config[:capass], "w", 0600) { |f| f.print pass }
        rescue Errno::EACCES => detail
            raise Puppet::Error, detail.to_s
        end
        return pass
    end

    def getpass
        if @config[:capass] and File.readable?(@config[:capass])
            return File.read(@config[:capass])
        else
            raise Puppet::Error, "Could not read CA passfile %s" % @config[:capass]
        end
    end

    def getcert
        if FileTest.exists?(@config[:cacert])
            @cert = OpenSSL::X509::Certificate.new(
                File.read(@config[:cacert])
            )
        else
            self.mkrootcert
        end
    end

    def getclientcsr(host)
        csrfile = host2csrfile(host)
        unless File.exists?(csrfile)
            return nil
        end

        return OpenSSL::X509::Request.new(File.read(csrfile))
    end

    def getclientcert(host)
        certfile = host2certfile(host)
        unless File.exists?(certfile)
            return [nil, nil]
        end

        return [OpenSSL::X509::Certificate.new(File.read(certfile)), @cert]
    end

    def list
        return Dir.entries(Puppet[:csrdir]).reject { |file|
            file =~ /^\.+$/
        }.collect { |file|
            file.sub(/\.pem$/, '')
        }
    end

    def mkrootcert
        cert = Certificate.new(
            :name => "CAcert",
            :cert => @config[:cacert],
            :encrypt => @config[:capass],
            :key => @config[:cakey],
            :selfsign => true,
            :length => 1825,
            :type => :ca
        )
        @cert = cert.mkselfsigned
        File.open(@config[:cacert], "w", 0660) { |f|
            f.puts @cert.to_pem
        }
        @key = cert.key
        return cert
    end

    def removeclientcsr(host)
        csrfile = host2csrfile(host)
        unless File.exists?(csrfile)
            raise Puppet::Error, "No certificate request for %s" % host
        end

        File.unlink(csrfile)
    end

    def setconfig(hash)
        @config = {}
        Puppet.config.params("ca").each { |param|
            param = param.intern if param.is_a? String
            if hash.include?(param)
                @config[param] = hash[param]
                Puppet[param] = hash[param]
                hash.delete(param)
            else
                @config[param] = Puppet[param]
            end
        }

        if hash.include?(:password)
            @config[:password] = hash[:password]
            hash.delete(:password)
        end

        if hash.length > 0
            raise ArgumentError, "Unknown parameters %s" % hash.keys.join(",")
        end

        [:cadir, :csrdir, :signeddir].each { |dir|
            unless @config[dir]
                raise Puppet::DevError, "%s is undefined" % dir
            end
            unless FileTest.exists?(@config[dir])
                Puppet.recmkdir(@config[dir])
            end
        }
    end

    def sign(csr)
        unless csr.is_a?(OpenSSL::X509::Request)
            raise Puppet::Error,
                "CA#sign only accepts OpenSSL::X509::Request objects, not %s" %
                csr.class
        end

        unless csr.verify(csr.public_key)
            raise Puppet::Error, "CSR sign verification failed"
        end

        # i should probably check key length...

        # read the ca cert in
        cacert = OpenSSL::X509::Certificate.new(
            File.read(@config[:cacert])
        )

        cakey = nil
        if @config[:password]
            cakey = OpenSSL::PKey::RSA.new(
                File.read(@config[:cakey]), @config[:password]
            )
        else
            system("ls -al %s" % Puppet[:capass])
            cakey = OpenSSL::PKey::RSA.new(
                File.read(@config[:cakey])
            )
        end

        unless cacert.check_private_key(cakey)
            raise Puppet::Error, "CA Certificate is invalid"
        end

        serial = File.read(@config[:serial]).chomp.hex
        newcert = Puppet::SSLCertificates.mkcert(
            :type => :server,
            :name => csr.subject,
            :days => @config[:ca_days],
            :issuer => cacert,
            :serial => serial,
            :publickey => csr.public_key
        )

        # increment the serial
        File.open(@config[:serial], "w") { |f|
            f << "%04X" % (serial + 1)
        }

        newcert.sign(cakey, OpenSSL::Digest::SHA1.new)

        self.storeclientcert(newcert)

        return [newcert, cacert]
    end

    def storeclientcsr(csr)
        host = thing2name(csr)

        csrfile = host2csrfile(host)
        if File.exists?(csrfile)
            raise Puppet::Error, "Certificate request for %s already exists" % host
        end

        File.open(csrfile, "w", 0660) { |f|
            f.print csr.to_pem
        }
    end

    def storeclientcert(cert)
        host = thing2name(cert)

        certfile = host2certfile(host)
        if File.exists?(certfile)
            Puppet.notice "Overwriting signed certificate %s for %s" %
                [certfile, host]
        end

        File.open(certfile, "w", 0660) { |f|
            f.print cert.to_pem
        }
    end
end

# $Id$