Skip to content

Commit

Permalink
Emissary for Android Update
Browse files Browse the repository at this point in the history
  • Loading branch information
imdawon committed May 25, 2024
1 parent 7ae0a37 commit 3cec5c0
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 26 deletions.
1 change: 1 addition & 0 deletions cmd/dashboard/ui/static/clients.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ <h2>Create Emissary Bundle</h2>
<option value="macos">macOS</option>
<option value="windows">Windows</option>
<option value="linux">Linux</option>
<option value="android">Android (13 and above)</option>
</select>
<input id="emissary-bundle-btn" type="submit" value="Create Emissary Bundle">
</form>
Expand Down
4 changes: 4 additions & 0 deletions cmd/dashboard/ui/static/css/clients.css
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,8 @@

.fleet-device {
margin-bottom: 50px;
}

#service-name {
font-size: large;
}
122 changes: 106 additions & 16 deletions cmd/drawbridge/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ func (d *Drawbridge) SetUpCAAndDependentServices(protectedServices []services.Pr
// An Emissary TCP Mutual TLS Key is used to allow the Emissary Client to connect to Drawbridge directly.
// The user will connect to the local proxy server the Emissary Client creates and all traffic will then flow
// through Drawbridge.
func (d *Drawbridge) CreateEmissaryClientTCPMutualTLSKey(clientId string, overrideDirectory ...string) (*string, error) {
func (d *Drawbridge) CreateEmissaryClientTCPMutualTLSKey(clientId, platform string, overrideDirectory ...string) (*string, error) {
var directoryToSave string
if len(overrideDirectory) == 0 {
directoryToSave = "./emissary_certs_and_key_here"
Expand Down Expand Up @@ -189,16 +189,35 @@ func (d *Drawbridge) CreateEmissaryClientTCPMutualTLSKey(clientId string, overri
return nil, err
}

certPrivKeyPEMBytes, err := x509.MarshalECPrivateKey(clientCertPrivKey)
if err != nil {
return nil, err
// Android is a special little platform. The Kotlin/Java stdlib seems to only have support for the
// PKCS8 format. We generate a key in this format for Android to avoid complicated conversion code
// on the Android client.
var certPrivKeyPEMBytes []byte
var certPrivKeyPEM *bytes.Buffer
if platform == "android" {
certPrivKeyPEMBytes, err = x509.MarshalPKCS8PrivateKey(clientCertPrivKey)
if err != nil {
return nil, err
}
certPrivKeyPEM = new(bytes.Buffer)
pem.Encode(certPrivKeyPEM, &pem.Block{
Type: "PRIVATE KEY",
Bytes: certPrivKeyPEMBytes,
})
// For non-Android platforms, use the EC Private Key format.
} else {
certPrivKeyPEMBytes, err = x509.MarshalECPrivateKey(clientCertPrivKey)
if err != nil {
return nil, err
}
certPrivKeyPEM = new(bytes.Buffer)
pem.Encode(certPrivKeyPEM, &pem.Block{
Type: "EC PRIVATE KEY",
Bytes: certPrivKeyPEMBytes,
})

}

certPrivKeyPEM := new(bytes.Buffer)
pem.Encode(certPrivKeyPEM, &pem.Block{
Type: "EC PRIVATE KEY",
Bytes: certPrivKeyPEMBytes,
})
// Save the file to disk for use by an Emissary client. This should be later used and saved in the db for downloading later.
err = utils.SaveFile("emissary-mtls-tcp.key", certPrivKeyPEM.String(), directoryToSave)
if err != nil {
Expand Down Expand Up @@ -357,7 +376,7 @@ func (d *Drawbridge) SetUpProtectedServiceTunnel() error {
slog.Error("Failed to tcp dial to actual target service", err)
}

slog.Debug(fmt.Sprintf("TCP Accept from Emissary client: %s\n", clientConn.RemoteAddr()))
slog.Debug(fmt.Sprintf("TCP Accept from Emissary client: %s", clientConn.RemoteAddr()))
// Copy data back and from client and server.
go io.Copy(resourceConn, clientConn)
io.Copy(clientConn, resourceConn)
Expand All @@ -371,7 +390,10 @@ func (d *Drawbridge) SetUpProtectedServiceTunnel() error {
// We pad the service id with zeros as we want a fixed-width id for easy parsing. This will allow support for up to 1000 Protected Services.
serviceList += fmt.Sprintf("%s%s,", utils.PadWithZeros(int(value.Service.ID)), value.Service.Name)
}
serviceConnectCommand := fmt.Sprintf("PS_LIST: %s", serviceList)
// The newline character is important for other platforms, such as Android,
// to properly read the string from the socket without blocking.
serviceConnectCommand := fmt.Sprintf("PS_LIST: %s\n", serviceList)
slog.Debug(fmt.Sprintf("PS_LIST values: %s\n", serviceConnectCommand))
clientConn.Write([]byte(serviceConnectCommand))
default:
}
Expand Down Expand Up @@ -428,10 +450,15 @@ type BundleFile struct {
// To accomplish this, we pull the latest version of Emissary from GitHub Releases, verify it is signed with the
// Drawbridge & Emissary signing key, generate the mTLS key(s) and cert, zip it all up, and allow the Drawbridge admin to download it.
func (d *Drawbridge) GenerateEmissaryBundle(config EmissaryConfig) (*BundleFile, error) {
if config.Platform != "macos" && config.Platform != "linux" && config.Platform != "windows" {
if config.Platform != "macos" && config.Platform != "linux" && config.Platform != "windows" && config.Platform != "android" {
return nil, fmt.Errorf("platform %s is not supported", config.Platform)
}

if config.Platform == "android" || config.Platform == "ios" {
slog.Debug("Making mobile platform Emissary Bundle")
return d.generateMobileEmissaryBundle(config.Platform)
}

// Get assets url
releaseResp, err := http.Get("https://api.github.com/repos/dhens/Emissary-Daemon/releases/latest")
if err != nil {
Expand Down Expand Up @@ -571,7 +598,7 @@ func (d *Drawbridge) GenerateEmissaryBundle(config EmissaryConfig) (*BundleFile,
return nil, fmt.Errorf("error generating uuid: %w", err)
}
certsAndKeysFolderPath := "./bundle_tmp/put_certificates_and_key_from_drawbridge_here"
emissaryCert, err := d.CreateEmissaryClientTCPMutualTLSKey(clientId, certsAndKeysFolderPath)
emissaryCert, err := d.CreateEmissaryClientTCPMutualTLSKey(clientId, config.Platform, certsAndKeysFolderPath)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -621,9 +648,9 @@ func (d *Drawbridge) GenerateEmissaryBundle(config EmissaryConfig) (*BundleFile,
}

func (d *Drawbridge) createEmissaryDevice(id, certificate string) error {
intOne := utils.RandInt(0, len(Adjectives))
intTwo := utils.RandInt(0, len(Animals))
deviceName := fmt.Sprintf("%s %s", Adjectives[intOne], Animals[intTwo])
adjectivesIndex := utils.RandInt(0, len(Adjectives))
animalsIndex := utils.RandInt(0, len(Animals))
deviceName := fmt.Sprintf("%s %s", Adjectives[adjectivesIndex], Animals[animalsIndex])

client := emissary.EmissaryClient{
ID: id,
Expand All @@ -637,3 +664,66 @@ func (d *Drawbridge) createEmissaryDevice(id, certificate string) error {
}
return nil
}

// Generate an Emissary Bundle for a mobile device.
// We can't fling .apk or .ipa files at mobile users, so we instead just ship the bundle with our certs, keypair, and drawbridge address.
func (d *Drawbridge) generateMobileEmissaryBundle(platform string) (*BundleFile, error) {
bundleTmpFolderPath := "./bundle_tmp"
// Create temporary directory used for placing Emissary files to zip up for use as the downloadable Emissary Bundle.
os.Mkdir(utils.CreateDrawbridgeFilePath(bundleTmpFolderPath), os.ModePerm)

// Generate and save the mTLS key(s) and cert
clientId, err := utils.NewUUID()
if err != nil {
return nil, fmt.Errorf("error generating uuid: %w", err)
}
certsAndKeysFolderPath := "./bundle_tmp/put_certificates_and_key_from_drawbridge_here"
emissaryCert, err := d.CreateEmissaryClientTCPMutualTLSKey(clientId, platform, certsAndKeysFolderPath)
if err != nil {
return nil, err
}
// Copy ca.crt next to keys
err = utils.CopyFile("./ca/ca.crt", certsAndKeysFolderPath)
if err != nil {
slog.Error("Emissary Bundle Creation", slog.Any("Error", fmt.Errorf("unable to copy the Drawbridge ca.crt file to the Emissary Bundle put_certificates_... folder: %s", err)))
return nil, err
}
// Generate and save bundle using Drawbridge listening address
listeningAddress, err := d.DB.GetDrawbridgeConfigValueByName("listening_address")
if err != nil {
return nil, err
}
if len(*listeningAddress) > 0 {
// TODO
// Change the port hardcoding and write the listening port in the lsiteningAddress config file instead.
utils.SaveFile("drawbridge.txt", fmt.Sprintf("%s:3100", *listeningAddress), "./bundle_tmp/bundle")
} else {
slog.Error("Emissary Bundle Creation", slog.String("Error", "Unable to get Drawbridge listening address. Unable to finish creating bundle."))
return nil, fmt.Errorf("error getting Drawbridge listening address")
}
// Zip up Emissary directory to bundles output folder.
bundledFilename := fmt.Sprintf("./android_bundle_%s", clientId)
// TODO
// return the file contents rather than writing to disk by default.
// there are tons of situations where we'd prefer to just hand off the bytes to the Drawbridge admin in the
// form of a file.
utils.ZipSource(bundleTmpFolderPath, bundledFilename)

// Serve to Drawbridge admin
slog.Debug("reading bundled emissary output file to send back to admin...")
bundledEmissaryZipFile := utils.ReadFile(bundledFilename)
// Remove temp folders
defer os.RemoveAll("./bundle_tmp")
defer os.RemoveAll("./emissary_download_scratch")
bundleFile := BundleFile{
Contents: bundledEmissaryZipFile,
Name: bundledFilename,
}

err = d.createEmissaryDevice(clientId, *emissaryCert)
if err != nil {
return nil, err
}
return &bundleFile, nil

}
2 changes: 1 addition & 1 deletion cmd/drawbridge/persistence/emissary_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func (r *SQLiteRepository) GetAllEmissaryClients() ([]*emissary.EmissaryClient,
}

func (r *SQLiteRepository) GetAllEmissaryClientCertificates() (map[string]emissary.DeviceCertificate, error) {
rows, err := r.db.Query("SELECT drawbridge_certificate AND id AND revoked FROM emissary_client WHERE revoked = 1")
rows, err := r.db.Query("SELECT drawbridge_certificate, id, revoked FROM emissary_client")
if err != nil {
return nil, fmt.Errorf("error getting all emissary clients: %s", err)
}
Expand Down
29 changes: 20 additions & 9 deletions cmd/reverse_proxy/ca/ca.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (
"dhens/drawbridge/cmd/utils"
"encoding/hex"
"encoding/pem"
"errors"
"fmt"
"log"
"log/slog"
Expand Down Expand Up @@ -93,6 +92,14 @@ func (c *CA) SetupCertificates() error {
MinVersion: tls.VersionTLS13,
}

// Populate the certificate authority's list of emissary certificates.
// Is used to lookup emissary client certs for revocation status to allow deny access to Drawbridge.
emissaryClientCertificates, err := c.DB.GetAllEmissaryClientCertificates()
if err != nil {
return err
}
c.CertificateList = emissaryClientCertificates

// Terminate function early as we have all of the cert and key data we need.
slog.Info("Loaded TLS Certs & Keys")
return nil
Expand Down Expand Up @@ -287,25 +294,29 @@ func (c *CA) SetupCertificates() error {
return nil
}

// THIS FUNCTION NEEDS TO BE FAST TO NOT DELAY HANDSHAKE
// Run for every Drawbridge + Emissary handshake to verify the presented cert is not revoked.
func (c *CA) verifyEmissaryCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
// Parse the peer certificate
// PEM encode
// Parse the peer certificate
func hashEmissaryCertificate(rawCert []byte) string {
caPEM := new(bytes.Buffer)
pem.Encode(caPEM, &pem.Block{
Type: "CERTIFICATE",
Bytes: rawCerts[0],
Bytes: rawCert,
})

// Calculate the SHA-256 hash of the peer certificate
hash := sha256.Sum256(caPEM.Bytes())
hexHash := hex.EncodeToString(hash[:])
return hexHash
}

// THIS FUNCTION NEEDS TO BE FAST TO NOT DELAY HANDSHAKE
// Run for every Drawbridge + Emissary handshake to verify the presented cert is not revoked.
func (c *CA) verifyEmissaryCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
// Parse the peer certificate
hexHash := hashEmissaryCertificate(rawCerts[0])
// Check if the certificate hash is in the revocation list
if c.CertificateList[hexHash].Revoked == 1 {
slog.Debug("peer cert is REVOKED")
return errors.New("peer certificate is revoked")
return fmt.Errorf("peer certificate is revoked")
}

// Additional certificate verification checks can be added here
Expand All @@ -322,7 +333,7 @@ func (c *CA) RevokeCertInCertificateRevocationList(shaCert string) {
defer revokedCertsMutex.Unlock()
cert, ok := c.CertificateList[shaCert]
if !ok {
slog.Error("Unable to revoke certificate", shaCert)
slog.Error("Unable to revoke certificate as it doesn't exist in the certificate list", shaCert)
}
certCopy := cert
certCopy.Revoked = 1
Expand Down

0 comments on commit 3cec5c0

Please sign in to comment.