aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRasmus Dahlberg <rasmus@rgdd.se>2022-10-13 17:47:14 +0200
committerRasmus Dahlberg <rasmus@rgdd.se>2022-10-13 17:54:34 +0200
commit0f006662e14f0f3c863caab227832fede572b9a0 (patch)
tree19a678020b3999ff665f4590c10d9517fac793d9
parent5ed1052e29b0eabb80bac387024c12b0a739a44d (diff)
Add onion address parsing
-rw-r--r--go.mod5
-rw-r--r--go.sum4
-rw-r--r--internal/testonly/testonly.go41
-rw-r--r--pkg/oaddr/oaddr.go58
-rw-r--r--pkg/oaddr/oaddr_test.go78
5 files changed, 186 insertions, 0 deletions
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
+}