Compare commits

...

10 commits
v0.1.0 ... main

22 changed files with 1308 additions and 162 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
bismuthd

11
LICENSE Normal file
View file

@ -0,0 +1,11 @@
Copyright 2024 Tera
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -1,3 +1,24 @@
# 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. The Bismuth protocol is a thin wrapper for any protocol that adds TLS-like features, without being TLS on its own.
## Application
### Building
Git clone this repository and check out a release (except for development, as you probably don't wanna develop on a release version):
```bash
git clone https://git.greysoh.dev/imterah/bismuthd
git checkout v0.1.0
```
Then, build the code:
```bash
go build .
```
### Usage
To get started, you'll need an exported armored public and private key pair.
After that, for usage help, run the help command:
```
./bismuthd help
```

3
client/README.md Normal file
View file

@ -0,0 +1,3 @@
# Bismuth Client
This is a wrapper around connections which lets you speak the Bismuth protocol.

View file

@ -1,62 +1,128 @@
package client package client
import ( import (
"crypto/cipher"
"crypto/rand"
"fmt" "fmt"
"net" "net"
"strings"
core "git.greysoh.dev/imterah/bismuthd/commons" core "git.greysoh.dev/imterah/bismuthd/commons"
"git.greysoh.dev/imterah/bismuthd/signingclient"
"golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/chacha20poly1305"
"github.com/ProtonMail/gopenpgp/v3/crypto" "github.com/ProtonMail/gopenpgp/v3/crypto"
) )
func (bismuth BismuthClient) encryptMessage(aead cipher.AEAD, msg []byte) ([]byte, error) { func computeNodes(children []*BismuthSignResultData) (int, int) {
nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+len(msg)+aead.Overhead()) totalServerCount := 0
passedServerCount := 0
if _, err := rand.Read(nonce); err != nil { for _, child := range children {
return []byte{}, err 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 totalServerCount, passedServerCount
return encryptedMsg, nil
} }
func (bismuth BismuthClient) decryptMessage(aead cipher.AEAD, encMsg []byte) ([]byte, error) { // Initializes the client. Should be done automatically if you call New()
if len(encMsg) < aead.NonceSize() { //
return []byte{}, fmt.Errorf("ciphertext too short") // 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. if bismuth.CertificateSignChecker == nil {
nonce, ciphertext := encMsg[:aead.NonceSize()], encMsg[aead.NonceSize():] bismuth.CertificateSignChecker = func(host, certificateFingerprint string, isSelfSigned bool) bool {
fmt.Println("WARNING: Using stub CertificateSignChecker. Returing true and ignoring arguments")
// Decrypt the message and check it wasn't tampered with. return true
decryptedData, err := aead.Open(nil, nonce, ciphertext, nil) }
if err != nil {
return []byte{}, err
} }
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
} }
type BismuthClient struct { func (bismuth *BismuthClient) checkIfDomainIsTrusted(servers, advertisedDomains []string) ([]*BismuthSignResultData, error) {
PublicKey *crypto.Key signResultData := make([]*BismuthSignResultData, len(servers))
PrivateKey *crypto.Key
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
} }
func (bismuth BismuthClient) Conn(conn net.Conn) (net.Conn, error) { // 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, *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 // 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 // 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() ownKey, err := bismuth.PublicKey.GetPublicKey()
if err != nil { if err != nil {
conn.Close() conn.Close()
return nil, err return nil, nil, err
} }
pubKeyLengthBytes := make([]byte, 3) pubKeyLengthBytes := make([]byte, 3)
@ -71,17 +137,17 @@ func (bismuth BismuthClient) Conn(conn net.Conn) (net.Conn, error) {
if _, err = conn.Read(messageMode); err != nil { if _, err = conn.Read(messageMode); err != nil {
conn.Close() conn.Close()
return nil, err return nil, nil, err
} }
if messageMode[0] != core.SendPublicKey { if messageMode[0] != core.SendPublicKey {
conn.Close() 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 { if _, err = conn.Read(pubKeyLengthBytes); err != nil {
conn.Close() conn.Close()
return nil, err return nil, nil, err
} }
pubKeyLength = core.Int24ToInt32(pubKeyLengthBytes) pubKeyLength = core.Int24ToInt32(pubKeyLengthBytes)
@ -89,12 +155,14 @@ func (bismuth BismuthClient) Conn(conn net.Conn) (net.Conn, error) {
if _, err = conn.Read(pubKeyBytes); err != nil { if _, err = conn.Read(pubKeyBytes); err != nil {
conn.Close() 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() conn.Close()
return nil, err return nil, nil, err
} }
// Then exchange the symmetric key // Then exchange the symmetric key
@ -103,19 +171,19 @@ func (bismuth BismuthClient) Conn(conn net.Conn) (net.Conn, error) {
if _, err = conn.Read(messageMode); err != nil { if _, err = conn.Read(messageMode); err != nil {
conn.Close() conn.Close()
return nil, err return nil, nil, err
} }
if messageMode[0] != core.SwitchToSymmetricKey { if messageMode[0] != core.SwitchToSymmetricKey {
conn.Close() 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) encryptedSymmKeyLengthInBytes := make([]byte, 3)
if _, err = conn.Read(encryptedSymmKeyLengthInBytes); err != nil { if _, err = conn.Read(encryptedSymmKeyLengthInBytes); err != nil {
conn.Close() conn.Close()
return nil, err return nil, nil, err
} }
encryptedSymmKeyLength := core.Int24ToInt32(encryptedSymmKeyLengthInBytes) encryptedSymmKeyLength := core.Int24ToInt32(encryptedSymmKeyLengthInBytes)
@ -123,31 +191,186 @@ func (bismuth BismuthClient) Conn(conn net.Conn) (net.Conn, error) {
if _, err = conn.Read(encryptedSymmKey); err != nil { if _, err = conn.Read(encryptedSymmKey); err != nil {
conn.Close() conn.Close()
return nil, err return nil, nil, err
} }
decHandleForSymmKey, err := bismuth.pgp.Decryption().DecryptionKey(bismuth.PrivateKey).New() decHandleForSymmKey, err := bismuth.pgp.Decryption().DecryptionKey(bismuth.PrivateKey).New()
if err != nil { if err != nil {
return nil, err conn.Close()
return nil, nil, err
} }
decryptedSymmKey, err := decHandleForSymmKey.Decrypt(encryptedSymmKey, crypto.Bytes) decryptedSymmKey, err := decHandleForSymmKey.Decrypt(encryptedSymmKey, crypto.Bytes)
if err != nil { if err != nil {
return nil, err conn.Close()
return nil, nil, err
} }
symmKeyInfo := decryptedSymmKey.Bytes() symmKeyInfo := decryptedSymmKey.Bytes()
if symmKeyInfo[0] != core.XChaCha20Poly1305 { if symmKeyInfo[0] != core.XChaCha20Poly1305 {
conn.Close() 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] symmKey := symmKeyInfo[1 : chacha20poly1305.KeySize+1]
aead, err := chacha20poly1305.NewX(symmKey) 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 // Start proxying
startForwardingPacket := []byte{ startForwardingPacket := []byte{
@ -158,7 +381,7 @@ func (bismuth BismuthClient) Conn(conn net.Conn) (net.Conn, error) {
if err != nil { if err != nil {
conn.Close() conn.Close()
return nil, err return nil, nil, err
} }
encryptedForwardPacketPacketSize := make([]byte, 3) encryptedForwardPacketPacketSize := make([]byte, 3)
@ -180,29 +403,5 @@ func (bismuth BismuthClient) Conn(conn net.Conn) (net.Conn, error) {
return core.BismuthConnWrapped{ return core.BismuthConnWrapped{
Bismuth: &bmConn, Bismuth: &bmConn,
}, nil }, &signResults, nil
}
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
} }

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
}

4
commons/README.md Normal file
View file

@ -0,0 +1,4 @@
# Commons between Bismuth Client and Server
This houses common internal libraries and constants between the bismuth client and server.
For most of the time, you're probably going to want the connection handler, which is in `conn.go`.

View file

@ -1,3 +1,5 @@
// Shared connection handler after both the client and server handshake successfully
package commons package commons
import ( import (
@ -9,11 +11,18 @@ import (
"time" "time"
) )
// Max size for a TCP packet // Maximum size for a TCP packet
var ConnStandardMaxBufSize = 65535 var ConnStandardMaxBufSize = 65535
var CryptHeader = 43 var cryptHeaderSize = 43
// Wild // Connection used after both the Bismuth client and server negotiate and start
// transmitting data.
//
// Note that using this has the same API as the net.Conn, but it isn't conformant to
// the interface due to using pointers rather than copies to access the struct.
//
// If you need the same interface, wrap this in WrappedBismuthConn.
// Wrapping BismuthConn is done automatically by the client and server.
type BismuthConn struct { type BismuthConn struct {
Aead cipher.AEAD Aead cipher.AEAD
PassedConn net.Conn PassedConn net.Conn
@ -26,10 +35,11 @@ type BismuthConn struct {
contentBufPos int contentBufPos int
contentBufSize int contentBufSize int
MaxBufSize int // Maximum buffer size to be used to internally buffer packets
MaxBufSize int
// If true, it enables using the content buffer maximum size
// instead of the TCP packet maximum size
AllowNonstandardPacketSizeLimit bool AllowNonstandardPacketSizeLimit bool
net.Conn
} }
func (bmConn *BismuthConn) DoInitSteps() { func (bmConn *BismuthConn) DoInitSteps() {
@ -68,6 +78,7 @@ func (bmConn *BismuthConn) decryptMessage(encMsg []byte) ([]byte, error) {
return decryptedData, nil return decryptedData, nil
} }
// After you update the property `bmConn.MaxBufSize`, call this function to resize the content buffer
func (bmConn *BismuthConn) ResizeContentBuf() error { func (bmConn *BismuthConn) ResizeContentBuf() error {
if !bmConn.initDone { if !bmConn.initDone {
return fmt.Errorf("bmConn not initialized") return fmt.Errorf("bmConn not initialized")
@ -95,6 +106,7 @@ func (bmConn *BismuthConn) ResizeContentBuf() error {
return nil return nil
} }
// Reads specifically from the buffer only. If nothing is in the buffer, nothing is returned.
func (bmConn *BismuthConn) ReadFromBuffer(b []byte) (n int, err error) { func (bmConn *BismuthConn) ReadFromBuffer(b []byte) (n int, err error) {
bmConn.lock.Lock() bmConn.lock.Lock()
defer bmConn.lock.Unlock() defer bmConn.lock.Unlock()
@ -126,6 +138,7 @@ func (bmConn *BismuthConn) ReadFromBuffer(b []byte) (n int, err error) {
return 0, nil return 0, nil
} }
// Reads specifically from the network. Be careful as using only this may overflow the buffer.
func (bmConn *BismuthConn) ReadFromNetwork(b []byte) (n int, err error) { func (bmConn *BismuthConn) ReadFromNetwork(b []byte) (n int, err error) {
bmConn.lock.Lock() bmConn.lock.Lock()
defer bmConn.lock.Unlock() defer bmConn.lock.Unlock()
@ -146,7 +159,7 @@ func (bmConn *BismuthConn) ReadFromNetwork(b []byte) (n int, err error) {
// - the max buffer size if 'AllowNonstandardPacketSizeLimit' is set // - the max buffer size if 'AllowNonstandardPacketSizeLimit' is set
// We check AFTER we read to make sure that we don't corrupt any future packets, because if we don't read the packet, // We check AFTER we read to make sure that we don't corrupt any future packets, because if we don't read the packet,
// it will think that the actual packet will be the start of the packet, and that would cause loads of problems. // it will think that the actual packet will be the start of the packet, and that would cause loads of problems.
if !bmConn.AllowNonstandardPacketSizeLimit && encryptedContentLength > uint32(65535+CryptHeader) { if !bmConn.AllowNonstandardPacketSizeLimit && encryptedContentLength > uint32(65535+cryptHeaderSize) {
return 0, fmt.Errorf("packet too large") return 0, fmt.Errorf("packet too large")
} else if bmConn.AllowNonstandardPacketSizeLimit && encryptedContentLength > uint32(bmConn.MaxBufSize) { } else if bmConn.AllowNonstandardPacketSizeLimit && encryptedContentLength > uint32(bmConn.MaxBufSize) {
return 0, fmt.Errorf("packet too large") return 0, fmt.Errorf("packet too large")
@ -187,6 +200,7 @@ func (bmConn *BismuthConn) ReadFromNetwork(b []byte) (n int, err error) {
return calcSize, nil return calcSize, nil
} }
// Reads from the Bismuth connection, using both the buffered and network methods
func (bmConn *BismuthConn) Read(b []byte) (n int, err error) { func (bmConn *BismuthConn) Read(b []byte) (n int, err error) {
if !bmConn.initDone { if !bmConn.initDone {
return 0, fmt.Errorf("bmConn not initialized") return 0, fmt.Errorf("bmConn not initialized")
@ -211,6 +225,7 @@ func (bmConn *BismuthConn) Read(b []byte) (n int, err error) {
return bufferReadSize + networkReadSize, nil return bufferReadSize + networkReadSize, nil
} }
// Encrypts and sends off a message
func (bmConn *BismuthConn) Write(b []byte) (n int, err error) { func (bmConn *BismuthConn) Write(b []byte) (n int, err error) {
encryptedMessage, err := bmConn.encryptMessage(b) encryptedMessage, err := bmConn.encryptMessage(b)
@ -251,8 +266,10 @@ func (bmConn *BismuthConn) SetWriteDeadline(time time.Time) error {
return bmConn.PassedConn.SetWriteDeadline(time) return bmConn.PassedConn.SetWriteDeadline(time)
} }
// TODO: remove this ugly hack if possible! There's probably a better way around this... // Wrapped BismuthConn struct. This is conformant to net.Conn, unlike above.
// To get the raw Bismuth struct, just get the Bismuth property:
//
// `bmConn.Bismuth` -> `BismuthConn`
type BismuthConnWrapped struct { type BismuthConnWrapped struct {
Bismuth *BismuthConn Bismuth *BismuthConn
} }

View file

@ -1,9 +1,13 @@
// Conversion libraries for 24 bit numbering instead of 32 bit numbering
package commons package commons
// Converts a 24 bit unsigned integer stored in a big-endian byte array to a 32 bit unsigned integer.
func Int24ToInt32(b []byte) uint32 { func Int24ToInt32(b []byte) uint32 {
return uint32(b[2]) | uint32(b[1])<<8 | uint32(b[0])<<16 return uint32(b[2]) | uint32(b[1])<<8 | uint32(b[0])<<16
} }
// Converts a 32 bit unsigned integer to a 24 bit unsigned integer in a byte array using big-endian ordering.
func Int32ToInt24(b []byte, int uint32) { func Int32ToInt24(b []byte, int uint32) {
b[0] = uint8((int >> 16) & 0xff) b[0] = uint8((int >> 16) & 0xff)
b[1] = uint8((int >> 8) & 0xff) b[1] = uint8((int >> 8) & 0xff)

View file

@ -1,18 +1,42 @@
// Enums used internally
package commons package commons
// Core Protocol Commands
const ( const (
// Sending public key back and forth
SendPublicKey = iota SendPublicKey = iota
// Sent by the client along with the symmetric key that is going to be used
SwitchToSymmetricKey SwitchToSymmetricKey
// Client sends what host they are connecting to.
ClientSendHost ClientSendHost
// Gets the signing servers trusting/signing the current server.
GetSigningServers GetSigningServers
// Gets the domains that are supported by this certificate (should be cross-checked)
GetTrustedDomains GetTrustedDomains
// Starts forwarding traffic over this protocol.
InitiateForwarding InitiateForwarding
) )
// Validation API Commands
const ( 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 XChaCha20Poly1305 = iota
) )
// Unsigned Integer Limits
const ( const (
BitLimit24 = 16_777_215 BitLimit24 = 16_777_215
BitLimit16 = 65535 BitLimit16 = 65535

330
main.go
View file

@ -3,6 +3,7 @@ package main
import ( import (
"context" "context"
_ "embed" _ "embed"
"encoding/hex"
"fmt" "fmt"
"net" "net"
"os" "os"
@ -11,6 +12,8 @@ import (
"git.greysoh.dev/imterah/bismuthd/client" "git.greysoh.dev/imterah/bismuthd/client"
core "git.greysoh.dev/imterah/bismuthd/commons" core "git.greysoh.dev/imterah/bismuthd/commons"
"git.greysoh.dev/imterah/bismuthd/server" "git.greysoh.dev/imterah/bismuthd/server"
"git.greysoh.dev/imterah/bismuthd/signingclient"
"git.greysoh.dev/imterah/bismuthd/signingserver"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"tailscale.com/net/socks5" "tailscale.com/net/socks5"
@ -19,7 +22,7 @@ import (
//go:embed ascii.txt //go:embed ascii.txt
var asciiArt string var asciiArt string
func bismuthClientEntrypoint(cCtx *cli.Context) error { func clientEntrypoint(cCtx *cli.Context) error {
pubKeyFile, err := os.ReadFile(cCtx.String("pubkey")) pubKeyFile, err := os.ReadFile(cCtx.String("pubkey"))
if err != nil { if err != nil {
@ -37,6 +40,8 @@ func bismuthClientEntrypoint(cCtx *cli.Context) error {
bismuth, err := client.New(pubKey, privKey) bismuth, err := client.New(pubKey, privKey)
log.Debugf("My key fingerprint is: %s", bismuth.PublicKey.GetFingerprint())
if err != nil { if err != nil {
return err return err
} }
@ -82,13 +87,15 @@ func bismuthClientEntrypoint(cCtx *cli.Context) error {
return nil, err return nil, err
} }
conn, err = bismuth.Conn(conn) conn, returnData, err := bismuth.Conn(conn)
if err != nil && err.Error() != "EOF" { if err != nil && err.Error() != "EOF" {
log.Errorf("failed to initialize bismuth connection to '%s:%s': '%s'", ip, port, err.Error()) log.Errorf("failed to initialize bismuth connection to '%s:%s': '%s'", ip, port, err.Error())
return nil, err return nil, err
} }
log.Debugf("Server key fingerprint for '%s' is: %s", addr, returnData.ServerPublicKey.GetFingerprint())
return conn, err return conn, err
} else { } else {
conn, err := net.Dial(network, addr) conn, err := net.Dial(network, addr)
@ -110,8 +117,17 @@ func bismuthClientEntrypoint(cCtx *cli.Context) error {
return nil return nil
} }
func bismuthServerEntrypoint(cCtx *cli.Context) error { func serverEntrypoint(cCtx *cli.Context) error {
relayServers := []string{} 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")) 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")) 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) connDialed, err := net.Dial("tcp", network)
if err != nil { if err != nil {
@ -183,7 +200,7 @@ func bismuthServerEntrypoint(cCtx *cli.Context) error {
}() }()
return nil return nil
}) }
if err != nil { if err != nil {
return err return err
@ -201,23 +218,206 @@ func bismuthServerEntrypoint(cCtx *cli.Context) error {
for { for {
conn, err := listener.Accept() 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()) 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 { if err != nil {
log.Warn(err.Error()) log.Warn(err.Error())
continue continue
} }
log.Debugf("Recieved connection from '%s'", conn.RemoteAddr().String())
go func() { go func() {
err := bismuth.HandleProxy(conn) err = bismuthServer.HandleProxy(conn)
if err != nil && err.Error() != "EOF" { 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() { func main() {
fmt.Println(asciiArt) fmt.Println(asciiArt)
fmt.Print("Implementation of the Bismuth protocol\n\n") fmt.Print("Implementation of the Bismuth protocol\n\n")
@ -282,7 +482,7 @@ func main() {
}, },
}, },
Usage: "client for the Bismuth protocol", Usage: "client for the Bismuth protocol",
Action: bismuthClientEntrypoint, Action: clientEntrypoint,
}, },
{ {
Name: "server", Name: "server",
@ -308,6 +508,14 @@ func main() {
Usage: "port to connect to", Usage: "port to connect to",
Required: true, 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{ &cli.StringFlag{
Name: "dest-ip", Name: "dest-ip",
Usage: "IP to listen on", Usage: "IP to listen on",
@ -320,7 +528,109 @@ func main() {
}, },
}, },
Usage: "server for the Bismuth protocol", 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",
}, },
}, },
} }

3
server/README.md Normal file
View file

@ -0,0 +1,3 @@
# Bismuth Server
This is the Bismuth server, which lets you speak the Bismuth protocol.

View file

@ -1,7 +1,6 @@
package server package server
import ( import (
"fmt"
"net" "net"
core "git.greysoh.dev/imterah/bismuthd/commons" core "git.greysoh.dev/imterah/bismuthd/commons"
@ -13,50 +12,7 @@ import (
"golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/chacha20poly1305"
) )
type BismuthServer struct { // Called to handle a connnection for Bismuth. The conn argument is the client you'd like to handle
PublicKey *crypto.Key
PrivateKey *crypto.Key
pgp *crypto.PGPHandle
SymmetricEncryptionAlgorithm int
SigningServers []string
// This is what's 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
}
// This is what's called to handle a connnection for Bismuth.
func (bismuth BismuthServer) HandleProxy(conn net.Conn) error { func (bismuth BismuthServer) HandleProxy(conn net.Conn) error {
serverState := "keyHandshake" serverState := "keyHandshake"
@ -206,7 +162,54 @@ func (bismuth BismuthServer) HandleProxy(conn net.Conn) error {
return err 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 {
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{ bmConn := core.BismuthConn{
Aead: aead, Aead: aead,
PassedConn: conn, PassedConn: conn,
@ -215,39 +218,16 @@ func (bismuth BismuthServer) HandleProxy(conn net.Conn) error {
bmConn.DoInitSteps() bmConn.DoInitSteps()
metadata := ClientMetadata{
ClientPublicKey: clientPublicKey,
}
err := bismuth.HandleConnection(core.BismuthConnWrapped{ err := bismuth.HandleConnection(core.BismuthConnWrapped{
Bismuth: &bmConn, Bismuth: &bmConn,
}) }, &metadata)
return err return err
} }
} }
} }
} }
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
}

3
tests/README.md Normal file
View file

@ -0,0 +1,3 @@
# Test Suites
These are either test suites or support code for test suites for Bismuth's client and server code.

View file

@ -19,6 +19,7 @@ var testProtocolTxRxBufCount = 32
// Tests protocol transmitting and receiving // 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. // 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) { func TestProtocolTxRx(t *testing.T) {
t.Log("running tests")
pubKeyCli, privKeyCli, err := CreateKeyring("alice", "alice@contoso.com") pubKeyCli, privKeyCli, err := CreateKeyring("alice", "alice@contoso.com")
if err != nil { if err != nil {
@ -52,7 +53,9 @@ func TestProtocolTxRx(t *testing.T) {
t.Fatalf("failed to listen on TCP for localhost (%s)", err.Error()) 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 { for entryCount, randomDataSlice := range randomDataSlices {
_, err = conn.Write(randomDataSlice) _, err = conn.Write(randomDataSlice)
@ -62,7 +65,7 @@ func TestProtocolTxRx(t *testing.T) {
} }
return nil return nil
}) }
// TODO: fix these warnings? // TODO: fix these warnings?
go func() { go func() {
@ -86,13 +89,15 @@ func TestProtocolTxRx(t *testing.T) {
t.Fatalf("failed to initialize bismuthClient (%s)", err.Error()) t.Fatalf("failed to initialize bismuthClient (%s)", err.Error())
} }
bismuthClient.CheckIfCertificatesAreSigned = false
originalConn, err := net.Dial("tcp", "127.0.0.1:"+strconv.Itoa(port)) originalConn, err := net.Dial("tcp", "127.0.0.1:"+strconv.Itoa(port))
if err != nil { if err != nil {
t.Fatalf("failed to connect to bismuth server (%s)", err.Error()) t.Fatalf("failed to connect to bismuth server (%s)", err.Error())
} }
conn, err := bismuthClient.Conn(originalConn) conn, _, err := bismuthClient.Conn(originalConn)
if err != nil { if err != nil {
t.Fatalf("bismuth client failed to handshake when connecting to server (%s)", err.Error()) t.Fatalf("bismuth client failed to handshake when connecting to server (%s)", err.Error())