diff --git a/ed25519/key.go b/ed25519/key.go new file mode 100644 index 0000000..2eda9ae --- /dev/null +++ b/ed25519/key.go @@ -0,0 +1,107 @@ +package ed25519 + +import ( + "bytes" + "crypto" + goEd25519 "crypto/ed25519" + "crypto/sha512" + "filippo.io/edwards25519" + "io" + "strconv" +) + +type PrivateKey []byte +type PublicKey goEd25519.PublicKey + +const ( + // PublicKeySize is the size, in bytes, of public keys as used in this package. + PublicKeySize = 32 + // PrivateKeySize is the size, in bytes, of private keys as used in this package. + PrivateKeySize = PrivateKeyFormSize + PublicKeySize + PrivateKeyFormSize = 64 + // SignatureSize is the size, in bytes, of signatures generated and verified by this package. + SignatureSize = 64 +) + +// Sign signs the message with privateKey and returns a signature. It will +// panic if len(privateKey) is not PrivateKeySize. +func Sign(privateKey PrivateKey, message []byte) []byte { + // Outline the function body so that the returned signature can be + // stack-allocated. + signature := make([]byte, SignatureSize) + sign(signature, privateKey, message) + return signature +} + +func sign(signature, privateKey, message []byte) { + if l := len(privateKey); l != PrivateKeySize { + panic("ed25519: bad private key length: " + strconv.Itoa(l)) + } + privateKey, publicKey := privateKey[:PrivateKeyFormSize], privateKey[PrivateKeyFormSize:] + + s, _ := edwards25519.NewScalar().SetBytesWithClamping(privateKey[:32]) + prefix := privateKey[32:] + + mh := sha512.New() + mh.Write(prefix) + mh.Write(message) + messageDigest := make([]byte, 0, sha512.Size) + messageDigest = mh.Sum(messageDigest) + r, _ := edwards25519.NewScalar().SetUniformBytes(messageDigest) + + R := (&edwards25519.Point{}).ScalarBaseMult(r) + + kh := sha512.New() + kh.Write(R.Bytes()) + kh.Write(publicKey) + kh.Write(message) + hramDigest := make([]byte, 0, sha512.Size) + hramDigest = kh.Sum(hramDigest) + k, _ := edwards25519.NewScalar().SetUniformBytes(hramDigest) + + S := edwards25519.NewScalar().MultiplyAdd(k, s, r) + + copy(signature[:32], R.Bytes()) + copy(signature[32:], S.Bytes()) +} + +// Verify reports whether sig is a valid signature of message by publicKey. It +// will panic if len(publicKey) is not PublicKeySize. +func Verify(publicKey PublicKey, message, sig []byte) bool { + return goEd25519.Verify(goEd25519.PublicKey(publicKey), message, sig) +} + +// NewKeyFromSeed calculates a private key from a seed. It will panic if +// len(seed) is not SeedSize. This function is provided for interoperability +// with RFC 8032. RFC 8032's private keys correspond to seeds in this +// package. +func NewKeyFromSeed(seed []byte) PrivateKey { + return NewKeyFromStandard(goEd25519.NewKeyFromSeed(seed)) +} + +// GenerateKey generates a public/private key pair using entropy from rand. +// If rand is nil, crypto/rand.Reader will be used. +func GenerateKey(rand io.Reader) (PublicKey, PrivateKey, error) { + publicKey, privateKey, err := goEd25519.GenerateKey(rand) + if err != nil { + return nil, nil, err + } + + return PublicKey(publicKey), NewKeyFromStandard(privateKey), nil +} + +// Public returns the PublicKey corresponding to priv. +func (priv PrivateKey) Public() crypto.PublicKey { + publicKey := make([]byte, PublicKeySize) + copy(publicKey, priv[PrivateKeyFormSize:]) + return PublicKey(publicKey) +} + +// Equal reports whether priv and x have the same value. +func (priv PrivateKey) Equal(x crypto.PrivateKey) bool { + xx, ok := x.(PrivateKey) + if !ok { + return false + } + return bytes.Equal(priv, xx) +} diff --git a/ed25519/utils.go b/ed25519/utils.go new file mode 100644 index 0000000..a513c42 --- /dev/null +++ b/ed25519/utils.go @@ -0,0 +1,39 @@ +package ed25519 + +import ( + goEd25519 "crypto/ed25519" + "crypto/sha512" + "filippo.io/edwards25519" + "strconv" +) + +func NewKeyFromStandard(key goEd25519.PrivateKey) PrivateKey { + if l := len(key); l != goEd25519.PrivateKeySize { + panic("ed25519: bad private key length: " + strconv.Itoa(l)) + } + seed, publicKey := key[:goEd25519.SeedSize], key[goEd25519.SeedSize:] + + h := sha512.Sum512(seed) + s, _ := edwards25519.NewScalar().SetBytesWithClamping(h[:32]) + + // (a || RH) + priv := s.Bytes() + priv = append(priv, publicKey...) + return append(priv, h[32:]...) +} + +func NewKeyFromRaw(h []byte) PrivateKey { + if l := len(h); l != PrivateKeyFormSize { + panic("ed25519: bad private key form length: " + strconv.Itoa(l)) + } + + s, _ := edwards25519.NewScalar().SetBytesWithClamping(h[:32]) + A := (&edwards25519.Point{}).ScalarBaseMult(s) + publicKey := A.Bytes() + + privateKey := make(PrivateKey, PrivateKeySize) + copy(privateKey, h) + copy(privateKey[PrivateKeyFormSize:], publicKey) + + return privateKey +} diff --git a/go.mod b/go.mod index dff88cb..30b23cf 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,10 @@ module git.gammaspectra.live/givna.me/dns-api go 1.18 + +require ( + filippo.io/edwards25519 v1.0.0 + golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e +) + +require golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..13c5efd --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= +filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..9367e4c --- /dev/null +++ b/utils.go @@ -0,0 +1,53 @@ +package dns_api + +import ( + "encoding/base32" + "encoding/base64" + "git.gammaspectra.live/givna.me/dns-api/ed25519" + "golang.org/x/crypto/sha3" + "strings" +) + +var base32Encoding = base32.NewEncoding(strings.ToLower("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567")).WithPadding(base32.NoPadding) + +const onionV3Version = byte(0x3) +const onionChecksumData = ".onion checksum" + +func PublicKeyToOnionV3(publicKey ed25519.PublicKey) string { + checksumBuf := make([]byte, 0, ed25519.PublicKeySize+1+len(onionChecksumData)) + buf := make([]byte, 0, ed25519.PublicKeySize+2+1) + + checksumBuf = append(checksumBuf, onionChecksumData...) + checksumBuf = append(checksumBuf, publicKey...) + checksumBuf = append(checksumBuf, onionV3Version) + + h := sha3.New256() + h.Write(checksumBuf) + checksum := h.Sum([]byte{})[:2] + + buf = append(buf, publicKey...) + buf = append(buf, checksum...) + buf = append(buf, onionV3Version) + return base32Encoding.EncodeToString(buf) +} + +const torPrivateKeyPrefix = "== ed25519v1-secret: type0 ==\x00\x00\x00" +const torPublicKeyPrefix = "== ed25519v1-public: type0 ==\x00\x00\x00" + +func DecodeTorPrivateKey(key string) ed25519.PrivateKey { + priv, err := base64.RawStdEncoding.DecodeString(key) + if err != nil { + return nil + } + + return ed25519.NewKeyFromRaw(priv[32:]) +} + +func DecodePrivateKey(buf string) ed25519.PrivateKey { + priv, err := base64.RawStdEncoding.DecodeString(buf) + if err != nil { + return nil + } + + return ed25519.NewKeyFromRaw(priv) +} diff --git a/utils_test.go b/utils_test.go new file mode 100644 index 0000000..b502233 --- /dev/null +++ b/utils_test.go @@ -0,0 +1,21 @@ +package dns_api + +import ( + "git.gammaspectra.live/givna.me/dns-api/ed25519" + "testing" +) + +const testPrivateKey = "PT0gZWQyNTUxOXYxLXNlY3JldDogdHlwZTAgPT0AAAC4WKtXqyPlfMRwf37uSI/0kZRY8rzFBCpcDPeCp15uUalOhEmCQtBYH7oihSe8J4Znf9Gzw1E7W377Y6K3ux8V" +const testOnionAddress = "testdqzsnwe6dpmbhgcm5arrvhqzhomxs27nn2sertphnmfx7z44gzyd" + +func TestPublicKeyToOnionV3(t *testing.T) { + privateKey := DecodeTorPrivateKey(testPrivateKey) + if privateKey == nil { + t.Fail() + } + + generatedAddress := PublicKeyToOnionV3(privateKey.Public().(ed25519.PublicKey)) + if generatedAddress != testOnionAddress { + t.Errorf("expected address %s, got %s", testOnionAddress, generatedAddress) + } +}