Fix issue with certificates conversion
[sdnc/oam.git] / installation / sdnc / src / main / scripts / installCerts.py
1 # ============LICENSE_START=======================================================
2 #  Copyright (C) 2019 Nordix Foundation.
3 # ================================================================================
4 #  extended by highstreet technologies GmbH (c) 2020
5 #  Copyright (c) 2021 Nokia Intellectual Property.
6 # ================================================================================
7 # Licensed under the Apache License, Version 2.0 (the "License");
8 # you may not use this file except in compliance with the License.
9 # You may obtain a copy of the License at
10 #
11 #      http://www.apache.org/licenses/LICENSE-2.0
12 #
13 # Unless required by applicable law or agreed to in writing, software
14 # distributed under the License is distributed on an "AS IS" BASIS,
15 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
18 #
19 # SPDX-License-Identifier: Apache-2.0
20 # ============LICENSE_END=========================================================
21 #
22
23
24 # coding=utf-8
25 import os
26 import re
27 import http.client
28 import base64
29 import time
30 import zipfile
31 import shutil
32 import subprocess
33 import logging
34
35 odl_home = os.environ['ODL_HOME']
36 log_directory = odl_home + '/data/log/'
37 log_file = log_directory + 'installCerts.log'
38 with open(os.path.join(log_directory, 'installCerts.log'), 'w') as fp:
39     pass
40 log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
41 if not os.path.exists(log_directory):
42     os.makedirs(log_directory)
43 logging.basicConfig(filename=log_file,level=logging.DEBUG,filemode='w',format=log_format)
44 print ('Start cert provisioning. Log file: ' + log_file);
45
46 Path = "/tmp"
47 if "ODL_CERT_DIR" in os.environ:
48     Path = os.environ['ODL_CERT_DIR']
49
50 zipFileList = []
51
52 username = os.environ['ODL_ADMIN_USERNAME']
53 password = os.environ['ODL_ADMIN_PASSWORD']
54 newpassword = os.environ.get('ODL_ADMIN_NEWPASSWORD')
55 TIMEOUT=1000
56 INTERVAL=30
57 timePassed=0
58
59 postKeystore= "/rests/operations/netconf-keystore:add-keystore-entry"
60 postPrivateKey= "/rests/operations/netconf-keystore:add-private-key"
61 postTrustedCertificate= "/rests/operations/netconf-keystore:add-trusted-certificate"
62
63 truststore_pass_file = Path + '/truststore.pass'
64 truststore_file = Path + '/truststore.jks'
65
66 keystore_pass_file = Path + '/keystore.pass'
67 keystore_file = Path + '/keystore.jks'
68
69 jks_files = [truststore_pass_file, keystore_pass_file, keystore_file, truststore_file]
70
71 envOdlFeaturesBoot='ODL_FEATURES_BOOT'
72 # Strategy sli-api is default
73 certreadyCmd="POST"
74 certreadyUrl="/rests/operations/SLI-API:healthcheck"
75
76 if "SDNRWT" in os.environ: 
77     sdnrWt = os.environ['SDNRWT']
78     if sdnrWt == "true":
79         certreadyCmd="GET"
80         certreadyUrl="/rests/data/network-topology:network-topology"
81 logging.info('ODL ready strategy with command %s and url %s', certreadyCmd, certreadyUrl)
82
83 odl_port = 8181
84 cred_string = username + ":" + password
85 headers = {'Authorization':'Basic %s' % base64.b64encode(cred_string.encode()).decode(),
86            'X-FromAppId': 'csit-sdnc',
87            'X-TransactionId': 'csit-sdnc',
88            'Accept':"application/json",
89            'Content-type':"application/yang-data+json"}
90
91 def readFile(folder, file):
92     key = open(Path + "/" + folder + "/" + file, "r")
93     fileRead = key.read()
94     key.close()
95     fileRead = "\n".join(fileRead.splitlines()[1:-1])
96     return fileRead
97
98 def readTrustedCertificate(folder, file):
99     listCert = list()
100     caPem = ""
101     startCa = False
102     key = open(folder + "/" + file, "r")
103     lines = key.readlines()
104     for line in lines:
105         if not "BEGIN CERTIFICATE" in line and not "END CERTIFICATE" in line and startCa:
106             caPem += line
107         elif "BEGIN CERTIFICATE" in line:
108             startCa = True
109         elif "END CERTIFICATE" in line:
110             startCa = False
111             listCert.append(caPem)
112             caPem = ""
113     return listCert
114
115 def makeKeystoreKey(clientKey, count):
116     odl_private_key = "ODL_private_key_%d" %count
117
118     json_keystore_key='{{\"input\": {{ \"key-credential\": {{\"key-id\": \"{odl_private_key}\", \"private-key\" : ' \
119                       '\"{clientKey}\",\"passphrase\" : \"\"}}}}}}'.format(
120         odl_private_key=odl_private_key,
121         clientKey=clientKey)
122
123     return json_keystore_key
124
125 def makePrivateKey(clientKey, clientCrt, certList, count):
126     caPem = ""
127     if certList:
128         for cert in certList:
129             caPem += '\"%s\",' % cert
130         caPem = caPem.rsplit(',', 1)[0]
131     odl_private_key="ODL_private_key_%d" %count
132
133     json_private_key='{{\"input\": {{ \"private-key\":{{\"name\": \"{odl_private_key}\", \"data\" : ' \
134                      '\"{clientKey}\",\"certificate-chain\":[\"{clientCrt}\",{caPem}]}}}}}}'.format(
135         odl_private_key=odl_private_key,
136         clientKey=clientKey,
137         clientCrt=clientCrt,
138         caPem=caPem)
139
140     return json_private_key
141
142 def makeTrustedCertificate(certList, count):
143     number = 0
144     json_cert_format = ""
145     for cert in certList:
146         cert_name = "xNF_CA_certificate_%d_%d" %(count, number)
147         json_cert_format += '{{\"name\": \"{trusted_name}\",\"certificate\":\"{cert}\"}},\n'.format(
148             trusted_name=cert_name,
149             cert=cert.strip())
150         number += 1
151
152     json_cert_format = json_cert_format.rsplit(',', 1)[0]
153     json_trusted_cert='{{\"input\": {{ \"trusted-certificate\": [{certificates}]}}}}'.format(
154         certificates=json_cert_format)
155     return json_trusted_cert
156
157
158 def makeRestconfPost(conn, json_file, apiCall):
159     req = conn.request("POST", apiCall, json_file, headers=headers)
160     res = conn.getresponse()
161     res.read()
162     if res.status != 200:
163         logging.error("Error here, response back wasnt 200: Response was : %d , %s" % (res.status, res.reason))
164     else:
165         logging.debug("Response :%s Reason :%s ",res.status, res.reason)
166
167 def extractZipFiles(zipFileList, count):
168     for zipFolder in zipFileList:
169         with zipfile.ZipFile(Path + "/" + zipFolder.strip(),"r") as zip_ref:
170             zip_ref.extractall(Path)
171         folder = zipFolder.rsplit(".")[0]
172         processFiles(folder, count)
173
174 def processFiles(folder, count):
175     logging.info('Process folder: %d %s', count, folder)
176     for file in os.listdir(Path + "/" + folder):
177         if os.path.isfile(Path + "/" + folder + "/" + file.strip()):
178             if ".key" in file:
179                 clientKey = readFile(folder, file.strip())
180             elif "trustedCertificate" in file:
181                 certList = readTrustedCertificate(Path + "/" + folder, file.strip())
182             elif ".crt" in file:
183                 clientCrt = readFile(folder, file.strip())
184         else:
185             logging.error("Could not find file %s" % file.strip())
186     shutil.rmtree(Path + "/" + folder)
187     post_content(clientKey, clientCrt, certList, count)
188
189 def post_content(clientKey, clientCrt, certList, count):
190     logging.info('Post content: %d', count)
191     conn = http.client.HTTPConnection("localhost",odl_port)
192
193     if clientKey:
194         json_keystore_key = makeKeystoreKey(clientKey, count)
195         logging.debug("Posting private key in to ODL keystore")
196         makeRestconfPost(conn, json_keystore_key, postKeystore)
197
198     if certList:
199         json_trusted_cert = makeTrustedCertificate(certList, count)
200         logging.debug("Posting trusted cert list in to ODL")
201         makeRestconfPost(conn, json_trusted_cert, postTrustedCertificate)
202
203     if clientKey and clientCrt and certList:
204         json_private_key = makePrivateKey(clientKey, clientCrt, certList, count)
205         logging.debug("Posting the cert in to ODL")
206         makeRestconfPost(conn, json_private_key, postPrivateKey)
207
208
209 def makeHealthcheckCall(headers, timePassed):
210     connected = False
211     # WAIT 10 minutes maximum and test every 30 seconds if HealthCheck API is returning 200
212     while timePassed < TIMEOUT:
213         try:
214             conn = http.client.HTTPConnection("localhost",odl_port)
215             req = conn.request(certreadyCmd, certreadyUrl,headers=headers)
216             res = conn.getresponse()
217             res.read()
218             httpStatus = res.status
219             if httpStatus == 200:
220                 logging.debug("Healthcheck Passed in %d seconds." %timePassed)
221                 connected = True
222                 break
223             else:
224                 logging.debug("Sleep: %d seconds before testing if Healthcheck worked. Total wait time up now is: %d seconds. Timeout is: %d seconds. Problem code was: %d" %(INTERVAL, timePassed, TIMEOUT, httpStatus))
225         except:
226             logging.error("Cannot execute REST call. Sleep: %d seconds before testing if Healthcheck worked. Total wait time up now is: %d seconds. Timeout is: %d seconds." %(INTERVAL, timePassed, TIMEOUT))
227         timePassed = timeIncrement(timePassed)
228
229     if timePassed > TIMEOUT:
230         logging.error("TIME OUT: Healthcheck not passed in  %d seconds... Could cause problems for testing activities..." %TIMEOUT)
231
232     return connected
233
234
235 def timeIncrement(timePassed):
236     time.sleep(INTERVAL)
237     timePassed = timePassed + INTERVAL
238     return timePassed
239
240
241 def get_pass(file_name):
242     try:
243         with open(file_name, 'r') as file_obj:
244             password = file_obj.read().strip()
245         return "'{}'".format(password)
246     except Exception as e:
247         logging.error("Error occurred while fetching password : %s", e)
248         exit()
249
250 def cleanup():
251     for file in os.listdir(Path):
252         if os.path.isfile(Path + '/' + file):
253             logging.debug("Cleaning up the file %s", Path + '/'+ file)
254             os.remove(Path + '/'+ file)
255
256
257 def jks_to_p12(file, password):
258     """Converts jks format into p12"""
259     try:
260         certList = []
261         key = None
262         cert = None
263         if (file.endswith('.jks')):
264              p12_file = file.replace('.jks', '.p12')
265              jks_cmd = 'keytool -importkeystore -srckeystore {src_file} -destkeystore {dest_file} -srcstoretype JKS -srcstorepass {src_pass} -deststoretype PKCS12 -deststorepass {dest_pass}'.format(src_file=file, dest_file=p12_file, src_pass=password, dest_pass=password)
266              logging.debug("Converting %s into p12 format", file)
267              os.system(jks_cmd)
268              file = p12_file
269              return file
270     except Exception as e:
271         logging.error("Error occurred while converting jks to p12 format : %s", e)
272
273
274 def make_cert_chain(cert_chain, pattern):
275     cert_list = []
276     if cert_chain:
277         cert_chain = cert_chain.decode('utf-8')
278         matches = re.findall(pattern, cert_chain, re.DOTALL | re.MULTILINE)
279         for cert in matches:
280             cert_list.append(cert.strip())
281         return cert_list
282     else:
283         logging.debug(" Certificate Chain empty: %s " % cert_chain)
284
285
286 def process_jks_files(count):
287     ca_cert_list = []
288     logging.info("Processing JKS files found in %s directory " % Path)
289     try:
290         if all([os.path.isfile(f) for f in jks_files]):
291             keystore_pass = get_pass(keystore_pass_file)
292             keystore_file_p12 = jks_to_p12(keystore_file, keystore_pass)
293
294             client_key_cmd = 'openssl pkcs12 -in {src_file} -nocerts -nodes -passin pass:{src_pass}'.format(
295                 src_file=keystore_file_p12, src_pass=keystore_pass)
296             client_crt_cmd = 'openssl pkcs12 -in {src_file} -clcerts -nokeys  -passin pass:{src_pass}'.format(
297                 src_file=keystore_file_p12, src_pass=keystore_pass)
298
299             truststore_pass = get_pass(truststore_pass_file)
300             truststore_p12 = jks_to_p12(truststore_file, truststore_pass)
301
302             trust_cert_cmd = 'openssl pkcs12 -in {src_file} -cacerts -nokeys -passin pass:{src_pass} '.format(
303                 src_file=truststore_p12, src_pass=truststore_pass)
304
305             key_pattern = r'(?<=-----BEGIN PRIVATE KEY-----).*?(?=-----END PRIVATE KEY-----)'
306             client_key = subprocess.check_output(client_key_cmd, shell=True)
307             if client_key:
308                 client_key = make_cert_chain(client_key, key_pattern)[0]
309                 logging.debug("Key Ok")
310
311             cert_pattern = r'(?<=-----BEGIN CERTIFICATE-----).*?(?=-----END CERTIFICATE-----)'
312             client_cert = subprocess.check_output(client_crt_cmd, shell=True)
313             if client_cert:
314                 client_cert = make_cert_chain(client_cert, cert_pattern)[0]
315                 logging.debug("Client Cert Ok")
316
317             ca_cert = subprocess.check_output(trust_cert_cmd, shell=True)
318             if ca_cert:
319                 ca_cert_list = make_cert_chain(ca_cert, cert_pattern)
320                 logging.debug("CA Cert Ok")
321
322             if client_key and client_cert and ca_cert:
323                 post_content(client_key, client_cert, ca_cert_list, count)
324         else:
325             logging.debug("No JKS files found in %s directory" % Path)
326     except subprocess.CalledProcessError as err:
327         print("CalledProcessError Execution of OpenSSL command failed: %s" % err)
328     except Exception as e:
329         logging.error("UnExpected Error while processing JKS files at {0}, Caused by: {1}".format(Path, e))
330
331 def replaceAdminPassword(username, password, newpassword):
332     if newpassword is None:
333         logging.info('Not to replace password for user %s', username)
334     else:
335         logging.info('Replace password for user %s', username)
336         try:
337             jsondata = '{\"password\": \"{newpassword}\"}'.format(newpassword=newpassword)
338             url = '/auth/v1/users/{username}@sdn'.format(username=username)
339             loggin.info("Url %s data $s", url, jsondata)
340             conn = http.client.HTTPConnection("localhost",odl_port)
341             req = conn.request("PUT", url, jsondata, headers=headers)
342             res = conn.getresponse()
343             res.read()
344             httpStatus = res.status
345             if httpStatus == 200:
346                 logging.debug("New password provided successfully for user %s", username)
347             else:
348                 logging.debug("Password change was not possible. Problem code was: %d", httpStatus)
349         except:
350             logging.error("Cannot execute REST call to set password.")
351
352
353 def readCertProperties():
354     '''
355     This function searches for manually copied zip file
356     containing certificates. This is required as part
357     of backward compatibility.
358     If not foud, it searches for jks certificates.
359     '''
360     connected = makeHealthcheckCall(headers, timePassed)
361     logging.info('Connected status: %s', connected)
362     if connected:
363         replaceAdminPassword(username, password, newpassword)
364         count = 0
365         if os.path.isfile(Path + "/certs.properties"):
366             with open(Path + "/certs.properties", "r") as f:
367                 for line in f:
368                     if not "*****" in line:
369                         zipFileList.append(line)
370                     else:
371                         extractZipFiles(zipFileList, count)
372                         count += 1
373                         del zipFileList[:]
374         else:
375             logging.debug("No certs.properties/zip files exist at: " + Path)
376             logging.info("Processing any  available jks/p12 files under cert directory")
377             process_jks_files(count)
378
379
380 readCertProperties()
381 logging.info('Cert installation ending')