indy
The indy library provides functions needed to use the HyperLedger Indy wire protocol. Messages in this protocol (whose mime type is "application/ssi-agent-wire
") are sent in a "packed" format, which includes specified encryption and signing. Since it is not possible to produce and consume these formats directly from KRL, the indy
library fills in the missing functionality.
Packing and Unpacking wire messages
pack
Requires a message, the public key of the intended recipient, and our ECI (event channel identifier), and returns a map which is suitable for an http:post
to another Indy Agent. Shown here are a sample Indy message, a call to indy:pack
to produce the packed format of that message, and what the packed format looks like:
message = { "@type":"did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/basicmessage/1.0/message", "~l10n":{"locale":"en"}, "sent_time":"2019-03-16T17:22:23.331Z", "content":"cupcake" } pm = indy:pack(message,[publicKey],meta:eci) // { "protected": "eyJlbmMiOiJ4Y2hhY2hhMjBwb2x5MTMwNV9pZXRmIiwidHlwIjoiSldNLzEuMCIsImFsZyI6IkF1d...ZTUlfYVAifX1dfQ==", // "iv": "C3KCoNmLCJ35z0K-", // "ciphertext": "1Nj2jTTzT_ztIAPUgD7hqkupb2t3_FEjIVbepL0jK-ohGOuOY7lT6oJA1QyItAjusk5XLgIrqRI0...28e9OafW7iph2Fm8Ij-FieU=", // "tag": "lwblT6uFSRwEJz3odAwYmQ==" // }
The original message is signed and encrypted using the private key associated with the ECI (aka DID (Distributed IDentifier)).
In practice, once a message has been packed, it will be sent to the service endpoint of the other Indy Agent:
http:post( service_endpoint, body=pm, headers={"content-type":"application/ssi-agent-wire"} ) setting(http_response)
The content-type should be "application/ssi-agent-wire".
unpack
Requires a packed message, and our ECI. A packed message, when received by an event, includes "protected"
, "iv"
, "ciphertext"
, and "tag"
as event attributes, and the attributes are suitable for direct use in calling the unpack
function. Again, our ECI is a distributed identifier, which includes its private key, used to decrypt the incoming message.
all = indy:unpack(event:attrs,meta:eci) their_key = all{"sender_key"} my_key = all{"recipient_key"} msg = all{"message"}.decode()
An unpacked message might look like this:
all == { "message":"{\"@type\": \"did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/basicmessage/1.0/message\", ...\"content\": \"Reply with: cupcake\"}", "sender_key":"6VNjQc2JDG1S8vQK4fn8A16WXbUDPzk436MumpQfdEsd", "recipient_key":"GLtgnUci67EVb834ErDRWyx8ub3S7aMLs7At5TqPQCZH" }
Notice the use of the String operator decode()
to produce the original message as a map:
{ "@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/basicmessage/1.0/message", "~l10n": {"locale": "en"}, "sent_time": "2019-03-16 17:22:23.285831+00:00", "content": "Reply with: cupcake" }
Signing and verifying message fields
Indy Agent messages can be produced and verified using the org.sovrin.agent_message
ruleset. It is available here on GitHub, and excerpts from its code are included below to show how the remaining functions in the indy
library are used.
Verifying a signed message field
verify_signed_field
Some incoming messages have signed fields. These need to be verified and reconstituted. A message with a signed field looks like this:
message = { "@type":"did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/response", "@id":"ec605c32-cdb4-4996-8e9f-62e729a42511", "~thread":{"thid":"cjtbre54y0028xophca46882p"}, "connection~sig":{ "@type":"did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/signature/1.0/ed25519Sha512_single", "signer":"B2gZj1vuTPSK1gNsvMT2w2SsTypLKMzamS6Jx9gvt9tD", "sig_data":"AAAAAFyNMM97IkRJRCI6ICJCNTh4cTNjeW5rRnRtM2NGckY4R3VBIiwgIkRJRERvYyI6...DozMDAwL2luZHkifV19fQ==", "signature":"SxGV0ZP9g8mzH9_otdxj2VXwjzvH3rhwd9oZI_Cs8wuasFbqyInKMeJm6PplBqHlOGiFrqMxYz64jEw5n0hGCw==" } }
The ruleset org.sovrin.agent_message
uses the indy:verify_signed_field
function to replace the signed field with a verified field of (nearly) the same name:
verify_signed_field = function(signed_field){ answer = indy:verify_signed_field(signed_field).klog("answer"); timestamp = answer{"timestamp"} .values() .reduce(function(a,dig){a*256+dig}); answer{"sig_verified"} => answer{"field"}.decode().put("timestamp",time:new(timestamp)) | null } verify_signatures = function(map){ map >< "connection~sig" => map.put("connection",verify_signed_field(map{"connection~sig"})) | map }
The indy:verify_signed_field
function takes in the signed field, and returns a map like the following one:
{ "sig_verified":true, "field":"{\"DID\": \"B58xq3cynkFtm3cFrF8GuA\", \"DIDDoc\": {\"@context\": \"https://w3id.org/did/v1\", ...}}", "timestamp":[0,0,0,0,92,141,48,207] }
It is recommended to use the org.sovrin.agent_message
ruleset rather than using this function directly. The ruleset can be used as a module, and provides
the verify_signatures
function, which can be used on incoming messages.
Signing a message field
Producing a signed field is also done in the org.sovrin.agent_message
ruleset, by this function:
sign_field = function(my_did,my_vk,field){ timestamp_bytes = toByteArray(time:now().time:strftime("%s")); sig_data_bytes = timestamp_bytes .append(field.encode().split("").map(function(x){ord(x)})); { "@type": t_sign_single, "signature": indy:crypto_sign(sig_data_bytes,my_did), "signer": my_vk, "sig_data": indy:sig_data(sig_data_bytes) } }
sig_data
This indy
library function is used in line 9 of the sign_field
function, to convert the KRL byte array (produced in lines 4-5) into a Base64 encoded string, as required by the wire protocol. It is recommended to use the org.sovrin.agent_message
ruleset rather than using this function directly.
crypto_sign
This indy
library function requires a byte array and our ECI. It produces the signature
required in a signed field, based on the data to be signed and our private key (associated with our DID (aka ECI)). This signature is incorporated into the signed field in line 7 of the sign_field
function. It is recommended to use the org.sovrin.agent_message
ruleset rather than using this function directly.
Producing the data byte array
The first eight bytes of the data are reserved for a timestamp. We produce this as an eight byte (big-endian) number of seconds since the start of the current epoch.
These bytes are produced by line 2 of the sign_field
function. The toByteArray
function this line uses is described in more detail in the page Looping using the reduce operator.
Copyright Picolabs | Licensed under Creative Commons.