Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
306 changes: 306 additions & 0 deletions janus/message-base.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
# SSI Base Message Format

> *Note: This is a rough document, and contains inconsistancies, missing sections, etc. These will iron out as work on this document progresses.*

The role of the Self-Soverign Identity Message Format is to act as a foundation for the message types that will power the SSI messaging ecosystem.
This format is intended to be as simple as possible, providing just enough structure to facilitate compatible communication.

Goals:
* Broad compatibility across SSI ecosystem
* Allow for innovation in message types
* Support communication from clients with commonly available libraries

## Base Envelope

The base message format is a JWT Payload. All JWT standard attributes are reserved. Additional attributes to messages are included based on the type specified. (See Message Types.)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been thinking about this a bit... The more I think, the more I'm concerned about JWT for a generic message system. I think it's fine for claims and claim sets (in use cases where correlation isn't an issue), because that's what it was designed for.

I see two different (orthogonal?) needs: (1) a general purpose message packaging protocol with types, wrapping, encryption, authentication, sub-protocol support (topics, sequencing, nesting) etc., and (2) a schema and standard for credentials (claims). JWT is the latter, with some encryption and signing on top.

I think a messaging protocol that is "inspired by JWT" is more of what we need for general purpose messaging. Just thinking out loud...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what I like too, and how I suspect we will land. "Inspired by JWT" is good language. Another benefit of naming it something else is that we can avoid the confusion between JWT as claim transfer and JWT as message format.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is our opportunity to steer things here. If we can decide what we like about JWTs and what we don't, future conversations will be much easier and much more fruitful.


Example:
```JSON
{
"@type": "example.com/basemessage",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe @type should be registered as a "public claim name" https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32#section-4.2?

"iss": "did:exa:11111111111",
"aud": "did:exa:22222222222",
"iat": "timestamp",
}
```
### Applicable JWT attributes

* **"iss"** (Issuer) - from DID
* **"sub"** (Subject)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JWT presents itself as "a compact, URL-safe means of representing claims to be transferred between two parties." For that reason, it makes sense that it has these common "claim names". However, these claim names assume the signing algorithm is a very simple one, like RSA, DSA, or ECDSA. More advanced ZK signing algorithms are clearly not contemplated. The JWT spec has one paragraph on privacy, and it has two options (1) encryption, and (2) don't include information. Correlation privacy isn't mentioned.

If we say we're using JWT, it will be understood that we're using it for claims exchange, which we can't. We can use JWS and JWE as "dumb" wrappers, but the main purpose of JWT will be lost on Sovrin. We could take the portions of JWT that don't hurt correlation privacy and use just those, but I suspect others in the SSI ecosystem will want to use "iss", "sub", etc.

* **"aud"** (Audience) - to DID
* **"exp"** (Expiration Time) - ts
* **"nbf"** (Not Before) - ts
* **"iat"** (Issued At) - ts
* **"jti"** (JWT ID) - unique id for JWT to prevent replay

Additional JOSE Headers (for JWS/JWE):

* **"typ"** (Type) - Should these be used as type designators?
* **"cty"** (Content Type)


## Message Types

Each message should have a type, specified by the top level @type attribute. The attribute value should be a URI that uniquely identifies the message type, and should be resolvable into a resource (ie, webpage) with details for message type use.

Each type indicates a particular version and format.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By format, do you mean JSON or YAML or MsgPack? I don't think we want to bind a type to a format do we. We'd end up with a cartesian product of two different concerns. Format can be handled generically and at a different layer than the domain-specific logic in a message handler.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean the contents of the message attributes. At this point, the outer envelope would be a consistent format: json, encoded with whatever we decide should be standard.


```JSON
{
"@type": "example.com/examplemessage/v1"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be flexible enough to reference a web address as well as a blockchain schema address. Perhaps this should be a URI with a couple of examples, one web, and one blockchain?

}
```

## Message Encoding

Messages can arrive encoded and packaged in several different formats as described here. It is up to the agent to decide if and how to handle these various forms depending on the security requirements and considering the required uses.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does the agent know the encoding and formatting? How does the agent know when it gets a blob of bits, what it is?


### Full Encryption
Messages will full encryption are packaged as described in this document.
### Common Crypto
Messages are encrypted or signed using only commonly available crypto libraries. The security may not be as good as Full Encryption, but allows integration with limited systems. This serves both Enterprise Integration and advanced IOT applications.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by "Full Encryption"? Is libsodium considered a commonly available crypto library?


Messages received with common crypto should be marked as such for internal processing. See the document about Security Context.
### No Crypto
There may be use cases for which crypto of any kind is difficult, such as simple IOT devices. Messages in this form send the basic payload unencoded, unsigned, and unencrypted. It is possible to include a shared secret as a bearer token.

Messages received with no crypto should be marked as such for internal processing, and the actions taken intentionally limited. See the document about Security Context.

## Encryption / Signing

The SSI Protocol uses elliptic curve cryptography to achieve message availability,
confidentiality, and integrity. It offers three crypto primitives:

```anon_crypt(msg, recipient_verkey)```
<blockquote>Encrypt only for recipient. Sender is anonymous. Parties can be unknown
to each other. The message is tamper evident.</blockquote>

```auth_crypt(msg, recipient_verkey, sender_sigkey)```
<blockquote>Encrypt only for recipient. Recipient learns sender’s verkey but can’t
prove to anybody else who the sender is (making the message repudiable).
Parties can be unknown to each other. The message is tamper evident.</blockquote>

```sign(msg, sender_sigkey)```
<blockquote>Associate a non-repudiable signature with a message.
Note that messages are not required to use non-repudiable signatures.</blockquote>

### Signed Message
Signing is only done when the use case requires it, for several reasons:
1. Authentication can be accomplished as part of the Public Key Authenticated Encryption (PKAE).
2. The Non-Repudiation offered by signing (DSA, ECDSA, EDDSA) is not always desired, as in the interrim steps of a negotiation. Signatures make a message non-repudiable.
3. Signatures are somewhat more expensive to compute and verify than other ways to authenticate, like PKAE.

If a signature is required, the original message is encoded as BASE64URL and added to a new object with two other signature fields:

```JSON
{
"signed": "<string encoded message>",
"sig": "<string encoded signature>",
"sig_type": "<algorithm used to sign>"
}
```

The `sig_type` tells us how the original message was encoded, what algorithm was used to create the signature, and how the signature is encoded.

For example, the `ED25519_B64U` signature scheme has the following characteristics:
* Pre-conditions: **None**
* Message encoding: **BASE64URL**
* Signing algorithm: **ed25519**
* Signature encoding: **BASE64URL**

More advanced signature schemes can be used. For example, the CL_CRED_1 algorithm has the following characteristics.
* Pre-conditions: **message is a typed CRED version 0.1 through 0.2, JSON**
* Message encoding: **none**
* Signing algorithm: **CL-RSA**
* Signature encoding: **none**

### Topical Messages
Messages are usually part of interactions.

> TODO: How does this fit in with the JWT headers?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because JWT is designed for claims, and is not a general message protocol, it's "sub" (subject) claim identifies who/what the claim is about. When the message isn't a claim, but a typed message in a protocol, then the topic makes more sense. The closest thing to JWT of a topic is the "jti" (JWT ID). Perhaps the "jti" could be co-opted for the tid, mid, and lmid, like this:

"jti": { "tid": "1", "mid": "1", "ptid": null, "lmid": 0}

or in a more compact way like this:

"jti": "1;1;;0"


#### Topic ID (`tid`)
Because multiple interactions can happen simultaneously, it's important to differentiate between them. This is done with a Topic ID or `tid`.

The first message in an interaction should set a `tid`
(128-bit random number) that the two parties use to keep
track of an interaction.

#### Message ID (`mid`)
Each message in an interaction needs a way to be uniquely
identified. This is done with Message ID (`mid`). The first
message from each party has a `mid` of 0,
the second message sent from each party is 1, and
so forth. A message is uniquely identified in an
interaction by its `tid`, the sender DID, and the `mid`. The combination of those three parts would be a way to uniquely identify a message.

#### Last Message ID (`lmid`)
In an interaction, it may be useful for the recipient of a message to know if their last message was received. A Last Message ID or `lmid` can be included to help detect missing messages.

#### Example
As an example, Alice is an issuer and she offers a credential to Bob.

* Alice establishes a Topic ID, 7.
* Alice sends a CRED_OFFER, `tid`=7, `mid`=0.
* Bob responded with a CRED_REQUEST, `tid`=7, `mid`=0, `lmid`=0.
* Alice sends a CRED, `tid`=7, `mid`=1, `lmid`=0.
* Bob responds with an ACK, `tid`=7, `mid`=1, `lmid`=1.

#### Nested interactions
Sometimes there are interactions that need to occur with the same party, while an existing interaction is in-flight.

When an interaction is nested within another, the initiator of a new interaction can include a Parent Topic ID (`ptid`). This signals to the other party that this is a topic that is branching off of an existing interaction.

#### Nested Example
As before, Alice is an issuer and she offers a credential to Bob. This time, she wants a bit more information before she is comfortable providing a credential.

* Alice establishes a Topic ID, 7.
* Alice sends a CRED_OFFER, `tid`=7, `mid`=0.
* Bob responded with a CRED_REQUEST, `tid`=7, `mid`=0, `lmid`=0.
* **Alice sends a PROOF_REQUEST, `tid`=11, `ptid`=7, `mid`=0.**
* **Bob sends a PROOF, `tid`=11,`mid`=0, `lmid`=0.**
* Alice sends a CRED, `tid`=7, `mid`=1, `lmid`=0.
* Bob responds with an ACK, `tid`=7, `mid`=1, `lmid`=1.

All of the steps are the same, except the two bolded steps that are part of a nested interaction.

#### Topic object
A topic object has the following fields discussed above:

* `tid`: A mutually agreed identifier for the interaction.
* `mid`: A message ID unique to the `tid` and sender.
* `ptid`: An optional parent `tid`. Used when branching or nesting a new interaction off of an existing one.
* `lmid`: A reference to the last message the sender received from the receiver. (Missing if it is the first message in an interaction.)

The topic object can be associated with a message in one of two way.

#### Wrapping Method
In this method, the original message is wrapped as the `msg` element of a new object, along with a `topic` object:

```json
{
"topic": { <topic object> },
"msg": { <message> }
}
```

#### Annotating method
A `@topic` member is included in the message object itself, as a peer to the root members of the object.
```json
{
"@topic": {...},
<other msg members>
}
```

### Forward Messages
A wrapper that includes an original message, plus a specific DID that is the intended recipient.

Structure:
```json
{
"fwd": <DID>,
"msg": <message>
}
```

### Encrypted Messages
There are two basic types of encrypted messages.
1. `anoncrypt`: public key anonymous encryption
1. `authcrypt`: public key authenticated encryption

#### Public Key Authenticated Encryption
Public Key Authenticated Encryption (PKAE) is a method where an elliptic curve diffie-hellman key exchange is used along with a nonce to compute a shared secret. This shared secret is used as a symmetric encryption key. The ciphertext can be decrypted, but only if the public key of the sender and the nonce used are communicated. The recipient knows the message is authentic because the symmetric key could only be known by the recipient and the sender. Authenticated Encryption is repudiable, meaning the recipient cannot prove to another party that the sender sent it. Because extra information is required to decrypt, this does not result in a self-contained message.

#### Anoncrypt Messages
`anoncrypt` messages use PKAE in conjunction with an ephemeral sender key pair to strongly encrypt a message to an intended recipient.

The anoncrypt function takes the msg and the recipient's verkey as arguments. A nonce is used, but because the ephemeral key is used only once, it is deterministic.

```
anoncrypt(msg, recipient_verkey)
```

Anoncrypt messages can be decrypted.
```
anondecrypt(msg, recipient_private_key)
```

#### Authcrypt Messages
`authcrypt` messages are like `anoncrypt` in that they are constructed with an ephemeral key pair. The difference is there are two attributes included with the message that make it possible to authenticate the sender: (1) sender's ver_key, and (2) signature over sender's ephemeral pub_key using the sender's sig_key (private counterpart to the ver_key). The sender's ver_key cannot be trusted. In order to trust the ver_key, the recipient must verify the signature. The signature proves to the recipient that the sender (the holder of the private counterpart to the ver_key) sent the message. This encryption retains the repudiable characteristic, in that the recipient cannot prove that the sender sent the message. They can only prove the sender once used the ephemeral key, which doesn't prove anything of any importance.

Internal message structure:
```json
{
"from": "<sender's ver_key>",
"sig": "<sender's ephemeral pub_key signed by sender>",
"msg": "<msg>"
}
```

#### Multiplexing Authcrypt Messages
In cases where two different edge agents need to receive the same message, but the message is being sent through a common cloud agent, one solution is simply to send two separate authcrypt messages.

If the message is large, this results in encrypting the message twice, and having to send it twice. It may be desireable to encrypt it and send it only once, while maintaining the requirement that both edge agents still be able to authenticate the message to the source.

This can be done by a process called **Multiplexing**.

In Multiplexing, the Authcrypt message is split into two parts. The **Meta** and the **Elemental**. At a high level, the Authcrypt Meta looks like a normal Authcrypt message, except the base message is moved into the Authcrypt Elemental.

The Authcrypt **Meta** message holds the hash and the key of the base message, but the base is held encrypted in the **Elemental**.

The sender generates a one-time random symmetric key, and encrypts the base message with this key. This is the Elemental.

Then the sender constructs an Authcrypt Meta message for each recipient. It substitutes a hash of the Elemental and the symmetric key for the base message.

Internal message structure (for each recipient):
```json
{
"from": "<sender's ver_key>",
"sig": "<sender's ephemeral pub_key signed by sender>",
"msg_hash": "<msg_hash>",
"msg_key": "<msg_key>"
}
```

The Meta message's internal structure can be the same for each recipient, including using the same ephemeral keypair. However, each message is encrypted _for_ each recipient using X25519 just like any other message.

The sender can then send a collection of Authcrypt Meta messages, one for each recipient, and one Authcrypt Elemental Message. The Cloud Agent can disperse the appropriate Authcrypt Meta message to each recipient, along with the Elemental Message.

The recipients decrypt the Authcrypt Meta messages and verify it like any other Authcrypt message. Then it hashes Elemental and verifies it matches the hash in the Meta. Then it decrypts Elemental using the key in Meta.

##### Encryption
To encrypt, the sender calls...
```
authcrypt(msg, recipient_verkey, sender_ver_key, sender_sig_key)
```

The authcrypt function...
1. Generates a new ephemeral keypair.
1. Signs the ephmeral public key using `sender_sig_key`.
1. Constructs a JSON object like so:
```json
{
"from": "<sender's ver_key>",
"sig": "<signature over sender's ephemeral pub_key>",
"msg": "<msg>"
}
```
1. Encrypts the object using the ephemeral private key.
1. Vigorously discards the ephemeral private key as it should only be used once.
1. Appends the ephemeral public key to the ciphertext

##### Decryption
To decrypt, the recipient calls...
```
authdecrypt(msg, recipient_private_key)
```

The authdecrypt function...

1. Removes the appended ephemeral public key
1. Uses the ephemeral public key and its own private key to decrypt the ciphertext.
1. Verifies that the signature over the ephemeral public key is correct using the `sender's ver_key`.

##### Implementation using libsodium
Anoncrypt is simply the sealed_box in libsodium. Authcrypt is similar, in that it uses the same deterministic nonce, and appends the ephemeral public key to the ciphertext. The main difference is the ephemeral public key must be signed in the message. Authcrypt uses a libsodium crypto_box with a deterministic nonce = hash(ephemeral pub key, recipient pub key), the same approach that sealed_box takes.