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 uses the BLS12-381 elliptic curve, in particular the induced
additive groups
for the pairing defined over those groups, and
We'll describe Bob's encrypt-then-sign scheme as presented in the code.
The encryption part of the scheme is based on ElGamal encryption.
Conventionally, this works over a single cyclic group, in this case
A "sender"
pub struct Receiver {
pk: G1Affine,
}
(Conventionally, this is obtained by first selecting a secret key
The sender follows a similar process, generating a key pair
struct Sender {
pub sk: Fr,
pub pk: G1Affine,
}
Again, following the ElGamal encryption scheme, the secret key
To encrypt a message
The ciphertext send
method
captures this encryption process:
pub struct ElGamal(G1Affine, G1Affine);
pub struct Message(G1Affine);
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)
}
}
The code does not contain the decryption logic for Receiver
(since presumably,
it is not relevant for the puzzle itself) but for completeness we'll describe it
here. Given the ciphertext
In other words, the receiver recovers the shared secret. Subtracting this from
The authentication part of Bob's scheme is based on BLS signatures. Once a
ciphertext authenticate
method and consists of hashing hasher()
function
in the code, before multiplying by
impl ElGamal {
pub fn hash_to_curve(&self) -> G2Affine {
let mut data = Vec::new();
self.serialize_uncompressed(&mut data).unwrap();
hasher().hash(&data).unwrap()
}
}
impl Sender {
pub fn authenticate(&self, c: &ElGamal) -> G2Affine {
let hash_c = c.hash_to_curve();
hash_c.mul(&self.sk).into_affine()
}
}
The code for verifying a signature is given in the check_auth
method of the
Auditor
struct. Given a claimed signature
pub struct Auditor {}
impl Auditor {
pub fn check_auth(sender_pk: G1Affine, c: &ElGamal, s: G2Affine) -> bool {
let lhs = Bls12_381::pairing(G1Projective::generator(), s);
let hash_c = c.hash_to_curve();
let rhs = Bls12_381::pairing(sender_pk, hash_c);
lhs == rhs
}
}
Indeed, if the arguments to the check are as claimed,
We are given a "blob" of data deserialized from the supplied .bin file,
presumably capturing an encrypted-then-signed message
pub struct Blob {
pub sender_pk: G1Affine,
pub c: ElGamal,
pub s: G2Affine,
pub rec_pk: G1Affine,
}
Furthermore, this blob
is correct, in the sense that it passes the auditor's
authentication check mentioned above
assert!(Auditor::check_auth(blob.sender_pk, &blob.c, blob.s));
We are also given the "message space" messages
, an array of 10 distinct
Message
s. One of these is the plaintext giving rise to the ciphertext
An important general point about ElGamal encryption is that the shared secret
established between the sender and receiver acts as a one-time pad. As such,
it should only be used once. Otherwise, notice that if an attacker obtains both
the ciphertext
While the secret key blob
is
unknown, we can at least use the above observation to obtain values
one for each of the 10 messages
For the other 9 choices of blob
passes the auditor's authentication check, the
following must hold
Hence in order to recover the plaintext, we want the message
let Blob { c, s, rec_pk, .. } = blob;
let hash_c = c.hash_to_curve();
let pos = messages.into_iter().position(|m| {
let possible_ss = c.1 - m.0;
let lhs = Bls12_381::pairing(rec_pk, s);
let rhs = Bls12_381::pairing(possible_ss, hash_c);
lhs == rhs
});
println!("index of plaintext message: {}", pos.unwrap());
Running this gives the desired index of the plaintext:
index of plaintext message: 3