Skip to content

Latest commit

 

History

History
341 lines (238 loc) · 12.9 KB

README.md

File metadata and controls

341 lines (238 loc) · 12.9 KB

SECIO 1.0.0

A stream security transport for libp2p. Streams wrapped by SECIO use secure sessions to encrypt all traffic.

SECIO is deprecated and we advise against using it. See this blog post for details.

Lifecycle Stage Maturity Level Status Latest Revision
3D Recommendation Deprecated r1, 2021-03-26

Authors: @jbenet, @bigs, @yusefnapora

Interest Group: @Stebalien, @richardschneider, @tomaka, @raulk

See the lifecycle document for context about maturity level and spec status.

Table of Contents

Implementations

Algorithm Support

SECIO allows participating peers to support a subset of the following algorithms.

Exchanges

The following elliptic curves are used for ephemeral key generation:

  • P-256
  • P-384
  • P-521

Ciphers

The following symmetric ciphers are used for encryption of messages once the SECIO channel is established:

  • AES-256
  • AES-128

Note that current versions of go-libp2p support the Blowfish cipher, however support for Blowfish will be dropped in future releases and should not be considered part of the SECIO spec.

Hashes

The following hash algorithms are used for key stretching and for HMACs once the SECIO channel is established:

  • SHA256
  • SHA512

Data Structures

The SECIO wire protocol features two message types defined in the version 2 syntax of the protobuf description language.

syntax = "proto2";

message Propose {
	optional bytes rand = 1;
	optional bytes pubkey = 2;
	optional string exchanges = 3;
	optional string ciphers = 4;
	optional string hashes = 5;
}

message Exchange {
	optional bytes epubkey = 1;
	optional bytes signature = 2;
}

These two messages, Propose and Exchange are the only serialized types required to implement SECIO.

Protocol

Prerequisites

Prior to undertaking the SECIO handshake described below, it is assumed that we have already established a dedicated bidirectional channel between both parties, and that both have agreed to proceed with the SECIO handshake using multistream-select or some other form of protocol negotiation.

Message framing

All messages sent over the wire are prefixed with the message length in bytes, encoded as an unsigned 32-bit Big Endian integer. The message length should always be inferior to 0x800000 (or 8MiB).

Proposal Generation

SECIO channel negotiation begins with a proposal phase.

Each side will construct a Propose protobuf message (as defined above), setting the fields as follows:

field value
rand A 16 byte random nonce, generated using the most secure means available
pubkey The sender's public key, serialized as described in the peer-id spec
exchanges A list of supported key exchanges as a comma-separated string
ciphers A list of supported ciphers as a comma-separated string
hashes A list of supported hashes as a comma-separated string

Both parties serialize this message and send it over the wire. If either party has prior knowledge of the other party's peer id, they may attempt to validate that the given public key can be used to generate the same peer id, and may close the connection if there is a mismatch.

Determining Roles and Algorithms

Next, the peers use a deterministic formula to compute their roles in the coming exchanges. Each peer computes:

oh1 := sha256(concat(remotePeerPubKeyBytes, myNonce))
oh2 := sha256(concat(myPubKeyBytes, remotePeerNonce))

Where myNonce is the rand component of the local peer's Propose message, and remotePeerNonce is the rand field from the remote peer's proposal.

With these hashes, determine which peer's preferences to favor. This peer will be referred to as the "preferred peer". If oh1 == oh2, then the peer is communicating with itself and should return an error. If oh1 < oh2, use the remote peer's preferences. If oh1 > oh2, prefer the local peer's preferences.

Given our preference, we now sort through each of the exchanges, ciphers, and hashes provided by both peers, selecting the first item from our preferred peer's set that is also shared by the other peer.

Key Exchange

Now the peers prepare a key exchange.

Both peers generate an ephemeral keypair using the elliptic curve algorithm that was chosen from the proposed exchanges in the previous step.

With keys generated, both peers create an Exchange message. First, they start by generating a "corpus" that they will sign.

corpus := concat(myProposalBytes, remotePeerProposalBytes, ephemeralPubKey)

The corpus is then signed using the permanent private key associated with the local peer's peer id, producing a byte array signature.

field value
epubkey The ephemeral public key, marshaled as described below
signature The signature of the corpus described above

The peers serialize their Exchange messages and write them over the wire. Upon receiving the remote peer's Exchange, the local peer will compute the remote peer's expected corpus using the known proposal bytes and the ephemeral public key sent by the remote peer in the Exchange. The signature can then be validated using the permanent public key of the remote peer obtained in the initial proposal.

Peers MUST close the connection if the signature does not validate.

Key marshaling

Within the Exchange message, ephemeral public keys are marshaled into the uncompressed form specified in section 4.3.6 of ANSI X9.62.

This is the behavior provided by the go standard library's elliptic.Marshal function.

Shared Secret Generation

Peers now generate their shared secret by combining their ephemeral private key with the remote peer's ephemeral public key.

First, the remote ephemeral public key is unmarshaled into a point on the elliptic curve used in the agreed-upon exchange algorithm. If the point is not valid for the agreed-upon curve, secret generation fails and the connection must be closed.

The remote ephemeral public key is then combined with the local ephemeral private key by means of elliptic curve scalar multiplication. The result of the multiplication is the shared secret, which will then be stretched to produce MAC and cipher keys, as described in the next section.

Key Stretching

The key stretching process uses an HMAC algorithm to derive encryption and MAC keys and a stream cipher initialization vector from the shared secret.

Key stretching produces the following three values for each peer:

  • A MAC key used to initialize an HMAC algorithm for message verification
  • A cipher key used to initialize a block cipher
  • An initialization vector (IV), used to generate a CTR stream cipher from the block cipher

The key stretching function will return two data structures k1 and k2, each containing the three values above.

Before beginning the stretching process, the size of the IV and cipher key are determined according to the agreed-upon cipher algorithm. The sizes (in bytes) used are as follows:

cipher type cipher key size IV size
AES-128 16 16
AES-256 32 16

The generated MAC key will always have a size of 20 bytes.

Once the sizes are known, we can compute the total size of the output we need to generate as outputSize := 2 * (ivSize + cipherKeySize + macKeySize).

The stretching algorithm will then proceed as follows:

First, an HMAC instance is initialized using the agreed upon hash function and shared secret.

A fixed seed value of "key expansion" (encoded into bytes as UTF-8) is fed into the HMAC to produce an initial digest a.

Then, the following process repeats until outputSize bytes have been generated:

  • reset the HMAC instance or generate a new one using the same hash function and shared secret
  • compute digest b by feeding a and the seed value into the HMAC:
    • b := hmac_digest(concat(a, "key expansion"))
  • append b to previously generated output (if any).
    • if, after appending b, the generated output exceeds outputSize, the output is truncated to outputSize and generation ends.
  • reset the HMAC and feed a into it, producing a new value for a to be used in the next iteration
    • a = hmac_digest(a)
  • repeat until outputSize is reached

Having generated outputSize bytes, the output is then split into six parts to produce the final return values k1 and k2:

| k1.IV | k1.CipherKey | k1.MacKey | k2.IV | k2.CipherKey | k2.MacKey |

The size of each field is determined by the cipher key and IV sizes detailed above.

Creating the Cipher and HMAC signer

With k1 and k2 computed, swap the two values if the remote peer is the preferred peer. After swapping if necessary, k1 becomes the local peer's key and k2 the remote peer's key.

Each peer now generates an HMAC signer using the agreed upon algorithm and the MacKey produced by the key stretcher.

Each peer will also initialize the agreed-upon block cipher using the generated CipherKey, and will then initialize a CTR stream cipher from the block cipher using the generated initialization vector IV.

Initiate Secure Channel

With the cipher and HMAC signer created, the secure channel is ready to be opened.

Secure Message Framing

To communicate over the channel, peers send packets containing an encrypted body and an HMAC signature of the encrypted body.

The encrypted body is produced by applying the stream cipher initialized previously to an arbitrary plaintext message payload. The encrypted data is then fed into the HMAC signer to produce the HMAC signature.

Once the encrypted body and HMAC signature are known, they are concatenated together, and their combined length is prefixed to the resulting payload.

Each packet is of the form:

[uint32 length of packet | encrypted body | hmac signature of encrypted body]

The packet length is in bytes, and it is encoded as an unsigned 32-bit integer in network (big endian) byte order.

Initial Packet Verification

The first packet transmitted by each peer must be the remote peer's nonce.

Each peer will decrypt the message body and validate the HMAC signature, comparing the decrypted output to the nonce recieved in the initial Propose message. If either peer is unable to validate the initial packet against the known nonce, they must abort the connection.

If both peers successfully validate the initial packet, the secure channel has been opened and is ready for use, using the framing rules described above.