From 0f006662e14f0f3c863caab227832fede572b9a0 Mon Sep 17 00:00:00 2001 From: Rasmus Dahlberg Date: Thu, 13 Oct 2022 17:47:14 +0200 Subject: Add onion address parsing --- go.mod | 5 +++ go.sum | 4 +++ internal/testonly/testonly.go | 41 +++++++++++++++++++++++ pkg/oaddr/oaddr.go | 58 ++++++++++++++++++++++++++++++++ pkg/oaddr/oaddr_test.go | 78 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 186 insertions(+) create mode 100644 go.sum create mode 100644 internal/testonly/testonly.go create mode 100644 pkg/oaddr/oaddr.go create mode 100644 pkg/oaddr/oaddr_test.go diff --git a/go.mod b/go.mod index 410cbdf..0f70188 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,8 @@ module sauteed-onions.org/onion-csr go 1.18 + +require ( + golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2 // indirect + golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..762518a --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2 h1:x8vtB3zMecnlqZIwJNUUpwYKYSqCz5jXbiyv0ZJJZeI= +golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2/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/internal/testonly/testonly.go b/internal/testonly/testonly.go new file mode 100644 index 0000000..fdd1ba1 --- /dev/null +++ b/internal/testonly/testonly.go @@ -0,0 +1,41 @@ +// Package testonly provides common functions used to setup tests +package testonly + +import ( + "crypto/ed25519" + "crypto/rand" + "crypto/rsa" + "encoding/hex" + "log" + "testing" +) + +// RSAPriv generates a new RSA key +func RSAPriv(t *testing.T) *rsa.PrivateKey { + t.Helper() + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + return priv +} + +// Ed25519Priv creates a new Ed25519 private key from a seed +func Ed25519Priv(t *testing.T, seed string) ed25519.PrivateKey { + t.Helper() + b := DecodeHex(t, seed) + if len(b) != ed25519.SeedSize { + log.Fatalf("invalid private key size: %d", len(b)) + } + return ed25519.NewKeyFromSeed(b) +} + +// DecodeHex decodes a hex-encoded string +func DecodeHex(t *testing.T, s string) []byte { + t.Helper() + b, err := hex.DecodeString(s) + if err != nil { + log.Fatal(err) + } + return b +} diff --git a/pkg/oaddr/oaddr.go b/pkg/oaddr/oaddr.go new file mode 100644 index 0000000..f065ec9 --- /dev/null +++ b/pkg/oaddr/oaddr.go @@ -0,0 +1,58 @@ +// Package oaddr provides onion address formatting +package oaddr + +import ( + "crypto" + "crypto/ed25519" + "encoding/base32" + "fmt" + "strings" + + "golang.org/x/crypto/sha3" +) + +// OnionAddress is an Ed25519 public key that represents a v3 onion address +type OnionAddress [ed25519.PublicKeySize]byte + +// New outputs an onion address from a public key as defined in RFC 8032 +func New(pub []byte) (addr OnionAddress, err error) { + if got, want := len(pub), ed25519.PublicKeySize; got != want { + return addr, fmt.Errorf("invalid public key size: %d", got) + } + + copy(addr[:], pub) + return addr, nil +} + +// NewFromSigner outputs an onion address for a given signer +func NewFromSigner(s crypto.Signer) (addr OnionAddress, err error) { + switch t := s.Public().(type) { + case ed25519.PublicKey: + addr, err = New(s.Public().(ed25519.PublicKey)) + default: + err = fmt.Errorf("unknown key type: %v", t) + } + return +} + +// String formats addr as defined in rend-spec-v3, see: +// https://gitweb.torproject.org/torspec.git/tree/rend-spec-v3.txt#n2160 +func (addr OnionAddress) String() string { + b := addr[:] + b = append(b, addr.checksum()...) + b = append(b, addr.version()...) + return strings.ToLower(base32.StdEncoding.EncodeToString(b)) + ".onion" +} + +func (addr OnionAddress) checksum() []byte { + h := sha3.New256() + h.Write([]byte(".onion checksum")) + h.Write(addr[:]) + h.Write(addr.version()) + sum := h.Sum(nil) + return sum[:2] +} + +func (addr OnionAddress) version() []byte { + return []byte{0x03} +} diff --git a/pkg/oaddr/oaddr_test.go b/pkg/oaddr/oaddr_test.go new file mode 100644 index 0000000..a37d4de --- /dev/null +++ b/pkg/oaddr/oaddr_test.go @@ -0,0 +1,78 @@ +package oaddr + +import ( + "crypto" + "crypto/ed25519" + "testing" + + "sauteed-onions.org/onion-csr/internal/testonly" +) + +const ( + testPriv = "a4007fabb23fae0f50fc45481553bf7d5d26b9fd8d76142c572606a6ebd7a2c1" + testPub = "da67efa9e06e724d999f8e0409b4a5a08aebd26005b63bef90d51a241d631cfd" + testAddr = "3jt67kpanzze3gm7rycatnffucfoxutaaw3dx34q2uncihlddt6tq3ad.onion" +) + +func TestNew(t *testing.T) { + for _, table := range []struct { + desc string + pub []byte + want OnionAddress + }{ + {"too short key", testonly.DecodeHex(t, testPub)[1:], newAddr(t, testPub)}, + {"too long key", append(testonly.DecodeHex(t, testPub), 0xff), newAddr(t, testPub)}, + {"valid", testonly.DecodeHex(t, testPub), newAddr(t, testPub)}, + } { + addr, err := New(table.pub) + if got, want := err != nil, table.desc != "valid"; got != want { + t.Errorf("%s: got error %v but wanted %v: %v", table.desc, got, want, err) + } + if err != nil { + continue + } + if got, want := addr, table.want; got != want { + t.Errorf("%s: got address\n%x\nbut wanted\n%x", table.desc, got[:], want[:]) + } + } +} + +func TestNewFromSigner(t *testing.T) { + for _, table := range []struct { + desc string + priv crypto.Signer + want OnionAddress + }{ + {"rsa key", testonly.RSAPriv(t), OnionAddress{}}, + {"valid", testonly.Ed25519Priv(t, testPriv), newAddr(t, testPub)}, + } { + addr, err := NewFromSigner(table.priv) + if got, want := err != nil, table.desc != "valid"; got != want { + t.Errorf("%s: got error %v but wanted %v: %v", table.desc, got, want, err) + } + if err != nil { + continue + } + if got, want := addr, table.want; got != want { + t.Errorf("%s: got address\n%x\nbut wanted\n%x", table.desc, got[:], want[:]) + } + } +} + +func TestString(t *testing.T) { + addr := newAddr(t, testPub) + want := testAddr + if got, want := addr.String(), want; got != want { + t.Errorf("got address\n%s\nbut wanted\n%s", got, want) + } +} + +func newAddr(t *testing.T, pub string) (addr OnionAddress) { + t.Helper() + b := testonly.DecodeHex(t, pub) + if got, want := len(b), ed25519.PublicKeySize; got != want { + t.Fatalf("invalid public key size: %d", got) + } + copy(addr[:], b) + return +} -- cgit v1.2.3