- Author: Nicolas IOOSS
- Date: 2024-01-30
- Puzzle: https://zkhack.dev/zkhackIV/puzzleF3.html, https://github.com/ZK-Hack/puzzle-chaos-theory
Bob designed a new one time scheme, that's based on the tried and true method of
encrypt + sign. He combined ElGamal encryption with BLS signatures in a clever
way, such that you use pairings to verify the encrypted message was not tampered
with. Alice, then, figured out a way to reveal the plaintexts...
This puzzle is about an encryption scheme which provides authentication, using standard algorithm.
The repository https://github.com/ZK-Hack/puzzle-chaos-theory contains a Rust project implementing the encryption scheme.
Let's take a look at src/main.rs
.
use ark_bls12_381::{g2::Config, Bls12_381, Fr, G1Affine, G1Projective, G2Affine, G2Projective};
This imports types from crate ark_bls12_381
which implements the elliptic curve BLS12-381.
This document uses the same notation as the write-up for the previous puzzle:
- There are actually two curves in BLS12-381, named
$G_1$ and$G_2$ . They are generated by two points,$g_1 \in G_1$ and$g_2 \in G_2$ , which share the same prime order$r$ . The curves use the same scalar field$\mathbb{F}_r$ . - Points on these curves can use several coordinate systems.
In Rust, the types
G1Affine
,G1Projective
,G2Affine
andG2Projective
represent points on the curves using affine coordinates and projective coordinates. - There exists a bilinear pairing function
$e: G_1 \times G_2 \rightarrow G_T$ which combines points on$G_1$ and$G_2$ into an element of group$G_T$ . - As Arkworks is using the additive notation for groups, this write-up is also using them: the product of a point
$P$ with a scalar$s$ is written$[s]P$ instead of$P^s$ .
The Rust file contains on lines 54-64:
impl Sender {
pub fn send(&self, m: Message, r: &Receiver) -> ElGamal {
let c_2: G1Affine = (r.pk.mul(&self.sk) + m.0).into_affine();
ElGamal(self.pk, c_2)
}
pub fn authenticate(&self, c: &ElGamal) -> G2Affine {
let hash_c = c.hash_to_curve();
hash_c.mul(&self.sk).into_affine()
}
}
These functions implement the encryption and signing scheme.
When a sender with private key
- The sender computes
$c_2 = [sk_s]Pk_r + msg \in G_1$ . - The ElGamal ciphertext is defined as
$c = (Pk_s, c_2) \in G_1^2$ . - The sender computes the hash
$h_c = H(c) \in G_2$ using a safe hash-to-curve function. - The sender computes the signature
$sig = [sk_s]h_c \in G_2$ .
The sender can send
This is implemented in function Auditor::check_auth
.
The receiver can verify the signature and decrypt with the private key
This encrypt-then-sign scheme uses the same private key in the encryption and the signature. This seems fishy, but this is similar as what some other schemes are doing.
- Using the sender private key to sign a message is how asymmetric-key signature algorithms work.
- Using the sender private key in the ElGamal encryption is a way to authenticate the channel of an encrypted message (both the sender and the recipient know
$[sk_s]Pk_r = [sk_r]Pk_s$ and nobody else).
However, encrypting with long-term (also known as static) keys enables an attacker to distinguish messages. The ciphertext of a message is always the same.
To prevent this issue, several protocols (such as Noise Protocol Framework and Signal's Extended Triple Diffie-Hellman) introduce a ephemeral keys.
For example here, if the sender encrypted the message with a random private key
Back to the puzzle, how can the reuse of the private key be exploited?
From an encrypted message, an attacker can compute pairings with the ElGamal ciphertext.
First, let's rewrite the encryption equation by moving
Therefore:
This equation does not involve any private key, so the attacker can verify whether the ciphertext encrypted
To make implementing this attack easier, let's rewrite the equation using
The puzzle provided an encrypted message (loaded from file blob.bin
) and 10 possible message (given by function generate_message_space
).
The attack can be implemented by computing the right-hand side for each message and comparing it with the left-hand side of the equation:
let h = blob.c.hash_to_curve();
let lhs = Bls12_381::pairing(blob.c.1, h) - Bls12_381::pairing(blob.rec_pk, blob.s);
for (i, msg) in messages.iter().enumerate() {
let rhs = Bls12_381::pairing(msg.0, h);
if rhs == lhs {
println!("It was message {i}");
}
}
This displays It was message 3
, so the message encrypted in blob.bin
is
Combining encryption and signature is usual in messaging protocols involving asymmetric-key cryptography. However, doing so with BLS curves without ephemeral keys gives attackers the ability to guess which message was encrypted, without knowing any private key.