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())