brontide: implement cipher stream key rotation
This commit implements key rotation for brontide as-per the current draft of the LN p2p crypto spec. Key rotation is currently performed every 1000 messages encrypted/decrypted with a cipherState object. Key rotation is performed by evaluating the HKDF (extracting exactly 64 bytes) with the current chaining key, and cipher key. The key rotation is to attempted after each nonce increment making implementation easy as the current nonce value will already be within the local scope.
This commit is contained in:
parent
297133316f
commit
ae84b6197b
@ -28,6 +28,10 @@ const (
|
|||||||
// lengthHeaderSize is the number of bytes used to prefix encode the
|
// lengthHeaderSize is the number of bytes used to prefix encode the
|
||||||
// length of a message payload.
|
// length of a message payload.
|
||||||
lengthHeaderSize = 2
|
lengthHeaderSize = 2
|
||||||
|
|
||||||
|
// keyRotationInterval is the number of messages sent on a single
|
||||||
|
// cipher stream before the keys are rotated forwards.
|
||||||
|
keyRotationInterval = 1000
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -42,7 +46,7 @@ var (
|
|||||||
// sent once the handshake has completed.
|
// sent once the handshake has completed.
|
||||||
type cipherState struct {
|
type cipherState struct {
|
||||||
// nonce is the nonce passed into the chacha20-poly1305 instance for
|
// nonce is the nonce passed into the chacha20-poly1305 instance for
|
||||||
// encryption+decryption. The nonce is incremented after each succesful
|
// encryption+decryption. The nonce is incremented after each successful
|
||||||
// encryption/decryption.
|
// encryption/decryption.
|
||||||
//
|
//
|
||||||
// TODO(roasbeef): this should actually be 96 bit
|
// TODO(roasbeef): this should actually be 96 bit
|
||||||
@ -54,6 +58,10 @@ type cipherState struct {
|
|||||||
// TODO(roasbeef): m-lock??
|
// TODO(roasbeef): m-lock??
|
||||||
secretKey [32]byte
|
secretKey [32]byte
|
||||||
|
|
||||||
|
// salt is an additional secret which is used during key rotation to
|
||||||
|
// generate new keys.
|
||||||
|
salt [32]byte
|
||||||
|
|
||||||
// cipher is an instance of the ChaCha20-Poly1305 AEAD construction
|
// cipher is an instance of the ChaCha20-Poly1305 AEAD construction
|
||||||
// created using the secretKey above.
|
// created using the secretKey above.
|
||||||
cipher cipher.AEAD
|
cipher cipher.AEAD
|
||||||
@ -64,6 +72,10 @@ type cipherState struct {
|
|||||||
func (c *cipherState) Encrypt(associatedData, cipherText, plainText []byte) []byte {
|
func (c *cipherState) Encrypt(associatedData, cipherText, plainText []byte) []byte {
|
||||||
defer func() {
|
defer func() {
|
||||||
c.nonce++
|
c.nonce++
|
||||||
|
|
||||||
|
if c.nonce > keyRotationInterval {
|
||||||
|
c.rotateKey()
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var nonce [12]byte
|
var nonce [12]byte
|
||||||
@ -78,6 +90,10 @@ func (c *cipherState) Encrypt(associatedData, cipherText, plainText []byte) []by
|
|||||||
func (c *cipherState) Decrypt(associatedData, plainText, cipherText []byte) ([]byte, error) {
|
func (c *cipherState) Decrypt(associatedData, plainText, cipherText []byte) ([]byte, error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
c.nonce++
|
c.nonce++
|
||||||
|
|
||||||
|
if c.nonce > keyRotationInterval {
|
||||||
|
c.rotateKey()
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var nonce [12]byte
|
var nonce [12]byte
|
||||||
@ -94,6 +110,37 @@ func (c *cipherState) InitializeKey(key [32]byte) {
|
|||||||
c.cipher = chacha20.NewChaCha20Poly1305(&c.secretKey)
|
c.cipher = chacha20.NewChaCha20Poly1305(&c.secretKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InitializeKeyWithSalt is identical to InitializeKey however it also sets the
|
||||||
|
// cipherState's salt field which is used for key rotation.
|
||||||
|
func (c *cipherState) InitializeKeyWithSalt(salt, key [32]byte) {
|
||||||
|
c.salt = salt
|
||||||
|
c.InitializeKey(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// rotateKey rotates the current encryption/decryption key for this cipherState
|
||||||
|
// instance. Key rotation is performed by ratcheting the current key forward
|
||||||
|
// using an HKDF invocation with the cipherState's salt as the salt, and the
|
||||||
|
// current key as the input.
|
||||||
|
func (c *cipherState) rotateKey() {
|
||||||
|
var (
|
||||||
|
info []byte
|
||||||
|
nextKey [32]byte
|
||||||
|
)
|
||||||
|
|
||||||
|
oldKey := c.secretKey
|
||||||
|
h := hkdf.New(sha256.New, c.salt[:], oldKey[:], info)
|
||||||
|
|
||||||
|
// hkdf(ck, k, zero)
|
||||||
|
// |
|
||||||
|
// | \
|
||||||
|
// | \
|
||||||
|
// ck k'
|
||||||
|
h.Read(c.salt[:])
|
||||||
|
h.Read(nextKey[:])
|
||||||
|
|
||||||
|
c.InitializeKey(nextKey)
|
||||||
|
}
|
||||||
|
|
||||||
// symmetricState encapsulates a cipherState object and houses the ephemeral
|
// symmetricState encapsulates a cipherState object and houses the ephemeral
|
||||||
// handshake digest state. This struct is used during the handshake to derive
|
// handshake digest state. This struct is used during the handshake to derive
|
||||||
// new shared secrets based off of the result of ECDH operations. Ultimately,
|
// new shared secrets based off of the result of ECDH operations. Ultimately,
|
||||||
@ -112,14 +159,14 @@ type symmetricState struct {
|
|||||||
// messages or payloads sent until the next DH operation is executed.
|
// messages or payloads sent until the next DH operation is executed.
|
||||||
tempKey [32]byte
|
tempKey [32]byte
|
||||||
|
|
||||||
// handshakeDigest is the cummulative hash digest of all handshake
|
// handshakeDigest is the cumulative hash digest of all handshake
|
||||||
// messages sent from start to finish. This value is never transmitted
|
// messages sent from start to finish. This value is never transmitted
|
||||||
// to the other side, but will be used as the AD when
|
// to the other side, but will be used as the AD when
|
||||||
// encrypting/decrypting messages using our AEAD construction.
|
// encrypting/decrypting messages using our AEAD construction.
|
||||||
handshakeDigest [32]byte
|
handshakeDigest [32]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// mixKey is implements a basic HKDF-based key rachet. This method is called
|
// mixKey is implements a basic HKDF-based key ratchet. This method is called
|
||||||
// with the result of each DH output generated during the handshake process.
|
// with the result of each DH output generated during the handshake process.
|
||||||
// The first 32 bytes extract from the HKDF reader is the next chaining key,
|
// The first 32 bytes extract from the HKDF reader is the next chaining key,
|
||||||
// then latter 32 bytes become the temp secret key using within any future AEAD
|
// then latter 32 bytes become the temp secret key using within any future AEAD
|
||||||
@ -143,7 +190,7 @@ func (s *symmetricState) mixKey(input []byte) {
|
|||||||
s.InitializeKey(s.tempKey)
|
s.InitializeKey(s.tempKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// mixHash hashes the passed input data into the cummulative handshake digest.
|
// mixHash hashes the passed input data into the cumulative handshake digest.
|
||||||
// The running result of this value (h) is used as the associated data in all
|
// The running result of this value (h) is used as the associated data in all
|
||||||
// decryption/encryption operations.
|
// decryption/encryption operations.
|
||||||
func (s *symmetricState) mixHash(data []byte) {
|
func (s *symmetricState) mixHash(data []byte) {
|
||||||
@ -191,7 +238,7 @@ func (s *symmetricState) InitializeSymmetric(protocolName []byte) {
|
|||||||
|
|
||||||
// handshakeState encapsulates the symmetricState and keeps track of all the
|
// handshakeState encapsulates the symmetricState and keeps track of all the
|
||||||
// public keys (static and ephemeral) for both sides during the handshake
|
// public keys (static and ephemeral) for both sides during the handshake
|
||||||
// transscript. If the handshake completes successfuly, then two instances of a
|
// transcript. If the handshake completes successfully, then two instances of a
|
||||||
// cipherState are emitted: one to encrypt messages from initiator to
|
// cipherState are emitted: one to encrypt messages from initiator to
|
||||||
// responder, and the other for the opposite direction.
|
// responder, and the other for the opposite direction.
|
||||||
type handshakeState struct {
|
type handshakeState struct {
|
||||||
@ -207,7 +254,7 @@ type handshakeState struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newHandshakeState returns a new instance of the handshake state initialized
|
// newHandshakeState returns a new instance of the handshake state initialized
|
||||||
// with the prologue and protocol name. If this is the respodner's handshake
|
// with the prologue and protocol name. If this is the responder's handshake
|
||||||
// state, then the remotePub can be nil.
|
// state, then the remotePub can be nil.
|
||||||
func newHandshakeState(initiator bool, prologue []byte,
|
func newHandshakeState(initiator bool, prologue []byte,
|
||||||
localPub *btcec.PrivateKey, remotePub *btcec.PublicKey) handshakeState {
|
localPub *btcec.PrivateKey, remotePub *btcec.PublicKey) handshakeState {
|
||||||
@ -218,10 +265,10 @@ func newHandshakeState(initiator bool, prologue []byte,
|
|||||||
remoteStatic: remotePub,
|
remoteStatic: remotePub,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the current chainking key and handshake digest to the hash of
|
// Set the current chaining key and handshake digest to the hash of the
|
||||||
// the protocol name, and additionally mix in the prologue. If either
|
// protocol name, and additionally mix in the prologue. If either sides
|
||||||
// sides disagree about the prologue or protocol name, then the
|
// disagree about the prologue or protocol name, then the handshake
|
||||||
// handshake will fail.
|
// will fail.
|
||||||
h.InitializeSymmetric([]byte(protocolName))
|
h.InitializeSymmetric([]byte(protocolName))
|
||||||
h.mixHash(prologue)
|
h.mixHash(prologue)
|
||||||
|
|
||||||
@ -245,7 +292,7 @@ func newHandshakeState(initiator bool, prologue []byte,
|
|||||||
// chacha20 AEAD cipher. On the wire, all messages are prefixed with an
|
// chacha20 AEAD cipher. On the wire, all messages are prefixed with an
|
||||||
// authenticated+encrypted length field. Additionally, the encrypted+auth'd
|
// authenticated+encrypted length field. Additionally, the encrypted+auth'd
|
||||||
// length prefix is used as the AD when encrypting+decryption messages. This
|
// length prefix is used as the AD when encrypting+decryption messages. This
|
||||||
// construction provides confidentiallity of packet length, avoids introducing
|
// construction provides confidentiality of packet length, avoids introducing
|
||||||
// a padding-oracle, and binds the encrypted packet length to the packet
|
// a padding-oracle, and binds the encrypted packet length to the packet
|
||||||
// itself.
|
// itself.
|
||||||
//
|
//
|
||||||
@ -311,7 +358,7 @@ const (
|
|||||||
// to responder. During act one the initiator generates a fresh ephemeral key,
|
// to responder. During act one the initiator generates a fresh ephemeral key,
|
||||||
// hashes it into the handshake digest, and performs an ECDH between this key
|
// hashes it into the handshake digest, and performs an ECDH between this key
|
||||||
// and the responder's static key. Future payloads are encrypted with a key
|
// and the responder's static key. Future payloads are encrypted with a key
|
||||||
// dervied from this result.
|
// derived from this result.
|
||||||
//
|
//
|
||||||
// -> e, es
|
// -> e, es
|
||||||
func (b *BrontideMachine) GenActOne() ([ActOneSize]byte, error) {
|
func (b *BrontideMachine) GenActOne() ([ActOneSize]byte, error) {
|
||||||
@ -342,9 +389,9 @@ func (b *BrontideMachine) GenActOne() ([ActOneSize]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RecvActOne processes the act one packet sent by the initiator. The responder
|
// RecvActOne processes the act one packet sent by the initiator. The responder
|
||||||
// executes the mirroed actions to that of the initiator extending the
|
// executes the mirrored actions to that of the initiator extending the
|
||||||
// handshake digest and deriving a new shared secret based on a ECDH with the
|
// handshake digest and deriving a new shared secret based on a ECDH with the
|
||||||
// initiator's ephemeral key and reponder's static key.
|
// initiator's ephemeral key and responder's static key.
|
||||||
func (b *BrontideMachine) RecvActOne(actOne [ActOneSize]byte) error {
|
func (b *BrontideMachine) RecvActOne(actOne [ActOneSize]byte) error {
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
@ -409,7 +456,7 @@ func (b *BrontideMachine) GenActTwo() ([ActTwoSize]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RecvActTwo processes the second packet (act two) sent from the responder to
|
// RecvActTwo processes the second packet (act two) sent from the responder to
|
||||||
// the initiator. A succesful processing of this packet authenticates the
|
// the initiator. A successful processing of this packet authenticates the
|
||||||
// initiator to the responder.
|
// initiator to the responder.
|
||||||
func (b *BrontideMachine) RecvActTwo(actTwo [ActTwoSize]byte) error {
|
func (b *BrontideMachine) RecvActTwo(actTwo [ActTwoSize]byte) error {
|
||||||
var (
|
var (
|
||||||
@ -441,9 +488,9 @@ func (b *BrontideMachine) RecvActTwo(actTwo [ActTwoSize]byte) error {
|
|||||||
|
|
||||||
// GenActThree creates the final (act three) packet of the handshake. Act three
|
// GenActThree creates the final (act three) packet of the handshake. Act three
|
||||||
// is to be sent from the initiator to the responder. The purpose of act three
|
// is to be sent from the initiator to the responder. The purpose of act three
|
||||||
// is to transmit the initiator's public key under strong forwad secrecy to the
|
// is to transmit the initiator's public key under strong forward secrecy to
|
||||||
// responder. This act also includes the final ECDH operation which yields the
|
// the responder. This act also includes the final ECDH operation which yields
|
||||||
// final session.
|
// the final session.
|
||||||
//
|
//
|
||||||
// -> s, se
|
// -> s, se
|
||||||
func (b *BrontideMachine) GenActThree() ([ActThreeSize]byte, error) {
|
func (b *BrontideMachine) GenActThree() ([ActThreeSize]byte, error) {
|
||||||
@ -469,7 +516,7 @@ func (b *BrontideMachine) GenActThree() ([ActThreeSize]byte, error) {
|
|||||||
|
|
||||||
// RecvActThree processes the final act (act three) sent from the initiator to
|
// RecvActThree processes the final act (act three) sent from the initiator to
|
||||||
// the responder. After processing this act, the responder learns of the
|
// the responder. After processing this act, the responder learns of the
|
||||||
// initiators's static public key. Decryption of the static key serves to
|
// initiator's static public key. Decryption of the static key serves to
|
||||||
// authenticate the initiator to the responder.
|
// authenticate the initiator to the responder.
|
||||||
func (b *BrontideMachine) RecvActThree(actThree [ActThreeSize]byte) error {
|
func (b *BrontideMachine) RecvActThree(actThree [ActThreeSize]byte) error {
|
||||||
var (
|
var (
|
||||||
@ -506,7 +553,7 @@ func (b *BrontideMachine) RecvActThree(actThree [ActThreeSize]byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// split is the final wrap-up act to be executed at the end of a succesful
|
// split is the final wrap-up act to be executed at the end of a successful
|
||||||
// three act handshake. This function creates to internal cipherState
|
// three act handshake. This function creates to internal cipherState
|
||||||
// instances: one which is used to encrypt messages from the initiator to the
|
// instances: one which is used to encrypt messages from the initiator to the
|
||||||
// responder, and another which is used to encrypt message for the opposite
|
// responder, and another which is used to encrypt message for the opposite
|
||||||
@ -520,25 +567,25 @@ func (b *BrontideMachine) split() {
|
|||||||
|
|
||||||
h := hkdf.New(sha256.New, b.chainingKey[:], empty, empty)
|
h := hkdf.New(sha256.New, b.chainingKey[:], empty, empty)
|
||||||
|
|
||||||
// If we're the initiator the the frist 32 bytes are used to encrypt
|
// If we're the initiator the first 32 bytes are used to encrypt our
|
||||||
// our messages and the second 32-bytes to decrypt their messages. For
|
// messages and the second 32-bytes to decrypt their messages. For the
|
||||||
// the responder the opposite is true.
|
// responder the opposite is true.
|
||||||
if b.initiator {
|
if b.initiator {
|
||||||
h.Read(sendKey[:])
|
h.Read(sendKey[:])
|
||||||
b.sendCipher = cipherState{}
|
b.sendCipher = cipherState{}
|
||||||
b.sendCipher.InitializeKey(sendKey)
|
b.sendCipher.InitializeKeyWithSalt(b.chainingKey, sendKey)
|
||||||
|
|
||||||
h.Read(recvKey[:])
|
h.Read(recvKey[:])
|
||||||
b.recvCipher = cipherState{}
|
b.recvCipher = cipherState{}
|
||||||
b.recvCipher.InitializeKey(recvKey)
|
b.recvCipher.InitializeKeyWithSalt(b.chainingKey, recvKey)
|
||||||
} else {
|
} else {
|
||||||
h.Read(recvKey[:])
|
h.Read(recvKey[:])
|
||||||
b.recvCipher = cipherState{}
|
b.recvCipher = cipherState{}
|
||||||
b.recvCipher.InitializeKey(recvKey)
|
b.recvCipher.InitializeKeyWithSalt(b.chainingKey, recvKey)
|
||||||
|
|
||||||
h.Read(sendKey[:])
|
h.Read(sendKey[:])
|
||||||
b.sendCipher = cipherState{}
|
b.sendCipher = cipherState{}
|
||||||
b.sendCipher.InitializeKey(sendKey)
|
b.sendCipher.InitializeKeyWithSalt(b.chainingKey, sendKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -577,7 +624,7 @@ func (b *BrontideMachine) WriteMessage(w io.Writer, p []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadMessage attemps to read the next message from the passed io.Reader. In
|
// ReadMessage attempts to read the next message from the passed io.Reader. In
|
||||||
// the case of an authentication error, a non-nil error is returned.
|
// the case of an authentication error, a non-nil error is returned.
|
||||||
func (b *BrontideMachine) ReadMessage(r io.Reader) ([]byte, error) {
|
func (b *BrontideMachine) ReadMessage(r io.Reader) ([]byte, error) {
|
||||||
var cipherLen [lengthHeaderSize + macSize]byte
|
var cipherLen [lengthHeaderSize + macSize]byte
|
||||||
@ -603,5 +650,3 @@ func (b *BrontideMachine) ReadMessage(r io.Reader) ([]byte, error) {
|
|||||||
// packet length is authenticated along with the packet itself.
|
// packet length is authenticated along with the packet itself.
|
||||||
return b.recvCipher.Decrypt(cipherLen[:], nil, ciperText)
|
return b.recvCipher.Decrypt(cipherLen[:], nil, ciperText)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(roasbeef): key rotation
|
|
||||||
|
Loading…
Reference in New Issue
Block a user