3 ms·
It's possible to rule out some (but not necessarily all) cross-protocol attacks with a simple and cheap tweak. I have not proved that this rules out everything,
by kwantam 7y ago
It's possible to rule out some (but not necessarily all) cross-protocol attacks with a simple and cheap tweak. I have not proved that this rules out everything, but it appears to be no weaker than the scheme sans tweak, and proofs shouldn't be too hard.
The high-level idea is to derive a new encryption key from the recipient's signing key, such that only the recipient knows the secret. This is similar to HD wallets[1] and some recent work by Dauterman et al.[2]. It's easy to show, following the work of Morita et al.[3], that EdDSA is secure against (certain) related-key attacks. This means that, even if an adversary has a signing oracle for the derived key, it cannot forge signatures under the original key.
Alas, this is incomparable with giving the adversary a decryption oracle for the derived key, which means it's worth thinking about whether it's possible to prove some extra properties. It's also worth thinking about the other direction: does a signing oracle for the original key let an attacker break the encryption scheme? Again, I haven't proved these things, but it seems like it ought to be possible.
Here's the idea in detail. First, definitions:
- q = 2^252 + 0x14def9dea2f79cd65812631a5cf5d3ed, the order of the Curve25519 group [1]
- (SK, PK) = (s, s * G) is an Ed25519 key pair (base point is G, * is elliptic curve scalar multiplication)
- <PK> is some well-known encoding of PK as a bitstring
- a || b is the concatenation of a and b
- ID is some fixed string that identifies this ciphersuite, say, "age_ssh_ed25519_encrypt_v1"
- I2OSP and OS2IP are standard conversions between integers and byte strings as defined in RFC8017 [5]
Now, to encrypt to the derived key, do the following:
1. Let t = OS2IP(SHA256(ID || I2OSP(0, 1) || <PK>) || SHA256(ID || I2OSP(1, 1) || <PK>)) mod q. In other words, t is a (nearly) uniformly random element of Zq obtained by hashing the recipient's public key.
2. Let PK' = t * PK, where * represents elliptic curve scalar multiplication.
3. Encrypt your message to PK' rather than to PK.
Finally, to decrypt, the recipient does the following:
1. Compute t exactly as above.
2. Let s' = s * t, where s = SK is the secret key.
3. Decrypt the message using s' as the secret key.
Note that in this scheme, everyone who encrypts to the recipient who owns (SK, PK) uses the same key. It's possible to derive a per-message key instead: choose a random nonce, hash it along with <PK> in the first step, and send it along with the message. Alternatively, you could derive a per-sender key by hashing both the sender's and recipient's keys. It's not obvious to me that per-message/per-sender keys improve security at all, and it's plausible that they somehow weaken it (since letting the attacker set the input to the hash allows it to choose among polynomially many PK'). So unless you decide that per-message keys are necessary for security, I wouldn't bother.
[1] https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki https://github.com/bitcoin/bips/blob/master/bip-0032.mediawi...
[2] https://arxiv.org/abs/1810.04660 https://arxiv.org/abs/1810.04660
[3] https://eprint.iacr.org/2015/1135 https://eprint.iacr.org/2015/1135
[4] https://cr.yp.to/ecdh.html https://cr.yp.to/ecdh.html
[5] https://tools.ietf.org/html/rfc8017 https://tools.ietf.org/html/rfc8017
- FiloSottile 7y agoI don’t honestly see what kind of attack could be defeated by multiplying by a fixed value (maybe because there isn’t one, or likely because I simply can’t) but sure, why not! It feels like good hygiene, can be done with just the X25519 function and a scalar field multiplication, and I imagine it can help with proofs. Any reason not to use a simpler SHA-512(ID || <PK>) value for t? BTW, I was about to report an errata for RFC 8017, until I realized they inverted both the order and the starting index in going from X_n to x_n... yeah, as much as I’d like not to redefine that function every time, I might keep calling it big endian fixed size encoding.