From 514ccee264cd69f6b3cf2222c55a9a5e20141ff8 Mon Sep 17 00:00:00 2001 From: imterah Date: Sat, 19 Oct 2024 21:25:49 +0000 Subject: [PATCH 1/9] chore: Adds license --- LICENSE | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..00f5567 --- /dev/null +++ b/LICENSE @@ -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. \ No newline at end of file From 8eb9bb3cee284831b35fed507f0b7e4abdadf193 Mon Sep 17 00:00:00 2001 From: greysoh Date: Sat, 19 Oct 2024 18:06:49 -0400 Subject: [PATCH 2/9] chore: Adds lots of documentation. --- .gitignore | 1 + README.md | 21 +++++++++++++++++++++ client/README.md | 3 +++ client/client.go | 10 +++++++++- commons/README.md | 4 ++++ commons/conn.go | 35 ++++++++++++++++++++++++++--------- commons/conv.go | 4 ++++ commons/enum.go | 15 +++++++++++++++ server/README.md | 3 +++ server/server.go | 16 ++++++++++++---- tests/README.md | 3 +++ 11 files changed, 101 insertions(+), 14 deletions(-) create mode 100644 .gitignore create mode 100644 client/README.md create mode 100644 commons/README.md create mode 100644 server/README.md create mode 100644 tests/README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad601fc --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +bismuthd diff --git a/README.md b/README.md index 0421a51..6eeccc4 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,24 @@ # Bismuth Protocol 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 +``` diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..6f929b6 --- /dev/null +++ b/client/README.md @@ -0,0 +1,3 @@ +# Bismuth Client + +This is a wrapper around connections which lets you speak the Bismuth protocol. diff --git a/client/client.go b/client/client.go index 5a210f0..7f49ba6 100644 --- a/client/client.go +++ b/client/client.go @@ -41,13 +41,18 @@ func (bismuth BismuthClient) decryptMessage(aead cipher.AEAD, encMsg []byte) ([] return decryptedData, nil } +// Bismuth Client type BismuthClient struct { - PublicKey *crypto.Key + // GOpenPGP public key + PublicKey *crypto.Key + // GOpenPGP private key PrivateKey *crypto.Key pgp *crypto.PGPHandle } +// 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) { // 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 @@ -183,6 +188,9 @@ func (bismuth BismuthClient) Conn(conn net.Conn) (net.Conn, error) { }, 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) diff --git a/commons/README.md b/commons/README.md new file mode 100644 index 0000000..ae36053 --- /dev/null +++ b/commons/README.md @@ -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`. diff --git a/commons/conn.go b/commons/conn.go index 6c2db99..7d7e364 100644 --- a/commons/conn.go +++ b/commons/conn.go @@ -1,3 +1,5 @@ +// Shared connection handler after both the client and server handshake successfully + package commons import ( @@ -9,11 +11,18 @@ import ( "time" ) -// Max size for a TCP packet +// Maximum size for a TCP packet 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 { Aead cipher.AEAD PassedConn net.Conn @@ -26,10 +35,11 @@ type BismuthConn struct { contentBufPos 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 - - net.Conn } func (bmConn *BismuthConn) DoInitSteps() { @@ -68,6 +78,7 @@ func (bmConn *BismuthConn) decryptMessage(encMsg []byte) ([]byte, error) { return decryptedData, nil } +// After you update the property `bmConn.MaxBufSize`, call this function to resize the content buffer func (bmConn *BismuthConn) ResizeContentBuf() error { if !bmConn.initDone { return fmt.Errorf("bmConn not initialized") @@ -95,6 +106,7 @@ func (bmConn *BismuthConn) ResizeContentBuf() error { 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) { bmConn.lock.Lock() defer bmConn.lock.Unlock() @@ -126,6 +138,7 @@ func (bmConn *BismuthConn) ReadFromBuffer(b []byte) (n int, err error) { 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) { bmConn.lock.Lock() 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 // 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. - if !bmConn.AllowNonstandardPacketSizeLimit && encryptedContentLength > uint32(65535+CryptHeader) { + if !bmConn.AllowNonstandardPacketSizeLimit && encryptedContentLength > uint32(65535+cryptHeaderSize) { return 0, fmt.Errorf("packet too large") } else if bmConn.AllowNonstandardPacketSizeLimit && encryptedContentLength > uint32(bmConn.MaxBufSize) { return 0, fmt.Errorf("packet too large") @@ -187,6 +200,7 @@ func (bmConn *BismuthConn) ReadFromNetwork(b []byte) (n int, err error) { 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) { if !bmConn.initDone { 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 } +// Encrypts and sends off a message func (bmConn *BismuthConn) Write(b []byte) (n int, err error) { encryptedMessage, err := bmConn.encryptMessage(b) @@ -251,8 +266,10 @@ func (bmConn *BismuthConn) SetWriteDeadline(time time.Time) error { 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 { Bismuth *BismuthConn } diff --git a/commons/conv.go b/commons/conv.go index bc81bf0..cc44091 100644 --- a/commons/conv.go +++ b/commons/conv.go @@ -1,9 +1,13 @@ +// Conversion libraries for 24 bit numbering instead of 32 bit numbering + 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 { 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) { b[0] = uint8((int >> 16) & 0xff) b[1] = uint8((int >> 8) & 0xff) diff --git a/commons/enum.go b/commons/enum.go index 5cd9ded..be1848f 100644 --- a/commons/enum.go +++ b/commons/enum.go @@ -1,18 +1,33 @@ +// Enums used internally + package commons +// Commands const ( + // SendPublicKey: Sending public key back and forth SendPublicKey = iota + // SwitchToSymmetricKey: 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. InitiateForwarding ) +// Encryption algorithms const ( + // Default and only encryption algorithm XChaCha20Poly1305 = iota ) +// Unsigned integer limits const ( BitLimit24 = 16_777_215 BitLimit16 = 65535 diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..c562f7f --- /dev/null +++ b/server/README.md @@ -0,0 +1,3 @@ +# Bismuth Server + +This is the Bismuth server, which lets you speak the Bismuth protocol. diff --git a/server/server.go b/server/server.go index 2357e4d..4e1c183 100644 --- a/server/server.go +++ b/server/server.go @@ -13,17 +13,22 @@ import ( "golang.org/x/crypto/chacha20poly1305" ) +// Bismuth Server type BismuthServer struct { - PublicKey *crypto.Key + // 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 - // This is what's called after a successful handshake & connection. + // Called after a successful handshake & connection. HandleConnection func(conn net.Conn) error } @@ -56,7 +61,7 @@ func (bismuth BismuthServer) decryptMessage(aead cipher.AEAD, encMsg []byte) ([] return decryptedData, nil } -// This is what's called to handle a connnection for Bismuth. +// 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" @@ -225,6 +230,9 @@ func (bismuth BismuthServer) HandleProxy(conn net.Conn) error { } } +// 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) diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..9844b40 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,3 @@ +# Test Suites + +These are either test suites or support code for test suites for Bismuth's client and server code. From a977e69aef5f159047c1487a33c0f33b3304cad7 Mon Sep 17 00:00:00 2001 From: greysoh Date: Sun, 20 Oct 2024 12:02:15 -0400 Subject: [PATCH 3/9] choore: Prepares support for certificate validation in client. --- client/client.go | 62 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/client/client.go b/client/client.go index 7f49ba6..f6601aa 100644 --- a/client/client.go +++ b/client/client.go @@ -41,6 +41,20 @@ func (bismuth BismuthClient) decryptMessage(aead cipher.AEAD, encMsg []byte) ([] return decryptedData, nil } +// 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) +// - `isTrustworthy`: If true, the certificate is signed by 51% of peers. +type CertCheckCallback func(host, certificateFingerprint string, isSelfSigned, isTrustworthy bool) bool + +// Connects to a server using a provided method, with host being the host: +// +// OwnConnMethodCallback("google.com:80") +type OwnConnMethodCallback func(address string) (net.Conn, error) + // Bismuth Client type BismuthClient struct { // GOpenPGP public key @@ -48,9 +62,48 @@ type BismuthClient struct { // GOpenPGP private key 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. + // See CertCheckCallback for more typing information. + CertificateSignChecker CertCheckCallback + + // Connects to a server (used for CheckIfCertificatesAreSigned if enabled/set to true). + ConnectToServer OwnConnMethodCallback + + // GopenPGP instance pgp *crypto.PGPHandle } +// 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() + } + + if bismuth.CertificateSignChecker == nil { + bismuth.CertificateSignChecker = func(host, certificateFingerprint string, isSelfSigned, isTrustworthy bool) bool { + fmt.Println("WARNING: Using stub CertificateSignChecker. Returing true and ignoring arguments") + return true + } + } + + if bismuth.ConnectToServer == nil { + bismuth.ConnectToServer = func(address string) (net.Conn, error) { + return net.Dial("tcp", address) + } + } + + return 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) { @@ -204,12 +257,15 @@ func New(pubKey string, privKey string) (*BismuthClient, error) { return nil, err } - pgp := crypto.PGP() - bismuth := BismuthClient{ PublicKey: publicKey, PrivateKey: privateKey, - pgp: pgp, + } + + err = bismuth.InitializeClient() + + if err != nil { + return nil, err } return &bismuth, nil From a5e479cc0c5c85420cc325c839289209819c9459 Mon Sep 17 00:00:00 2001 From: greysoh Date: Sun, 20 Oct 2024 12:13:23 -0400 Subject: [PATCH 4/9] chore: Seperate code into multiple files for client and server. --- client/client.go | 100 ----------------------------------------------- client/typing.go | 46 ++++++++++++++++++++++ client/utils.go | 68 ++++++++++++++++++++++++++++++++ commons/enum.go | 30 +++++++++----- server/server.go | 79 ------------------------------------- server/typing.go | 26 ++++++++++++ server/utils.go | 69 ++++++++++++++++++++++++++++++++ 7 files changed, 230 insertions(+), 188 deletions(-) create mode 100644 client/typing.go create mode 100644 client/utils.go create mode 100644 server/typing.go create mode 100644 server/utils.go diff --git a/client/client.go b/client/client.go index f6601aa..d3f26d3 100644 --- a/client/client.go +++ b/client/client.go @@ -1,8 +1,6 @@ package client import ( - "crypto/cipher" - "crypto/rand" "fmt" "net" @@ -12,74 +10,6 @@ import ( "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 -} - -// 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) -// - `isTrustworthy`: If true, the certificate is signed by 51% of peers. -type CertCheckCallback func(host, certificateFingerprint string, isSelfSigned, isTrustworthy bool) bool - -// Connects to a server using a provided method, with host being the host: -// -// OwnConnMethodCallback("google.com:80") -type OwnConnMethodCallback func(address string) (net.Conn, error) - -// Bismuth Client -type BismuthClient struct { - // GOpenPGP public key - PublicKey *crypto.Key - // GOpenPGP private key - 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. - // See CertCheckCallback for more typing information. - CertificateSignChecker CertCheckCallback - - // Connects to a server (used for CheckIfCertificatesAreSigned if enabled/set to true). - ConnectToServer OwnConnMethodCallback - - // GopenPGP instance - pgp *crypto.PGPHandle -} - // 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(). @@ -240,33 +170,3 @@ func (bismuth BismuthClient) Conn(conn net.Conn) (net.Conn, error) { 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 - } - - bismuth := BismuthClient{ - PublicKey: publicKey, - PrivateKey: privateKey, - } - - err = bismuth.InitializeClient() - - if err != nil { - return nil, err - } - - return &bismuth, nil -} diff --git a/client/typing.go b/client/typing.go new file mode 100644 index 0000000..de537ec --- /dev/null +++ b/client/typing.go @@ -0,0 +1,46 @@ +package client + +import ( + "net" + + "github.com/ProtonMail/gopenpgp/v3/crypto" +) + +// 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) +// - `isTrustworthy`: If true, the certificate is signed by 51% of peers. +type CertCheckCallback func(host, certificateFingerprint string, isSelfSigned, isTrustworthy bool) bool + +// Connects to a server using a provided method, with host being the host: +// +// OwnConnMethodCallback("google.com:80") +type OwnConnMethodCallback func(address string) (net.Conn, error) + +// Bismuth Client +type BismuthClient struct { + // GOpenPGP public key + PublicKey *crypto.Key + // GOpenPGP private key + 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. + // See CertCheckCallback for more typing information. + CertificateSignChecker CertCheckCallback + + // Connects to a server (used for CheckIfCertificatesAreSigned if enabled/set to true). + ConnectToServer OwnConnMethodCallback + + // GopenPGP instance + pgp *crypto.PGPHandle +} diff --git a/client/utils.go b/client/utils.go new file mode 100644 index 0000000..dc236f5 --- /dev/null +++ b/client/utils.go @@ -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 +} diff --git a/commons/enum.go b/commons/enum.go index be1848f..9e78c88 100644 --- a/commons/enum.go +++ b/commons/enum.go @@ -2,32 +2,44 @@ 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. + // Currently unimplemented. ClientSendHost - // GetSigningServers: Currently unimplemented. // Gets the signing servers trusting/signing the current server. + // Currently unimplemented. GetSigningServers - // GetTrustedDomains: Currently unimplemented. // Gets the domains that are supported by this certificate (should be cross-checked) + // Currently unimplemented. 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 diff --git a/server/server.go b/server/server.go index 4e1c183..b211f6e 100644 --- a/server/server.go +++ b/server/server.go @@ -1,7 +1,6 @@ package server import ( - "fmt" "net" core "git.greysoh.dev/imterah/bismuthd/commons" @@ -13,54 +12,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" @@ -229,33 +180,3 @@ func (bismuth BismuthServer) HandleProxy(conn net.Conn) error { } } } - -// 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 -} diff --git a/server/typing.go b/server/typing.go new file mode 100644 index 0000000..9f830c3 --- /dev/null +++ b/server/typing.go @@ -0,0 +1,26 @@ +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 + + 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 +} diff --git a/server/utils.go b/server/utils.go new file mode 100644 index 0000000..3016844 --- /dev/null +++ b/server/utils.go @@ -0,0 +1,69 @@ +package server + +import ( + "crypto/cipher" + "crypto/rand" + "fmt" + "net" + + "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 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 +} From e527413faf81c61c35a0e25f2f84f885928f63e5 Mon Sep 17 00:00:00 2001 From: imterah Date: Thu, 24 Oct 2024 11:39:46 -0400 Subject: [PATCH 5/9] feature: Gets signing server and client infrastructure working. --- README.md | 2 +- client/client.go | 133 ++++++++++++-- client/typing.go | 87 ++++++--- commons/enum.go | 3 - main.go | 312 +++++++++++++++++++++++++++++++-- server/server.go | 55 +++++- server/typing.go | 14 +- server/utils.go | 4 +- signingclient/signingclient.go | 69 ++++++++ signingserver/signingserver.go | 210 ++++++++++++++++++++++ signingserver/typing.go | 14 ++ signingserver/utils.go | 15 ++ tests/integration_test.go | 9 +- 13 files changed, 865 insertions(+), 62 deletions(-) create mode 100644 signingclient/signingclient.go create mode 100644 signingserver/signingserver.go create mode 100644 signingserver/typing.go create mode 100644 signingserver/utils.go diff --git a/README.md b/README.md index 6eeccc4..b9129f5 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/client/client.go b/client/client.go index d3f26d3..2fef111 100644 --- a/client/client.go +++ b/client/client.go @@ -3,6 +3,7 @@ package client import ( "fmt" "net" + "strings" core "git.greysoh.dev/imterah/bismuthd/commons" "golang.org/x/crypto/chacha20poly1305" @@ -25,7 +26,15 @@ func (bismuth *BismuthClient) InitializeClient() error { } } + 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) } @@ -36,15 +45,24 @@ func (bismuth *BismuthClient) InitializeClient() 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, 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) @@ -59,17 +77,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) @@ -77,12 +95,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 @@ -91,19 +111,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) @@ -111,31 +131,110 @@ 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) + // 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) + + // Request trusted proxies + + trustedProxyRequest := make([]byte, 1) + trustedProxyRequest[0] = core.GetSigningServers + + encryptedTrustedProxyRequest, err := bismuth.encryptMessage(aead, trustedProxyRequest) + + if err != nil { + conn.Close() + return nil, nil, err + } + + trustedProxyLength := make([]byte, 3) + core.Int32ToInt24(trustedProxyLength, uint32(len(encryptedTrustedProxyRequest))) + + conn.Write(trustedProxyLength) + conn.Write(encryptedTrustedProxyRequest) + + if _, err = conn.Read(trustedProxyLength); err != nil { + conn.Close() + return nil, nil, err + } + + encryptedTrustedProxyResponse := make([]byte, core.Int24ToInt32(trustedProxyLength)) + + if _, err = conn.Read(encryptedTrustedProxyResponse); err != nil { + conn.Close() + return nil, nil, err + } + + trustedProxyResponse, err := bismuth.decryptMessage(aead, encryptedTrustedProxyResponse) + + if err != nil { + conn.Close() + return nil, nil, err + } + + if trustedProxyResponse[0] != core.GetSigningServers { + conn.Close() + return nil, nil, fmt.Errorf("server failed to return its signing servers") + } + + signingServers := strings.Split(string(trustedProxyResponse[1:]), "\n") + isServerSelfSigned := len(trustedProxyResponse)-1 == 0 + + if !isServerSelfSigned { + fmt.Printf("acquired signing servers: '%s'\n", strings.Join(signingServers, ", ")) + } else { + fmt.Println("server is self signed, not printing (non-existent) signing servers") + } + + signResults := BismuthSignResults{ + OverallTrustScore: 100, + ServerPublicKey: serverPublicKey, + } + // Start proxying startForwardingPacket := []byte{ @@ -146,7 +245,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) @@ -168,5 +267,5 @@ func (bismuth BismuthClient) Conn(conn net.Conn) (net.Conn, error) { return core.BismuthConnWrapped{ Bismuth: &bmConn, - }, nil + }, &signResults, nil } diff --git a/client/typing.go b/client/typing.go index de537ec..ad9a532 100644 --- a/client/typing.go +++ b/client/typing.go @@ -6,25 +6,11 @@ import ( "github.com/ProtonMail/gopenpgp/v3/crypto" ) -// 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) -// - `isTrustworthy`: If true, the certificate is signed by 51% of peers. -type CertCheckCallback func(host, certificateFingerprint string, isSelfSigned, isTrustworthy bool) bool - -// Connects to a server using a provided method, with host being the host: -// -// OwnConnMethodCallback("google.com:80") -type OwnConnMethodCallback func(address string) (net.Conn, error) - // Bismuth Client type BismuthClient struct { - // GOpenPGP public key + // GOpenPGP public key for the client PublicKey *crypto.Key - // GOpenPGP private key + // GOpenPGP private key for the client PrivateKey *crypto.Key // Check if the certificates are signed if enabled. @@ -34,13 +20,72 @@ type BismuthClient struct { // 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. - // See CertCheckCallback for more typing information. - CertificateSignChecker CertCheckCallback - // Connects to a server (used for CheckIfCertificatesAreSigned if enabled/set to true). - ConnectToServer OwnConnMethodCallback + // 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) + // - `isTrustworthy`: If true, the certificate is signed by 51% or more of peers. + // + // 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, isTrustworthy 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. + // + // 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 + IsTrustingAlready bool + // If true, server is trusting the previous server + IsTrustingRootServer 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 +} diff --git a/commons/enum.go b/commons/enum.go index 9e78c88..3c7f4ff 100644 --- a/commons/enum.go +++ b/commons/enum.go @@ -9,13 +9,10 @@ const ( // Sent by the client along with the symmetric key that is going to be used SwitchToSymmetricKey // Client sends what host they are connecting to. - // Currently unimplemented. ClientSendHost // Gets the signing servers trusting/signing the current server. - // Currently unimplemented. GetSigningServers // Gets the domains that are supported by this certificate (should be cross-checked) - // Currently unimplemented. GetTrustedDomains // Starts forwarding traffic over this protocol. InitiateForwarding diff --git a/main.go b/main.go index 4ded7a1..1dd63f9 100644 --- a/main.go +++ b/main.go @@ -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,8 @@ func bismuthClientEntrypoint(cCtx *cli.Context) error { return nil } -func bismuthServerEntrypoint(cCtx *cli.Context) error { - relayServers := []string{} +func serverEntrypoint(cCtx *cli.Context) error { + signingServers := []string{} pubKeyFile, err := os.ReadFile(cCtx.String("pubkey")) @@ -130,7 +137,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.NewBismuthServer(pubKey, privKey, signingServers, core.XChaCha20Poly1305) + bismuth.HandleConnection = func(connBismuth net.Conn, _ *server.ClientMetadata) error { connDialed, err := net.Dial("tcp", network) if err != nil { @@ -183,7 +191,7 @@ func bismuthServerEntrypoint(cCtx *cli.Context) error { }() return nil - }) + } if err != nil { return err @@ -201,23 +209,197 @@ 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.") + + signServers := []string{} + + 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.NewBismuthServer(pubKey, privKey, signServers, 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% 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% for certificate") + } + + isTrusted, err := signingclient.IsDomainTrusted(conn, keyFingerprint, domainList) + fmt.Printf("is certificate trusted: %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 +464,7 @@ func main() { }, }, Usage: "client for the Bismuth protocol", - Action: bismuthClientEntrypoint, + Action: clientEntrypoint, }, { Name: "server", @@ -308,6 +490,14 @@ func main() { Usage: "port to connect to", Required: true, }, + &cli.StringFlag{ + Name: "signing-servers", + Usage: "servers trusting/\"signing\" the public key. seperated using colons", + }, + &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 +510,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: "domain-names", + Usage: "domain names the key is authorized to use. seperated using colons", + }, + &cli.StringFlag{ + Name: "signing-server", + 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", }, }, } diff --git a/server/server.go b/server/server.go index b211f6e..c6f6c7f 100644 --- a/server/server.go +++ b/server/server.go @@ -162,7 +162,54 @@ 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 { + 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, @@ -171,9 +218,13 @@ func (bismuth BismuthServer) HandleProxy(conn net.Conn) error { bmConn.DoInitSteps() + metadata := ClientMetadata{ + ClientPublicKey: clientPublicKey, + } + err := bismuth.HandleConnection(core.BismuthConnWrapped{ Bismuth: &bmConn, - }) + }, &metadata) return err } diff --git a/server/typing.go b/server/typing.go index 9f830c3..bab25c4 100644 --- a/server/typing.go +++ b/server/typing.go @@ -13,14 +13,24 @@ type BismuthServer struct { // 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 + // 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) error + 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 } diff --git a/server/utils.go b/server/utils.go index 3016844..f57c280 100644 --- a/server/utils.go +++ b/server/utils.go @@ -4,7 +4,6 @@ import ( "crypto/cipher" "crypto/rand" "fmt" - "net" "github.com/ProtonMail/gopenpgp/v3/crypto" ) @@ -41,7 +40,7 @@ func (bismuth BismuthServer) decryptMessage(aead cipher.AEAD, encMsg []byte) ([] // 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) { +func NewBismuthServer(pubKey string, privKey string, signServers []string, encryptionAlgo int) (*BismuthServer, error) { publicKey, err := crypto.NewKeyFromArmored(pubKey) if err != nil { @@ -59,7 +58,6 @@ func NewBismuthServer(pubKey string, privKey string, signServers []string, encry bismuth := BismuthServer{ PublicKey: publicKey, PrivateKey: privateKey, - HandleConnection: connHandler, SigningServers: signServers, SymmetricEncryptionAlgorithm: encryptionAlgo, pgp: pgp, diff --git a/signingclient/signingclient.go b/signingclient/signingclient.go new file mode 100644 index 0000000..0fe3ebb --- /dev/null +++ b/signingclient/signingclient.go @@ -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 +} diff --git a/signingserver/signingserver.go b/signingserver/signingserver.go new file mode 100644 index 0000000..b226537 --- /dev/null +++ b/signingserver/signingserver.go @@ -0,0 +1,210 @@ +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) + + fmt.Println("keyFingerLen") + + if _, err := conn.Read(keyFingerprintLength); err != nil { + return err + } + + keyFingerprintBytes := make([]byte, binary.BigEndian.Uint16(keyFingerprintLength)) + + fmt.Println("keyFingerBytes") + + if _, err := conn.Read(keyFingerprintBytes); err != nil { + return err + } + + keyFingerprint := hex.EncodeToString(keyFingerprintBytes) + + serverDomainListLength := make([]byte, 2) + + fmt.Println("serverDomainListLen") + + if _, err := conn.Read(serverDomainListLength); err != nil { + return err + } + + serverDomainListBytes := make([]byte, binary.BigEndian.Uint16(serverDomainListLength)) + + fmt.Println("serverDomainList") + fmt.Printf("len: %d\n", binary.BigEndian.Uint16(serverDomainListLength)) + + if _, err := conn.Read(serverDomainListBytes); err != nil { + return err + } + + fmt.Println("done") + + 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) + } + } + } +} diff --git a/signingserver/typing.go b/signingserver/typing.go new file mode 100644 index 0000000..e15f7cd --- /dev/null +++ b/signingserver/typing.go @@ -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 +} diff --git a/signingserver/utils.go b/signingserver/utils.go new file mode 100644 index 0000000..955524a --- /dev/null +++ b/signingserver/utils.go @@ -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 +} diff --git a/tests/integration_test.go b/tests/integration_test.go index 9e56be7..aa657fe 100644 --- a/tests/integration_test.go +++ b/tests/integration_test.go @@ -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.NewBismuthServer(pubKeyServ, privKeyServ, []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() { @@ -92,7 +95,7 @@ func TestProtocolTxRx(t *testing.T) { 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()) From 86ad3e174f95607331744d4fce8e1f42d5e039b8 Mon Sep 17 00:00:00 2001 From: imterah Date: Thu, 24 Oct 2024 11:43:47 -0400 Subject: [PATCH 6/9] chore: Remove extra code. --- main.go | 2 +- signingserver/signingserver.go | 12 ------------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/main.go b/main.go index 1dd63f9..e2e0f18 100644 --- a/main.go +++ b/main.go @@ -391,7 +391,7 @@ func signCert(cCtx *cli.Context) error { } isTrusted, err := signingclient.IsDomainTrusted(conn, keyFingerprint, domainList) - fmt.Printf("is certificate trusted: %t\n", isTrusted) + fmt.Printf("Certificate trust status: %t\n", isTrusted) if !isTrusted { os.Exit(1) diff --git a/signingserver/signingserver.go b/signingserver/signingserver.go index b226537..0ff29e7 100644 --- a/signingserver/signingserver.go +++ b/signingserver/signingserver.go @@ -84,41 +84,29 @@ func (signServer *BismuthSigningServer) connHandler(conn net.Conn, metadata *ser // This is probably a bit too big, but I'd like to air on the side of caution here... keyFingerprintLength := make([]byte, 2) - fmt.Println("keyFingerLen") - if _, err := conn.Read(keyFingerprintLength); err != nil { return err } keyFingerprintBytes := make([]byte, binary.BigEndian.Uint16(keyFingerprintLength)) - fmt.Println("keyFingerBytes") - if _, err := conn.Read(keyFingerprintBytes); err != nil { return err } keyFingerprint := hex.EncodeToString(keyFingerprintBytes) - serverDomainListLength := make([]byte, 2) - fmt.Println("serverDomainListLen") - if _, err := conn.Read(serverDomainListLength); err != nil { return err } serverDomainListBytes := make([]byte, binary.BigEndian.Uint16(serverDomainListLength)) - fmt.Println("serverDomainList") - fmt.Printf("len: %d\n", binary.BigEndian.Uint16(serverDomainListLength)) - if _, err := conn.Read(serverDomainListBytes); err != nil { return err } - fmt.Println("done") - serverDomainList := strings.Split(string(serverDomainListBytes), "\n") // We can't trust anything if they aren't advertising any domains/IPs From 00a57443d48d4615b9ca2aa6fdf5b168482c0a87 Mon Sep 17 00:00:00 2001 From: imterah Date: Fri, 25 Oct 2024 11:59:16 -0400 Subject: [PATCH 7/9] feature: Adds signing validation support to the client. --- client/client.go | 251 +++++++++++++++++++++++++++++--------- client/typing.go | 24 ++-- main.go | 38 ++++-- server/server.go | 2 + server/utils.go | 3 +- tests/integration_test.go | 4 +- 6 files changed, 242 insertions(+), 80 deletions(-) diff --git a/client/client.go b/client/client.go index 2fef111..824b6f9 100644 --- a/client/client.go +++ b/client/client.go @@ -6,11 +6,34 @@ import ( "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 computeNodes(children []*BismuthSignResultData) (int, int) { + totalServerCount := 0 + passedServerCount := 0 + + 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 + } + } + + return totalServerCount, passedServerCount +} + // 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(). @@ -20,7 +43,7 @@ func (bismuth *BismuthClient) InitializeClient() error { } if bismuth.CertificateSignChecker == nil { - bismuth.CertificateSignChecker = func(host, certificateFingerprint string, isSelfSigned, isTrustworthy bool) bool { + bismuth.CertificateSignChecker = func(host, certificateFingerprint string, isSelfSigned bool) bool { fmt.Println("WARNING: Using stub CertificateSignChecker. Returing true and ignoring arguments") return true } @@ -40,9 +63,46 @@ func (bismuth *BismuthClient) InitializeClient() error { } } + bismuth.CheckIfCertificatesAreSigned = true + return nil } +func (bismuth *BismuthClient) checkIfDomainIsTrusted(servers, advertisedDomains []string) ([]*BismuthSignResultData, error) { + signResultData := make([]*BismuthSignResultData, len(servers)) + + 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, *BismuthSignResults, error) { @@ -158,6 +218,139 @@ func (bismuth *BismuthClient) Conn(conn net.Conn) (net.Conn, *BismuthSignResults 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 + } else { + fmt.Printf("ERROR: failed to verify servers (%s).\n", err.Error()) + signResults.OverallTrustScore = 0 + } + + totalServerCount, passedServerCount = computeNodes(rootNode.ChildNodes) + } 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)) @@ -179,62 +372,6 @@ func (bismuth *BismuthClient) Conn(conn net.Conn) (net.Conn, *BismuthSignResults conn.Write(hostInformationSize) conn.Write(encryptedHostInformationPacket) - // Request trusted proxies - - trustedProxyRequest := make([]byte, 1) - trustedProxyRequest[0] = core.GetSigningServers - - encryptedTrustedProxyRequest, err := bismuth.encryptMessage(aead, trustedProxyRequest) - - if err != nil { - conn.Close() - return nil, nil, err - } - - trustedProxyLength := make([]byte, 3) - core.Int32ToInt24(trustedProxyLength, uint32(len(encryptedTrustedProxyRequest))) - - conn.Write(trustedProxyLength) - conn.Write(encryptedTrustedProxyRequest) - - if _, err = conn.Read(trustedProxyLength); err != nil { - conn.Close() - return nil, nil, err - } - - encryptedTrustedProxyResponse := make([]byte, core.Int24ToInt32(trustedProxyLength)) - - if _, err = conn.Read(encryptedTrustedProxyResponse); err != nil { - conn.Close() - return nil, nil, err - } - - trustedProxyResponse, err := bismuth.decryptMessage(aead, encryptedTrustedProxyResponse) - - if err != nil { - conn.Close() - return nil, nil, err - } - - if trustedProxyResponse[0] != core.GetSigningServers { - conn.Close() - return nil, nil, fmt.Errorf("server failed to return its signing servers") - } - - signingServers := strings.Split(string(trustedProxyResponse[1:]), "\n") - isServerSelfSigned := len(trustedProxyResponse)-1 == 0 - - if !isServerSelfSigned { - fmt.Printf("acquired signing servers: '%s'\n", strings.Join(signingServers, ", ")) - } else { - fmt.Println("server is self signed, not printing (non-existent) signing servers") - } - - signResults := BismuthSignResults{ - OverallTrustScore: 100, - ServerPublicKey: serverPublicKey, - } - // Start proxying startForwardingPacket := []byte{ diff --git a/client/typing.go b/client/typing.go index ad9a532..873a94f 100644 --- a/client/typing.go +++ b/client/typing.go @@ -27,13 +27,12 @@ type BismuthClient struct { // - `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) - // - `isTrustworthy`: If true, the certificate is signed by 51% or more of peers. // // 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, isTrustworthy bool) bool + 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. @@ -49,7 +48,8 @@ type BismuthClient struct { // Connects to a server. // This function will only be called if client.CheckIfCertificatesAreSigned is true. // - // client.ConnectToServer("google.com:80") + // Example usage in the client source: + // client.ConnectToServer("google.com:80") ConnectToServer func(address string) (net.Conn, error) // GopenPGP instance @@ -62,9 +62,7 @@ type BismuthSignResultData struct { ChildNodes []*BismuthSignResultData // If true, the server is already trusting this node - IsTrustingAlready bool - // If true, server is trusting the previous server - IsTrustingRootServer bool + IsTrusting bool } type BismuthSignResults struct { @@ -79,13 +77,17 @@ type BismuthSignResults struct { type BismuthCertificates struct { // The host of the server - host string + Host string // A fingerprint of the servers key - certificateFingerprint string + CertificateFingerprint string // Certificate UserID - certificateUsername string - certificateMail string + CertificateUsername string + CertificateMail string // If true, the certificate is self signed - isSelfSigned bool + IsSelfSigned bool + + // If true, the client should not prompt the user, and automatically + // add the certificate instead. + ShouldAutomaticallyAdd bool } diff --git a/main.go b/main.go index e2e0f18..373320d 100644 --- a/main.go +++ b/main.go @@ -118,7 +118,16 @@ func clientEntrypoint(cCtx *cli.Context) error { } func serverEntrypoint(cCtx *cli.Context) error { - signingServers := []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")) @@ -137,7 +146,7 @@ func serverEntrypoint(cCtx *cli.Context) error { network := fmt.Sprintf("%s:%s", cCtx.String("source-ip"), cCtx.String("source-port")) - bismuth, err := server.NewBismuthServer(pubKey, privKey, signingServers, core.XChaCha20Poly1305) + 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) @@ -231,7 +240,16 @@ 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.") - signServers := []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")) @@ -254,7 +272,7 @@ func signingServerEntrypoint(cCtx *cli.Context) error { return err } - bismuthServer, err := server.NewBismuthServer(pubKey, privKey, signServers, core.XChaCha20Poly1305) + bismuthServer, err := server.New(pubKey, privKey, signServers, domainList, core.XChaCha20Poly1305) if err != nil { return nil @@ -328,7 +346,7 @@ func verifyCert(cCtx *cli.Context) error { } if certResults.OverallTrustScore < 50 { - return fmt.Errorf("overall trust score is below 50% for certificate") + return fmt.Errorf("overall trust score is below 50 percent for certificate") } fmt.Println("Sending signing request to sign server...") @@ -387,7 +405,7 @@ func signCert(cCtx *cli.Context) error { } if certResults.OverallTrustScore < 50 { - return fmt.Errorf("overall trust score is below 50% for certificate") + return fmt.Errorf("overall trust score is below 50 percent for certificate") } isTrusted, err := signingclient.IsDomainTrusted(conn, keyFingerprint, domainList) @@ -492,7 +510,7 @@ func main() { }, &cli.StringFlag{ Name: "signing-servers", - Usage: "servers trusting/\"signing\" the public key. seperated using colons", + Usage: "servers trusting/\"signing\" the public key. seperated using semicolons", }, &cli.StringFlag{ Name: "domain-names", @@ -537,11 +555,11 @@ func main() { Value: "9090", }, &cli.StringFlag{ - Name: "domain-names", - Usage: "domain names the key is authorized to use. seperated using colons", + Name: "signing-servers", + Usage: "servers trusting/\"signing\" the public key. seperated using semicolons", }, &cli.StringFlag{ - Name: "signing-server", + Name: "domain-names", Usage: "domain names the key is authorized to use. seperated using colons", }, }, diff --git a/server/server.go b/server/server.go index c6f6c7f..e9d61bd 100644 --- a/server/server.go +++ b/server/server.go @@ -1,6 +1,7 @@ package server import ( + "fmt" "net" core "git.greysoh.dev/imterah/bismuthd/commons" @@ -191,6 +192,7 @@ func (bismuth BismuthServer) HandleProxy(conn net.Conn) error { 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) { diff --git a/server/utils.go b/server/utils.go index f57c280..aff3186 100644 --- a/server/utils.go +++ b/server/utils.go @@ -40,7 +40,7 @@ func (bismuth BismuthServer) decryptMessage(aead cipher.AEAD, encMsg []byte) ([] // 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) (*BismuthServer, error) { +func New(pubKey string, privKey string, signServers []string, trustedDomains []string, encryptionAlgo int) (*BismuthServer, error) { publicKey, err := crypto.NewKeyFromArmored(pubKey) if err != nil { @@ -59,6 +59,7 @@ func NewBismuthServer(pubKey string, privKey string, signServers []string, encry PublicKey: publicKey, PrivateKey: privateKey, SigningServers: signServers, + TrustedDomains: trustedDomains, SymmetricEncryptionAlgorithm: encryptionAlgo, pgp: pgp, } diff --git a/tests/integration_test.go b/tests/integration_test.go index aa657fe..bc2e9da 100644 --- a/tests/integration_test.go +++ b/tests/integration_test.go @@ -53,7 +53,7 @@ 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) + bismuth, err := server.New(pubKeyServ, privKeyServ, []string{}, []string{}, commons.XChaCha20Poly1305) bismuth.HandleConnection = func(conn net.Conn, _ *server.ClientMetadata) error { for entryCount, randomDataSlice := range randomDataSlices { @@ -89,6 +89,8 @@ 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 { From e062975a0b65fefb03b611f4c2fd23f5c0548a09 Mon Sep 17 00:00:00 2001 From: imterah Date: Fri, 25 Oct 2024 12:01:02 -0400 Subject: [PATCH 8/9] fix: Fixes potential crashing issues. --- client/client.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/client.go b/client/client.go index 824b6f9..bdb7cf6 100644 --- a/client/client.go +++ b/client/client.go @@ -331,12 +331,11 @@ func (bismuth *BismuthClient) Conn(conn net.Conn) (net.Conn, *BismuthSignResults 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 } - - totalServerCount, passedServerCount = computeNodes(rootNode.ChildNodes) } else if isCertSigned { rootNode.IsTrusting = isCertSigned From 3a58a120331cda4333c267a272b0c3d75138962a Mon Sep 17 00:00:00 2001 From: imterah Date: Fri, 25 Oct 2024 12:17:22 -0400 Subject: [PATCH 9/9] fix: Removes debug print left over in server code. --- server/server.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/server.go b/server/server.go index e9d61bd..c6f6c7f 100644 --- a/server/server.go +++ b/server/server.go @@ -1,7 +1,6 @@ package server import ( - "fmt" "net" core "git.greysoh.dev/imterah/bismuthd/commons" @@ -192,7 +191,6 @@ func (bismuth BismuthServer) HandleProxy(conn net.Conn) error { 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) {