def signup_info_from_serialized(cls, serialized_signup_info): """ Takes signup information that has been serialized to JSON and reconstitutes into an EnclaveSignupInfo object. Args: serialized_signup_info: JSON serialized signup info Returns: An EnclaveSignupInfo object with sealed signup info data stripped out as serialized signup info doesn't contain it. """ deserialized_signup_info = json2dict(serialized_signup_info) # Note - serialized signup info shouldn't have sealed signup # data. signup_info = \ EnclaveSignupInfo( poet_public_key=deserialized_signup_info.get( 'poet_public_key'), proof_data=deserialized_signup_info.get( 'proof_data'), anti_sybil_id=deserialized_signup_info.get( 'anti_sybil_id'), serialized_signup_info=serialized_signup_info) return signup_info
def unseal_signup_data(cls, sealed_signup_data): """ Args: sealed_signup_data: Sealed signup data that was returned previously in a EnclaveSignupInfo object from a call to create_signup_info Returns: A string The hex encoded PoET public key that was extracted from the sealed data """ # Reverse the process we used in creating "sealed" signup info. # Specifically, we will do a base 64 decode, which gives us JSON # we can convert back to a dictionary we can use to get the # data we need signup_data = \ json2dict(base64.b64decode(sealed_signup_data).decode()) # Since the signing module uses strings for both private (WIF encoded) # and public (hex encoded) key canonical formats, we don't have to # decode. with cls._lock: cls._poet_public_key = str(signup_data.get('poet_public_key')) cls._poet_private_key = str(signup_data.get('poet_private_key')) cls._active_wait_timer = None return signup_data.get('poet_public_key')
def wait_timer_from_serialized(cls, serialized_timer, signature): """ Takes wait timer that has been serialized to JSON and reconstitutes into an EnclaveWaitTimer object. Args: serialized_timer (str): JSON serialized wait timer signature (str): Signature over serialized timer Returns: An EnclaveWaitTimer object """ deserialized_timer = json2dict(serialized_timer) timer = \ EnclaveWaitTimer( validator_address=str(deserialized_timer.get( 'validator_address')), duration=float(deserialized_timer.get( 'duration')), previous_certificate_id=str(deserialized_timer.get( 'previous_certificate_id')), local_mean=float(deserialized_timer.get( 'local_mean')), signature=signature, serialized_timer=serialized_timer) timer.request_time = float(deserialized_timer.get('request_time')) return timer
def unseal_signup_data(cls, sealed_signup_data): """ Args: sealed_signup_data: Sealed signup data that was returned previously in a EnclaveSignupInfo object from a call to create_signup_info Returns: A string The hex encoded PoET public key that was extracted from the sealed data """ # Reverse the process we used in creating "sealed" signup info. # Specifically, we will do a base 64 decode, which gives us JSON # we can convert back to a dictionary we can use to get the # data we need signup_data = \ json2dict(base64.b64decode(sealed_signup_data.encode()).decode()) with cls._lock: cls._poet_public_key = str(signup_data.get('poet_public_key')) cls._poet_private_key = str(signup_data.get('poet_private_key')) cls._active_wait_timer = None return signup_data.get('poet_public_key')
def wait_certificate_from_serialized(cls, serialized_certificate, signature): """ Takes wait certificate that has been serialized to JSON and reconstitutes into an EnclaveWaitCertificate object. Args: serialized_certificate (str): JSON serialized wait certificate signature (str): Signature over serialized certificate Returns: An EnclaveWaitCertificate object """ deserialized_certificate = json2dict(serialized_certificate) certificate = \ EnclaveWaitCertificate( duration=float(deserialized_certificate.get('duration')), previous_certificate_id=str( deserialized_certificate.get('previous_certificate_id')), local_mean=float(deserialized_certificate.get('local_mean')), request_time=float( deserialized_certificate.get('request_time')), validator_address=str( deserialized_certificate.get('validator_address')), nonce=str(deserialized_certificate.get('nonce')), block_hash=str(deserialized_certificate.get( 'block_hash')), signature=signature, serialized_certificate=serialized_certificate) return certificate
def create_wait_timer(cls, sealed_signup_data, validator_address, previous_certificate_id, local_mean): with cls._lock: # Extract keys from the 'sealed' signup data signup_data = \ json2dict( base64.b64decode(sealed_signup_data.encode()).decode()) poet_private_key = signup_data['poet_private_key'] if poet_private_key is None: raise \ ValueError( 'Invalid signup data. No poet private key.') try: poet_private_key = Secp256k1PrivateKey.from_hex( poet_private_key) except ParseError: raise \ ValueError( 'Invalid signup data. Badly formatted poet key(s).') # In a TEE implementation we would increment the HW counter here. # We can't usefully simulate a HW counter though. # Create some value from the cert ID. We are just going to use # the seal key to sign the cert ID. We will then use the # low-order 64 bits to change that to a number [0, 1] tag = \ base64.b64decode( cls._context.sign( previous_certificate_id.encode(), cls._seal_private_key)) tagd = float(struct.unpack('Q', tag[-8:])[0]) / (2**64 - 1) # Now compute the duration with a minimum wait time guaranteed duration = \ _PoetEnclaveSimulator.MINIMUM_WAIT_TIME \ - local_mean * math.log(tagd) # Create and sign the wait timer wait_timer = \ EnclaveWaitTimer( validator_address=validator_address, duration=duration, previous_certificate_id=previous_certificate_id, local_mean=local_mean) wait_timer.signature = \ cls._context.sign( wait_timer.serialize().encode(), poet_private_key) return wait_timer
def unseal_signup_data(cls, sealed_signup_data): """ Args: sealed_signup_data: Sealed signup data that was returned previously in a EnclaveSignupInfo object from a call to create_signup_info Returns: A string The hex encoded PoET public key that was extracted from the sealed data """ # Reverse the process we used in creating "sealed" signup info. # Specifically, we will do a base 64 decode, which gives us JSON # we can convert back to a dictionary we can use to get the # data we need signup_data = \ json2dict(base64.b64decode(sealed_signup_data.encode()).decode()) return signup_data.get('poet_public_key')
def create_wait_certificate(cls, sealed_signup_data, wait_timer, block_hash): with cls._lock: # Extract keys from the 'sealed' signup data if sealed_signup_data is None: raise ValueError('Sealed Signup Data is None') signup_data = \ json2dict( base64.b64decode(sealed_signup_data.encode()).decode()) poet_private_key = signup_data['poet_private_key'] poet_public_key = signup_data['poet_public_key'] if poet_private_key is None or poet_public_key is None: raise \ ValueError( 'Invalid signup data. No poet key(s).') try: poet_public_key = Secp256k1PublicKey.from_hex(poet_public_key) poet_private_key = Secp256k1PrivateKey.from_hex( poet_private_key) except ParseError: raise \ ValueError( 'Invalid signup data. Badly formatted poet key(s).') # Several criteria need to be met before we can create a wait # certificate: # 1. This signup data was used to sign this timer. # i.e. the key sealed / unsealed by the TEE signed this # wait timer. # 2. This timer has expired # 3. This timer has not timed out # # In a TEE implementation we would check HW counter agreement. # We can't usefully simulate a HW counter though. # i.e. wait_timer.counter_value == signup_data.counter.value # # Note - we make a concession for the genesis block (i.e., a wait # timer for which the previous certificate ID is the Null # identifier) in that we don't require the timer to have expired # and we don't worry about the timer having timed out. if wait_timer is None or \ not cls._context.verify( wait_timer.signature, wait_timer.serialize().encode(), poet_public_key): raise \ ValueError( 'Validator is not using the current wait timer') is_not_genesis_block = \ (wait_timer.previous_certificate_id != NULL_BLOCK_IDENTIFIER) now = time.time() expire_time = \ wait_timer.request_time + \ wait_timer.duration if is_not_genesis_block and now < expire_time: raise \ ValueError( 'Cannot create wait certificate because timer has ' 'not expired') time_out_time = \ wait_timer.request_time + \ wait_timer.duration + \ TIMER_TIMEOUT_PERIOD if is_not_genesis_block and time_out_time < now: raise \ ValueError( 'Cannot create wait certificate because timer ' 'has timed out') # Create a random nonce for the certificate. For our "random" # nonce we will take the timer signature, concat that with the # current time, JSON-ize it and create a SHA-256 hash over it. # Probably not considered random by security professional # standards, but it is good enough for the simulator. random_string = \ dict2json({ 'wait_timer_signature': wait_timer.signature, 'now': datetime.datetime.utcnow().isoformat() }) nonce = hashlib.sha256(random_string.encode()).hexdigest() # First create a new enclave wait certificate using the data # provided and then sign the certificate with the PoET private key wait_certificate = \ EnclaveWaitCertificate.wait_certificate_with_wait_timer( wait_timer=wait_timer, nonce=nonce, block_hash=block_hash) wait_certificate.signature = \ cls._context.sign( wait_certificate.serialize().encode(), poet_private_key) # In a TEE implementation we would increment the HW counter here # to prevent replay. # We can't usefully simulate a HW counter though. return wait_certificate
def verify_signup_info(cls, signup_info, originator_public_key_hash, most_recent_wait_certificate_id): # Verify the attestation verification report signature proof_data_dict = json2dict(signup_info.proof_data) verification_report = proof_data_dict.get('verification_report') if verification_report is None: raise ValueError('Verification report is missing from proof data') signature = proof_data_dict.get('signature') if signature is None: raise ValueError('Signature is missing from proof data') try: cls._report_public_key.verify(base64.b64decode(signature.encode()), verification_report.encode(), padding.PKCS1v15(), hashes.SHA256()) except InvalidSignature: raise ValueError('Verification report signature is invalid') verification_report_dict = json2dict(verification_report) # Verify that the verification report contains an ID field if 'id' not in verification_report_dict: raise ValueError('Verification report does not contain an ID') # Verify that the verification report contains an EPID pseudonym and # that it matches the anti-Sybil ID epid_pseudonym = verification_report_dict.get('epidPseudonym') if epid_pseudonym is None: raise \ ValueError( 'Verification report does not contain an EPID pseudonym') if epid_pseudonym != signup_info.anti_sybil_id: raise \ ValueError( 'The anti-Sybil ID in the verification report [{0}] does ' 'not match the one contained in the signup information ' '[{1}]'.format( epid_pseudonym, signup_info.anti_sybil_id)) # Verify that the verification report contains a PSE manifest status # and it is OK pse_manifest_status = \ verification_report_dict.get('pseManifestStatus') if pse_manifest_status is None: raise \ ValueError( 'Verification report does not contain a PSE manifest ' 'status') if pse_manifest_status.upper() != 'OK': raise \ ValueError( 'PSE manifest status is {} (i.e., not OK)'.format( pse_manifest_status)) # Verify that the verification report contains a PSE manifest hash pse_manifest_hash = \ verification_report_dict.get('pseManifestHash') if pse_manifest_hash is None: raise \ ValueError( 'Verification report does not contain a PSE manifest ' 'hash') # Verify that the proof data contains evidence payload evidence_payload = proof_data_dict.get('evidence_payload') if evidence_payload is None: raise ValueError('Evidence payload is missing from proof data') # Verify that the evidence payload contains a PSE manifest and then # use it to make sure that the PSE manifest hash is what we expect pse_manifest = evidence_payload.get('pse_manifest') if pse_manifest is None: raise ValueError('Evidence payload does not include PSE manifest') expected_pse_manifest_hash = \ base64.b64encode( hashlib.sha256( pse_manifest.encode()).hexdigest().encode()).decode() if pse_manifest_hash.upper() != expected_pse_manifest_hash.upper(): raise \ ValueError( 'PSE manifest hash {0} does not match {1}'.format( pse_manifest_hash, expected_pse_manifest_hash)) # Verify that the verification report contains an enclave quote status # and the status is OK enclave_quote_status = \ verification_report_dict.get('isvEnclaveQuoteStatus') if enclave_quote_status is None: raise \ ValueError( 'Verification report does not contain an enclave quote ' 'status') if enclave_quote_status.upper() != 'OK': raise \ ValueError( 'Enclave quote status is {} (i.e., not OK)'.format( enclave_quote_status)) # Verify that the verification report contains an enclave quote enclave_quote = verification_report_dict.get('isvEnclaveQuoteBody') if enclave_quote is None: raise \ ValueError( 'Verification report does not contain an enclave quote') # The ISV enclave quote body is base 64 encoded, so decode it and then # create an SGX quote structure from it so we can inspect sgx_quote = sgx_structs.SgxQuote() sgx_quote.parse_from_bytes(base64.b64decode(enclave_quote)) # The report body should be SHA256(SHA256(OPK)|PPK) # # NOTE - since the code that created the report data is in the enclave # code, this code needs to be kept in sync with it. Any changes to how # the report data is created, needs to be reflected in how we re-create # the report data for verification. hash_input = \ '{0}{1}'.format( originator_public_key_hash.upper(), cls._poet_public_key.upper()).encode() hash_value = hashlib.sha256(hash_input).digest() expected_report_data = \ hash_value + \ (b'\x00' * (sgx_structs.SgxReportData.STRUCT_SIZE - len(hash_value))) if sgx_quote.report_body.report_data.d != expected_report_data: raise \ ValueError( 'AVR report data [{0}] not equal to [{1}]'.format( sgx_quote.report_body.report_data.d.hex(), expected_report_data.hex())) # Compare the enclave measurement against the expected valid enclave # measurement. # # NOTE - this is only a temporary check. Instead of checking against # a predefined enclave measurement value, we should be configured with # a set of one or more enclave measurement values that we will # consider as valid. if sgx_quote.report_body.mr_enclave.m != \ cls.__VALID_ENCLAVE_MEASUREMENT__: raise \ ValueError( 'AVR enclave measurement [{0}] not equal to [{1}]'.format( sgx_quote.report_body.mr_enclave.m.hex(), cls.__VALID_ENCLAVE_MEASUREMENT__.hex())) # Compare the enclave basename in the verification report against the # expected enclave basename. # # NOTE - this is only a temporary check. Instead of checking against # a predefined enclave basenme value, we should be configured with a # set of one or more enclave basenames that we will consider as valid. if sgx_quote.basename.name != cls.__VALID_BASENAME__: raise \ ValueError( 'AVR enclave basename [{0}] not equal to [{1}]'.format( sgx_quote.basename.name.hex(), cls.__VALID_BASENAME__.hex())) # Verify that the wait certificate ID in the verification report # matches the provided wait certificate ID. The wait certificate ID # is stored in the nonce field. nonce = verification_report_dict.get('nonce') if nonce is None: raise \ ValueError( 'Verification report does not have a nonce')