More tests for torutil and test explanation

This commit is contained in:
Chad Retz 2018-05-19 09:46:12 -05:00
parent 00909b144c
commit f33268f084
6 changed files with 280 additions and 11 deletions

View File

@ -11,6 +11,11 @@ Features:
* Supports statically compiled Tor to embed Tor into the binary * Supports statically compiled Tor to embed Tor into the binary
* Supports both V2 and V3 onion services * Supports both V2 and V3 onion services
See info below, the [API docs](http://godoc.org/github.com/cretz/bine), and the [examples](examples). The project is
MIT licensed. The Tor docs/specs and https://github.com/yawning/bulb were great helps when building this.
## Example
It is really easy to create an onion service. For example, assuming `tor` is on the `PATH`, this bit of code will show It is really easy to create an onion service. For example, assuming `tor` is on the `PATH`, this bit of code will show
a directory server of the current directory: a directory server of the current directory:
@ -72,5 +77,11 @@ t, err := tor.Start(nil, &tor.StartConf{ProcessCreator: embedded.NewCreator()})
Tested on Windows, the original exe file is ~7MB. With Tor statically linked it comes to ~24MB, but Tor does not have to Tested on Windows, the original exe file is ~7MB. With Tor statically linked it comes to ~24MB, but Tor does not have to
be distributed separately. Of course take notice of all licenses in accompanying projects. be distributed separately. Of course take notice of all licenses in accompanying projects.
Also take a look at the [API docs](http://godoc.org/github.com/cretz/bine) and the [examples](examples). The project is ## Testing
MIT licensed. The Tor docs/specs and https://github.com/yawning/bulb were great helps when building this.
To test, a simple `go test ./...` from the base of the repository will work (add in a `-v` in there to see the tests).
The integration tests in `tests` however will be skipped. To execute those tests, `-tor` must be passed to the test.
Also, `tor` must be on the `PATH` or `-tor.path` must be set to the path of the `tor` executable. Even with those flags,
only the integration tests that do not connect to the Tor network are run. To also include the tests that use the Tor
network, add the `-tor.network` flag. For details Tor logs during any of the integration tests, use the `-tor.verbose`
flag.

View File

@ -33,7 +33,7 @@ func TestMain(m *testing.M) {
func GlobalEnabledNetworkContext(t *testing.T) *TestContext { func GlobalEnabledNetworkContext(t *testing.T) *TestContext {
if !torEnabled || !torIncludeNetworkTests { if !torEnabled || !torIncludeNetworkTests {
t.Skip("Only runs if -tor and -tor.network is set") t.Skip("Only runs if -tor and -tor.network are set")
} }
if globalEnabledNetworkContext == nil { if globalEnabledNetworkContext == nil {
ctx := NewTestContext(t, nil) ctx := NewTestContext(t, nil)

View File

@ -16,8 +16,8 @@ import (
var serviceIDEncoding = base32.StdEncoding.WithPadding(base32.NoPadding) var serviceIDEncoding = base32.StdEncoding.WithPadding(base32.NoPadding)
// OnionServiceIDFromPrivateKey generates the onion service ID from the given // OnionServiceIDFromPrivateKey generates the onion service ID from the given
// private key. This panics if the private key is not a crypto/*rsa.PrivateKey // private key. This panics if the private key is not a 1024-bit
// or github.com/cretz/bine/torutil/ed25519.KeyPair. // crypto/*rsa.PrivateKey or github.com/cretz/bine/torutil/ed25519.KeyPair.
func OnionServiceIDFromPrivateKey(key crypto.PrivateKey) string { func OnionServiceIDFromPrivateKey(key crypto.PrivateKey) string {
switch k := key.(type) { switch k := key.(type) {
case *rsa.PrivateKey: case *rsa.PrivateKey:
@ -29,8 +29,8 @@ func OnionServiceIDFromPrivateKey(key crypto.PrivateKey) string {
} }
// OnionServiceIDFromPublicKey generates the onion service ID from the given // OnionServiceIDFromPublicKey generates the onion service ID from the given
// public key. This panics if the public key is not a crypto/*rsa.PublicKey or // public key. This panics if the public key is not a 1024-bit
// github.com/cretz/bine/torutil/ed25519.PublicKey. // crypto/*rsa.PublicKey or github.com/cretz/bine/torutil/ed25519.PublicKey.
func OnionServiceIDFromPublicKey(key crypto.PublicKey) string { func OnionServiceIDFromPublicKey(key crypto.PublicKey) string {
switch k := key.(type) { switch k := key.(type) {
case *rsa.PublicKey: case *rsa.PublicKey:
@ -38,12 +38,15 @@ func OnionServiceIDFromPublicKey(key crypto.PublicKey) string {
case ed25519.PublicKey: case ed25519.PublicKey:
return OnionServiceIDFromV3PublicKey(k) return OnionServiceIDFromV3PublicKey(k)
} }
panic(fmt.Sprintf("Unrecognized private key type: %T", key)) panic(fmt.Sprintf("Unrecognized public key type: %T", key))
} }
// OnionServiceIDFromV2PublicKey generates a V2 service ID for the given // OnionServiceIDFromV2PublicKey generates a V2 service ID for the given
// RSA-1024 public key. // RSA-1024 public key. Panics if not a 1024-bit key.
func OnionServiceIDFromV2PublicKey(key *rsa.PublicKey) string { func OnionServiceIDFromV2PublicKey(key *rsa.PublicKey) string {
if key.N.BitLen() != 1024 {
panic("RSA key not 1024 bit")
}
h := sha1.New() h := sha1.New()
h.Write(x509.MarshalPKCS1PublicKey(key)) h.Write(x509.MarshalPKCS1PublicKey(key))
return strings.ToLower(serviceIDEncoding.EncodeToString(h.Sum(nil)[:10])) return strings.ToLower(serviceIDEncoding.EncodeToString(h.Sum(nil)[:10]))
@ -60,3 +63,23 @@ func OnionServiceIDFromV3PublicKey(key ed25519.PublicKey) string {
keyBytes[34] = 0x03 keyBytes[34] = 0x03
return strings.ToLower(serviceIDEncoding.EncodeToString(keyBytes[:])) return strings.ToLower(serviceIDEncoding.EncodeToString(keyBytes[:]))
} }
// PublicKeyFromV3OnionServiceID returns a public key for the given service ID
// or an error if the service ID is invalid.
func PublicKeyFromV3OnionServiceID(id string) (ed25519.PublicKey, error) {
byts, err := serviceIDEncoding.DecodeString(strings.ToUpper(id))
if err != nil {
return nil, err
} else if len(byts) != 35 {
return nil, fmt.Errorf("Invalid id length")
} else if byts[34] != 0x03 {
return nil, fmt.Errorf("Invalid version")
}
// Do a checksum check
key := ed25519.PublicKey(byts[:32])
checkSum := sha3.Sum256(append(append([]byte(".onion checksum"), key...), 0x03))
if byts[32] != checkSum[0] || byts[33] != checkSum[1] {
return nil, fmt.Errorf("Invalid checksum")
}
return key, nil
}

126
torutil/key_test.go Normal file
View File

@ -0,0 +1,126 @@
package torutil
import (
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"math/big"
"testing"
"github.com/cretz/bine/torutil/ed25519"
"github.com/stretchr/testify/require"
)
func genRsa(t *testing.T, bits int) *rsa.PrivateKey {
k, e := rsa.GenerateKey(rand.Reader, bits)
require.NoError(t, e)
return k
}
func genEd25519(t *testing.T) ed25519.KeyPair {
k, e := ed25519.GenerateKey(nil)
require.NoError(t, e)
return k
}
func TestOnionServiceIDFromPrivateKey(t *testing.T) {
assert := func(key interface{}, shouldPanic bool) {
if shouldPanic {
require.Panics(t, func() { OnionServiceIDFromPrivateKey(key) })
} else {
require.NotPanics(t, func() { OnionServiceIDFromPrivateKey(key) })
}
}
assert(nil, true)
assert("bad type", true)
assert(genRsa(t, 512), true)
assert(genRsa(t, 1024), false)
assert(genEd25519(t), false)
}
func TestOnionServiceIDFromPublicKey(t *testing.T) {
assert := func(key interface{}, shouldPanic bool) {
if shouldPanic {
require.Panics(t, func() { OnionServiceIDFromPublicKey(key) })
} else {
require.NotPanics(t, func() { OnionServiceIDFromPublicKey(key) })
}
}
assert(nil, true)
assert("bad type", true)
assert(genRsa(t, 512).Public(), true)
assert(genRsa(t, 1024), true)
assert(genRsa(t, 1024).Public(), false)
assert(genEd25519(t), true)
assert(genEd25519(t).Public(), false)
}
func TestOnionServiceIDFromV2PublicKey(t *testing.T) {
keyNs := []string{
// Good:
"146324450904690776677821409274235322093351159271459646966603918354799061259062657420293876128692345182038558188684966983800327732948675482476772223940488746110835191444662533167597590461666544121677987412778085089886835490778554764504249900341150942052002951429704745527158573712253866271451928082512868548761",
"128765593328258418045179848773717342016715415508670816023595649916344640363150769867803188216600032423256896646578968925175093584252663716464819945657362904808776223191437446288878991355616138261909405164010386485361833909203128674413041630645284466111155610996017814867550636519109125461589251061597106654453",
"142816677715940797613971689484332187730646681999601531244837211468050734148365138492918019219363903243436898624689103727294808675158556496441738627693945143098034304873441312947712853824963023184593797741228534339590785521072446422663170639163836372239933736851693970208563926767141632739068954958552435402293",
"145115352997770387235216741368218582671004692828327605746752722031765658311649572143281396789387808261614671508963042791801662334421789227429337599249357503724975792005849908733936522427330824294880823009884401313371327997012363609954851207630328042324027016587584799514594101157535904741483269310276131442141",
"147719637109219630754585551462675301139659936682064979504052824885582296579356301771435242063159743126441027484306731955552256555531866636211668612294755914990702770530441483651548916585382488692381916953093261634746890673551241873307767188168965986976533243218915179497387875035829308609534245761833108189053",
// Bad (512 bit):
"12406459612976799354275054531003074054562219068852891594185203203668633138039185159716483674833390801567933368800574140712590387835931746258315639847176501",
}
matchingIDs := []string{
"kqxsrkmm272hqvbj",
"75rzoc3nxzucidqb",
"l2vxsdecx6yita6r",
"hzma3bmo7mtyr5mq",
"prek6tayypvteljb",
"",
}
for i, keyN := range keyNs {
pubKey := &rsa.PublicKey{E: 65537, N: new(big.Int)}
pubKey.N, _ = pubKey.N.SetString(keyN, 10)
matchingID := matchingIDs[i]
if matchingID == "" {
require.Panics(t, func() { OnionServiceIDFromV2PublicKey(pubKey) })
} else {
require.Equal(t, matchingID, OnionServiceIDFromV2PublicKey(pubKey))
}
}
}
func TestOnionServiceIDFromV3PublicKey(t *testing.T) {
base64Keys := []string{
"SLne6D/uawqUj23619GbeYCd6HnzYPqyUvF8/xyz/3XNVpkgnonQI+J5NQVSGkppD1b0M87+qOtUBmVXsd7H3w",
"kPUs5aPoqISZVbg0q7coW+mNCODlcL4O7k2QWFOCC0gOQBiDm+g4Xz48lqucA7o2HIQ3gBdL5rlB6+q1tFdJwQ",
"YGzw/EwpcqfWb5UWIw652Ps4vTKu38VgX7Qo16XvOWjNWQK9YmfgARYiGQ1XYXEAKBJvoq8x+rKFbQN3FG1F6w",
"IJIZcWE57n5WCvHU2x7GkpBCIw0S0vWd+QyrE5RifGwPtYsbtxjyOxlb754Z0zXLZc+yQUp9hMQt5dt/YNpMag",
"SD7d4I6ZOjNlcqR2g4ptFJUw0tUHPQvfk92sExvnJ1uofPw9T9LUaaEs3rE/1yoGWKI4YejAzaTJXF9wrWQyuA",
}
matchingIDs := []string{
"2s2wk473fmotzgh6l2ycigrwegnurlzufatjm3bglrb36zbvlerskxad",
"tmcpdbgklpbywqyjpr7fijvjl7qjihd7pyubosbeohefec2m2thvzoqd",
"nrcan5uye2fwazixubug6pzrzp6ofjez43bjcyfoxhgxyygxbhgs4zqd",
"g2csv4kavhunvs45vxxc5ljz775d5a4ycqo4m4nrwpk3b4gryvz2zdyd",
"jviaiibaz7r6wqxttj5i2bi4zjfilmsevplwwtxdfyjph2sdmq5osdid",
}
for i, base64Key := range base64Keys {
key, err := base64.RawStdEncoding.DecodeString(base64Key)
require.NoError(t, err)
pubKey := ed25519.PrivateKey(key).PublicKey()
matchingID := matchingIDs[i]
require.Equal(t, matchingID, OnionServiceIDFromV3PublicKey(pubKey))
// Check verify here too
derivedPubKey, err := PublicKeyFromV3OnionServiceID(matchingID)
require.NoError(t, err)
require.Equal(t, pubKey, derivedPubKey)
// Let's mangle the matchingID a bit
tooLong := matchingID + "ddddd"
_, err = PublicKeyFromV3OnionServiceID(tooLong)
require.EqualError(t, err, "Invalid id length")
badVersion := matchingID[:len(matchingID)-1] + "e"
_, err = PublicKeyFromV3OnionServiceID(badVersion)
require.EqualError(t, err, "Invalid version")
badChecksum := []byte(matchingID)
badChecksum[len(badChecksum)-3] = 'q'
_, err = PublicKeyFromV3OnionServiceID(string(badChecksum))
require.EqualError(t, err, "Invalid checksum")
}
}

View File

@ -46,7 +46,7 @@ var simpleQuotedStringEscapeReplacer = strings.NewReplacer(
// EscapeSimpleQuotedString calls EscapeSimpleQuotedStringContents and then // EscapeSimpleQuotedString calls EscapeSimpleQuotedStringContents and then
// surrounds the entire string with double quotes. // surrounds the entire string with double quotes.
func EscapeSimpleQuotedString(str string) string { func EscapeSimpleQuotedString(str string) string {
return "\"" + simpleQuotedStringEscapeReplacer.Replace(str) + "\"" return "\"" + EscapeSimpleQuotedStringContents(str) + "\""
} }
// EscapeSimpleQuotedStringContents escapes backslashes, double quotes, // EscapeSimpleQuotedStringContents escapes backslashes, double quotes,
@ -74,7 +74,7 @@ func UnescapeSimpleQuotedString(str string) (string, error) {
} }
// UnescapeSimpleQuotedStringContents unescapes backslashes, double quotes, // UnescapeSimpleQuotedStringContents unescapes backslashes, double quotes,
// newlines, and carriage returns. // newlines, and carriage returns. Also errors if those aren't escaped.
func UnescapeSimpleQuotedStringContents(str string) (string, error) { func UnescapeSimpleQuotedStringContents(str string) (string, error) {
ret := "" ret := ""
escaping := false escaping := false
@ -91,6 +91,8 @@ func UnescapeSimpleQuotedStringContents(str string) (string, error) {
} }
ret += "\"" ret += "\""
escaping = false escaping = false
case '\r', '\n':
return "", fmt.Errorf("Unescaped newline or carriage return")
default: default:
if escaping { if escaping {
if c == 'r' { if c == 'r' {
@ -103,6 +105,7 @@ func UnescapeSimpleQuotedStringContents(str string) (string, error) {
} else { } else {
ret += string(c) ret += string(c)
} }
escaping = false
} }
} }
return ret, nil return ret, nil

106
torutil/string_test.go Normal file
View File

@ -0,0 +1,106 @@
package torutil
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestPartitionString(t *testing.T) {
assert := func(str string, ch byte, expectedA string, expectedB string, expectedOk bool) {
a, b, ok := PartitionString(str, ch)
require.Equal(t, expectedA, a)
require.Equal(t, expectedB, b)
require.Equal(t, expectedOk, ok)
}
assert("foo:bar", ':', "foo", "bar", true)
assert(":bar", ':', "", "bar", true)
assert("foo:", ':', "foo", "", true)
assert("foo", ':', "foo", "", false)
assert("foo:bar:baz", ':', "foo", "bar:baz", true)
}
func TestPartitionStringFromEnd(t *testing.T) {
assert := func(str string, ch byte, expectedA string, expectedB string, expectedOk bool) {
a, b, ok := PartitionStringFromEnd(str, ch)
require.Equal(t, expectedA, a)
require.Equal(t, expectedB, b)
require.Equal(t, expectedOk, ok)
}
assert("foo:bar", ':', "foo", "bar", true)
assert(":bar", ':', "", "bar", true)
assert("foo:", ':', "foo", "", true)
assert("foo", ':', "foo", "", false)
assert("foo:bar:baz", ':', "foo:bar", "baz", true)
}
func TestEscapeSimpleQuotedStringIfNeeded(t *testing.T) {
assert := func(str string, shouldBeDiff bool) {
maybeEscaped := EscapeSimpleQuotedStringIfNeeded(str)
if shouldBeDiff {
require.NotEqual(t, str, maybeEscaped)
} else {
require.Equal(t, str, maybeEscaped)
}
}
assert("foo", false)
assert(" foo", true)
assert("f\\oo", true)
assert("fo\"o", true)
assert("f\roo", true)
assert("fo\no", true)
}
func TestEscapeSimpleQuotedString(t *testing.T) {
require.Equal(t, "\"foo\"", EscapeSimpleQuotedString("foo"))
}
func TestEscapeSimpleQuotedStringContents(t *testing.T) {
assert := func(str string, expected string) {
require.Equal(t, expected, EscapeSimpleQuotedStringContents(str))
}
assert("foo", "foo")
assert("f\\oo", "f\\\\oo")
assert("f\\noo", "f\\\\noo")
assert("f\n o\ro", "f\\n o\\ro")
assert("fo\r\\\"o", "fo\\r\\\\\\\"o")
}
func TestUnescapeSimpleQuotedStringIfNeeded(t *testing.T) {
assert := func(str string, expectedStr string, expectedErr bool) {
actualStr, actualErr := UnescapeSimpleQuotedStringIfNeeded(str)
require.Equal(t, expectedStr, actualStr)
require.Equal(t, expectedErr, actualErr != nil)
}
assert("foo", "foo", false)
assert("\"foo\"", "foo", false)
assert("\"f\"oo\"", "", true)
}
func TestUnescapeSimpleQuotedString(t *testing.T) {
assert := func(str string, expectedStr string, expectedErr bool) {
actualStr, actualErr := UnescapeSimpleQuotedString(str)
require.Equal(t, expectedStr, actualStr)
require.Equal(t, expectedErr, actualErr != nil)
}
assert("foo", "", true)
assert("\"foo\"", "foo", false)
assert("\"f\"oo\"", "", true)
}
func TestUnescapeSimpleQuotedStringContents(t *testing.T) {
assert := func(str string, expectedStr string, expectedErr bool) {
actualStr, actualErr := UnescapeSimpleQuotedStringContents(str)
require.Equal(t, expectedStr, actualStr)
require.Equal(t, expectedErr, actualErr != nil)
}
assert("foo", "foo", false)
assert("f\\\\oo", "f\\oo", false)
assert("f\\\\noo", "f\\noo", false)
assert("f\\n o\\ro", "f\n o\ro", false)
assert("fo\\r\\\\\\\"o", "fo\r\\\"o", false)
assert("f\"oo", "", true)
assert("f\roo", "", true)
assert("f\noo", "", true)
assert("f\\oo", "", true)
}