def main(): """Main method of the Tenant Webapp Server. This method is encapsulated in a function for packaging to allow it to be called as a function by an external program.""" webapp_port = config.getint('webapp', 'webapp_port') logger.info( 'Starting Tenant WebApp (tornado) on port %d use <Ctrl-C> to stop', webapp_port) # Figure out where our static files are located if getattr(sys, 'frozen', False): # static directory must be bundled with the script root_dir = os.path.dirname(os.path.abspath(sys.executable)) else: # instead try to locate static directory relative to script root_dir = os.path.dirname(os.path.abspath(__file__)) if not os.path.exists(root_dir + "/static/"): raise Exception( f'Static resource directory could not be found in {root_dir}!') app = tornado.web.Application([ (r"/webapp/.*", WebAppHandler), (r"/(?:v[0-9]/)?agents/.*", AgentsHandler), (r"/(?:v[0-9]/)?logs/.*", AgentsHandler), (r'/static/(.*)', tornado.web.StaticFileHandler, { 'path': root_dir + "/static/" }), (r".*", MainHandler), ]) # WebApp Server TLS server_context = get_tls_context() server_context.verify_mode = ssl.CERT_NONE # ssl.CERT_REQUIRED # Set up server server = tornado.httpserver.HTTPServer(app, ssl_options=server_context) server.bind(webapp_port, address='0.0.0.0') server.start( config.getint('cloud_verifier', 'multiprocessing_pool_num_workers')) try: tornado.ioloop.IOLoop.instance().start() except KeyboardInterrupt: tornado.ioloop.IOLoop.instance().stop()
def test_022_agent_quotes_identity_get(self): """Test agent's GET /quotes/identity Interface""" self.assertIsNotNone( aik_tpm, "Required value not set. Previous step may have failed?") nonce = tpm_abstract.TPM_Utilities.random_password(20) numretries = config.getint("tenant", "max_retries") while numretries >= 0: test_022_agent_quotes_identity_get = RequestsClient( tenant_templ.agent_base_url, tls_enabled=True, ignore_hostname=True) response = test_022_agent_quotes_identity_get.get( f"/v{self.api_version}/quotes/identity?nonce={nonce}", data=None, cert=tenant_templ.agent_cert, verify=False, # TODO: use agent certificate ) if response.status_code == 200: break numretries -= 1 time.sleep(config.getint("tenant", "retry_interval")) self.assertEqual(response.status_code, 200, "Non-successful Agent identity return code!") json_response = response.json() # Ensure response is well-formed self.assertIn("results", json_response, "Malformed response body!") self.assertIn("quote", json_response["results"], "Malformed response body!") self.assertIn("pubkey", json_response["results"], "Malformed response body!") # Check the quote identity failure = tpm_instance.check_quote( tenant_templ.agent_uuid, nonce, json_response["results"]["pubkey"], json_response["results"]["quote"], aik_tpm, hash_alg=algorithms.Hash(json_response["results"]["hash_alg"]), ) self.assertTrue(not failure, "Invalid quote!")
def do_verify(self): """ Perform verify using a random generated challenge """ challenge = TPM_Utilities.random_password(20) numtries = 0 while True: try: cloudagent_base_url = (f'{self.agent_ip}:{self.agent_port}') do_verify = RequestsClient(cloudagent_base_url, tls_enabled=False) response = do_verify.get( (f'/keys/verify?challenge={challenge}'), cert=self.cert, verify=False) except Exception as e: if response.status_code in (503, 504): numtries += 1 maxr = config.getint('tenant', 'max_retries') if numtries >= maxr: logger.error( f"Cannot establish connection to agent on {self.agent_ip} with port {self.agent_port}" ) sys.exit() retry = config.getfloat('tenant', 'retry_interval') logger.info( f"Verifier connection to agent at {self.agent_ip} refused {numtries}/{maxr} times, trying again in {retry} seconds..." ) time.sleep(retry) continue raise e response_body = response.json() if response.status_code == 200: if "results" not in response_body or 'hmac' not in response_body[ 'results']: logger.critical( f"Error: unexpected http response body from Cloud Agent: {response.status_code}" ) break mac = response_body['results']['hmac'] ex_mac = crypto.do_hmac(self.K, challenge) if mac == ex_mac: logger.info("Key derivation successful") else: logger.error("Key derivation failed") else: keylime_logging.log_http_response(logger, logging.ERROR, response_body) retry = config.getfloat('tenant', 'retry_interval') logger.warning( f"Key derivation not yet complete...trying again in {retry} seconds...Ctrl-C to stop" ) time.sleep(retry) continue break
def await_notifications(callback, revocation_cert_path): # keep old typo "listen_notfications" around for a few versions assert config.getboolean( "cloud_agent", "listen_notifications", fallback=False) or config.getboolean( "cloud_agent", "listen_notfications", fallback=False) try: import zmq # pylint: disable=import-outside-toplevel except ImportError as error: raise Exception( "install PyZMQ for 'listen_notifications' option") from error global cert_key if revocation_cert_path is None: raise Exception("must specify revocation_cert_path") context = zmq.Context() mysock = context.socket(zmq.SUB) mysock.setsockopt(zmq.SUBSCRIBE, b"") mysock.connect(f"tcp://{config.get('general', 'receive_revocation_ip')}:" f"{config.getint('general', 'receive_revocation_port')}") logger.info( "Waiting for revocation messages on 0mq %s:%s", config.get("general", "receive_revocation_ip"), config.getint("general", "receive_revocation_port"), ) while True: rawbody = mysock.recv() body = json.loads(rawbody) if cert_key is None: # load up the CV signing public key if revocation_cert_path is not None and os.path.exists( revocation_cert_path): logger.info("Lazy loading the revocation certificate from %s", revocation_cert_path) with open(revocation_cert_path, "rb") as f: certpem = f.read() cert_key = crypto.x509_import_pubkey(certpem) if cert_key is None: logger.warning( "Unable to check signature of revocation message: %s not available", revocation_cert_path) elif "signature" not in body or body["signature"] == "none": logger.warning("No signature on revocation message from server") elif not crypto.rsa_verify(cert_key, body["msg"].encode("utf-8"), body["signature"].encode("utf-8")): logger.error("Invalid revocation message siganture %s", body) else: message = json.loads(body["msg"]) logger.debug("Revocation signature validated for revocation: %s", message) callback(message)
def test_022_agent_quotes_identity_get(self): """Test agent's GET /v2/quotes/identity Interface""" global aik self.assertIsNotNone( aik, "Required value not set. Previous step may have failed?") nonce = tpm_abstract.TPM_Utilities.random_password(20) numretries = config.getint('tenant', 'max_retries') while numretries >= 0: test_022_agent_quotes_identity_get = RequestsClient( tenant_templ.agent_base_url, tls_enabled=False) response = test_022_agent_quotes_identity_get.get( f'/v{self.api_version}/quotes/identity?nonce={nonce}', data=None, cert="", verify=False) if response.status_code == 200: break numretries -= 1 time.sleep(config.getint('tenant', 'max_retries')) self.assertEqual(response.status_code, 200, "Non-successful Agent identity return code!") json_response = response.json() # Ensure response is well-formed self.assertIn("results", json_response, "Malformed response body!") self.assertIn("quote", json_response["results"], "Malformed response body!") self.assertIn("pubkey", json_response["results"], "Malformed response body!") # Check the quote identity self.assertTrue( tpm.check_quote(tenant_templ.agent_uuid, nonce, json_response["results"]["pubkey"], json_response["results"]["quote"], aik), "Invalid quote!")
def __run(self, cmd, expectedcode=tpm_abstract.AbstractTPM.EXIT_SUCESS, raiseOnError=True, lock=True, outputpaths=None): env = _get_cmd_env() # Backwards compat with string input (force all to be dict) if isinstance(outputpaths, str): outputpaths = [outputpaths] # Handle stubbing the TPM out fprt = tpm1.__fingerprint(cmd) if config.STUB_TPM and config.TPM_CANNED_VALUES is not None: stub = _stub_command(fprt, lock, outputpaths) if stub: return stub numtries = 0 while True: if lock: with self.tpmutilLock: retDict = cmd_exec.run( cmd=cmd, expectedcode=expectedcode, raiseOnError=False, outputpaths=outputpaths, env=env) else: retDict = cmd_exec.run( cmd=cmd, expectedcode=expectedcode, raiseOnError=False, outputpaths=outputpaths, env=env) code = retDict['code'] retout = retDict['retout'] # keep trying to communicate with TPM if there was an I/O error if code == tpm_abstract.AbstractTPM.TPM_IO_ERR: numtries += 1 maxr = config.getint('cloud_agent', 'max_retries') if numtries >= maxr: logger.error("TPM appears to be in use by another application. Keylime is incompatible with other TPM TSS applications like trousers/tpm-tools. Please uninstall or disable.") break retry = config.getfloat('cloud_agent', 'retry_interval') logger.info("Failed to call TPM %d/%d times, trying again in %f seconds..." % (numtries, maxr, retry)) time.sleep(retry) continue break # Don't bother continuing if TPM call failed and we're raising on error if code != expectedcode and raiseOnError: raise Exception("Command: %s returned %d, expected %d, output %s" % (cmd, code, expectedcode, retout)) # Metric output if lock or self.tpmutilLock.locked(): _output_metrics(fprt, cmd, retDict, outputpaths) return retDict
def worker(): context = zmq.Context(1) frontend = context.socket(zmq.SUB) frontend.bind("ipc:///tmp/keylime.verifier.ipc") frontend.setsockopt(zmq.SUBSCRIBE, b'') # Socket facing services backend = context.socket(zmq.PUB) backend.bind( "tcp://%s:%s" % (config.get('cloud_verifier', 'revocation_notifier_ip'), config.getint('cloud_verifier', 'revocation_notifier_port'))) zmq.device(zmq.FORWARDER, frontend, backend)
def mk_cacert(name=None): """ Make a CA certificate. Returns the certificate, private key and public key. """ req, pk = mk_request(config.getint('ca', 'cert_bits'), config.get('ca', 'cert_ca_name')) pkey = req.get_pubkey() cert = X509.X509() cert.set_serial_number(1) cert.set_version(2) mk_cert_valid(cert, config.getint('ca', 'cert_ca_lifetime')) if name is None: name = config.get('ca', 'cert_ca_name') issuer = X509.X509_Name() issuer.C = config.get('ca', 'cert_country') issuer.CN = name issuer.ST = config.get('ca', 'cert_state') issuer.L = config.get('ca', 'cert_locality') issuer.O = config.get('ca', 'cert_organization') issuer.OU = config.get('ca', 'cert_org_unit') cert.set_issuer(issuer) cert.set_subject(cert.get_issuer()) cert.set_pubkey(pkey) cert.add_ext(X509.new_extension('basicConstraints', 'CA:TRUE')) cert.add_ext( X509.new_extension('subjectKeyIdentifier', str(cert.get_fingerprint()))) cert.add_ext( X509.new_extension('crlDistributionPoints', 'URI:http://localhost/crl.pem')) cert.add_ext(X509.new_extension('keyUsage', 'keyCertSign, cRLSign')) cert.sign(pk, 'sha256') return cert, pk, pkey
def mk_signed_cert(cacert, ca_privkey, name, serialnum): """ Create a CA cert + server cert + server private key. """ cert_req, privkey = mk_request(config.getint("ca", "cert_bits"), common_name=name) pubkey = privkey.public_key() cert_req = cert_req.public_key(pubkey) cert_req = cert_req.serial_number(serialnum) cert_req = mk_cert_valid(cert_req) cert_req = cert_req.issuer_name(cacert.issuer) # Extensions. extensions = [ # OID 2.16.840.1.113730.1.13 is Netscape Comment. # http://oid-info.com/get/2.16.840.1.113730.1.13 x509.UnrecognizedExtension( oid=x509.ObjectIdentifier("2.16.840.1.113730.1.13"), value=b"SSL Server", ), # Subject Alternative Name. x509.SubjectAlternativeName([x509.DNSName(name)]), # CRL Distribution Points. x509.CRLDistributionPoints([ x509.DistributionPoint( full_name=[ x509.UniformResourceIdentifier("http://localhost/crl.pem"), ], relative_name=None, reasons=None, crl_issuer=None, ), ]), ] for ext in extensions: cert_req = cert_req.add_extension(ext, critical=False) cert = cert_req.sign( private_key=ca_privkey, algorithm=hashes.SHA256(), backend=default_backend(), ) return cert, privkey
def worker(tosend): context = zmq.Context() mysock = context.socket(zmq.PUB) mysock.connect("ipc:///tmp/keylime.verifier.ipc") # wait 100ms for connect to happen time.sleep(0.2) # now send it out via 0mq logger.info("Sending revocation event to listening nodes...") for i in range(config.getint('cloud_verifier', 'max_retries')): try: mysock.send_string(json.dumps(tosend)) break except Exception as e: logger.debug("Unable to publish revocation message %d times, trying again in %f seconds: %s" % ( i, config.getfloat('cloud_verifier', 'retry_interval'), e)) time.sleep(config.getfloat('cloud_verifier', 'retry_interval')) mysock.close()
def await_notifications(callback, revocation_cert_path): global cert_key if revocation_cert_path is None: raise Exception("must specify revocation_cert_path") context = zmq.Context() mysock = context.socket(zmq.SUB) mysock.setsockopt(zmq.SUBSCRIBE, b'') mysock.connect(f"tcp://{config.get('general', 'receive_revocation_ip')}:" f"{config.getint('general', 'receive_revocation_port')}") logger.info('Waiting for revocation messages on 0mq %s:%s', config.get('general', 'receive_revocation_ip'), config.getint('general', 'receive_revocation_port')) while True: rawbody = mysock.recv() body = json.loads(rawbody) if cert_key is None: # load up the CV signing public key if revocation_cert_path is not None and os.path.exists( revocation_cert_path): logger.info("Lazy loading the revocation certificate from %s", revocation_cert_path) with open(revocation_cert_path, "rb") as f: certpem = f.read() cert_key = crypto.x509_import_pubkey(certpem) if cert_key is None: logger.warning( "Unable to check signature of revocation message: %s not available", revocation_cert_path) elif 'signature' not in body or body['signature'] == 'none': logger.warning("No signature on revocation message from server") elif not crypto.rsa_verify(cert_key, body['msg'].encode('utf-8'), body['signature'].encode('utf-8')): logger.error("Invalid revocation message siganture %s", body) else: message = json.loads(body['msg']) logger.debug("Revocation signature validated for revocation: %s", message) callback(message)
def mk_cacert(name=None): """ Make a CA certificate. Returns the certificate, private key and public key. """ if name is None: name = config.get("ca", "cert_ca_name") csr = { "CN": name, "key": { "algo": "rsa", "size": config.getint('ca', 'cert_bits') }, "names": [{ "C": config.get('ca', 'cert_country'), "L": config.get('ca', 'cert_locality'), "O": config.get('ca', 'cert_organization'), "OU": config.get('ca', 'cert_org_unit'), "ST": config.get('ca', 'cert_state') }] } try: start_cfssl() body = post_cfssl('api/v1/cfssl/init_ca', csr) finally: stop_cfssl() if body['success']: privkey = serialization.load_pem_private_key( body['result']['private_key'].encode('utf-8'), password=None, backend=default_backend(), ) cert = x509.load_pem_x509_certificate( data=body['result']['certificate'].encode('utf-8'), backend=default_backend(), ) return cert, privkey, cert.public_key() raise Exception("Unable to create CA")
def worker_webhook(tosend, url): retry_interval = config.getfloat('cloud_verifier', 'retry_interval') session = requests.session() logger.info("Sending revocation event via webhook...") for i in range(config.getint('cloud_verifier', 'max_retries')): try: response = session.post(url, json=tosend) if response.status_code in [200, 202]: break logger.debug( f"Unable to publish revocation message {i} times via webhook, " f"trying again in {retry_interval} seconds. " f"Server returned status code: {response.status_code}") except requests.exceptions.RequestException as e: logger.debug( f"Unable to publish revocation message {i} times via webhook, " f"trying again in {retry_interval} seconds: {e} ") time.sleep(retry_interval)
def mk_signed_cert(cacert, ca_pk, name, serialnum): """ Create a CA cert + server cert + server private key. """ # unused, left for history. cert_req, pk = mk_request(config.getint('ca', 'cert_bits'), cn=name) cert = X509.X509() cert.set_serial_number(serialnum) cert.set_version(2) mk_cert_valid(cert) cert.add_ext(X509.new_extension('nsComment', 'SSL sever')) cert.add_ext(X509.new_extension('subjectAltName', 'DNS:%s' % name)) cert.add_ext( X509.new_extension('crlDistributionPoints', 'URI:http://localhost/crl.pem')) cert.set_subject(cert_req.get_subject()) cert.set_pubkey(cert_req.get_pubkey()) cert.set_issuer(cacert.get_issuer()) cert.sign(ca_pk, 'sha256') return cert, pk
def worker(tosend): context = zmq.Context() mysock = context.socket(zmq.PUB) mysock.connect(f"ipc://{_SOCKET_PATH}") # wait 100ms for connect to happen time.sleep(0.2) # now send it out via 0mq logger.info("Sending revocation event to listening nodes...") for i in range(config.getint('cloud_verifier', 'max_retries')): try: mysock.send_string(json.dumps(tosend)) break except Exception as e: interval = config.getfloat('cloud_verifier', 'retry_interval') exponential_backoff = config.getboolean( 'cloud_verifier', 'exponential_backoff') next_retry = retry.retry_time(exponential_backoff, interval, i, logger) logger.debug( "Unable to publish revocation message %d times, trying again in %f seconds: %s", i, next_retry, e) time.sleep(next_retry) mysock.close()
def __init__(self): """ Set up required values and TLS """ self.nonce = None self.agent_ip = None self.agent_port = config.get('cloud_agent', 'cloudagent_port') self.verifier_ip = config.get('tenant', 'cloudverifier_ip') self.verifier_port = config.get('tenant', 'cloudverifier_port') self.registrar_ip = config.get('tenant', 'registrar_ip') self.registrar_port = config.get('tenant', 'registrar_port') self.webapp_port = config.getint('webapp', 'webapp_port') if not config.REQUIRE_ROOT and self.webapp_port < 1024: self.webapp_port += 2000 self.webapp_ip = config.get('webapp', 'webapp_ip') self.my_cert, self.my_priv_key = self.get_tls_context() self.cert = (self.my_cert, self.my_priv_key) if config.getboolean('general', "enable_tls"): self.tls_enabled = True else: self.tls_enabled = False self.cert = "" logger.warning( "Warning: TLS is currently disabled, keys will be sent in the clear! This should only be used for testing.")
def do_POST(self): """This method services the POST request typically from either the Tenant or the Cloud Verifier. Only tenant and cloudverifier uri's are supported. Both requests require a nonce parameter. The Cloud verifier requires an additional mask parameter. If the uri or parameters are incorrect, a 400 response is returned. """ rest_params = config.get_restful_params(self.path) if rest_params is None: config.echo_json_response(self, 405, "Not Implemented: Use /keys/ interface") return content_length = int(self.headers.get('Content-Length', 0)) if content_length <= 0: logger.warning( 'POST returning 400 response, expected content in message. url: %s', self.path) config.echo_json_response(self, 400, "expected content in message") return post_body = self.rfile.read(content_length) json_body = json.loads(post_body) b64_encrypted_key = json_body['encrypted_key'] decrypted_key = crypto.rsa_decrypt(self.server.rsaprivatekey, base64.b64decode(b64_encrypted_key)) have_derived_key = False if rest_params["keys"] == "ukey": self.server.add_U(decrypted_key) self.server.auth_tag = json_body['auth_tag'] self.server.payload = json_body.get('payload', None) have_derived_key = self.server.attempt_decryption() elif rest_params["keys"] == "vkey": self.server.add_V(decrypted_key) have_derived_key = self.server.attempt_decryption() else: logger.warning('POST returning response. uri not supported: %s', self.path) config.echo_json_response(self, 400, "uri not supported") return logger.info('POST of %s key returning 200', ('V', 'U')[rest_params["keys"] == "ukey"]) config.echo_json_response(self, 200, "Success") # no key yet, then we're done if not have_derived_key: return # woo hoo we have a key # ok lets write out the key now secdir = secure_mount.mount( ) # confirm that storage is still securely mounted # clean out the secure dir of any previous info before we extract files if os.path.isdir("%s/unzipped" % secdir): shutil.rmtree("%s/unzipped" % secdir) # write out key file f = open(secdir + "/" + self.server.enc_keyname, 'w') f.write(base64.b64encode(self.server.K).decode()) f.close() # stow the U value for later tpm_instance.write_key_nvram(self.server.final_U) # optionally extend a hash of they key and payload into specified PCR tomeasure = self.server.K # if we have a good key, now attempt to write out the encrypted payload dec_path = "%s/%s" % (secdir, config.get('cloud_agent', "dec_payload_file")) enc_path = "%s/encrypted_payload" % config.WORK_DIR dec_payload = None enc_payload = None if self.server.payload is not None: dec_payload = crypto.decrypt(self.server.payload, bytes(self.server.K)) enc_payload = self.server.payload elif os.path.exists(enc_path): # if no payload provided, try to decrypt one from a previous run stored in encrypted_payload with open(enc_path, 'rb') as f: enc_payload = f.read() try: dec_payload = crypto.decrypt(enc_payload, self.server.K) logger.info("Decrypted previous payload in %s to %s", enc_path, dec_path) except Exception as e: logger.warning( "Unable to decrypt previous payload %s with derived key: %s", enc_path, e) os.remove(enc_path) enc_payload = None # also write out encrypted payload to be decrytped next time if enc_payload is not None: with open(enc_path, 'wb') as f: f.write(self.server.payload.encode('utf-8')) # deal with payload payload_thread = None if dec_payload is not None: tomeasure = tomeasure + dec_payload # see if payload is a zip zfio = io.BytesIO(dec_payload) if config.getboolean( 'cloud_agent', 'extract_payload_zip') and zipfile.is_zipfile(zfio): logger.info("Decrypting and unzipping payload to %s/unzipped", secdir) with zipfile.ZipFile(zfio, 'r') as f: f.extractall('%s/unzipped' % secdir) # run an included script if one has been provided initscript = config.get('cloud_agent', 'payload_script') if initscript != "": def initthread(): env = os.environ.copy() env['AGENT_UUID'] = self.server.agent_uuid proc = subprocess.Popen(["/bin/bash", initscript], env=env, shell=False, cwd='%s/unzipped' % secdir, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) while True: line = proc.stdout.readline() if line == '' and proc.poll() is not None: break if line: logger.debug("init-output: %s", line.strip()) # should be a no-op as poll already told us it's done proc.wait() if not os.path.exists("%s/unzipped/%s" % (secdir, initscript)): logger.info( "No payload script %s found in %s/unzipped", initscript, secdir) else: logger.info("Executing payload script: %s/unzipped/%s", secdir, initscript) payload_thread = threading.Thread(target=initthread) else: logger.info("Decrypting payload to %s", dec_path) with open(dec_path, 'wb') as f: f.write(dec_payload) zfio.close() # now extend a measurement of the payload and key if there was one pcr = config.getint('cloud_agent', 'measure_payload_pcr') if 0 < pcr < 24: logger.info("extending measurement of payload into PCR %s", pcr) measured = tpm_instance.hashdigest(tomeasure) tpm_instance.extendPCR(pcr, measured) if payload_thread is not None: payload_thread.start() return
def mk_signed_cert(cacert, ca_pk, name, serialnum): del cacert, serialnum csr = { "request": { "CN": name, "hosts": [ name, ], "key": { "algo": "rsa", "size": config.getint('ca', 'cert_bits') }, "names": [{ "C": config.get('ca', 'cert_country'), "L": config.get('ca', 'cert_locality'), "O": config.get('ca', 'cert_organization'), "OU": config.get('ca', 'cert_org_unit'), "ST": config.get('ca', 'cert_state') }] } } # check CRL distribution point disturl = config.get('ca', 'cert_crl_dist') if disturl == 'default': disturl = "http://%s:%s/crl.der" % (socket.getfqdn(), config.CRL_PORT) # set up config for cfssl server cfsslconfig = { "signing": { "default": { "usages": [ "client auth", "server auth", "key agreement", "key encipherment", "signing", "digital signature", "data encipherment" ], "expiry": "8760h", "crl_url": disturl, } } } secdir = secure_mount.mount() try: # need to temporarily write out the private key with no password # to tmpfs ca_pk.save_key('%s/ca-key.pem' % secdir, None) with open('%s/cfsslconfig.yml' % secdir, 'w') as f: json.dump(cfsslconfig, f) cmdline = "-config=%s/cfsslconfig.yml" % secdir priv_key = os.path.abspath("%s/ca-key.pem" % secdir) cmdline += " -ca-key %s -ca cacert.crt" % (priv_key) start_cfssl(cmdline) body = post_cfssl('api/v1/cfssl/newcert', csr) finally: stop_cfssl() os.remove('%s/ca-key.pem' % secdir) os.remove('%s/cfsslconfig.yml' % secdir) if body['success']: pk = EVP.load_key_string(body['result']['private_key'].encode('utf-8')) cert = X509.load_cert_string( body['result']['certificate'].encode("utf-8")) return cert, pk raise Exception("Unable to create cert for %s" % name)
def init_add(self, args): """ Set up required values. Command line options can overwrite these config values Arguments: args {[string]} -- agent_ip|agent_port|cv_agent_ip """ if "agent_ip" in args: self.agent_ip = args["agent_ip"] if 'agent_port' in args and args['agent_port'] is not None: self.agent_port = args['agent_port'] if 'cv_agent_ip' in args and args['cv_agent_ip'] is not None: self.cv_cloudagent_ip = args['cv_agent_ip'] else: self.cv_cloudagent_ip = self.agent_ip # Make sure all keys exist in dictionary if "file" not in args: args["file"] = None if "keyfile" not in args: args["keyfile"] = None if "payload" not in args: args["payload"] = None if "ca_dir" not in args: args["ca_dir"] = None if "incl_dir" not in args: args["incl_dir"] = None if "ca_dir_pw" not in args: args["ca_dir_pw"] = None # Set up accepted algorithms self.accept_tpm_hash_algs = config.get( 'tenant', 'accept_tpm_hash_algs').split(',') self.accept_tpm_encryption_algs = config.get( 'tenant', 'accept_tpm_encryption_algs').split(',') self.accept_tpm_signing_algs = config.get( 'tenant', 'accept_tpm_signing_algs').split(',') # Set up PCR values tpm_policy = config.get('tenant', 'tpm_policy') if "tpm_policy" in args and args["tpm_policy"] is not None: tpm_policy = args["tpm_policy"] self.tpm_policy = TPM_Utilities.readPolicy(tpm_policy) logger.info(f"TPM PCR Mask from policy is {self.tpm_policy['mask']}") vtpm_policy = config.get('tenant', 'vtpm_policy') if "vtpm_policy" in args and args["vtpm_policy"] is not None: vtpm_policy = args["vtpm_policy"] self.vtpm_policy = TPM_Utilities.readPolicy(vtpm_policy) logger.info(f"TPM PCR Mask from policy is {self.vtpm_policy['mask']}") if args.get("ima_sign_verification_keys") is not None: # Auto-enable IMA (or-bit mask) self.tpm_policy['mask'] = "0x%X" % (int(self.tpm_policy['mask'], 0) | (1 << config.IMA_PCR)) # Add all IMA file signing verification keys to a keyring ima_keyring = ima_file_signatures.ImaKeyring() for filename in args["ima_sign_verification_keys"]: pubkey = ima_file_signatures.get_pubkey_from_file(filename) if not pubkey: raise UserError("File '%s' is not a file with a key" % filename) ima_keyring.add_pubkey(pubkey) self.ima_sign_verification_keys = ima_keyring.to_string() # Read command-line path string allowlist al_data = None if "allowlist" in args and args["allowlist"] is not None: self.enforce_pcrs(list(self.tpm_policy.keys()), [config.IMA_PCR], "IMA") # Auto-enable IMA (or-bit mask) self.tpm_policy['mask'] = "0x%X" % (int(self.tpm_policy['mask'], 0) | (1 << config.IMA_PCR)) if isinstance(args["allowlist"], str): if args["allowlist"] == "default": args["allowlist"] = config.get('tenant', 'allowlist') al_data = ima.read_allowlist(args["allowlist"]) elif isinstance(args["allowlist"], list): al_data = args["allowlist"] else: raise UserError("Invalid allowlist provided") # Read command-line path string IMA exclude list excl_data = None if "ima_exclude" in args and args["ima_exclude"] is not None: if isinstance(args["ima_exclude"], str): if args["ima_exclude"] == "default": args["ima_exclude"] = config.get('tenant', 'ima_excludelist') excl_data = ima.read_excllist(args["ima_exclude"]) elif isinstance(args["ima_exclude"], list): excl_data = args["ima_exclude"] else: raise UserError("Invalid exclude list provided") # Set up IMA if TPM_Utilities.check_mask(self.tpm_policy['mask'], config.IMA_PCR) or \ TPM_Utilities.check_mask(self.vtpm_policy['mask'], config.IMA_PCR): # Process allowlists self.allowlist = ima.process_allowlists(al_data, excl_data) # Read command-line path string TPM2 event log template if "mb_refstate" in args and args["mb_refstate"] is not None: self.enforce_pcrs(list(self.tpm_policy.keys()), config.MEASUREDBOOT_PCRS, "measured boot") # Auto-enable TPM2 event log (or-bit mask) for _pcr in config.MEASUREDBOOT_PCRS: self.tpm_policy['mask'] = "0x%X" % (int( self.tpm_policy['mask'], 0) | (1 << _pcr)) logger.info( f"TPM PCR Mask automatically modified is {self.tpm_policy['mask']} to include IMA/Event log PCRs" ) if isinstance(args["mb_refstate"], str): if args["mb_refstate"] == "default": args["mb_refstate"] = config.get('tenant', 'mb_refstate') al_data = 'test' elif isinstance(args["mb_refstate"], list): al_data = args["mb_refstate"] else: raise UserError( "Invalid measured boot reference state (intended state) provided" ) # if none if (args["file"] is None and args["keyfile"] is None and args["ca_dir"] is None): raise UserError( "You must specify one of -k, -f, or --cert to specify the key/contents to be securely delivered to the agent" ) if args["keyfile"] is not None: if args["file"] is not None or args["ca_dir"] is not None: raise UserError( "You must specify one of -k, -f, or --cert to specify the key/contents to be securely delivered to the agent" ) # read the keys in if isinstance(args["keyfile"], dict) and "data" in args["keyfile"]: if isinstance(args["keyfile"]["data"], list) and len( args["keyfile"]["data"]) == 1: keyfile = args["keyfile"]["data"][0] if keyfile is None: raise UserError("Invalid key file contents") f = io.StringIO(keyfile) else: raise UserError("Invalid key file provided") else: f = open(args["keyfile"], 'r') self.K = base64.b64decode(f.readline()) self.U = base64.b64decode(f.readline()) self.V = base64.b64decode(f.readline()) f.close() # read the payload in (opt.) if isinstance(args["payload"], dict) and "data" in args["payload"]: if isinstance(args["payload"]["data"], list) and len(args["payload"]["data"]) > 0: self.payload = args["payload"]["data"][0] else: if args["payload"] is not None: f = open(args["payload"], 'r') self.payload = f.read() f.close() if args["file"] is not None: if args["keyfile"] is not None or args["ca_dir"] is not None: raise UserError( "You must specify one of -k, -f, or --cert to specify the key/contents to be securely delivered to the agent" ) if isinstance(args["file"], dict) and "data" in args["file"]: if isinstance(args["file"]["data"], list) and len(args["file"]["data"]) > 0: contents = args["file"]["data"][0] if contents is None: raise UserError("Invalid file payload contents") else: raise UserError("Invalid file payload provided") else: with open(args["file"], 'r') as f: contents = f.read() ret = user_data_encrypt.encrypt(contents) self.K = ret['k'] self.U = ret['u'] self.V = ret['v'] self.payload = ret['ciphertext'] if args["ca_dir"] is None and args["incl_dir"] is not None: raise UserError( "--include option is only valid when used with --cert") if args["ca_dir"] is not None: if args["file"] is not None or args["keyfile"] is not None: raise UserError( "You must specify one of -k, -f, or --cert to specify the key/contents to be securely delivered to the agent" ) if args["ca_dir"] == 'default': args["ca_dir"] = config.CA_WORK_DIR if "ca_dir_pw" in args and args["ca_dir_pw"] is not None: ca_util.setpassword(args["ca_dir_pw"]) if not os.path.exists(args["ca_dir"]) or not os.path.exists( "%s/cacert.crt" % args["ca_dir"]): logger.warning(" CA directory does not exist. Creating...") ca_util.cmd_init(args["ca_dir"]) if not os.path.exists("%s/%s-private.pem" % (args["ca_dir"], self.agent_uuid)): ca_util.cmd_mkcert(args["ca_dir"], self.agent_uuid) cert_pkg, serial, subject = ca_util.cmd_certpkg( args["ca_dir"], self.agent_uuid) # support revocation if not os.path.exists( "%s/RevocationNotifier-private.pem" % args["ca_dir"]): ca_util.cmd_mkcert(args["ca_dir"], "RevocationNotifier") rev_package, _, _ = ca_util.cmd_certpkg(args["ca_dir"], "RevocationNotifier") # extract public and private keys from package sf = io.BytesIO(rev_package) with zipfile.ZipFile(sf) as zf: privkey = zf.read("RevocationNotifier-private.pem") cert = zf.read("RevocationNotifier-cert.crt") # put the cert of the revoker into the cert package sf = io.BytesIO(cert_pkg) with zipfile.ZipFile(sf, 'a', compression=zipfile.ZIP_STORED) as zf: zf.writestr('RevocationNotifier-cert.crt', cert) # add additional files to zip if args["incl_dir"] is not None: if isinstance(args["incl_dir"], dict) and "data" in args[ "incl_dir"] and "name" in args["incl_dir"]: if isinstance(args["incl_dir"]["data"], list) and isinstance( args["incl_dir"]["name"], list): if len(args["incl_dir"]["data"]) != len( args["incl_dir"]["name"]): raise UserError("Invalid incl_dir provided") for i in range(len(args["incl_dir"]["data"])): zf.writestr( os.path.basename( args["incl_dir"]["name"][i]), args["incl_dir"]["data"][i]) else: if os.path.exists(args["incl_dir"]): files = next(os.walk(args["incl_dir"]))[2] for filename in files: with open( "%s/%s" % (args["incl_dir"], filename), 'rb') as f: zf.writestr(os.path.basename(f.name), f.read()) else: logger.warning( f'Specified include directory {args["incl_dir"]} does not exist. Skipping...' ) cert_pkg = sf.getvalue() # put the private key into the data to be send to the CV self.revocation_key = privkey # encrypt up the cert package ret = user_data_encrypt.encrypt(cert_pkg) self.K = ret['k'] self.U = ret['u'] self.V = ret['v'] self.metadata = {'cert_serial': serial, 'subject': subject} self.payload = ret['ciphertext'] if self.payload is not None and len(self.payload) > config.getint( 'tenant', 'max_payload_size'): raise UserError("Payload size %s exceeds max size %d" % (len( self.payload), config.getint('tenant', 'max_payload_size')))
def main(): for ML in [config.MEASUREDBOOT_ML, config.IMA_ML]: if not os.access(ML, os.F_OK): logger.warning( 'Measurement list path %s not accessible by agent. Any attempt to instruct it to access this path - via "keylime_tenant" CLI - will result in agent process dying', ML, ) ima_log_file = None if os.path.exists(config.IMA_ML): ima_log_file = open(config.IMA_ML, "r", encoding="utf-8") # pylint: disable=consider-using-with tpm_log_file_data = None if os.path.exists(config.MEASUREDBOOT_ML): with open(config.MEASUREDBOOT_ML, "rb") as tpm_log_file: tpm_log_file_data = base64.b64encode(tpm_log_file.read()) if config.get("cloud_agent", "agent_uuid") == "dmidecode": if os.getuid() != 0: raise RuntimeError( "agent_uuid is configured to use dmidecode, but current process is not running as root." ) cmd = ["which", "dmidecode"] ret = cmd_exec.run(cmd, raiseOnError=False) if ret["code"] != 0: raise RuntimeError( "agent_uuid is configured to use dmidecode, but it's is not found on the system." ) # initialize the tmpfs partition to store keys if it isn't already available secdir = secure_mount.mount() # Now that operations requiring root privileges are done, drop privileges # if 'run_as' is available in the configuration. if os.getuid() == 0: run_as = config.get("cloud_agent", "run_as", fallback="") if run_as != "": user_utils.chown(secdir, run_as) user_utils.change_uidgid(run_as) logger.info("Dropped privileges to %s", run_as) else: logger.warning( "Cannot drop privileges since 'run_as' is empty or missing in keylime.conf agent section." ) # Instanitate TPM class instance_tpm = tpm() # get params for initialization registrar_ip = config.get("cloud_agent", "registrar_ip") registrar_port = config.get("cloud_agent", "registrar_port") # get params for the verifier to contact the agent contact_ip = os.getenv("KEYLIME_AGENT_CONTACT_IP", None) if contact_ip is None and config.has_option("cloud_agent", "agent_contact_ip"): contact_ip = config.get("cloud_agent", "agent_contact_ip") contact_port = os.getenv("KEYLIME_AGENT_CONTACT_PORT", None) if contact_port is None and config.has_option("cloud_agent", "agent_contact_port"): contact_port = config.get("cloud_agent", "agent_contact_port", fallback="invalid") # change dir to working dir fs_util.ch_dir(config.WORK_DIR) # set a conservative general umask os.umask(0o077) # initialize tpm (ekcert, ek_tpm, aik_tpm) = instance_tpm.tpm_init( self_activate=False, config_pw=config.get("cloud_agent", "tpm_ownerpassword") ) # this tells initialize not to self activate the AIK # Warn if kernel version is <5.10 and another algorithm than SHA1 is used, # because otherwise IMA will not work kernel_version = tuple(platform.release().split("-")[0].split(".")) if tuple(map(int, kernel_version)) < ( 5, 10, 0) and instance_tpm.defaults["hash"] != algorithms.Hash.SHA1: logger.warning( "IMA attestation only works on kernel versions <5.10 with SHA1 as hash algorithm. " 'Even if ascii_runtime_measurements shows "%s" as the ' "algorithm, it might be just padding zeros", (instance_tpm.defaults["hash"]), ) if ekcert is None and instance_tpm.is_emulator(): ekcert = "emulator" # now we need the UUID try: agent_uuid = config.get("cloud_agent", "agent_uuid") except configparser.NoOptionError: agent_uuid = None if agent_uuid == "hash_ek": ek_pubkey = pubkey_from_tpm2b_public(base64.b64decode(ek_tpm)) ek_pubkey_pem = ek_pubkey.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo) agent_uuid = hashlib.sha256(ek_pubkey_pem).hexdigest() elif agent_uuid == "generate" or agent_uuid is None: agent_uuid = str(uuid.uuid4()) elif agent_uuid == "dmidecode": cmd = ["dmidecode", "-s", "system-uuid"] ret = cmd_exec.run(cmd) sys_uuid = ret["retout"][0].decode("utf-8") agent_uuid = sys_uuid.strip() try: uuid.UUID(agent_uuid) except ValueError as e: raise RuntimeError( # pylint: disable=raise-missing-from f"The UUID returned from dmidecode is invalid: {str(e)}") elif agent_uuid == "hostname": agent_uuid = socket.getfqdn() elif agent_uuid == "environment": agent_uuid = os.getenv("KEYLIME_AGENT_UUID", None) if agent_uuid is None: raise RuntimeError( "Env variable KEYLIME_AGENT_UUID is empty, but agent_uuid is set to 'environment'" ) elif not validators.valid_uuid(agent_uuid): raise RuntimeError("The UUID is not valid") if not validators.valid_agent_id(agent_uuid): raise RuntimeError( "The agent ID set via agent uuid parameter use invalid characters") logger.info("Agent UUID: %s", agent_uuid) serveraddr = (config.get("cloud_agent", "cloudagent_ip"), config.getint("cloud_agent", "cloudagent_port")) keylime_ca = config.get("cloud_agent", "keylime_ca") if keylime_ca == "default": keylime_ca = os.path.join(config.WORK_DIR, "cv_ca", "cacert.crt") server = CloudAgentHTTPServer(serveraddr, Handler, agent_uuid, contact_ip, ima_log_file, tpm_log_file_data) if server.mtls_cert_enabled: context = web_util.generate_mtls_context(server.mtls_cert_path, server.rsakey_path, keylime_ca, logger=logger) server.socket = context.wrap_socket(server.socket, server_side=True) else: if (not config.getboolean( "cloud_agent", "enable_insecure_payload", fallback=False) and config.get("cloud_agent", "payload_script") != ""): raise RuntimeError( "agent mTLS is disabled, while a tenant can instruct the agent to execute code on the node. " 'In order to allow the running of the agent, "enable_insecure_payload" has to be set to "True"' ) serverthread = threading.Thread(target=server.serve_forever, daemon=True) # register it and get back a blob mtls_cert = "disabled" if server.mtls_cert: mtls_cert = server.mtls_cert.public_bytes(serialization.Encoding.PEM) keyblob = registrar_client.doRegisterAgent(registrar_ip, registrar_port, agent_uuid, ek_tpm, ekcert, aik_tpm, mtls_cert, contact_ip, contact_port) if keyblob is None: instance_tpm.flush_keys() raise Exception("Registration failed") # get the ephemeral registrar key key = instance_tpm.activate_identity(keyblob) if key is None: instance_tpm.flush_keys() raise Exception("Activation failed") # tell the registrar server we know the key retval = registrar_client.doActivateAgent(registrar_ip, registrar_port, agent_uuid, key) if not retval: instance_tpm.flush_keys() raise Exception("Registration failed on activate") # Start revocation listener in a new process to not interfere with tornado revocation_process = multiprocessing.Process(target=revocation_listener, daemon=True) revocation_process.start() logger.info( "Starting Cloud Agent on %s:%s with API version %s. Use <Ctrl-C> to stop", serveraddr[0], serveraddr[1], keylime_api_version.current_version(), ) serverthread.start() def shutdown_handler(*_): logger.info("TERM Signal received, shutting down...") logger.debug("Stopping revocation notifier...") revocation_process.terminate() logger.debug("Shutting down HTTP server...") server.shutdown() server.server_close() serverthread.join() logger.debug("HTTP server stopped...") revocation_process.join() logger.debug("Revocation notifier stopped...") secure_mount.umount() logger.debug("Umounting directories...") instance_tpm.flush_keys() logger.debug("Flushed keys successfully") sys.exit(0) signal.signal(signal.SIGTERM, shutdown_handler) signal.signal(signal.SIGQUIT, shutdown_handler) signal.signal(signal.SIGINT, shutdown_handler) # Keep the main thread alive by waiting for the server thread serverthread.join()
def do_POST(self): """This method services the POST request typically from either the Tenant or the Cloud Verifier. Only tenant and cloudverifier uri's are supported. Both requests require a nonce parameter. The Cloud verifier requires an additional mask parameter. If the uri or parameters are incorrect, a 400 response is returned. """ rest_params = web_util.get_restful_params(self.path) if rest_params is None: web_util.echo_json_response( self, 405, "Not Implemented: Use /keys/ or /notifications/ interface") return if not rest_params["api_version"]: web_util.echo_json_response(self, 400, "API Version not supported") return content_length = int(self.headers.get("Content-Length", 0)) if content_length <= 0: logger.warning( "POST returning 400 response, expected content in message. url: %s", self.path) web_util.echo_json_response(self, 400, "expected content in message") return post_body = self.rfile.read(content_length) try: json_body = json.loads(post_body) except Exception as e: logger.warning( "POST returning 400 response, could not parse body data: %s", e) web_util.echo_json_response(self, 400, "content is invalid") return if "notifications" in rest_params: if rest_params["notifications"] == "revocation": revocation_notifier.process_revocation( json_body, perform_actions, cert_path=self.server.revocation_cert_path) web_util.echo_json_response(self, 200, "Success") else: web_util.echo_json_response( self, 400, "Only /notifications/revocation is supported") return if rest_params.get("keys", None) not in ["ukey", "vkey"]: web_util.echo_json_response( self, 400, "Only /keys/ukey or /keys/vkey are supported") return try: b64_encrypted_key = json_body["encrypted_key"] decrypted_key = crypto.rsa_decrypt( self.server.rsaprivatekey, base64.b64decode(b64_encrypted_key)) except (ValueError, KeyError, TypeError) as e: logger.warning( "POST returning 400 response, could not parse body data: %s", e) web_util.echo_json_response(self, 400, "content is invalid") return have_derived_key = False if rest_params["keys"] == "ukey": if "auth_tag" not in json_body: logger.warning( "POST returning 400 response, U key provided without an auth_tag" ) web_util.echo_json_response(self, 400, "auth_tag is missing") return self.server.add_U(decrypted_key) self.server.auth_tag = json_body["auth_tag"] self.server.payload = json_body.get("payload", None) have_derived_key = self.server.attempt_decryption() elif rest_params["keys"] == "vkey": self.server.add_V(decrypted_key) have_derived_key = self.server.attempt_decryption() else: logger.warning("POST returning response. uri not supported: %s", self.path) web_util.echo_json_response(self, 400, "uri not supported") return logger.info("POST of %s key returning 200", ("V", "U")[rest_params["keys"] == "ukey"]) web_util.echo_json_response(self, 200, "Success") # no key yet, then we're done if not have_derived_key: return # woo hoo we have a key # ok lets write out the key now secdir = secure_mount.mount( ) # confirm that storage is still securely mounted # clean out the secure dir of any previous info before we extract files if os.path.isdir(os.path.join(secdir, "unzipped")): shutil.rmtree(os.path.join(secdir, "unzipped")) # write out key file with open(os.path.join(secdir, self.server.enc_keyname), "w", encoding="utf-8") as f: f.write(base64.b64encode(self.server.K).decode()) # stow the U value for later tpm_instance.write_key_nvram(self.server.final_U) # optionally extend a hash of they key and payload into specified PCR tomeasure = self.server.K # if we have a good key, now attempt to write out the encrypted payload dec_path = os.path.join(secdir, config.get("cloud_agent", "dec_payload_file")) enc_path = os.path.join(config.WORK_DIR, "encrypted_payload") dec_payload = None enc_payload = None if self.server.payload is not None: if not self.server.mtls_cert_enabled and not config.getboolean( "cloud_agent", "enable_insecure_payload", fallback=False): logger.warning( 'agent mTLS is disabled, and unless "enable_insecure_payload" is set to "True", payloads cannot be deployed' ) enc_payload = None else: dec_payload = crypto.decrypt(self.server.payload, bytes(self.server.K)) enc_payload = self.server.payload elif os.path.exists(enc_path): # if no payload provided, try to decrypt one from a previous run stored in encrypted_payload with open(enc_path, "rb") as f: enc_payload = f.read() try: dec_payload = crypto.decrypt(enc_payload, self.server.K) logger.info("Decrypted previous payload in %s to %s", enc_path, dec_path) except Exception as e: logger.warning( "Unable to decrypt previous payload %s with derived key: %s", enc_path, e) os.remove(enc_path) enc_payload = None # also write out encrypted payload to be decrytped next time if enc_payload is not None: with open(enc_path, "wb") as f: f.write(self.server.payload.encode("utf-8")) # deal with payload payload_thread = None if dec_payload is not None: tomeasure = tomeasure + dec_payload # see if payload is a zip zfio = io.BytesIO(dec_payload) if config.getboolean( "cloud_agent", "extract_payload_zip") and zipfile.is_zipfile(zfio): logger.info("Decrypting and unzipping payload to %s/unzipped", secdir) with zipfile.ZipFile(zfio, "r") as f: f.extractall(os.path.join(secdir, "unzipped")) # run an included script if one has been provided initscript = config.get("cloud_agent", "payload_script") if initscript != "": def initthread(): env = os.environ.copy() env["AGENT_UUID"] = self.server.agent_uuid with subprocess.Popen( ["/bin/bash", initscript], env=env, shell=False, cwd=os.path.join(secdir, "unzipped"), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) as proc: for line in iter(proc.stdout.readline, b""): logger.debug("init-output: %s", line.strip()) # should be a no-op as poll already told us it's done proc.wait() if not os.path.exists( os.path.join(secdir, "unzipped", initscript)): logger.info( "No payload script %s found in %s/unzipped", initscript, secdir) else: logger.info("Executing payload script: %s/unzipped/%s", secdir, initscript) payload_thread = threading.Thread(target=initthread, daemon=True) else: logger.info("Decrypting payload to %s", dec_path) with open(dec_path, "wb") as f: f.write(dec_payload) zfio.close() # now extend a measurement of the payload and key if there was one pcr = config.getint("cloud_agent", "measure_payload_pcr") if 0 < pcr < 24: logger.info("extending measurement of payload into PCR %s", pcr) measured = tpm_instance.hashdigest(tomeasure) tpm_instance.extendPCR(pcr, measured) if payload_thread is not None: payload_thread.start() return
def main(argv=sys.argv): registrar_common.start( config.get('registrar', 'provider_registrar_ip'), config.getint('registrar', 'provider_registrar_tls_port'), config.getint('registrar', 'provider_registrar_port'))
def main(): for ML in [config.MEASUREDBOOT_ML, config.IMA_ML]: if not os.access(ML, os.F_OK): logger.warning( "Measurement list path %s not accessible by agent. Any attempt to instruct it to access this path - via \"keylime_tenant\" CLI - will result in agent process dying", ML) ima_log_file = None if os.path.exists(config.IMA_ML): ima_log_file = open(config.IMA_ML, 'r', encoding="utf-8") tpm_log_file_data = None if os.path.exists(config.MEASUREDBOOT_ML): with open(config.MEASUREDBOOT_ML, 'rb') as tpm_log_file: tpm_log_file_data = base64.b64encode(tpm_log_file.read()) if config.get('cloud_agent', 'agent_uuid') == 'dmidecode': if os.getuid() != 0: raise RuntimeError('agent_uuid is configured to use dmidecode, ' 'but current process is not running as root.') cmd = ['which', 'dmidecode'] ret = cmd_exec.run(cmd, raiseOnError=False) if ret['code'] != 0: raise RuntimeError('agent_uuid is configured to use dmidecode, ' 'but it\'s is not found on the system.') # initialize the tmpfs partition to store keys if it isn't already available secdir = secure_mount.mount() # Now that operations requiring root privileges are done, drop privileges # if 'run_as' is available in the configuration. if os.getuid() == 0: run_as = config.get('cloud_agent', 'run_as', fallback='') if run_as != '': user_utils.chown(secdir, run_as) user_utils.change_uidgid(run_as) logger.info(f"Dropped privileges to {run_as}") else: logger.warning( "Cannot drop privileges since 'run_as' is empty or missing in keylime.conf agent section." ) # Instanitate TPM class instance_tpm = tpm() # get params for initialization registrar_ip = config.get('cloud_agent', 'registrar_ip') registrar_port = config.get('cloud_agent', 'registrar_port') # get params for the verifier to contact the agent contact_ip = os.getenv("KEYLIME_AGENT_CONTACT_IP", None) if contact_ip is None and config.has_option('cloud_agent', 'agent_contact_ip'): contact_ip = config.get('cloud_agent', 'agent_contact_ip') contact_port = os.getenv("KEYLIME_AGENT_CONTACT_PORT", None) if contact_port is None and config.has_option('cloud_agent', 'agent_contact_port'): contact_port = config.get('cloud_agent', 'agent_contact_port', fallback="invalid") # change dir to working dir fs_util.ch_dir(config.WORK_DIR) # set a conservative general umask os.umask(0o077) # initialize tpm (ekcert, ek_tpm, aik_tpm) = instance_tpm.tpm_init( self_activate=False, config_pw=config.get('cloud_agent', 'tpm_ownerpassword') ) # this tells initialize not to self activate the AIK virtual_agent = instance_tpm.is_vtpm() # Warn if kernel version is <5.10 and another algorithm than SHA1 is used, # because otherwise IMA will not work kernel_version = tuple(platform.release().split("-")[0].split(".")) if tuple(map(int, kernel_version)) < ( 5, 10, 0) and instance_tpm.defaults["hash"] != algorithms.Hash.SHA1: logger.warning( "IMA attestation only works on kernel versions <5.10 with SHA1 as hash algorithm. " "Even if ascii_runtime_measurements shows \"%s\" as the " "algorithm, it might be just padding zeros", (instance_tpm.defaults["hash"])) if ekcert is None: if virtual_agent: ekcert = 'virtual' elif instance_tpm.is_emulator(): ekcert = 'emulator' # now we need the UUID try: agent_uuid = config.get('cloud_agent', 'agent_uuid') except configparser.NoOptionError: agent_uuid = None if agent_uuid == 'hash_ek': ek_pubkey = pubkey_from_tpm2b_public(base64.b64decode(ek_tpm)) ek_pubkey_pem = ek_pubkey.public_bytes( encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo) agent_uuid = hashlib.sha256(ek_pubkey_pem).hexdigest() elif agent_uuid == 'generate' or agent_uuid is None: agent_uuid = str(uuid.uuid4()) elif agent_uuid == 'dmidecode': cmd = ['dmidecode', '-s', 'system-uuid'] ret = cmd_exec.run(cmd) sys_uuid = ret['retout'][0].decode('utf-8') agent_uuid = sys_uuid.strip() try: uuid.UUID(agent_uuid) except ValueError as e: raise RuntimeError( "The UUID returned from dmidecode is invalid: %s" % e) # pylint: disable=raise-missing-from elif agent_uuid == 'hostname': agent_uuid = socket.getfqdn() elif agent_uuid == 'environment': agent_uuid = os.getenv("KEYLIME_AGENT_UUID", None) if agent_uuid is None: raise RuntimeError( "Env variable KEYLIME_AGENT_UUID is empty, but agent_uuid is set to 'environment'" ) elif not validators.valid_uuid(agent_uuid): raise RuntimeError("The UUID is not valid") if not validators.valid_agent_id(agent_uuid): raise RuntimeError( "The agent ID set via agent uuid parameter use invalid characters") if config.STUB_VTPM and config.TPM_CANNED_VALUES is not None: # Use canned values for stubbing jsonIn = config.TPM_CANNED_VALUES if "add_vtpm_to_group" in jsonIn: # The value we're looking for has been canned! agent_uuid = jsonIn['add_vtpm_to_group']['retout'] else: # Our command hasn't been canned! raise Exception("Command %s not found in canned json!" % ("add_vtpm_to_group")) logger.info("Agent UUID: %s", agent_uuid) serveraddr = (config.get('cloud_agent', 'cloudagent_ip'), config.getint('cloud_agent', 'cloudagent_port')) keylime_ca = config.get('cloud_agent', 'keylime_ca') if keylime_ca == "default": keylime_ca = os.path.join(config.WORK_DIR, 'cv_ca', 'cacert.crt') server = CloudAgentHTTPServer(serveraddr, Handler, agent_uuid, contact_ip, ima_log_file, tpm_log_file_data) context = web_util.generate_mtls_context(server.mtls_cert_path, server.rsakey_path, keylime_ca, logger=logger) server.socket = context.wrap_socket(server.socket, server_side=True) serverthread = threading.Thread(target=server.serve_forever, daemon=True) # register it and get back a blob mtls_cert = server.mtls_cert.public_bytes(serialization.Encoding.PEM) keyblob = registrar_client.doRegisterAgent(registrar_ip, registrar_port, agent_uuid, ek_tpm, ekcert, aik_tpm, mtls_cert, contact_ip, contact_port) if keyblob is None: instance_tpm.flush_keys() raise Exception("Registration failed") # get the ephemeral registrar key key = instance_tpm.activate_identity(keyblob) if key is None: instance_tpm.flush_keys() raise Exception("Activation failed") # tell the registrar server we know the key retval = registrar_client.doActivateAgent(registrar_ip, registrar_port, agent_uuid, key) if not retval: instance_tpm.flush_keys() raise Exception("Registration failed on activate") # Start revocation listener in a new process to not interfere with tornado revocation_process = multiprocessing.Process(target=revocation_listener, daemon=True) revocation_process.start() logger.info( "Starting Cloud Agent on %s:%s with API version %s. Use <Ctrl-C> to stop", serveraddr[0], serveraddr[1], keylime_api_version.current_version()) serverthread.start() def shutdown_handler(*_): logger.info("TERM Signal received, shutting down...") logger.debug("Stopping revocation notifier...") revocation_process.terminate() logger.debug("Shutting down HTTP server...") server.shutdown() server.server_close() serverthread.join() logger.debug("HTTP server stopped...") revocation_process.join() logger.debug("Revocation notifier stopped...") secure_mount.umount() logger.debug("Umounting directories...") instance_tpm.flush_keys() logger.debug("Flushed keys successfully") sys.exit(0) signal.signal(signal.SIGTERM, shutdown_handler) signal.signal(signal.SIGQUIT, shutdown_handler) signal.signal(signal.SIGINT, shutdown_handler) # Keep the main thread alive by waiting for the server thread serverthread.join()
def mk_signed_cert(cacert, ca_pk, name, serialnum): del serialnum csr = { "request": { "CN": name, "hosts": [ name, ], "key": { "algo": "rsa", "size": config.getint('ca', 'cert_bits') }, "names": [{ "C": config.get('ca', 'cert_country'), "L": config.get('ca', 'cert_locality'), "O": config.get('ca', 'cert_organization'), "OU": config.get('ca', 'cert_org_unit'), "ST": config.get('ca', 'cert_state') }] } } # check CRL distribution point disturl = config.get('ca', 'cert_crl_dist') if disturl == 'default': disturl = f"http://{socket.getfqdn()}:{config.CRL_PORT}/crl.der" # set up config for cfssl server cfsslconfig = { "signing": { "default": { "usages": [ "client auth", "server auth", "key agreement", "key encipherment", "signing", "digital signature", "data encipherment" ], "expiry": "8760h", "crl_url": disturl, } } } secdir = secure_mount.mount() try: # need to temporarily write out the private key with no password # to tmpfs. with os.fdopen( os.open(f"{secdir}/ca-key.pem", os.O_WRONLY | os.O_CREAT, 0o600), 'wb') as f: f.write( ca_pk.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption(), )) with open(os.path.join(secdir, 'cfsslconfig.yml'), 'w', encoding="utf-8") as f: json.dump(cfsslconfig, f) with open(f"{secdir}/cacert.crt", 'wb') as f: f.write(cacert.public_bytes(serialization.Encoding.PEM)) cmdline = "-config=%s/cfsslconfig.yml" % secdir privkey_path = os.path.abspath(f"{secdir}/ca-key.pem") cacert_path = os.path.abspath(f"{secdir}/cacert.crt") cmdline += f" -ca-key {privkey_path} -ca {cacert_path}" start_cfssl(cmdline) body = post_cfssl('api/v1/cfssl/newcert', csr) finally: stop_cfssl() os.remove('%s/ca-key.pem' % secdir) os.remove('%s/cfsslconfig.yml' % secdir) os.remove('%s/cacert.crt' % secdir) if body['success']: pk = serialization.load_pem_private_key( body['result']['private_key'].encode('utf-8'), password=None, backend=default_backend(), ) cert = x509.load_pem_x509_certificate( data=body['result']['certificate'].encode('utf-8'), backend=default_backend(), ) return cert, pk raise Exception("Unable to create cert for %s" % name)
async def process_agent(agent, new_operational_state): # Convert to dict if the agent arg is a db object if not isinstance(agent, dict): agent = _from_db_obj(agent) session = get_session() try: main_agent_operational_state = agent['operational_state'] try: stored_agent = session.query(VerfierMain).filter_by( agent_id=str(agent['agent_id'])).first() except SQLAlchemyError as e: logger.error('SQLAlchemy Error: %s', e) # if the user did terminated this agent if stored_agent.operational_state == states.TERMINATED: logger.warning("Agent %s terminated by user.", agent['agent_id']) if agent['pending_event'] is not None: tornado.ioloop.IOLoop.current().remove_timeout( agent['pending_event']) session.query(VerfierMain).filter_by( agent_id=agent['agent_id']).delete() session.commit() return # if the user tells us to stop polling because the tenant quote check failed if stored_agent.operational_state == states.TENANT_FAILED: logger.warning("Agent %s has failed tenant quote. Stopping polling", agent['agent_id']) if agent['pending_event'] is not None: tornado.ioloop.IOLoop.current().remove_timeout( agent['pending_event']) return # If failed during processing, log regardless and drop it on the floor # The administration application (tenant) can GET the status and act accordingly (delete/retry/etc). if new_operational_state in (states.FAILED, states.INVALID_QUOTE): agent['operational_state'] = new_operational_state # issue notification for invalid quotes if new_operational_state == states.INVALID_QUOTE: cloud_verifier_common.notify_error(agent) if agent['pending_event'] is not None: tornado.ioloop.IOLoop.current().remove_timeout( agent['pending_event']) for key in exclude_db: if key in agent: del agent[key] session.query(VerfierMain).filter_by( agent_id=agent['agent_id']).update(agent) session.commit() logger.warning("Agent %s failed, stopping polling", agent['agent_id']) return # propagate all state, but remove none DB keys first (using exclude_db) try: agent_db = dict(agent) for key in exclude_db: if key in agent_db: del agent_db[key] session.query(VerfierMain).filter_by( agent_id=agent_db['agent_id']).update(agent_db) session.commit() except SQLAlchemyError as e: logger.error('SQLAlchemy Error: %s', e) # if new, get a quote if (main_agent_operational_state == states.START and new_operational_state == states.GET_QUOTE): agent['num_retries'] = 0 agent['operational_state'] = states.GET_QUOTE await invoke_get_quote(agent, True) return if (main_agent_operational_state == states.GET_QUOTE and new_operational_state == states.PROVIDE_V): agent['num_retries'] = 0 agent['operational_state'] = states.PROVIDE_V await invoke_provide_v(agent) return if (main_agent_operational_state in (states.PROVIDE_V, states.GET_QUOTE) and new_operational_state == states.GET_QUOTE): agent['num_retries'] = 0 interval = config.getfloat('cloud_verifier', 'quote_interval') agent['operational_state'] = states.GET_QUOTE if interval == 0: await invoke_get_quote(agent, False) else: logger.debug("Setting up callback to check again in %f seconds", interval) # set up a call back to check again cb = functools.partial(invoke_get_quote, agent, False) pending = tornado.ioloop.IOLoop.current().call_later(interval, cb) agent['pending_event'] = pending return maxr = config.getint('cloud_verifier', 'max_retries') retry = config.getfloat('cloud_verifier', 'retry_interval') if (main_agent_operational_state == states.GET_QUOTE and new_operational_state == states.GET_QUOTE_RETRY): if agent['num_retries'] >= maxr: logger.warning("Agent %s was not reachable for quote in %d tries, setting state to FAILED", agent['agent_id'], maxr) if agent['first_verified']: # only notify on previously good agents cloud_verifier_common.notify_error( agent, msgtype='comm_error') else: logger.debug("Communication error for new agent. No notification will be sent") await process_agent(agent, states.FAILED) else: agent['operational_state'] = states.GET_QUOTE cb = functools.partial(invoke_get_quote, agent, True) agent['num_retries'] += 1 logger.info("Connection to %s refused after %d/%d tries, trying again in %f seconds", agent['ip'], agent['num_retries'], maxr, retry) tornado.ioloop.IOLoop.current().call_later(retry, cb) return if (main_agent_operational_state == states.PROVIDE_V and new_operational_state == states.PROVIDE_V_RETRY): if agent['num_retries'] >= maxr: logger.warning("Agent %s was not reachable to provide v in %d tries, setting state to FAILED", agent['agent_id'], maxr) cloud_verifier_common.notify_error( agent, msgtype='comm_error') await process_agent(agent, states.FAILED) else: agent['operational_state'] = states.PROVIDE_V cb = functools.partial(invoke_provide_v, agent) agent['num_retries'] += 1 logger.info("Connection to %s refused after %d/%d tries, trying again in %f seconds", agent['ip'], agent['num_retries'], maxr, retry) tornado.ioloop.IOLoop.current().call_later(retry, cb) return raise Exception("nothing should ever fall out of this!") except Exception as e: logger.error("Polling thread error: %s", e) logger.exception(e)
def do_quote(self): """ Perform TPM quote by GET towards Agent Raises: UserError: Connection handler """ self.nonce = TPM_Utilities.random_password(20) numtries = 0 response = None # Note: We need a specific retry handler (perhaps in common), no point having localised unless we have too. while True: try: params = '/quotes/identity?nonce=%s' % (self.nonce) cloudagent_base_url = f'{self.agent_ip}:{self.agent_port}' do_quote = RequestsClient(cloudagent_base_url, tls_enabled=False) response = do_quote.get( params, cert=self.cert ) response_body = response.json() except Exception as e: if response.status_code in (503, 504): numtries += 1 maxr = config.getint('tenant', 'max_retries') if numtries >= maxr: logger.error("Tenant cannot establish connection to agent on %s with port %s", self.agent_ip, self.agent_port) sys.exit() retry = config.getfloat('tenant', 'retry_interval') logger.info("Tenant connection to agent at %s refused %s/%s times, trying again in %s seconds...", self.agent_ip, numtries, maxr, retry) time.sleep(retry) continue raise e break try: if response is not None and response.status_code != 200: raise UserError( "Status command response: %d Unexpected response from Cloud Agent." % response.status) if "results" not in response_body: raise UserError( "Error: unexpected http response body from Cloud Agent: %s" % str(response.status)) quote = response_body["results"]["quote"] logger.debug("Agent_quote received quote: %s", quote) public_key = response_body["results"]["pubkey"] logger.debug("Agent_quote received public key: %s", public_key) # Ensure hash_alg is in accept_tpm_hash_algs list hash_alg = response_body["results"]["hash_alg"] logger.debug("Agent_quote received hash algorithm: %s", hash_alg) if not algorithms.is_accepted(hash_alg, config.get('tenant', 'accept_tpm_hash_algs').split(',')): raise UserError( "TPM Quote is using an unaccepted hash algorithm: %s" % hash_alg) # Ensure enc_alg is in accept_tpm_encryption_algs list enc_alg = response_body["results"]["enc_alg"] logger.debug("Agent_quote received encryption algorithm: %s", enc_alg) if not algorithms.is_accepted(enc_alg, config.get('tenant', 'accept_tpm_encryption_algs').split(',')): raise UserError( "TPM Quote is using an unaccepted encryption algorithm: %s" % enc_alg) # Ensure sign_alg is in accept_tpm_encryption_algs list sign_alg = response_body["results"]["sign_alg"] logger.debug("Agent_quote received signing algorithm: %s", sign_alg) if not algorithms.is_accepted(sign_alg, config.get('tenant', 'accept_tpm_signing_algs').split(',')): raise UserError( "TPM Quote is using an unaccepted signing algorithm: %s" % sign_alg) if not self.validate_tpm_quote(public_key, quote, hash_alg): raise UserError( "TPM Quote from cloud agent is invalid for nonce: %s" % self.nonce) logger.info("Quote from %s validated", self.agent_ip) # encrypt U with the public key encrypted_U = crypto.rsa_encrypt( crypto.rsa_import_pubkey(public_key), self.U) b64_encrypted_u = base64.b64encode(encrypted_U) logger.debug("b64_encrypted_u: %s", b64_encrypted_u.decode('utf-8')) data = { 'encrypted_key': b64_encrypted_u, 'auth_tag': self.auth_tag } if self.payload is not None: data['payload'] = self.payload u_json_message = json.dumps(data) # post encrypted U back to CloudAgent params = '/keys/ukey' cloudagent_base_url = ( f'{self.agent_ip}:{self.agent_port}' ) post_ukey = RequestsClient(cloudagent_base_url, tls_enabled=False) response = post_ukey.post( params, data=u_json_message ) if response.status_code == 503: logger.error("Cannot connect to Agent at %s with Port %s. Connection refused.", self.agent_ip, self.agent_port) sys.exit() elif response.status_code == 504: logger.error("Verifier at %s with Port %s timed out.", self.verifier_ip, self.verifier_port) sys.exit() if response.status_code != 200: keylime_logging.log_http_response( logger, logging.ERROR, response_body) raise UserError( "Posting of Encrypted U to the Cloud Agent failed with response code %d" % response.status) except Exception as e: self.do_cvstop() raise e
def init_add(self, args): """ Set up required values. Command line options can overwrite these config values Arguments: args {[string]} -- agent_ip|agent_port|cv_agent_ip """ if "agent_ip" in args: self.agent_ip = args["agent_ip"] if 'agent_port' in args and args['agent_port'] is not None: self.agent_port = args['agent_port'] if 'cv_agent_ip' in args and args['cv_agent_ip'] is not None: self.cv_cloudagent_ip = args['cv_agent_ip'] else: self.cv_cloudagent_ip = self.agent_ip # Make sure all keys exist in dictionary if "file" not in args: args["file"] = None if "keyfile" not in args: args["keyfile"] = None if "payload" not in args: args["payload"] = None if "ca_dir" not in args: args["ca_dir"] = None if "incl_dir" not in args: args["incl_dir"] = None if "ca_dir_pw" not in args: args["ca_dir_pw"] = None # Set up accepted algorithms self.accept_tpm_hash_algs = config.get( 'tenant', 'accept_tpm_hash_algs').split(',') self.accept_tpm_encryption_algs = config.get( 'tenant', 'accept_tpm_encryption_algs').split(',') self.accept_tpm_signing_algs = config.get( 'tenant', 'accept_tpm_signing_algs').split(',') self.process_allowlist(args) # if none if (args["file"] is None and args["keyfile"] is None and args["ca_dir"] is None): raise UserError( "You must specify one of -k, -f, or --cert to specify the key/contents to be securely delivered to the agent") if args["keyfile"] is not None: if args["file"] is not None or args["ca_dir"] is not None: raise UserError( "You must specify one of -k, -f, or --cert to specify the key/contents to be securely delivered to the agent") # read the keys in if isinstance(args["keyfile"], dict) and "data" in args["keyfile"]: if isinstance(args["keyfile"]["data"], list) and len(args["keyfile"]["data"]) == 1: keyfile = args["keyfile"]["data"][0] if keyfile is None: raise UserError("Invalid key file contents") f = io.StringIO(keyfile) else: raise UserError("Invalid key file provided") else: f = open(args["keyfile"], 'r') self.K = base64.b64decode(f.readline()) self.U = base64.b64decode(f.readline()) self.V = base64.b64decode(f.readline()) f.close() # read the payload in (opt.) if isinstance(args["payload"], dict) and "data" in args["payload"]: if isinstance(args["payload"]["data"], list) and len(args["payload"]["data"]) > 0: self.payload = args["payload"]["data"][0] else: if args["payload"] is not None: f = open(args["payload"], 'r') self.payload = f.read() f.close() if args["file"] is not None: if args["keyfile"] is not None or args["ca_dir"] is not None: raise UserError( "You must specify one of -k, -f, or --cert to specify the key/contents to be securely delivered to the agent") if isinstance(args["file"], dict) and "data" in args["file"]: if isinstance(args["file"]["data"], list) and len(args["file"]["data"]) > 0: contents = args["file"]["data"][0] if contents is None: raise UserError("Invalid file payload contents") else: raise UserError("Invalid file payload provided") else: with open(args["file"], 'r') as f: contents = f.read() ret = user_data_encrypt.encrypt(contents) self.K = ret['k'] self.U = ret['u'] self.V = ret['v'] self.payload = ret['ciphertext'] if args["ca_dir"] is None and args["incl_dir"] is not None: raise UserError( "--include option is only valid when used with --cert") if args["ca_dir"] is not None: if args["file"] is not None or args["keyfile"] is not None: raise UserError( "You must specify one of -k, -f, or --cert to specify the key/contents to be securely delivered to the agent") if args["ca_dir"] == 'default': args["ca_dir"] = config.CA_WORK_DIR if "ca_dir_pw" in args and args["ca_dir_pw"] is not None: ca_util.setpassword(args["ca_dir_pw"]) if not os.path.exists(args["ca_dir"]) or not os.path.exists("%s/cacert.crt" % args["ca_dir"]): logger.warning("CA directory does not exist. Creating...") ca_util.cmd_init(args["ca_dir"]) if not os.path.exists("%s/%s-private.pem" % (args["ca_dir"], self.agent_uuid)): ca_util.cmd_mkcert(args["ca_dir"], self.agent_uuid) cert_pkg, serial, subject = ca_util.cmd_certpkg( args["ca_dir"], self.agent_uuid) # support revocation if not os.path.exists("%s/RevocationNotifier-private.pem" % args["ca_dir"]): ca_util.cmd_mkcert(args["ca_dir"], "RevocationNotifier") rev_package, _, _ = ca_util.cmd_certpkg( args["ca_dir"], "RevocationNotifier") # extract public and private keys from package sf = io.BytesIO(rev_package) with zipfile.ZipFile(sf) as zf: privkey = zf.read("RevocationNotifier-private.pem") cert = zf.read("RevocationNotifier-cert.crt") # put the cert of the revoker into the cert package sf = io.BytesIO(cert_pkg) with zipfile.ZipFile(sf, 'a', compression=zipfile.ZIP_STORED) as zf: zf.writestr('RevocationNotifier-cert.crt', cert) # add additional files to zip if args["incl_dir"] is not None: if isinstance(args["incl_dir"], dict) and "data" in args["incl_dir"] and "name" in args["incl_dir"]: if isinstance(args["incl_dir"]["data"], list) and isinstance(args["incl_dir"]["name"], list): if len(args["incl_dir"]["data"]) != len(args["incl_dir"]["name"]): raise UserError("Invalid incl_dir provided") for i in range(len(args["incl_dir"]["data"])): zf.writestr(os.path.basename( args["incl_dir"]["name"][i]), args["incl_dir"]["data"][i]) else: if os.path.exists(args["incl_dir"]): files = next(os.walk(args["incl_dir"]))[2] for filename in files: with open("%s/%s" % (args["incl_dir"], filename), 'rb') as f: zf.writestr( os.path.basename(f.name), f.read()) else: logger.warning('Specified include directory %s does not exist. Skipping...', args["incl_dir"]) cert_pkg = sf.getvalue() # put the private key into the data to be send to the CV self.revocation_key = privkey # encrypt up the cert package ret = user_data_encrypt.encrypt(cert_pkg) self.K = ret['k'] self.U = ret['u'] self.V = ret['v'] self.metadata = {'cert_serial': serial, 'subject': subject} self.payload = ret['ciphertext'] if self.payload is not None and len(self.payload) > config.getint('tenant', 'max_payload_size'): raise UserError("Payload size %s exceeds max size %d" % ( len(self.payload), config.getint('tenant', 'max_payload_size')))
def main(): """Main method of the Cloud Verifier Server. This method is encapsulated in a function for packaging to allow it to be called as a function by an external program.""" cloudverifier_port = config.get('cloud_verifier', 'cloudverifier_port') cloudverifier_host = config.get('cloud_verifier', 'cloudverifier_ip') # allow tornado's max upload size to be configurable max_upload_size = None if config.has_option('cloud_verifier', 'max_upload_size'): max_upload_size = int(config.get('cloud_verifier', 'max_upload_size')) VerfierMain.metadata.create_all(engine, checkfirst=True) session = get_session() try: query_all = session.query(VerfierMain).all() for row in query_all: if row.operational_state in states.APPROVED_REACTIVATE_STATES: row.operational_state = states.START session.commit() except SQLAlchemyError as e: logger.error('SQLAlchemy Error: %s', e) num = session.query(VerfierMain.agent_id).count() if num > 0: agent_ids = session.query(VerfierMain.agent_id).all() logger.info("Agent ids in db loaded from file: %s", agent_ids) logger.info('Starting Cloud Verifier (tornado) on port %s, use <Ctrl-C> to stop', cloudverifier_port) app = tornado.web.Application([ (r"/(?:v[0-9]/)?agents/.*", AgentsHandler), (r"/(?:v[0-9]/)?allowlists/.*", AllowlistHandler), (r".*", MainHandler), ]) context = cloud_verifier_common.init_mtls() # after TLS is up, start revocation notifier if config.getboolean('cloud_verifier', 'revocation_notifier'): logger.info("Starting service for revocation notifications on port %s", config.getint('cloud_verifier', 'revocation_notifier_port')) revocation_notifier.start_broker() sockets = tornado.netutil.bind_sockets( int(cloudverifier_port), address=cloudverifier_host) task_id = tornado.process.fork_processes(config.getint( 'cloud_verifier', 'multiprocessing_pool_num_workers')) asyncio.set_event_loop(asyncio.new_event_loop()) # Auto reactivate agent if task_id == 0: asyncio.ensure_future(activate_agents()) server = tornado.httpserver.HTTPServer(app, ssl_options=context, max_buffer_size=max_upload_size) server.add_sockets(sockets) try: tornado.ioloop.IOLoop.instance().start() except KeyboardInterrupt: tornado.ioloop.IOLoop.instance().stop() if config.getboolean('cloud_verifier', 'revocation_notifier'): revocation_notifier.stop_broker()
def main(): for ML in [config.MEASUREDBOOT_ML, config.IMA_ML]: if not os.access(ML, os.F_OK): logger.warning( "Measurement list path %s not accessible by agent. Any attempt to instruct it to access this path - via \"keylime_tenant\" CLI - will result in agent process dying", ML) if config.get('cloud_agent', 'agent_uuid') == 'dmidecode': if os.getuid() != 0: raise RuntimeError('agent_uuid is configured to use dmidecode, ' 'but current process is not running as root.') cmd = ['which', 'dmidecode'] ret = cmd_exec.run(cmd, raiseOnError=False) if ret['code'] != 0: raise RuntimeError('agent_uuid is configured to use dmidecode, ' 'but it\'s is not found on the system.') # Instanitate TPM class instance_tpm = tpm() # get params for initialization registrar_ip = config.get('cloud_agent', 'registrar_ip') registrar_port = config.get('cloud_agent', 'registrar_port') # initialize the tmpfs partition to store keys if it isn't already available secdir = secure_mount.mount() # change dir to working dir config.ch_dir(config.WORK_DIR, logger) # initialize tpm (ekcert, ek_tpm, aik_tpm) = instance_tpm.tpm_init( self_activate=False, config_pw=config.get('cloud_agent', 'tpm_ownerpassword') ) # this tells initialize not to self activate the AIK virtual_agent = instance_tpm.is_vtpm() # try to get some TPM randomness into the system entropy pool instance_tpm.init_system_rand() if ekcert is None: if virtual_agent: ekcert = 'virtual' elif instance_tpm.is_emulator(): ekcert = 'emulator' # now we need the UUID try: agent_uuid = config.get('cloud_agent', 'agent_uuid') except configparser.NoOptionError: agent_uuid = None if agent_uuid == 'openstack': agent_uuid = openstack.get_openstack_uuid() elif agent_uuid == 'hash_ek': agent_uuid = hashlib.sha256(ek_tpm).hexdigest() elif agent_uuid == 'generate' or agent_uuid is None: agent_uuid = str(uuid.uuid4()) elif agent_uuid == 'dmidecode': cmd = ['dmidecode', '-s', 'system-uuid'] ret = cmd_exec.run(cmd) sys_uuid = ret['retout'].decode('utf-8') agent_uuid = sys_uuid.strip() elif agent_uuid == 'hostname': agent_uuid = socket.getfqdn() if config.STUB_VTPM and config.TPM_CANNED_VALUES is not None: # Use canned values for stubbing jsonIn = config.TPM_CANNED_VALUES if "add_vtpm_to_group" in jsonIn: # The value we're looking for has been canned! agent_uuid = jsonIn['add_vtpm_to_group']['retout'] else: # Our command hasn't been canned! raise Exception("Command %s not found in canned json!" % ("add_vtpm_to_group")) logger.info("Agent UUID: %s", agent_uuid) # register it and get back a blob keyblob = registrar_client.doRegisterAgent(registrar_ip, registrar_port, agent_uuid, ek_tpm, ekcert, aik_tpm) if keyblob is None: instance_tpm.flush_keys() raise Exception("Registration failed") # get the ephemeral registrar key key = instance_tpm.activate_identity(keyblob) if key is None: instance_tpm.flush_keys() raise Exception("Activation failed") # tell the registrar server we know the key retval = False retval = registrar_client.doActivateAgent(registrar_ip, registrar_port, agent_uuid, key) if not retval: instance_tpm.flush_keys() raise Exception("Registration failed on activate") serveraddr = (config.get('cloud_agent', 'cloudagent_ip'), config.getint('cloud_agent', 'cloudagent_port')) server = CloudAgentHTTPServer(serveraddr, Handler, agent_uuid) serverthread = threading.Thread(target=server.serve_forever) logger.info("Starting Cloud Agent on %s:%s use <Ctrl-C> to stop", serveraddr[0], serveraddr[1]) serverthread.start() # want to listen for revocations? if config.getboolean('cloud_agent', 'listen_notfications'): cert_path = config.get('cloud_agent', 'revocation_cert') if cert_path == "default": cert_path = '%s/unzipped/RevocationNotifier-cert.crt' % (secdir) elif cert_path[0] != '/': # if it is a relative, convert to absolute in work_dir cert_path = os.path.abspath('%s/%s' % (config.WORK_DIR, cert_path)) def perform_actions(revocation): actionlist = [] # load the actions from inside the keylime module actionlisttxt = config.get('cloud_agent', 'revocation_actions') if actionlisttxt.strip() != "": actionlist = actionlisttxt.split(',') actionlist = ["revocation_actions.%s" % i for i in actionlist] # load actions from unzipped if os.path.exists("%s/unzipped/action_list" % secdir): with open("%s/unzipped/action_list" % secdir, 'r') as f: actionlisttxt = f.read() if actionlisttxt.strip() != "": localactions = actionlisttxt.strip().split(',') for action in localactions: if not action.startswith('local_action_'): logger.warning( "Invalid local action: %s. Must start with local_action_", action) else: actionlist.append(action) uzpath = "%s/unzipped" % secdir if uzpath not in sys.path: sys.path.append(uzpath) for action in actionlist: logger.info("Executing revocation action %s", action) try: module = importlib.import_module(action) execute = getattr(module, 'execute') asyncio.get_event_loop().run_until_complete( execute(revocation)) except Exception as e: logger.warning( "Exception during execution of revocation action %s: %s", action, e) try: while True: try: revocation_notifier.await_notifications( perform_actions, revocation_cert_path=cert_path) except Exception as e: logger.exception(e) logger.warning( "No connection to revocation server, retrying in 10s..." ) time.sleep(10) except KeyboardInterrupt: logger.info("TERM Signal received, shutting down...") instance_tpm.flush_keys() server.shutdown() else: try: while True: time.sleep(1) except KeyboardInterrupt: logger.info("TERM Signal received, shutting down...") instance_tpm.flush_keys() server.shutdown()