feature: Adds signing validation support to the client.

This commit is contained in:
imterah 2024-10-25 11:59:16 -04:00
parent 86ad3e174f
commit 00a57443d4
Signed by: imterah
GPG key ID: 8FA7DD57BA6CEA37
6 changed files with 242 additions and 80 deletions

View file

@ -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{

View file

@ -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
}

38
main.go
View file

@ -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",
},
},

View file

@ -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) {

View file

@ -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,
}

View file

@ -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 {