Skip to content

Commit

Permalink
blinded25519: add python port, and KATs
Browse files Browse the repository at this point in the history
  • Loading branch information
leif committed Oct 15, 2024
1 parent 5779fd0 commit b02c707
Show file tree
Hide file tree
Showing 3 changed files with 204 additions and 0 deletions.
155 changes: 155 additions & 0 deletions misc/blinded25519.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
"""
This is near-verbatim port of blinded25519.go written with a little help from
the author of the original (3-bit hacker).
>>> msg = b"Hello World"
>>> a = SigningKey(b"1" * 32)
>>> A = a.verify_key
>>> assert A.verify(a.sign(msg)) == msg
>>> factor1 = b"2" * 32
>>> b = a.blind(factor1)
>>> B = A.blind(factor1)
>>> assert B.verify(b.sign(msg)) == msg
>>> factor2 = b"3" * 32
>>> c = b.blind(factor2)
>>> C = B.blind(factor2)
>>> assert C.verify(c.sign(msg)) == msg
>>> c2 = a.blind(factor2).blind(factor1)
>>> C2 = A.blind(factor2).blind(factor1)
>>> assert bytes(c2) == bytes(c)
>>> assert bytes(C2) == bytes(C)
>>> assert C2.verify(c2.sign(msg)) == msg
"""

import hashlib
import nacl.signing
from nacl.bindings import (
crypto_scalarmult_ed25519,
crypto_scalarmult_ed25519_base_noclamp,
)

Sum512 = lambda s: hashlib.sha512(s).digest()
Sum512_256 = lambda s: hashlib.new("sha512-256", s).digest()


class scalar:
"""
This is the parts of https://pkg.go.dev/filippo.io/edwards25519#Scalar
which were needed for verbatim porting hpqc's blinded25519.go to Python.
Note that unlike the golang, this is just a utility class of static
methods.
"""

l = 2**252 + 27742317777372353535851937790883648493

@staticmethod
def Multiply(x: bytes, y: bytes) -> bytes:
return (
(
int.from_bytes(x, byteorder="little")
* int.from_bytes(y, byteorder="little")
)
% scalar.l
).to_bytes(length=32, byteorder="little")

@staticmethod
def MultiplyAdd(x: bytes, y: bytes, z: bytes) -> bytes:
return (
(
int.from_bytes(x, byteorder="little")
* int.from_bytes(y, byteorder="little")
+ int.from_bytes(z, byteorder="little")
)
% scalar.l
).to_bytes(length=32, byteorder="little")

@staticmethod
def SetUniformBytes(b: bytes) -> bytes:
"mod l"
return (int.from_bytes(b, byteorder="little") % scalar.l).to_bytes(
length=32, byteorder="little"
)

@staticmethod
def clamp(b: bytes) -> bytes:
b = list(b)
b[0] &= 248
b[31] &= 63
b[31] |= 64
b = int.from_bytes(bytes(b), byteorder="little")
b %= scalar.l
return b.to_bytes(byteorder="little", length=32)


class SigningKey(nacl.signing.SigningKey):
def __init__(self, *a, **kw):
super(SigningKey, self).__init__(*a, **kw)
self.verify_key = VerifyKey(self.verify_key.encode())

def blind(self, factor):
digest = Sum512(self.encode())[:32]
clamped = scalar.clamp(digest)
return BlindedSigningKey(clamped).blind(factor)


class BlindedSigningKey(SigningKey):
def __init__(self, *a, **kw):
nacl.signing.SigningKey.__init__(self, *a, **kw)
self.verify_key = VerifyKey(
crypto_scalarmult_ed25519_base_noclamp(self.encode())
)

def sign(self, message):
"this returns signature concatenated with message, to match nacl's API"
digest1 = Sum512(self.encode())
md = Sum512(digest1[32:] + message + digest1[33:])
mdReduced = scalar.SetUniformBytes(md)
encodedR = crypto_scalarmult_ed25519_base_noclamp(mdReduced)
hramDigest = Sum512(encodedR + self.verify_key.encode() + message)
hramDigestReduced = scalar.SetUniformBytes(hramDigest)
sNew = scalar.MultiplyAdd(hramDigestReduced, self.encode(), mdReduced)
signature = encodedR + sNew
return signature + message

def blind(self, factor):
return BlindedSigningKey(
scalar.Multiply(self.encode(), scalar.clamp(Sum512_256(factor)))
)


class VerifyKey(nacl.signing.VerifyKey):
def blind(self, factor):
return VerifyKey(crypto_scalarmult_ed25519(Sum512_256(factor), self.encode()))


if __name__ == "__main__":
import doctest
import os
from base64 import b64encode, b64decode

doctest.testmod(verbose=True)

tests = [
line.split(b",")
for line in open(os.path.dirname(__file__) + "/kat.csv")
.read()
.strip()
.encode()
.split(b"\n")
]

for seed, expected_pk, msg, expected_sig, *factors in tests:
key = SigningKey(seed)
cur_pk = key.verify_key
assert expected_pk == b64encode(bytes(cur_pk))
assert expected_sig == b64encode(key.sign(msg)[:64])
while factors:
factor, expected_pk, expected_sig, *factors = factors
key = key.blind(factor)
cur_pk = cur_pk.blind(factor)
assert bytes(cur_pk) == bytes(key.verify_key)
assert expected_pk == b64encode(bytes(cur_pk))
sig = key.sign(msg)[:64]
assert cur_pk.verify(msg, sig) == msg
assert expected_sig == b64encode(sig)
1 change: 1 addition & 0 deletions misc/kat.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
seed0000000000000000000000000000,GNBrKE1JwUDF9F1sR268gWbATvlxf5XWMZUS7OanKnw=,Message One,aLo3XmalCtmHsStGnzDDwf7GgeE5r1y0Z83GM+a/WdWlSD8ZzFrMvZrnbdDNQyO0zO5M6GkYD0wa+x5z7KAIDw==,factor1_________________________,T/pNMlt0w7UC64HEclfruy314M4njR8Iv23VHmkJrJk=,1xPkV1Uy04ehmHgxHJ8HChfaLyMV0w83CKq7HtaKmVfJZOm8IIhJe77h3k0cgDDchALpd2+nvrPTW5EgtO8pDw==,factor2_________________________,sbjkeH6h/cZn+G/WY1VEKdDKPnXiP9SU4pt7Xu1woJw=,sYcDWAvB8gg/9RyD0DiaShY2oA+EmalDfQp9b/1ZJy3dK+VdHnnNIbF42iGQ3bffh9Yt/6cAMjZnBZk7IfVoBA==,factor3_________________________,nnY0KIvyzeKpk1lhPaY6y3e5F/WO8E0/q5DaXm18YCk=,0ZxOdjYegTFrn/xIrP6vQR6qFSq+DpI08A7RsB7z2tPoK3iaQXY97+mKWYxF+1T6qNBksRyqvQ4n5xRbEIGUDQ==
48 changes: 48 additions & 0 deletions sign/ed25519/blinded25519_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@
package ed25519

import (
"encoding/base64"
"encoding/csv"
"io"
"math/rand"
"os"
"testing"
"testing/quick"
"time"

"crypto/ed25519"
"filippo.io/edwards25519"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -194,3 +198,47 @@ func TestBlinding(t *testing.T) {
t.Error("failed bothwork", err)
}
}

func TestKat(t *testing.T) {
assert := assert.New(t)
file, err := os.Open("../../misc/kat.csv")
defer file.Close()
require.NoError(t, err)
reader := csv.NewReader(file)
reader.FieldsPerRecord = -1 // Allow variable number of fields
rows, err := reader.ReadAll()
require.NoError(t, err)

for _, R := range rows {
var key *PrivateKey
key_seed, expected_pk, message, expected_sig, factor, R := R[0], R[1], R[2], R[3], R[4], R[5:]
key = new(PrivateKey)
key.privKey = ed25519.NewKeyFromSeed([]byte(key_seed))
key.pubKey.pubKey = []byte(key.privKey[32:])
key.pubKey.rebuildB64String()
cur_pk := key.PublicKey()
assert.Equal(true, CheckPublicKey(cur_pk))
assert.Equal(expected_pk, base64.StdEncoding.EncodeToString(key.PublicKey().Bytes()))
sig, err := key.Sign(nil, []byte(message), nil)
require.NoError(t, err)
assert.Equal(expected_sig, base64.StdEncoding.EncodeToString(sig))
expected_pk, expected_sig, R = R[0], R[1], R[2:]
blinded_secret_key := key.Blind([]byte(factor))
cur_pk = cur_pk.Blind([]byte(factor))
assert.Equal(true, CheckPublicKey(cur_pk))
sig = blinded_secret_key.Sign([]byte(message))
assert.Equal(expected_pk, base64.StdEncoding.EncodeToString(cur_pk.Bytes()))
assert.Equal(expected_sig, base64.StdEncoding.EncodeToString(sig))
assert.Equal(true, cur_pk.Verify(sig, []byte(message)))
for len(R) > 0 {
factor, expected_pk, expected_sig, R = R[0], R[1], R[2], R[3:]
blinded_secret_key = blinded_secret_key.Blind([]byte(factor))
cur_pk = cur_pk.Blind([]byte(factor))
assert.Equal(true, CheckPublicKey(cur_pk))
sig = blinded_secret_key.Sign([]byte(message))
assert.Equal(expected_pk, base64.StdEncoding.EncodeToString(cur_pk.Bytes()))
assert.Equal(expected_sig, base64.StdEncoding.EncodeToString(sig))
assert.Equal(true, cur_pk.Verify(sig, []byte(message)))
}
}
}

0 comments on commit b02c707

Please sign in to comment.