Merge pull request 'Adds cross-verifier' (#1) from cross-verifier into main

Reviewed-on: https://git.greysoh.dev/imterah/bismuthd/pulls/1
This commit is contained in:
Tera 8 2024-10-25 16:06:30 +00:00
commit 8a0974447a
14 changed files with 1224 additions and 174 deletions

View file

@ -1,4 +1,4 @@
# Bismuth Protocol
# Bismuth Protocol [![Go Reference](https://pkg.go.dev/badge/git.greysoh.dev/imterah/bismuthd.svg)](https://pkg.go.dev/git.greysoh.dev/imterah/bismuthd)
The Bismuth protocol is a thin wrapper for any protocol that adds TLS-like features, without being TLS on its own.

View file

@ -1,67 +1,128 @@
package client
import (
"crypto/cipher"
"crypto/rand"
"fmt"
"net"
"strings"
core "git.greysoh.dev/imterah/bismuthd/commons"
"git.greysoh.dev/imterah/bismuthd/signingclient"
"golang.org/x/crypto/chacha20poly1305"
"github.com/ProtonMail/gopenpgp/v3/crypto"
)
func (bismuth BismuthClient) encryptMessage(aead cipher.AEAD, msg []byte) ([]byte, error) {
nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+len(msg)+aead.Overhead())
func computeNodes(children []*BismuthSignResultData) (int, int) {
totalServerCount := 0
passedServerCount := 0
if _, err := rand.Read(nonce); err != nil {
return []byte{}, err
for _, child := range children {
totalServerCount += 1
if child.IsTrusting {
passedServerCount += 1
}
if len(child.ChildNodes) != 0 {
recievedTotalCount, recievedPassedCount := computeNodes(child.ChildNodes)
totalServerCount += recievedTotalCount
passedServerCount += recievedPassedCount
}
}
encryptedMsg := aead.Seal(nonce, nonce, msg, nil)
return encryptedMsg, nil
return totalServerCount, passedServerCount
}
func (bismuth BismuthClient) decryptMessage(aead cipher.AEAD, encMsg []byte) ([]byte, error) {
if len(encMsg) < aead.NonceSize() {
return []byte{}, fmt.Errorf("ciphertext too short")
// Initializes the client. Should be done automatically if you call New()
//
// If you don't call client.New(), you *MUST* call this function before running bismuth.Conn().
func (bismuth *BismuthClient) InitializeClient() error {
if bismuth.pgp == nil {
bismuth.pgp = crypto.PGP()
}
// Split nonce and ciphertext.
nonce, ciphertext := encMsg[:aead.NonceSize()], encMsg[aead.NonceSize():]
// Decrypt the message and check it wasn't tampered with.
decryptedData, err := aead.Open(nil, nonce, ciphertext, nil)
if err != nil {
return []byte{}, err
if bismuth.CertificateSignChecker == nil {
bismuth.CertificateSignChecker = func(host, certificateFingerprint string, isSelfSigned bool) bool {
fmt.Println("WARNING: Using stub CertificateSignChecker. Returing true and ignoring arguments")
return true
}
}
return decryptedData, nil
if bismuth.AddCertificatesToSignCache == nil {
bismuth.AddCertificatesToSignCache = func(certificates []*BismuthCertificates) {
// do nothing
}
}
if bismuth.ConnectToServer == nil {
bismuth.CheckIfCertificatesAreSigned = true
bismuth.ConnectToServer = func(address string) (net.Conn, error) {
return net.Dial("tcp", address)
}
}
bismuth.CheckIfCertificatesAreSigned = true
return nil
}
// Bismuth Client
type BismuthClient struct {
// GOpenPGP public key
PublicKey *crypto.Key
// GOpenPGP private key
PrivateKey *crypto.Key
func (bismuth *BismuthClient) checkIfDomainIsTrusted(servers, advertisedDomains []string) ([]*BismuthSignResultData, error) {
signResultData := make([]*BismuthSignResultData, len(servers))
pgp *crypto.PGPHandle
for index, server := range servers {
baseConn, err := bismuth.ConnectToServer(server)
if err != nil {
return signResultData, err
}
defer baseConn.Close()
conn, signResultsForConn, err := bismuth.Conn(baseConn)
if err != nil {
return signResultData, err
}
isTrusted, err := signingclient.IsDomainTrusted(conn, signResultsForConn.ServerPublicKey.GetFingerprintBytes(), advertisedDomains)
if signResultsForConn.OverallTrustScore < 50 {
isTrusted = false
}
signResultData[index] = &BismuthSignResultData{
IsTrusting: isTrusted,
ChildNodes: []*BismuthSignResultData{
signResultsForConn.Node,
},
}
}
return signResultData, nil
}
// Connects to a Bismuth server. This wraps an existing net.Conn interface.
// The returned net.Conn is the server, but over bismuth.
func (bismuth BismuthClient) Conn(conn net.Conn) (net.Conn, error) {
func (bismuth *BismuthClient) Conn(conn net.Conn) (net.Conn, *BismuthSignResults, error) {
// Yes, I'm aware defer exists. It won't work if I use it in this context. I'll shank anyone that complains
// Exchange our public keys first
hostAndIP := conn.RemoteAddr().String()
hostAndIPColonIndex := strings.Index(hostAndIP, ":")
if hostAndIPColonIndex == -1 {
return nil, nil, fmt.Errorf("failed to get colon in remote address")
}
host := hostAndIP[:hostAndIPColonIndex]
ownKey, err := bismuth.PublicKey.GetPublicKey()
if err != nil {
conn.Close()
return nil, err
return nil, nil, err
}
pubKeyLengthBytes := make([]byte, 3)
@ -76,17 +137,17 @@ func (bismuth BismuthClient) Conn(conn net.Conn) (net.Conn, error) {
if _, err = conn.Read(messageMode); err != nil {
conn.Close()
return nil, err
return nil, nil, err
}
if messageMode[0] != core.SendPublicKey {
conn.Close()
return nil, fmt.Errorf("server failed to return its public key")
return nil, nil, fmt.Errorf("server failed to return its public key")
}
if _, err = conn.Read(pubKeyLengthBytes); err != nil {
conn.Close()
return nil, err
return nil, nil, err
}
pubKeyLength = core.Int24ToInt32(pubKeyLengthBytes)
@ -94,12 +155,14 @@ func (bismuth BismuthClient) Conn(conn net.Conn) (net.Conn, error) {
if _, err = conn.Read(pubKeyBytes); err != nil {
conn.Close()
return nil, err
return nil, nil, err
}
if _, err = crypto.NewKey(pubKeyBytes); err != nil {
serverPublicKey, err := crypto.NewKey(pubKeyBytes)
if err != nil {
conn.Close()
return nil, err
return nil, nil, err
}
// Then exchange the symmetric key
@ -108,19 +171,19 @@ func (bismuth BismuthClient) Conn(conn net.Conn) (net.Conn, error) {
if _, err = conn.Read(messageMode); err != nil {
conn.Close()
return nil, err
return nil, nil, err
}
if messageMode[0] != core.SwitchToSymmetricKey {
conn.Close()
return nil, fmt.Errorf("server failed to return symmetric key")
return nil, nil, fmt.Errorf("server failed to return symmetric key")
}
encryptedSymmKeyLengthInBytes := make([]byte, 3)
if _, err = conn.Read(encryptedSymmKeyLengthInBytes); err != nil {
conn.Close()
return nil, err
return nil, nil, err
}
encryptedSymmKeyLength := core.Int24ToInt32(encryptedSymmKeyLengthInBytes)
@ -128,31 +191,186 @@ func (bismuth BismuthClient) Conn(conn net.Conn) (net.Conn, error) {
if _, err = conn.Read(encryptedSymmKey); err != nil {
conn.Close()
return nil, err
return nil, nil, err
}
decHandleForSymmKey, err := bismuth.pgp.Decryption().DecryptionKey(bismuth.PrivateKey).New()
if err != nil {
return nil, err
conn.Close()
return nil, nil, err
}
decryptedSymmKey, err := decHandleForSymmKey.Decrypt(encryptedSymmKey, crypto.Bytes)
if err != nil {
return nil, err
conn.Close()
return nil, nil, err
}
symmKeyInfo := decryptedSymmKey.Bytes()
if symmKeyInfo[0] != core.XChaCha20Poly1305 {
conn.Close()
return nil, fmt.Errorf("unsupported encryption method recieved")
return nil, nil, fmt.Errorf("unsupported encryption method recieved")
}
symmKey := symmKeyInfo[1 : chacha20poly1305.KeySize+1]
aead, err := chacha20poly1305.NewX(symmKey)
// Request trusted domains
trustedDomainsRequest := make([]byte, 1)
trustedDomainsRequest[0] = core.GetTrustedDomains
encryptedTrustedDomainRequest, err := bismuth.encryptMessage(aead, trustedDomainsRequest)
if err != nil {
conn.Close()
return nil, nil, err
}
trustedDomainLength := make([]byte, 3)
core.Int32ToInt24(trustedDomainLength, uint32(len(encryptedTrustedDomainRequest)))
conn.Write(trustedDomainLength)
conn.Write(encryptedTrustedDomainRequest)
if _, err = conn.Read(trustedDomainLength); err != nil {
conn.Close()
return nil, nil, err
}
encryptedTrustedDomainResponse := make([]byte, core.Int24ToInt32(trustedDomainLength))
if _, err = conn.Read(encryptedTrustedDomainResponse); err != nil {
conn.Close()
return nil, nil, err
}
trustedDomainResponse, err := bismuth.decryptMessage(aead, encryptedTrustedDomainResponse)
if err != nil {
conn.Close()
return nil, nil, err
}
if trustedDomainResponse[0] != core.GetTrustedDomains {
conn.Close()
return nil, nil, fmt.Errorf("server failed to return its signing servers")
}
trustedDomains := strings.Split(string(trustedDomainResponse[1:]), "\n")
// Request signing servers
signingServerRequest := make([]byte, 1)
signingServerRequest[0] = core.GetSigningServers
encryptedSigningServerRequest, err := bismuth.encryptMessage(aead, signingServerRequest)
if err != nil {
conn.Close()
return nil, nil, err
}
signingRequestLength := make([]byte, 3)
core.Int32ToInt24(signingRequestLength, uint32(len(encryptedSigningServerRequest)))
conn.Write(signingRequestLength)
conn.Write(encryptedSigningServerRequest)
if _, err = conn.Read(signingRequestLength); err != nil {
conn.Close()
return nil, nil, err
}
encryptedSigningRequestResponse := make([]byte, core.Int24ToInt32(signingRequestLength))
if _, err = conn.Read(encryptedSigningRequestResponse); err != nil {
conn.Close()
return nil, nil, err
}
signingServerResponse, err := bismuth.decryptMessage(aead, encryptedSigningRequestResponse)
if err != nil {
conn.Close()
return nil, nil, err
}
if signingServerResponse[0] != core.GetSigningServers {
conn.Close()
return nil, nil, fmt.Errorf("server failed to return its signing servers")
}
// Check if the server is signed
signingServers := strings.Split(string(signingServerResponse[1:]), "\n")
isServerSelfSigned := len(signingServers)-1 == 0 || len(trustedDomains)-1 == 0
rootNode := &BismuthSignResultData{
ChildNodes: []*BismuthSignResultData{},
IsTrusting: false,
}
signResults := BismuthSignResults{
OverallTrustScore: 0,
ServerPublicKey: serverPublicKey,
Node: rootNode,
}
totalServerCount, passedServerCount := 0, 0
if bismuth.CheckIfCertificatesAreSigned {
serverKeyFingerprint := serverPublicKey.GetFingerprint()
isCertSigned := bismuth.CertificateSignChecker(host, serverKeyFingerprint, isServerSelfSigned)
if !isServerSelfSigned || !isCertSigned {
domainTrustResults, err := bismuth.checkIfDomainIsTrusted(signingServers, trustedDomains)
if err == nil {
rootNode.ChildNodes = domainTrustResults
totalServerCount, passedServerCount = computeNodes(rootNode.ChildNodes)
} else {
fmt.Printf("ERROR: failed to verify servers (%s).\n", err.Error())
signResults.OverallTrustScore = 0
}
} else if isCertSigned {
rootNode.IsTrusting = isCertSigned
totalServerCount, passedServerCount = 1, 1
rootNode.IsTrusting = true
}
} else {
totalServerCount, passedServerCount = 1, 1
}
if totalServerCount != 0 {
signResults.OverallTrustScore = int((float32(passedServerCount) / float32(totalServerCount)) * 100)
}
// After that, we send what host we are connecting to (enables fronting/proxy services)
hostInformation := make([]byte, 1+len(host))
hostInformation[0] = core.ClientSendHost
copy(hostInformation[1:], []byte(host))
encryptedHostInformationPacket, err := bismuth.encryptMessage(aead, hostInformation)
if err != nil {
conn.Close()
return nil, nil, err
}
hostInformationSize := make([]byte, 3)
core.Int32ToInt24(hostInformationSize, uint32(len(encryptedHostInformationPacket)))
conn.Write(hostInformationSize)
conn.Write(encryptedHostInformationPacket)
// Start proxying
startForwardingPacket := []byte{
@ -163,7 +381,7 @@ func (bismuth BismuthClient) Conn(conn net.Conn) (net.Conn, error) {
if err != nil {
conn.Close()
return nil, err
return nil, nil, err
}
encryptedForwardPacketPacketSize := make([]byte, 3)
@ -185,32 +403,5 @@ func (bismuth BismuthClient) Conn(conn net.Conn) (net.Conn, error) {
return core.BismuthConnWrapped{
Bismuth: &bmConn,
}, nil
}
// Creates a new BismuthClient.
//
// Both `pubKey` and `privKey` are armored PGP public and private keys respectively.
func New(pubKey string, privKey string) (*BismuthClient, error) {
publicKey, err := crypto.NewKeyFromArmored(pubKey)
if err != nil {
return nil, err
}
privateKey, err := crypto.NewKeyFromArmored(privKey)
if err != nil {
return nil, err
}
pgp := crypto.PGP()
bismuth := BismuthClient{
PublicKey: publicKey,
PrivateKey: privateKey,
pgp: pgp,
}
return &bismuth, nil
}, &signResults, nil
}

93
client/typing.go Normal file
View file

@ -0,0 +1,93 @@
package client
import (
"net"
"github.com/ProtonMail/gopenpgp/v3/crypto"
)
// Bismuth Client
type BismuthClient struct {
// GOpenPGP public key for the client
PublicKey *crypto.Key
// GOpenPGP private key for the client
PrivateKey *crypto.Key
// Check if the certificates are signed if enabled.
//
// If true, "cross-verifies" the server to make sure the certificates are signed.
//
// If false, all certificates will be reported as being self signed because we can't
// really prove otherwise.
CheckIfCertificatesAreSigned bool
// Checks to see if a certificate is trusted in the client cache.
//
// - `host`: The host of the server.
// - `certificateFingerprint`: A fingerprint of the servers key.
// - `isSelfSigned`: If true, the certificate is either actually self-signed, or
// verification is dsabled (CheckIfCertificatesAreSigned in BismuthClient is false)
//
// This function will only be called if client.CheckIfCertificatesAreSigned is true.
//
// Example usage inside the Bismuth client source:
// client.CertificateSignChecker("example.com:9090", "6c5eaff6f5c65e65e6f6ce6fc", false, true)
CertificateSignChecker func(host, certificateFingerprint string, isSelfSigned bool) bool
// If any certificates are false in the certificate cache, and the client has determined that
// they may need to be added, this function will get called.
//
// All of the certificates that will be called by this function in arguments are ones that
// client.CertificateSignChecker has reported to be untrustworthy, but not all untrustworthy
// certificates will be reported, as they can be trusted by future nodes that you have already
// trusted.
//
// This function will only be called if client.CheckIfCertificatesAreSigned is true.
AddCertificatesToSignCache func(certificates []*BismuthCertificates)
// Connects to a server.
// This function will only be called if client.CheckIfCertificatesAreSigned is true.
//
// Example usage in the client source:
// client.ConnectToServer("google.com:80")
ConnectToServer func(address string) (net.Conn, error)
// GopenPGP instance
pgp *crypto.PGPHandle
}
// Sign result data for the node
type BismuthSignResultData struct {
// Future node pointers in the tree
ChildNodes []*BismuthSignResultData
// If true, the server is already trusting this node
IsTrusting bool
}
type BismuthSignResults struct {
// Overall trust score calculated
OverallTrustScore int
// Parent node in tree for sign results
Node *BismuthSignResultData
// GopenPGP public key
ServerPublicKey *crypto.Key
}
type BismuthCertificates struct {
// The host of the server
Host string
// A fingerprint of the servers key
CertificateFingerprint string
// Certificate UserID
CertificateUsername string
CertificateMail string
// If true, the certificate is self signed
IsSelfSigned bool
// If true, the client should not prompt the user, and automatically
// add the certificate instead.
ShouldAutomaticallyAdd bool
}

68
client/utils.go Normal file
View file

@ -0,0 +1,68 @@
package client
import (
"crypto/cipher"
"crypto/rand"
"fmt"
"github.com/ProtonMail/gopenpgp/v3/crypto"
)
func (bismuth BismuthClient) encryptMessage(aead cipher.AEAD, msg []byte) ([]byte, error) {
nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+len(msg)+aead.Overhead())
if _, err := rand.Read(nonce); err != nil {
return []byte{}, err
}
encryptedMsg := aead.Seal(nonce, nonce, msg, nil)
return encryptedMsg, nil
}
func (bismuth BismuthClient) decryptMessage(aead cipher.AEAD, encMsg []byte) ([]byte, error) {
if len(encMsg) < aead.NonceSize() {
return []byte{}, fmt.Errorf("ciphertext too short")
}
// Split nonce and ciphertext.
nonce, ciphertext := encMsg[:aead.NonceSize()], encMsg[aead.NonceSize():]
// Decrypt the message and check it wasn't tampered with.
decryptedData, err := aead.Open(nil, nonce, ciphertext, nil)
if err != nil {
return []byte{}, err
}
return decryptedData, nil
}
// Creates a new BismuthClient.
//
// Both `pubKey` and `privKey` are armored PGP public and private keys respectively.
func New(pubKey string, privKey string) (*BismuthClient, error) {
publicKey, err := crypto.NewKeyFromArmored(pubKey)
if err != nil {
return nil, err
}
privateKey, err := crypto.NewKeyFromArmored(privKey)
if err != nil {
return nil, err
}
bismuth := BismuthClient{
PublicKey: publicKey,
PrivateKey: privateKey,
}
err = bismuth.InitializeClient()
if err != nil {
return nil, err
}
return &bismuth, nil
}

View file

@ -2,32 +2,41 @@
package commons
// Commands
// Core Protocol Commands
const (
// SendPublicKey: Sending public key back and forth
// Sending public key back and forth
SendPublicKey = iota
// SwitchToSymmetricKey: Sent by the client along with the symmetric key that is going to be used
// Sent by the client along with the symmetric key that is going to be used
SwitchToSymmetricKey
// ClientSendHost: Currently unimplemented.
// Client sends what host they are connecting to.
ClientSendHost
// GetSigningServers: Currently unimplemented.
// Gets the signing servers trusting/signing the current server.
GetSigningServers
// GetTrustedDomains: Currently unimplemented.
// Gets the domains that are supported by this certificate (should be cross-checked)
GetTrustedDomains
// InitiateForwarding: Starts forwarding traffic over this protocol.
// Starts forwarding traffic over this protocol.
InitiateForwarding
)
// Encryption algorithms
// Validation API Commands
const (
// Checks if the domains are valid for a specified key
AreDomainsValidForKey = iota
// Validate a server and keys
ValidateKey
// Status codes
Success
Failure
InternalError
)
// Encryption Algorithms
const (
// Default and only encryption algorithm
XChaCha20Poly1305 = iota
)
// Unsigned integer limits
// Unsigned Integer Limits
const (
BitLimit24 = 16_777_215
BitLimit16 = 65535

330
main.go
View file

@ -3,6 +3,7 @@ package main
import (
"context"
_ "embed"
"encoding/hex"
"fmt"
"net"
"os"
@ -11,6 +12,8 @@ import (
"git.greysoh.dev/imterah/bismuthd/client"
core "git.greysoh.dev/imterah/bismuthd/commons"
"git.greysoh.dev/imterah/bismuthd/server"
"git.greysoh.dev/imterah/bismuthd/signingclient"
"git.greysoh.dev/imterah/bismuthd/signingserver"
"github.com/charmbracelet/log"
"github.com/urfave/cli/v2"
"tailscale.com/net/socks5"
@ -19,7 +22,7 @@ import (
//go:embed ascii.txt
var asciiArt string
func bismuthClientEntrypoint(cCtx *cli.Context) error {
func clientEntrypoint(cCtx *cli.Context) error {
pubKeyFile, err := os.ReadFile(cCtx.String("pubkey"))
if err != nil {
@ -37,6 +40,8 @@ func bismuthClientEntrypoint(cCtx *cli.Context) error {
bismuth, err := client.New(pubKey, privKey)
log.Debugf("My key fingerprint is: %s", bismuth.PublicKey.GetFingerprint())
if err != nil {
return err
}
@ -82,13 +87,15 @@ func bismuthClientEntrypoint(cCtx *cli.Context) error {
return nil, err
}
conn, err = bismuth.Conn(conn)
conn, returnData, err := bismuth.Conn(conn)
if err != nil && err.Error() != "EOF" {
log.Errorf("failed to initialize bismuth connection to '%s:%s': '%s'", ip, port, err.Error())
return nil, err
}
log.Debugf("Server key fingerprint for '%s' is: %s", addr, returnData.ServerPublicKey.GetFingerprint())
return conn, err
} else {
conn, err := net.Dial(network, addr)
@ -110,8 +117,17 @@ func bismuthClientEntrypoint(cCtx *cli.Context) error {
return nil
}
func bismuthServerEntrypoint(cCtx *cli.Context) error {
relayServers := []string{}
func serverEntrypoint(cCtx *cli.Context) error {
var domainList []string
var signServers []string
if cCtx.String("domain-names") != "" {
domainList = strings.Split(cCtx.String("domain-names"), ":")
}
if cCtx.String("signing-servers") != "" {
signServers = strings.Split(cCtx.String("signing-servers"), ";")
}
pubKeyFile, err := os.ReadFile(cCtx.String("pubkey"))
@ -130,7 +146,8 @@ func bismuthServerEntrypoint(cCtx *cli.Context) error {
network := fmt.Sprintf("%s:%s", cCtx.String("source-ip"), cCtx.String("source-port"))
bismuth, err := server.NewBismuthServer(pubKey, privKey, relayServers, core.XChaCha20Poly1305, func(connBismuth net.Conn) error {
bismuth, err := server.New(pubKey, privKey, signServers, domainList, core.XChaCha20Poly1305)
bismuth.HandleConnection = func(connBismuth net.Conn, _ *server.ClientMetadata) error {
connDialed, err := net.Dial("tcp", network)
if err != nil {
@ -183,7 +200,7 @@ func bismuthServerEntrypoint(cCtx *cli.Context) error {
}()
return nil
})
}
if err != nil {
return err
@ -201,23 +218,206 @@ func bismuthServerEntrypoint(cCtx *cli.Context) error {
for {
conn, err := listener.Accept()
if err != nil {
log.Warnf("failed to accept connection: '%s'", err.Error())
continue
}
log.Debugf("Recieved connection from '%s'", conn.RemoteAddr().String())
go func() {
err := bismuth.HandleProxy(conn)
if err != nil && err.Error() != "EOF" {
log.Warnf("connection crashed/dropped during proxy handling: '%s'", err.Error())
}
}()
}
}
func signingServerEntrypoint(cCtx *cli.Context) error {
log.Warn("Using the built-in bismuth signing server in production is a horrible idea as it has no validation!")
log.Warn("Consider writing using a custom solution that's based on the signing server code, rather than the default implementation.")
var domainList []string
var signServers []string
if cCtx.String("domain-names") != "" {
domainList = strings.Split(cCtx.String("domain-names"), ":")
}
if cCtx.String("signing-servers") != "" {
signServers = strings.Split(cCtx.String("signing-servers"), ";")
}
pubKeyFile, err := os.ReadFile(cCtx.String("pubkey"))
if err != nil {
return err
}
privKeyFile, err := os.ReadFile(cCtx.String("privkey"))
if err != nil {
return err
}
pubKey := string(pubKeyFile)
privKey := string(privKeyFile)
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%s", cCtx.String("ip"), cCtx.String("port")))
if err != nil {
return err
}
bismuthServer, err := server.New(pubKey, privKey, signServers, domainList, core.XChaCha20Poly1305)
if err != nil {
return nil
}
// I'd like to use the SigningServer struct, but I can't really do that
_, err = signingserver.New(bismuthServer)
if err != nil {
return nil
}
defer listener.Close()
log.Info("Bismuth signing server is listening...")
for {
conn, err := listener.Accept()
if err != nil {
log.Warn(err.Error())
continue
}
log.Debugf("Recieved connection from '%s'", conn.RemoteAddr().String())
go func() {
err := bismuth.HandleProxy(conn)
err = bismuthServer.HandleProxy(conn)
if err != nil && err.Error() != "EOF" {
log.Warnf("Connection crashed/dropped during proxy handling: '%s'", err.Error())
log.Warnf("connection crashed/dropped during proxy handling: '%s'", err.Error())
return
}
}()
}
}
func verifyCert(cCtx *cli.Context) error {
domainList := strings.Split(cCtx.String("domain-names"), ":")
pubKeyFile, err := os.ReadFile(cCtx.String("pubkey"))
if err != nil {
return err
}
privKeyFile, err := os.ReadFile(cCtx.String("privkey"))
if err != nil {
return err
}
pubKey := string(pubKeyFile)
privKey := string(privKeyFile)
bismuthClient, err := client.New(pubKey, privKey)
if err != nil {
return err
}
dialedConn, err := net.Dial("tcp", cCtx.String("signing-server"))
if err != nil {
return err
}
conn, certResults, err := bismuthClient.Conn(dialedConn)
if err != nil {
return err
}
if certResults.OverallTrustScore < 50 {
return fmt.Errorf("overall trust score is below 50 percent for certificate")
}
fmt.Println("Sending signing request to sign server...")
hasBeenTrusted, err := signingclient.RequestDomainToBeTrusted(conn, domainList, "")
if hasBeenTrusted {
fmt.Println("Server has been successfully signed.")
} else {
fmt.Println("Server has not been successfully signed.")
os.Exit(1)
}
return nil
}
func signCert(cCtx *cli.Context) error {
domainList := strings.Split(cCtx.String("domain-names"), ":")
keyFingerprint, err := hex.DecodeString(cCtx.String("key-fingerprint"))
if err != nil {
return err
}
pubKeyFile, err := os.ReadFile(cCtx.String("pubkey"))
if err != nil {
return err
}
privKeyFile, err := os.ReadFile(cCtx.String("privkey"))
if err != nil {
return err
}
pubKey := string(pubKeyFile)
privKey := string(privKeyFile)
bismuthClient, err := client.New(pubKey, privKey)
if err != nil {
return err
}
dialedConn, err := net.Dial("tcp", cCtx.String("signing-server"))
if err != nil {
return err
}
conn, certResults, err := bismuthClient.Conn(dialedConn)
if err != nil {
return err
}
if certResults.OverallTrustScore < 50 {
return fmt.Errorf("overall trust score is below 50 percent for certificate")
}
isTrusted, err := signingclient.IsDomainTrusted(conn, keyFingerprint, domainList)
fmt.Printf("Certificate trust status: %t\n", isTrusted)
if !isTrusted {
os.Exit(1)
}
return nil
}
func main() {
fmt.Println(asciiArt)
fmt.Print("Implementation of the Bismuth protocol\n\n")
@ -282,7 +482,7 @@ func main() {
},
},
Usage: "client for the Bismuth protocol",
Action: bismuthClientEntrypoint,
Action: clientEntrypoint,
},
{
Name: "server",
@ -308,6 +508,14 @@ func main() {
Usage: "port to connect to",
Required: true,
},
&cli.StringFlag{
Name: "signing-servers",
Usage: "servers trusting/\"signing\" the public key. seperated using semicolons",
},
&cli.StringFlag{
Name: "domain-names",
Usage: "domain names the key is authorized to use. seperated using colons",
},
&cli.StringFlag{
Name: "dest-ip",
Usage: "IP to listen on",
@ -320,7 +528,109 @@ func main() {
},
},
Usage: "server for the Bismuth protocol",
Action: bismuthServerEntrypoint,
Action: serverEntrypoint,
},
{
Name: "test-sign-server",
Aliases: []string{"tss"},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "pubkey",
Usage: "path to PGP public key",
Required: true,
},
&cli.StringFlag{
Name: "privkey",
Usage: "path to PGP private key",
Required: true,
},
&cli.StringFlag{
Name: "ip",
Usage: "IP to listen on",
Value: "0.0.0.0",
},
&cli.StringFlag{
Name: "port",
Usage: "port to listen on",
Value: "9090",
},
&cli.StringFlag{
Name: "signing-servers",
Usage: "servers trusting/\"signing\" the public key. seperated using semicolons",
},
&cli.StringFlag{
Name: "domain-names",
Usage: "domain names the key is authorized to use. seperated using colons",
},
},
Usage: "test signing server for the Bismuth protocol",
Action: signingServerEntrypoint,
},
{
Name: "sign-tool",
Aliases: []string{"st"},
Subcommands: []*cli.Command{
{
Name: "is-verified",
Aliases: []string{"i", "iv", "cv"},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "key-fingerprint",
Usage: "fingerprint of key",
Required: true,
},
&cli.StringFlag{
Name: "pubkey",
Usage: "path to PGP public key",
},
&cli.StringFlag{
Name: "privkey",
Usage: "path to PGP private key",
},
&cli.StringFlag{
Name: "domain-names",
Usage: "domain names the key is authorized to use. seperated using colons",
Required: true,
},
&cli.StringFlag{
Name: "signing-server",
Usage: "signing server to use",
Required: true,
},
},
Usage: "check if a certificate is verified for Bismuth",
Action: signCert,
},
{
Name: "verify-cert",
Aliases: []string{"v", "vc"},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "pubkey",
Usage: "path to PGP public key",
Required: true,
},
&cli.StringFlag{
Name: "privkey",
Usage: "path to PGP private key",
Required: true,
},
&cli.StringFlag{
Name: "domain-names",
Usage: "domain names the key is authorized to use. seperated using colons",
Required: true,
},
&cli.StringFlag{
Name: "signing-server",
Usage: "signing server to use",
Required: true,
},
},
Usage: "verifies certificate for Bismuth",
Action: verifyCert,
},
},
Usage: "signing tool for Bismuth",
},
},
}

View file

@ -13,54 +13,6 @@ import (
"golang.org/x/crypto/chacha20poly1305"
)
// Bismuth Server
type BismuthServer struct {
// Public key to use for transmission
PublicKey *crypto.Key
// Private key to use for transmission
PrivateKey *crypto.Key
pgp *crypto.PGPHandle
// Algorithm to use for encryption (currently XChaCha20Poly1305 is the only option)
SymmetricEncryptionAlgorithm int
// Servers that are signing this server. If none, this server becomes self-signed
// in the clients eyes
SigningServers []string
// Called after a successful handshake & connection.
HandleConnection func(conn net.Conn) error
}
func (bismuth BismuthServer) encryptMessage(aead cipher.AEAD, msg []byte) ([]byte, error) {
nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+len(msg)+aead.Overhead())
if _, err := rand.Read(nonce); err != nil {
return []byte{}, err
}
encryptedMsg := aead.Seal(nonce, nonce, msg, nil)
return encryptedMsg, nil
}
func (bismuth BismuthServer) decryptMessage(aead cipher.AEAD, encMsg []byte) ([]byte, error) {
if len(encMsg) < aead.NonceSize() {
return []byte{}, fmt.Errorf("ciphertext too short")
}
// Split nonce and ciphertext.
nonce, ciphertext := encMsg[:aead.NonceSize()], encMsg[aead.NonceSize():]
// Decrypt the message and check it wasn't tampered with.
decryptedData, err := aead.Open(nil, nonce, ciphertext, nil)
if err != nil {
return []byte{}, err
}
return decryptedData, nil
}
// Called to handle a connnection for Bismuth. The conn argument is the client you'd like to handle
func (bismuth BismuthServer) HandleProxy(conn net.Conn) error {
serverState := "keyHandshake"
@ -211,7 +163,55 @@ func (bismuth BismuthServer) HandleProxy(conn net.Conn) error {
return err
}
if packet[0] == core.InitiateForwarding {
switch packet[0] {
case core.GetSigningServers:
totalPacketContents := make([]byte, 1)
totalPacketContents[0] = core.GetSigningServers
for index, signServer := range bismuth.SigningServers {
totalPacketContents = append(totalPacketContents, []byte(signServer)...)
if index+1 != len(bismuth.SigningServers) {
totalPacketContents = append(totalPacketContents, '\n')
}
}
encryptedPacket, err := bismuth.encryptMessage(aead, totalPacketContents)
if err != nil {
return err
}
encryptedPacketLength := make([]byte, 3)
core.Int32ToInt24(encryptedPacketLength, uint32(len(encryptedPacket)))
conn.Write(encryptedPacketLength)
conn.Write(encryptedPacket)
case core.GetTrustedDomains:
totalPacketContents := make([]byte, 1)
totalPacketContents[0] = core.GetTrustedDomains
for index, trustedDomain := range bismuth.TrustedDomains {
fmt.Println("building trusted domains")
totalPacketContents = append(totalPacketContents, []byte(trustedDomain)...)
if index+1 != len(bismuth.TrustedDomains) {
totalPacketContents = append(totalPacketContents, '\n')
}
}
encryptedPacket, err := bismuth.encryptMessage(aead, totalPacketContents)
if err != nil {
return err
}
encryptedPacketLength := make([]byte, 3)
core.Int32ToInt24(encryptedPacketLength, uint32(len(encryptedPacket)))
conn.Write(encryptedPacketLength)
conn.Write(encryptedPacket)
case core.InitiateForwarding:
bmConn := core.BismuthConn{
Aead: aead,
PassedConn: conn,
@ -220,42 +220,16 @@ func (bismuth BismuthServer) HandleProxy(conn net.Conn) error {
bmConn.DoInitSteps()
metadata := ClientMetadata{
ClientPublicKey: clientPublicKey,
}
err := bismuth.HandleConnection(core.BismuthConnWrapped{
Bismuth: &bmConn,
})
}, &metadata)
return err
}
}
}
}
// Initializes a Bismuth server.
//
// Both `pubKey` and `privKey` are armored PGP public and private keys respectively.
func NewBismuthServer(pubKey string, privKey string, signServers []string, encryptionAlgo int, connHandler func(conn net.Conn) error) (*BismuthServer, error) {
publicKey, err := crypto.NewKeyFromArmored(pubKey)
if err != nil {
return nil, err
}
privateKey, err := crypto.NewKeyFromArmored(privKey)
if err != nil {
return nil, err
}
pgp := crypto.PGP()
bismuth := BismuthServer{
PublicKey: publicKey,
PrivateKey: privateKey,
HandleConnection: connHandler,
SigningServers: signServers,
SymmetricEncryptionAlgorithm: encryptionAlgo,
pgp: pgp,
}
return &bismuth, nil
}

36
server/typing.go Normal file
View file

@ -0,0 +1,36 @@
package server
import (
"net"
"github.com/ProtonMail/gopenpgp/v3/crypto"
)
// Bismuth Server
type BismuthServer struct {
// Public key to use for transmission
PublicKey *crypto.Key
// Private key to use for transmission
PrivateKey *crypto.Key
// GopenPGP instance
pgp *crypto.PGPHandle
// Algorithm to use for encryption (currently XChaCha20Poly1305 is the only option)
SymmetricEncryptionAlgorithm int
// Servers that are signing this server. If none, this server becomes self-signed
// in the clients eyes.
SigningServers []string
// Domains that the certificate is authorized to use. This will be checked by the
// signing servers.
TrustedDomains []string
// Called after a successful handshake & connection.
HandleConnection func(conn net.Conn, metadata *ClientMetadata) error
}
// Metadata from the client that may be helpful for the server to have.
type ClientMetadata struct {
// Client's public key
ClientPublicKey *crypto.Key
}

68
server/utils.go Normal file
View file

@ -0,0 +1,68 @@
package server
import (
"crypto/cipher"
"crypto/rand"
"fmt"
"github.com/ProtonMail/gopenpgp/v3/crypto"
)
func (bismuth BismuthServer) encryptMessage(aead cipher.AEAD, msg []byte) ([]byte, error) {
nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+len(msg)+aead.Overhead())
if _, err := rand.Read(nonce); err != nil {
return []byte{}, err
}
encryptedMsg := aead.Seal(nonce, nonce, msg, nil)
return encryptedMsg, nil
}
func (bismuth BismuthServer) decryptMessage(aead cipher.AEAD, encMsg []byte) ([]byte, error) {
if len(encMsg) < aead.NonceSize() {
return []byte{}, fmt.Errorf("ciphertext too short")
}
// Split nonce and ciphertext.
nonce, ciphertext := encMsg[:aead.NonceSize()], encMsg[aead.NonceSize():]
// Decrypt the message and check it wasn't tampered with.
decryptedData, err := aead.Open(nil, nonce, ciphertext, nil)
if err != nil {
return []byte{}, err
}
return decryptedData, nil
}
// Initializes a Bismuth server.
//
// Both `pubKey` and `privKey` are armored PGP public and private keys respectively.
func New(pubKey string, privKey string, signServers []string, trustedDomains []string, encryptionAlgo int) (*BismuthServer, error) {
publicKey, err := crypto.NewKeyFromArmored(pubKey)
if err != nil {
return nil, err
}
privateKey, err := crypto.NewKeyFromArmored(privKey)
if err != nil {
return nil, err
}
pgp := crypto.PGP()
bismuth := BismuthServer{
PublicKey: publicKey,
PrivateKey: privateKey,
SigningServers: signServers,
TrustedDomains: trustedDomains,
SymmetricEncryptionAlgorithm: encryptionAlgo,
pgp: pgp,
}
return &bismuth, nil
}

View file

@ -0,0 +1,69 @@
package signingclient
import (
"encoding/binary"
"net"
"strings"
core "git.greysoh.dev/imterah/bismuthd/commons"
)
func IsDomainTrusted(conn net.Conn, keyFingerprint []byte, domainList []string) (bool, error) {
domainListAsString := strings.Join(domainList, "\n")
keyFingerprintSize := len(keyFingerprint)
domainListSize := len(domainListAsString)
domainTrustedCommand := make([]byte, 1+2+2+keyFingerprintSize+domainListSize)
domainTrustedCommand[0] = core.AreDomainsValidForKey
currentOffset := 1
binary.BigEndian.PutUint16(domainTrustedCommand[currentOffset:currentOffset+2], uint16(keyFingerprintSize))
copy(domainTrustedCommand[2+currentOffset:2+currentOffset+keyFingerprintSize], keyFingerprint)
currentOffset += 2 + keyFingerprintSize
binary.BigEndian.PutUint16(domainTrustedCommand[currentOffset:currentOffset+2], uint16(domainListSize))
copy(domainTrustedCommand[2+currentOffset:2+currentOffset+domainListSize], []byte(domainListAsString))
conn.Write(domainTrustedCommand)
requestResponse := make([]byte, 1)
if _, err := conn.Read(requestResponse); err != nil {
return false, err
}
return requestResponse[0] == core.Success, nil
}
func RequestDomainToBeTrusted(conn net.Conn, domainList []string, additionalInformation string) (bool, error) {
domainListAsString := strings.Join(domainList, "\n")
domainListSize := len(domainListAsString)
additionalInfoSize := len(additionalInformation)
requestDomainTrust := make([]byte, 1+2+2+domainListSize+additionalInfoSize)
requestDomainTrust[0] = core.ValidateKey
currentOffset := 1
binary.BigEndian.PutUint16(requestDomainTrust[currentOffset:currentOffset+2], uint16(domainListSize))
copy(requestDomainTrust[2+currentOffset:2+currentOffset+domainListSize], []byte(domainListAsString))
currentOffset += 2 + domainListSize
binary.BigEndian.PutUint16(requestDomainTrust[currentOffset:currentOffset+2], uint16(additionalInfoSize))
copy(requestDomainTrust[2:currentOffset:2+currentOffset+additionalInfoSize], []byte(additionalInformation))
conn.Write(requestDomainTrust)
requestResponse := make([]byte, 1)
if _, err := conn.Read(requestResponse); err != nil {
return false, err
}
return requestResponse[0] == core.Success, nil
}

View file

@ -0,0 +1,198 @@
package signingserver
import (
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"fmt"
"net"
"strings"
core "git.greysoh.dev/imterah/bismuthd/commons"
"git.greysoh.dev/imterah/bismuthd/server"
)
func (signServer *BismuthSigningServer) InitializeServer() error {
if signServer.AddVerifyHandler == nil {
fmt.Println("WARN: You are using the default AddVerifyHandler in SignServer! This is a bad idea. Please write your own implementation!")
signServer.AddVerifyHandler = func(serverAddr string, serverKeyFingerprint string, serverDomainList []string, additionalClientProvidedInfo string) (bool, error) {
domainListHash := sha256.Sum256([]byte(strings.Join(serverDomainList, ":")))
signServer.builtInVerifyMapStore[serverAddr+".fingerprint"] = serverKeyFingerprint
signServer.builtInVerifyMapStore[serverAddr+".domainListHash"] = hex.EncodeToString(domainListHash[:])
return true, nil
}
}
if signServer.VerifyServerHandler == nil {
fmt.Println("WARN: You are using the default VerifyServerHandler in SignServer! This is a bad idea. Please write your own implementation!")
signServer.VerifyServerHandler = func(serverAddr string, serverKeyFingerprint string, serverDomainList []string) (bool, error) {
domainListHash := sha256.Sum256([]byte(strings.Join(serverDomainList, ":")))
domainListHashHex := hex.EncodeToString(domainListHash[:])
if storedKeyFingerprint, ok := signServer.builtInVerifyMapStore[serverAddr+".fingerprint"]; ok {
if storedKeyFingerprint != serverKeyFingerprint {
return false, nil
}
} else {
return false, nil
}
if storedDomainListHashHex, ok := signServer.builtInVerifyMapStore[serverAddr+".domainListHash"]; ok {
if storedDomainListHashHex != domainListHashHex {
return false, nil
}
} else {
return false, nil
}
return true, nil
}
}
if signServer.builtInVerifyMapStore == nil {
signServer.builtInVerifyMapStore = map[string]string{}
}
signServer.BismuthServer.HandleConnection = signServer.connHandler
return nil
}
func (signServer *BismuthSigningServer) connHandler(conn net.Conn, metadata *server.ClientMetadata) error {
defer conn.Close()
requestType := make([]byte, 1)
hostAndIP := conn.RemoteAddr().String()
hostAndIPColonIndex := strings.Index(hostAndIP, ":")
if hostAndIPColonIndex == -1 {
return fmt.Errorf("failed to get colon in remote address")
}
host := hostAndIP[:hostAndIPColonIndex]
clientKeyFingerprint := metadata.ClientPublicKey.GetFingerprint()
for {
if _, err := conn.Read(requestType); err != nil {
return err
}
if requestType[0] == core.AreDomainsValidForKey {
// This is probably a bit too big, but I'd like to air on the side of caution here...
keyFingerprintLength := make([]byte, 2)
if _, err := conn.Read(keyFingerprintLength); err != nil {
return err
}
keyFingerprintBytes := make([]byte, binary.BigEndian.Uint16(keyFingerprintLength))
if _, err := conn.Read(keyFingerprintBytes); err != nil {
return err
}
keyFingerprint := hex.EncodeToString(keyFingerprintBytes)
serverDomainListLength := make([]byte, 2)
if _, err := conn.Read(serverDomainListLength); err != nil {
return err
}
serverDomainListBytes := make([]byte, binary.BigEndian.Uint16(serverDomainListLength))
if _, err := conn.Read(serverDomainListBytes); err != nil {
return err
}
serverDomainList := strings.Split(string(serverDomainListBytes), "\n")
// We can't trust anything if they aren't advertising any domains/IPs
if len(serverDomainList) == 0 {
requestResponse := make([]byte, 1)
requestResponse[0] = core.Failure
conn.Write(requestResponse)
continue
}
isVerified, err := signServer.VerifyServerHandler(host, keyFingerprint, serverDomainList)
if err != nil {
requestResponse := make([]byte, 1)
requestResponse[0] = core.InternalError
conn.Write(requestResponse)
return err
}
if isVerified {
requestResponse := make([]byte, 1)
requestResponse[0] = core.Success
conn.Write(requestResponse)
} else {
requestResponse := make([]byte, 1)
requestResponse[0] = core.Failure
conn.Write(requestResponse)
}
} else if requestType[0] == core.ValidateKey {
// This is probably a bit too big, but I'd like to air on the side of caution here...
serverDomainListLength := make([]byte, 2)
if _, err := conn.Read(serverDomainListLength); err != nil {
return err
}
serverDomainListBytes := make([]byte, binary.BigEndian.Uint16(serverDomainListLength))
if _, err := conn.Read(serverDomainListBytes); err != nil {
return err
}
serverDomainList := strings.Split(string(serverDomainListBytes), "\n")
additionalArgumentsLength := make([]byte, 2)
var additionalArgumentsSize uint16
if _, err := conn.Read(additionalArgumentsLength); err != nil {
return err
}
additionalArgumentsSize = binary.BigEndian.Uint16(additionalArgumentsLength)
additionalArguments := ""
if additionalArgumentsSize != 0 {
additionalArgumentsBytes := make([]byte, additionalArgumentsSize)
if _, err := conn.Read(additionalArgumentsBytes); err != nil {
return err
}
additionalArguments = string(additionalArgumentsBytes)
}
isAddedToTrust, err := signServer.AddVerifyHandler(host, clientKeyFingerprint, serverDomainList, additionalArguments)
if err != nil {
return err
}
if isAddedToTrust {
requestResponse := make([]byte, 1)
requestResponse[0] = core.Success
conn.Write(requestResponse)
} else {
requestResponse := make([]byte, 1)
requestResponse[0] = core.Failure
conn.Write(requestResponse)
}
}
}
}

14
signingserver/typing.go Normal file
View file

@ -0,0 +1,14 @@
package signingserver
import "git.greysoh.dev/imterah/bismuthd/server"
type AddVerifyHandlerCallback func(serverAddr string, serverKeyFingerprint string, serverAdvertisedTrustList []string, additionalClientProvidedInfo string) (bool, error)
type VerifyServerHandlerCallback func(serverAddr string, serverKeyFingerprint string, serverDomainList []string) (bool, error)
type BismuthSigningServer struct {
BismuthServer *server.BismuthServer
AddVerifyHandler AddVerifyHandlerCallback
VerifyServerHandler VerifyServerHandlerCallback
builtInVerifyMapStore map[string]string
}

15
signingserver/utils.go Normal file
View file

@ -0,0 +1,15 @@
package signingserver
import "git.greysoh.dev/imterah/bismuthd/server"
func New(bismuthServer *server.BismuthServer) (*BismuthSigningServer, error) {
signServer := BismuthSigningServer{
BismuthServer: bismuthServer,
}
if err := signServer.InitializeServer(); err != nil {
return nil, err
}
return &signServer, nil
}

View file

@ -19,6 +19,7 @@ var testProtocolTxRxBufCount = 32
// Tests protocol transmitting and receiving
// This is designed to be a nightmare scenario for the protocol to push the limits on what would be possible.
func TestProtocolTxRx(t *testing.T) {
t.Log("running tests")
pubKeyCli, privKeyCli, err := CreateKeyring("alice", "alice@contoso.com")
if err != nil {
@ -52,7 +53,9 @@ func TestProtocolTxRx(t *testing.T) {
t.Fatalf("failed to listen on TCP for localhost (%s)", err.Error())
}
bismuth, err := server.NewBismuthServer(pubKeyServ, privKeyServ, []string{}, commons.XChaCha20Poly1305, func(conn net.Conn) error {
bismuth, err := server.New(pubKeyServ, privKeyServ, []string{}, []string{}, commons.XChaCha20Poly1305)
bismuth.HandleConnection = func(conn net.Conn, _ *server.ClientMetadata) error {
for entryCount, randomDataSlice := range randomDataSlices {
_, err = conn.Write(randomDataSlice)
@ -62,7 +65,7 @@ func TestProtocolTxRx(t *testing.T) {
}
return nil
})
}
// TODO: fix these warnings?
go func() {
@ -86,13 +89,15 @@ func TestProtocolTxRx(t *testing.T) {
t.Fatalf("failed to initialize bismuthClient (%s)", err.Error())
}
bismuthClient.CheckIfCertificatesAreSigned = false
originalConn, err := net.Dial("tcp", "127.0.0.1:"+strconv.Itoa(port))
if err != nil {
t.Fatalf("failed to connect to bismuth server (%s)", err.Error())
}
conn, err := bismuthClient.Conn(originalConn)
conn, _, err := bismuthClient.Conn(originalConn)
if err != nil {
t.Fatalf("bismuth client failed to handshake when connecting to server (%s)", err.Error())