Sending Secure Messages

 In some situations it is desirable to send secure messages from one pico to another pico. With KRL it is possible to either sign a message or to encrypt a message which can then be verified or decrypted respectively by the receiving pico. 

Secure messaging is made available via four engine functions (click the link for documentation on each function):

Picos and Keys

Every channel in a pico has a public-private key pair associated with it. You can't access the private key directly, but you can use them with functions that allow for inter-pico message signing and encryption. The ECI of the channel is a decentralized identifier (DID) that is derived from the keys. 

Subscriptions contain a channel to another pico for transmitting messages and a channel from the other pico for receiving messages. Exchanging signed or encrypted messages with another pico requires a subscription between the two picos. When a subscription is created the picos exchange public keys that can be used to sign or encrypt messages sent to each other. 

Signing

Signing an event that is sent to another pico uses the signChannelMessage() function. The function requires an ECI from the subscription and the message which can be any valid KRL value. 

The following sample code shows how to send a signed message to another pico. 

 rule mischief_hat_lifted {
    select when mischief hat_lifted
    foreach subscriptions:established() setting (subscription)
      pre {
        map = {"test": 1}
        message = map.encode() // signChannelMessage requires a string so objects should be encoded
        signed_message = engine:signChannelMessage(subscription{"Rx"}, message)
      }
      event:send({
         "eci": subs_attrs{"outbound_eci"},
         "eid": "hat-lifted",
         "domain": "mischief",
         "type": "hat_lifted",
         "attrs": {"signed_message": signed_message, "sub_id" : subscription{"Id"}}
        })
  }

The following rule shows how to verify a signed message. The key used to verify a signed message is stored on the subscription map and can be accessed with the key "other_verify_key".

rule mischief_hat_lifted {
    select when mischief hat_lifted
    pre {
		subID = event:attr("sub_id")
        subscription = subscriptions:established("Id", subID).head()
        verified_message = engine:verifySignedMessage(subscription{"Tx_verify_key"}, event:attr("signed_message"))
    }
    if verified_message != false then // If verifying the signed message fails then it will return false
      noop()
    fired {
      ent:message := verified_message.decode() // Objects will need to be decoded before being treated as an object
    } else {
      raise wrangler event "signature_verification_failed"
    }
  }

When a message fails to verify a false value is returned. Since KRL supports truthy and falsy values (e.g. 0 evaluates to false), it is recommended that if such a value needs to be sent to encapsulate it into an object to make verification more clear.

Encrypting

Encryption is achieved through the use of a Diffie-Hellman Key Exchange to create a shared secret between two parties that will allow for encryption and decryption. 

The public key used to generate the shared secret is stored in the subscription map and can be accessed via the key "other_encryption_public_key". 

The following rule shows code for encrypting and sending an encrypted message.

 rule mischief_encrypted {
    select when mischief encrypted
    foreach subscriptions:established() setting (subscription)
      pre {
        map = {"encryption": 1}
        message = map.encode() // The encryptChannelMessage requires a string so objects should be encoded
        eci = subscription{"Rx"}
		subID = subscription{"Id"}
        encrypted_object = engine:encryptChannelMessage(eci, message, subscription{"Tx_public_key"})
      }
      event:send({
         "eci": subs_attrs{"outbound_eci"},
         "eid": "hat-lifted",
         "domain": "mischief",
         "type": "encrypted",
         "attrs": {	"encryptedMessage": encrypted_object{"encryptedMessage"}, 
					"sub_id" : subID, 
					"nonce": encrypted_object{"nonce"}}
        })
  }
encryptChannelMessage returns an object that has the nonce and encrypted message. Both will need to be sent to the other pico in order to be decrypted.

A sample rule showing how to decrypt a message can be found below.

The key given to the engine to generate the shared secret can be accessed in the same way as for encrypting a message. It is stored on the subscription and can be accessed via the key "other_encryption_public_key". The nonce to decypt should be packaged with the encrypted message sent by the other pico.

 rule mischief_hat_lifted_encrypted {
    select when mischief encrypted
    pre {
		subID = event:attr("sub_id")
        subscription = subscriptions:established("Id", subID).head()
        nonce = event:attr("nonce")
        encrypted_message = event:attr("encryptedMessage")

        decrypted_message = engine:decryptChannelMessage(subscription{"Rx"}, 
														 encrypted_message, 
														 nonce, 
														 subscription{"Tx_public_key"})
    }
    if decrypted_message != false then
      noop()
    fired {
      ent:decrypted_message := decrypted_message.decode() // Objects will need to be decoded before being treated as an object
    } else {
      raise wrangler event "decryption_failure"
    }
  }

When a message fails to decrypt a false value is returned. Since KRL supports truthy and falsy values (e.g. 0 evaluates to false), it is recommended that if such a value needs to be sent to encapsulate it into an object to make decrypting more clear.

Copyright Picolabs | Licensed under Creative Commons.