Add docstring to certificate verification
[ganeti-github.git] / lib / tools / common.py
1 #
2 #
3
4 # Copyright (C) 2014 Google Inc.
5 # All rights reserved.
6 #
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions are
9 # met:
10 #
11 # 1. Redistributions of source code must retain the above copyright notice,
12 # this list of conditions and the following disclaimer.
13 #
14 # 2. Redistributions in binary form must reproduce the above copyright
15 # notice, this list of conditions and the following disclaimer in the
16 # documentation and/or other materials provided with the distribution.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
19 # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
20 # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
22 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30 """Common functions for tool scripts.
31
32 """
33
34 import logging
35 import OpenSSL
36 import os
37 import time
38 from cStringIO import StringIO
39
40 from ganeti import constants
41 from ganeti import errors
42 from ganeti import pathutils
43 from ganeti import utils
44 from ganeti import serializer
45 from ganeti import ssconf
46 from ganeti import ssh
47
48
49 def VerifyOptions(parser, opts, args):
50 """Verifies options and arguments for correctness.
51
52 """
53 if args:
54 parser.error("No arguments are expected")
55
56 return opts
57
58
59 def _VerifyCertificateStrong(cert_pem, error_fn,
60 _check_fn=utils.CheckNodeCertificate):
61 """Verifies a certificate against the local node daemon certificate.
62
63 Includes elaborate tests of encodings etc., and returns formatted
64 certificate.
65
66 @type cert_pem: string
67 @param cert_pem: Certificate and key in PEM format
68 @type error_fn: callable
69 @param error_fn: function to call in case of an error
70 @rtype: string
71 @return: Formatted key and certificate
72
73 """
74 try:
75 cert = \
76 OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert_pem)
77 except Exception, err:
78 raise error_fn("(stdin) Unable to load certificate: %s" % err)
79
80 try:
81 key = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, cert_pem)
82 except OpenSSL.crypto.Error, err:
83 raise error_fn("(stdin) Unable to load private key: %s" % err)
84
85 # Check certificate with given key; this detects cases where the key given on
86 # stdin doesn't match the certificate also given on stdin
87 try:
88 utils.X509CertKeyCheck(cert, key)
89 except OpenSSL.SSL.Error:
90 raise error_fn("(stdin) Certificate is not signed with given key")
91
92 # Standard checks, including check against an existing local certificate
93 # (no-op if that doesn't exist)
94 _check_fn(cert)
95
96 key_encoded = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)
97 cert_encoded = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM,
98 cert)
99 complete_cert_encoded = key_encoded + cert_encoded
100 if not cert_pem == complete_cert_encoded:
101 logging.error("The certificate differs after being reencoded. Please"
102 " renew the certificates cluster-wide to prevent future"
103 " inconsistencies.")
104
105 # Format for storing on disk
106 buf = StringIO()
107 buf.write(cert_pem)
108 return buf.getvalue()
109
110
111 def _VerifyCertificateSoft(cert_pem, error_fn,
112 _check_fn=utils.CheckNodeCertificate):
113 """Verifies a certificate against the local node daemon certificate.
114
115 @type cert_pem: string
116 @param cert_pem: Certificate in PEM format (no key)
117
118 """
119 try:
120 OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, cert_pem)
121 except OpenSSL.crypto.Error, err:
122 pass
123 else:
124 raise error_fn("No private key may be given")
125
126 try:
127 cert = \
128 OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert_pem)
129 except Exception, err:
130 raise errors.X509CertError("(stdin)",
131 "Unable to load certificate: %s" % err)
132
133 _check_fn(cert)
134
135
136 def VerifyCertificateSoft(data, error_fn, _verify_fn=_VerifyCertificateSoft):
137 """Verifies cluster certificate if existing.
138
139 @type data: dict
140 @type error_fn: callable
141 @param error_fn: function to call in case of an error
142 @rtype: string
143 @return: Formatted key and certificate
144
145 """
146 cert = data.get(constants.SSHS_NODE_DAEMON_CERTIFICATE)
147 if cert:
148 _verify_fn(cert, error_fn)
149
150
151 def VerifyCertificateStrong(data, error_fn,
152 _verify_fn=_VerifyCertificateStrong):
153 """Verifies cluster certificate. Throws error when not existing.
154
155 @type data: dict
156 @type error_fn: callable
157 @param error_fn: function to call in case of an error
158 @rtype: string
159 @return: Formatted key and certificate
160
161 """
162 cert = data.get(constants.NDS_NODE_DAEMON_CERTIFICATE)
163 if not cert:
164 raise error_fn("Node daemon certificate must be specified")
165
166 return _verify_fn(cert, error_fn)
167
168
169 def VerifyClusterName(data, error_fn,
170 _verify_fn=ssconf.VerifyClusterName):
171 """Verifies cluster name.
172
173 @type data: dict
174
175 """
176 name = data.get(constants.SSHS_CLUSTER_NAME)
177 if name:
178 _verify_fn(name)
179 else:
180 raise error_fn("Cluster name must be specified")
181
182
183 def LoadData(raw, data_check):
184 """Parses and verifies input data.
185
186 @rtype: dict
187
188 """
189 return serializer.LoadAndVerifyJson(raw, data_check)
190
191
192 def GenerateRootSshKeys(error_fn, _suffix="", _homedir_fn=None):
193 """Generates root's SSH keys for this node.
194
195 """
196 ssh.InitSSHSetup(error_fn=error_fn, _homedir_fn=_homedir_fn, _suffix=_suffix)
197
198
199 def GenerateClientCertificate(
200 data, error_fn, client_cert=pathutils.NODED_CLIENT_CERT_FILE,
201 signing_cert=pathutils.NODED_CERT_FILE):
202 """Regenerates the client certificate of the node.
203
204 @type data: string
205 @param data: the JSON-formated input data
206
207 """
208 if not os.path.exists(signing_cert):
209 raise error_fn("The signing certificate '%s' cannot be found."
210 % signing_cert)
211
212 # TODO: This sets the serial number to the number of seconds
213 # since epoch. This is technically not a correct serial number
214 # (in the way SSL is supposed to be used), but it serves us well
215 # enough for now, as we don't have any infrastructure for keeping
216 # track of the number of signed certificates yet.
217 serial_no = int(time.time())
218
219 # The hostname of the node is provided with the input data.
220 hostname = data.get(constants.NDS_NODE_NAME)
221 if not hostname:
222 raise error_fn("No hostname found.")
223
224 utils.GenerateSignedSslCert(client_cert, serial_no, signing_cert,
225 common_name=hostname)