Ephemeral curve25519 public key
@item Signature @tab
64-byte, fixed length opaque data @tab
- ed25519 signature for that packet's header
+ ed25519 signature for that packet's header over all previous fields.
@end multitable
-Signature is calculated over all previous fields.
-
All following encryption is done in AEAD mode using
@url{https://cr.yp.to/chacha.html, ChaCha20}-@url{https://en.wikipedia.org/wiki/Poly1305, Poly1305}
-algorithms. Data is divided on 128 KiB blocks. Each block is encrypted with
-increasing nonce counter.
-
-Authenticated and encrypted size come after the header:
-
-@multitable @columnfractions 0.2 0.3 0.5
-@headitem @tab XDR type @tab Value
-@item Size @tab
- unsigned hyper integer @tab
- Payload size.
-@end multitable
+algorithms. Authenticated data is BLAKE3-256 hash of the unsigned
+portion of the header (the same data used in the signature). Size is
+XDR-encoded unsigned hyper integer, carrying the payload size, encrypted
+as a single AEAD-block (with the tag) independently from the following
+blocks. It is encoded with the zero nonce.
-Then comes the actual payload.
+Payload with possible padding is divided on 128 KiB blocks blocks. They
+are encrypted with the same authenticated data and increasing big-endian
+64-bit nonce, starting at 1.
Each node has static @strong{exchange} and @strong{signature} keypairs.
When node A want to send encrypted packet to node B, it:
@item derives 32-bytes AEAD encryption key with BLAKE3 derivation
function. Source key is the derived ephemeral key. Context is
@verb{|N N C P E 0x00 0x00 0x05|} magic number
+@item calculates authenticated data: it is BLAKE3-256 hash of the
+ unsigned header (same used for signing)
@item encrypts size, appends its authenticated ciphertext to the header
+ (with authenticated data, nonce=0)
@item encrypts each payload block, appending its authenticated ciphertext
+ (with authenticated data, nonce starting at 1, increasing with each block)
@item possibly appends any kind of "junk" noise data to hide real
payload's size from the adversary (generated using BLAKE3 XOF, with
the key derived from the ephemeral one and context string of
func aeadProcess(
aead cipher.AEAD,
- nonce []byte,
+ nonce, ad []byte,
doEncrypt bool,
r io.Reader,
w io.Writer,
readBytes += n
ctrIncr(ciphCtr)
if doEncrypt {
- toWrite = aead.Seal(buf[:0], nonce, buf[:n], nil)
+ toWrite = aead.Seal(buf[:0], nonce, buf[:n], ad)
} else {
- toWrite, err = aead.Open(buf[:0], nonce, buf[:n], nil)
+ toWrite, err = aead.Open(buf[:0], nonce, buf[:n], ad)
if err != nil {
return readBytes, err
}
ExchPub: *pubEph,
Sign: *signature,
}
+ ad := blake3.Sum256(tbsBuf.Bytes())
tbsBuf.Reset()
if _, err = xdr.Marshal(&tbsBuf, &pktEnc); err != nil {
return nil, err
fullSize := pktBuf.Len() + int(size)
sizeBuf := make([]byte, 8+aead.Overhead())
binary.BigEndian.PutUint64(sizeBuf, uint64(sizeWithTags(int64(fullSize))))
- if _, err = out.Write(aead.Seal(sizeBuf[:0], nonce, sizeBuf[:8], nil)); err != nil {
+ if _, err = out.Write(aead.Seal(sizeBuf[:0], nonce, sizeBuf[:8], ad[:])); err != nil {
return nil, err
}
lr := io.LimitedReader{R: data, N: size}
mr := io.MultiReader(&pktBuf, &lr)
- written, err := aeadProcess(aead, nonce, true, mr, out)
+ written, err := aeadProcess(aead, nonce, ad[:], true, mr, out)
if err != nil {
return nil, err
}
return pktEncRaw, nil
}
-func TbsVerify(our *NodeOur, their *Node, pktEnc *PktEnc) (bool, error) {
+func TbsVerify(our *NodeOur, their *Node, pktEnc *PktEnc) ([]byte, bool, error) {
tbs := PktTbs{
Magic: MagicNNCPEv5,
Nice: pktEnc.Nice,
}
var tbsBuf bytes.Buffer
if _, err := xdr.Marshal(&tbsBuf, &tbs); err != nil {
- return false, err
+ return nil, false, err
}
- return ed25519.Verify(their.SignPub, tbsBuf.Bytes(), pktEnc.Sign[:]), nil
+ return tbsBuf.Bytes(), ed25519.Verify(their.SignPub, tbsBuf.Bytes(), pktEnc.Sign[:]), nil
}
func PktEncRead(
if *pktEnc.Recipient != *our.Id {
return nil, 0, errors.New("Invalid recipient")
}
- verified, err := TbsVerify(our, their, &pktEnc)
+ tbsRaw, verified, err := TbsVerify(our, their, &pktEnc)
if err != nil {
return nil, 0, err
}
if !verified {
return their, 0, errors.New("Invalid signature")
}
+ ad := blake3.Sum256(tbsRaw)
sharedKey := new([32]byte)
curve25519.ScalarMult(sharedKey, our.ExchPrv, &pktEnc.ExchPub)
if _, err = io.ReadFull(data, sizeBuf); err != nil {
return their, 0, err
}
- sizeBuf, err = aead.Open(sizeBuf[:0], nonce, sizeBuf, nil)
+ sizeBuf, err = aead.Open(sizeBuf[:0], nonce, sizeBuf, ad[:])
if err != nil {
return their, 0, err
}
size := int64(binary.BigEndian.Uint64(sizeBuf))
lr := io.LimitedReader{R: data, N: size}
- written, err := aeadProcess(aead, nonce, false, &lr, out)
+ written, err := aeadProcess(aead, nonce, ad[:], false, &lr, out)
if err != nil {
return their, int64(written), err
}
return
}
nonce := make([]byte, aead.NonceSize())
- written, err := aeadProcess(aead, nonce, true, r, tmpW)
+ written, err := aeadProcess(aead, nonce, nil, true, r, tmpW)
if err != nil {
rerr = err
return
}
r, w := io.Pipe()
go func() {
- if _, err := aeadProcess(aead, nonce, false, bufio.NewReader(src), w); err != nil {
+ if _, err := aeadProcess(aead, nonce, nil, false, bufio.NewReader(src), w); err != nil {
w.CloseWithError(err) // #nosec G104
}
}()