summaryrefslogtreecommitdiffstats
path: root/lib/puppet/network/handler/ca.rb
blob: ebb6fc42704b29c5071a703fdf9fc9e9a5d7123d (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
require 'openssl'
require 'puppet'
require 'puppet/sslcertificates'
require 'xmlrpc/server'

# Much of this was taken from QuickCert:
#   http://segment7.net/projects/ruby/QuickCert/

class Puppet::Network::Handler
  class CA < Handler
    attr_reader :ca

    desc "Provides an interface for signing CSRs.  Accepts a CSR and returns
    the CA certificate and the signed certificate, or returns nil if
    the cert is not signed."

    @interface = XMLRPC::Service::Interface.new("puppetca") { |iface|
      iface.add_method("array getcert(csr)")
    }

    def autosign
      if defined?(@autosign)
        @autosign
      else
        Puppet[:autosign]
      end
    end

    # FIXME autosign? should probably accept both hostnames and IP addresses
    def autosign?(hostname)
      # simple values are easy
      if autosign == true or autosign == false
        return autosign
      end

      # we only otherwise know how to handle files
      unless autosign =~ /^\//
        raise Puppet::Error, "Invalid autosign value #{autosign.inspect}"
      end

      unless FileTest.exists?(autosign)
        unless defined?(@@warnedonautosign)
          @@warnedonautosign = true
          Puppet.info "Autosign is enabled but #{autosign} is missing"
        end
        return false
      end
      auth = Puppet::Network::AuthStore.new
      File.open(autosign) { |f|
        f.each { |line|
          next if line =~ /^\s*#/
          next if line =~ /^\s*$/
          auth.allow(line.chomp)
        }
      }

      # for now, just cheat and pass a fake IP address to allowed?
      auth.allowed?(hostname, "127.1.1.1")
    end

    def initialize(hash = {})
      Puppet.settings.use(:main, :ssl, :ca)
      @autosign = hash[:autosign] if hash.include? :autosign

      @ca = Puppet::SSLCertificates::CA.new(hash)
    end

    # our client sends us a csr, and we either store it for later signing,
    # or we sign it right away
    def getcert(csrtext, client = nil, clientip = nil)
      csr = OpenSSL::X509::Request.new(csrtext)

      # Use the hostname from the CSR, not from the network.
      subject = csr.subject

      nameary = subject.to_a.find { |ary|
        ary[0] == "CN"
      }

      if nameary.nil?
        Puppet.err(
          "Invalid certificate request: could not retrieve server name"
        )
        return "invalid"
      end

      hostname = nameary[1]

      unless @ca
        Puppet.notice "Host #{hostname} asked for signing from non-CA master"
        return ""
      end

      # We used to save the public key, but it's basically unnecessary
      # and it mucks with the permissions requirements.
      # save_pk(hostname, csr.public_key)

      certfile = File.join(Puppet[:certdir], [hostname, "pem"].join("."))

      # first check to see if we already have a signed cert for the host
      cert, cacert = ca.getclientcert(hostname)
      if cert and cacert
        Puppet.info "Retrieving existing certificate for #{hostname}"
        unless csr.public_key.to_s == cert.public_key.to_s
          raise Puppet::Error, "Certificate request does not match existing certificate; run 'puppetca --clean #{hostname}'."
        end
        return [cert.to_pem, cacert.to_pem]
      elsif @ca
        if self.autosign?(hostname) or client.nil?
          Puppet.info "Signing certificate for CA server" if client.nil?
          # okay, we don't have a signed cert
          # if we're a CA and autosign is turned on, then go ahead and sign
          # the csr and return the results
          Puppet.info "Signing certificate for #{hostname}"
          cert, cacert = @ca.sign(csr)
          #Puppet.info "Cert: #{cert.class}; Cacert: #{cacert.class}"
          return [cert.to_pem, cacert.to_pem]
        else # just write out the csr for later signing
          if @ca.getclientcsr(hostname)
            Puppet.info "Not replacing existing request from #{hostname}"
          else
            Puppet.notice "Host #{hostname} has a waiting certificate request"
            @ca.storeclientcsr(csr)
          end
          return ["", ""]
        end
      else
        raise "huh?"
      end
    end

    private

    # Save the public key.
    def save_pk(hostname, public_key)
      pkeyfile = File.join(Puppet[:publickeydir], [hostname, "pem"].join('.'))

      if FileTest.exists?(pkeyfile)
        currentkey = File.open(pkeyfile) { |k| k.read }
        unless currentkey == public_key.to_s
          raise Puppet::Error, "public keys for #{hostname} differ"
        end
      else
        File.open(pkeyfile, "w", 0644) { |f|
          f.print public_key.to_s
        }
      end
    end
  end
end