The starting point are Schnorr signatures. Or rather, the Schnorr
sigma protocol for proving knowledge of the discrete logarithm.
The Prover knows an $x$ such that $x \cdot G = X$, and would like to prove
knowledge of $x$ without revealing it. They do this with the following sigma
protocol:
$$
\begin{aligned}
&k \xleftarrow{R} \mathbb{F}_q&\cr
&K \gets k \cdot G&\cr
&&\stackrel{K}{\longrightarrow}&\cr
&&&e \xleftarrow{R} \mathbb{F}_q\cr
&&\stackrel{e}{\longleftarrow}&\cr
&s \gets k - e x&\cr
&&\stackrel{s}{\longrightarrow}&\cr
&&& K \stackrel{?}{=} s \cdot G + e \cdot X\cr
\end{aligned}
$$
Now, this can be turned into a signature scheme
in the usual way, by doing a Fiat-Shamir transform
to replace the random challenge with a deterministic
challenge, by hashing $K$, along with the public key $X$, and a message $m$.
This is how you get Schnorr signatures.
To get designated verifier Schnorr signatures,
instead of proving that you know $x_0$ such that
$x_0 \cdot G = X_0$, you prove that either you know
such an $x_0$, or that you know a $x_1$ such that
$x_1 \cdot G = X_1$, where $X_1$ is the designated verifier.
This will allow the designated verifier to forge
the protocol, since they know the
discrete logarithm of $X_1$.
Thus, we need a way to take the "or" of two sigma protocols,
and apply it to this situation. We can apply
the CDS-OR transform for this, as explained in
Protocol 1 of Improved OR-Composition of Sigma Protocols.
Compiled to this specific case, the scheme looks like this:
$$
\begin{aligned}
&s_1, e_1 \xleftarrow{R} \mathbb{F}_q\cr
&K_1 \gets s_1 \cdot G + e_1 \cdot X\cr
&k_0 \xleftarrow{R} \mathbb{F}_q&\cr
&K_0 \gets k_0 \cdot G&\cr
&&\stackrel{K_0, K_1}{\longrightarrow}&\cr
&&&e \xleftarrow{R} \mathbb{F}_q\cr
&&\stackrel{e}{\longleftarrow}&\cr
&e_0 \gets e - e_1\cr
&s_0 \gets k_0 - e_0 x&\cr
&&\stackrel{e_0, e_1, s_0, s_1}{\longrightarrow}&\cr
&&& e_0 + e_1 \stackrel{?}{=} e\cr
&&& K_0 \stackrel{?}{=} s_0 \cdot G + e_0 \cdot X\cr
&&& K_1 \stackrel{?}{=} s_1 \cdot G + e_1 \cdot X\cr
\end{aligned}
$$
Here I've presented the scheme in the case where you know $x_0$,
but naturally the scheme works if you know $x_1$ instead,
just with what calculations you do flipped.
The idea is that "half" of the protocol is the result of
simulating a single Schnorr protocol for the discrete logarithm
you don't know. Because of this, the verifier is convinced
you know one of the two discrete logarithms, but doesn't know which.
If we naively Fiat-Shamir this, our signature consists
of $(K_0, K_1, e_0, e_1, s_0, s_1)$.
The first observation is that since $e_0 + e_1$ must equal $e$,
we can just send $e_0$, and have the verifier compute
$e_1 \gets e - e_0$.
The second trick is that instead of sending $K_0$ and $K_1$,
we can instead send some value $\hat{e}$ which we claim
to be the output of the Fiat-Shamir challenge. The verifier
can recompute $K_b \gets s_b \cdot G + e_b \cdot X$,
and then check if $H(K_0, K_1, \ldots) \stackrel{?}{=} \hat{e}$.
This is an equivalent scheme, but replaces two points with
an extra scalar.
The end result is a signature:
$$
(\hat{e}, e_0, s_0, s_1)
$$
which consists of just 4 scalars, which is only twice
the size of a Schnorr signature.
For the curve, I used Ristretto.
The natural choice was to use SHA-512 for hashing to create
a scalar.
For the hash, you want to include the public key $X_0$,
the designated verifier $X_1$, along with the commitments $K_0, K_1$,
and finally the message.
One trick I pulled was to include $x_0 \cdot X_1 = x_1 \cdot X_0 = \text{DH}(X_0, X_1)$ inside of the hash. This is the shared secret
between the signer and the verifier. This makes it so that
someone who isn't either the signer or the designated verifier
can't even tell whether or not the signature is valid.
The fact that the designed verifier can forge the signature
should make them not trust it either, but this adds an extra
layer of security as well.
In order to avoid length extension problems, since
the exchange is secret, I calculate the hash as:
$$
\text{HMAC-SHA-512}(\text{DH}(X_0, X_1), \text{ctx} || X_0 || X_1 || K_0 || K_1 || m)
$$
Where $\text{ctx}$ is the
string deevee challenge context 2022-06-17
.
Using an HMAC avoids any issues with length extensions. An additional
step for "best practices" would be to do a KDF on the DH result
before using it as an HMAC key, but this doesn't seem to be necessary.