summaryrefslogtreecommitdiffstats
path: root/lib/puppet/ssl/host.rb
blob: d3805eb20a7ddc7cc36998ee6232c7db81373a36 (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
require 'puppet/ssl'
require 'puppet/ssl/key'
require 'puppet/ssl/certificate'
require 'puppet/ssl/certificate_request'
require 'puppet/ssl/certificate_revocation_list'
require 'puppet/util/constant_inflector'

# The class that manages all aspects of our SSL certificates --
# private keys, public keys, requests, etc.
class Puppet::SSL::Host
    # Yay, ruby's strange constant lookups.
    Key = Puppet::SSL::Key
    Certificate = Puppet::SSL::Certificate
    CertificateRequest = Puppet::SSL::CertificateRequest
    CertificateRevocationList = Puppet::SSL::CertificateRevocationList

    extend Puppet::Util::ConstantInflector

    attr_reader :name
    attr_accessor :ca

    attr_writer :key, :certificate, :certificate_request

    CA_NAME = "ca"

    # This is the constant that people will use to mark that a given host is
    # a certificate authority.
    def self.ca_name
        CA_NAME
    end

    class << self
        attr_reader :ca_location
    end

    # Configure how our various classes interact with their various terminuses.
    def self.configure_indirection(terminus, cache = nil)
        Certificate.terminus_class = terminus
        CertificateRequest.terminus_class = terminus
        CertificateRevocationList.terminus_class = terminus

        if cache
            # This is weird; we don't actually cache our keys or CRL, we
            # use what would otherwise be the cache as our normal
            # terminus.
            Key.terminus_class = cache
        else
            Key.terminus_class = terminus
        end

        if cache
            Certificate.cache_class = cache
            CertificateRequest.cache_class = cache
            CertificateRevocationList.cache_class = cache
        end
    end

    # Specify how we expect to interact with our certificate authority.
    def self.ca_location=(mode)
        raise ArgumentError, "CA Mode can only be :local, :remote, or :none" unless [:local, :remote, :none].include?(mode)

        @ca_mode = mode

        case @ca_mode
        when :local:
            # Our ca is local, so we use it as the ultimate source of information
            # And we cache files locally.
            configure_indirection :ca, :file
        when :remote:
            configure_indirection :rest, :file
        when :none:
            # We have no CA, so we just look in the local file store.
            configure_indirection :file
        end
    end

    # Remove all traces of a given host
    def self.destroy(name)
        [Key, Certificate, CertificateRequest].inject(false) do |result, klass|
            if klass.destroy(name)
                result = true
            end
            result
        end
    end

    # Search for more than one host, optionally only specifying
    # an interest in hosts with a given file type.
    # This just allows our non-indirected class to have one of
    # indirection methods.
    def self.search(options = {})
        classes = [Key, CertificateRequest, Certificate]
        if klass = options[:for]
            classlist = [klass].flatten
        else
            classlist = [Key, CertificateRequest, Certificate]
        end

        # Collect the results from each class, flatten them, collect all of the names, make the name list unique,
        # then create a Host instance for each one.
        classlist.collect { |klass| klass.search }.flatten.collect { |r| r.name }.uniq.collect do |name|
            new(name)
        end
    end

    # Is this a ca host, meaning that all of its files go in the CA location?
    def ca?
        ca
    end

    def key
        return nil unless @key ||= Key.find(name)
        @key
    end

    # This is the private key; we can create it from scratch
    # with no inputs.
    def generate_key
        @key = Key.new(name)
        @key.generate
        @key.save
        true
    end

    def certificate_request
        return nil unless @certificate_request ||= CertificateRequest.find(name)
        @certificate_request
    end

    # Our certificate request requires the key but that's all.
    def generate_certificate_request
        generate_key unless key
        @certificate_request = CertificateRequest.new(name)
        @certificate_request.generate(key.content)
        @certificate_request.save
        return true
    end

    def certificate
        return nil unless @certificate ||= Certificate.find(name)
        @certificate
    end

    # Generate all necessary parts of our ssl host.
    def generate
        generate_key unless key
        generate_certificate_request unless certificate_request

        # If we can get a CA instance, then we're a valid CA, and we
        # should use it to sign our request; else, just try to read
        # the cert.
        if ! certificate() and ca = Puppet::SSL::CertificateAuthority.instance
            ca.sign(self.name)
        end
    end

    def initialize(name = nil)
        @name = (name || Puppet[:certname]).downcase
        @key = @certificate = @certificate_request = nil
        @ca = (name == self.class.ca_name)
    end

    # Extract the public key from the private key.
    def public_key
        key.content.public_key
    end

    # Create/return a store that uses our SSL info to validate
    # connections.
    def ssl_store(purpose = OpenSSL::X509::PURPOSE_ANY)
        unless defined?(@ssl_store) and @ssl_store
            @ssl_store = OpenSSL::X509::Store.new
            @ssl_store.purpose = purpose

            @ssl_store.add_file(Puppet[:localcacert])

            # If there's a CRL, add it to our store.
            if crl = Puppet::SSL::CertificateRevocationList.find("ca")
                @ssl_store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK_ALL|OpenSSL::X509::V_FLAG_CRL_CHECK
                @ssl_store.add_crl(crl.content)
            end
            return @ssl_store
        end
        @ssl_store
    end

    # Attempt to retrieve a cert, if we don't already have one.
    def wait_for_cert(time)
        return :existing if certificate
        begin
            generate

            return :new if certificate
        rescue StandardError => detail
            Puppet.err "Could not request certificate: %s" % detail.to_s
            if time < 1
                puts "Exiting; failed to retrieve certificate and watiforcert is disabled"
                exit(1)
            else
                sleep(time)
            end
            retry
        end

        if time < 1
            puts "Exiting; no certificate found and waitforcert is disabled"
            exit(1) 
        end

        while true do
            sleep time
            begin
                break if certificate
                Puppet.notice "Did not receive certificate"
            rescue StandardError => detail
                Puppet.err "Could not request certificate: %s" % detail.to_s
            end
        end
        return :new
    end
end

require 'puppet/ssl/certificate_authority'