Skip to content

Commit

Permalink
Initial support for Linux system-wide certificates (qzind#1131)
Browse files Browse the repository at this point in the history
Adds Linux system-wide certificates
Adds Epiphany browser support
Closes qzind#518
  • Loading branch information
tresf authored May 22, 2023
1 parent acb5c5b commit ebd7ad0
Show file tree
Hide file tree
Showing 6 changed files with 437 additions and 119 deletions.
150 changes: 109 additions & 41 deletions src/qz/installer/LinuxInstaller.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package qz.installer;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
Expand Down Expand Up @@ -31,6 +30,12 @@ public class LinuxInstaller extends Installer {
public static final String CHROME_POLICY = "{ \"URLWhitelist\": [\"" + DATA_DIR + "://*\"] }";

private String destination = "/opt/" + PROPS_FILE;
private String sudoer;

public LinuxInstaller() {
super();
sudoer = getSudoer();
}

public void setDestination(String destination) {
this.destination = destination;
Expand Down Expand Up @@ -188,18 +193,114 @@ public Installer removeSystemSettings() {
*/
public void spawn(List<String> args) throws Exception {
if(!SystemUtilities.isAdmin()) {
// Not admin, just run as the existing user
ShellUtilities.execute(args.toArray(new String[args.size()]));
return;
}

// Get user's environment from dbus, etc
HashMap<String, String> env = getUserEnv(sudoer);
if(env.size() == 0) {
throw new Exception("Unable to get dbus info; can't spawn instance");
}

// Prepare the environment
String[] envp = new String[env.size() + ShellUtilities.envp.length];
int i = 0;
// Keep existing env
for(String keep : ShellUtilities.envp) {
envp[i++] = keep;
}
for(String key :env.keySet()) {
envp[i++] = String.format("%s=%s", key, env.get(key));
}

// Concat "sudo|su", sudoer, "nohup", args
ArrayList<String> argsList = sudoCommand(sudoer, true, args);

// Spawn
log.info("Executing: {}", Arrays.toString(argsList.toArray()));
Runtime.getRuntime().exec(argsList.toArray(new String[argsList.size()]), envp);
}

/**
* Constructs a command to help running as another user using "sudo" or "su"
*/
public static ArrayList<String> sudoCommand(String sudoer, boolean async, List<String> cmds) {
ArrayList<String> sudo = new ArrayList<>();
if(StringUtils.isEmpty(sudoer) || !userExists(sudoer)) {
throw new UnsupportedOperationException(String.format("Parameter [sudoer: %s] is empty or the provided user was not found", sudoer));
}
if(ShellUtilities.execute("which", "sudo") // check if sudo exists
|| ShellUtilities.execute("sudo", "-u", sudoer, "-v")) { // check if user can login
// Pass directly into "sudo"
log.info("Guessing that this system prefers \"sudo\" over \"su\".");
sudo.add("sudo");

// Add calling user
sudo.add("-E"); // preserve environment
sudo.add("-u");
sudo.add(sudoer);

// Add "background" task support
if(async) {
sudo.add("nohup");
}
if(cmds != null && cmds.size() > 0) {
// Add additional commands
sudo.addAll(cmds);
}
} else {
// Build and escape for "su"
log.info("Guessing that this system prefers \"su\" over \"sudo\".");
sudo.add("su");

// Add calling user
sudo.add(sudoer);

sudo.add("-c");

// Add "background" task support
if(async) {
sudo.add("nohup");
}
if(cmds != null && cmds.size() > 0) {
// Add additional commands
sudo.addAll(Arrays.asList(StringUtils.join(cmds, "\" \"") + "\""));
}
}
return sudo;
}

/**
* Gets the most likely non-root user account that the installer is running from
*/
private static String getSudoer() {
String sudoer = ShellUtilities.executeRaw("logname").trim();
if(sudoer.isEmpty() || SystemUtilities.isSolaris()) {
sudoer = System.getenv("SUDO_USER");
}
return sudoer;
}

if(sudoer != null && !sudoer.trim().isEmpty()) {
sudoer = sudoer.trim();
} else {
throw new Exception("Unable to get current user, can't spawn instance");
/**
* Uses two common POSIX techniques for testing if the provided user account exists
*/
private static boolean userExists(String user) {
return ShellUtilities.execute("id", "-u", user) ||
ShellUtilities.execute("getent", "passwd", user);
}

/**
* Attempts to extract user environment variables from the dbus process to
* allow starting a graphical application as the current user.
*
* If this fails, items such as the user's desktop theme may not be known to Java
* at runtime resulting in the Swing L&F instead of the Gtk L&F.
*/
private static HashMap<String, String> getUserEnv(String matchingUser) {
if(!SystemUtilities.isAdmin()) {
throw new UnsupportedOperationException("Administrative access is required");
}

String[] dbusMatches = { "ibus-daemon.*--panel", "dbus-daemon.*--config-file="};
Expand Down Expand Up @@ -251,10 +352,10 @@ public void spawn(List<String> args) throws Exception {
}

// Only add vars for the current user
if(sudoer.trim().equals(tempEnv.get("USER"))) {
if(matchingUser.trim().equals(tempEnv.get("USER"))) {
env.putAll(tempEnv);
} else {
log.debug("Expected USER={} but got USER={}, skipping results for {}", sudoer, tempEnv.get("USER"), pid);
log.debug("Expected USER={} but got USER={}, skipping results for {}", matchingUser, tempEnv.get("USER"), pid);
}

// Use gtk theme
Expand All @@ -264,40 +365,7 @@ public void spawn(List<String> args) throws Exception {
}
}
}

if(env.size() == 0) {
throw new Exception("Unable to get dbus info; can't spawn instance");
}

// Prepare the environment
String[] envp = new String[env.size() + ShellUtilities.envp.length];
int i = 0;
// Keep existing env
for(String keep : ShellUtilities.envp) {
envp[i++] = keep;
}
for(String key :env.keySet()) {
envp[i++] = String.format("%s=%s", key, env.get(key));
}

// Determine if this environment likes sudo
String[] sudoCmd = { "sudo", "-E", "-u", sudoer, "nohup" };
String[] suCmd = { "su", sudoer, "-c", "nohup" };

ArrayList<String> argsList = new ArrayList<>();
if(ShellUtilities.execute("which", "sudo")) {
// Pass directly into sudo
argsList.addAll(Arrays.asList(sudoCmd));
argsList.addAll(args);
} else {
// Build and escape for su
argsList.addAll(Arrays.asList(suCmd));
argsList.addAll(Arrays.asList(StringUtils.join(args, "\" \"") + "\""));
}

// Spawn
log.info("Executing: {}", Arrays.toString(argsList.toArray()));
Runtime.getRuntime().exec(argsList.toArray(new String[argsList.size()]), envp);
return env;
}

}
30 changes: 30 additions & 0 deletions src/qz/installer/certificate/CertificateManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@
package qz.installer.certificate;

import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.x500.AttributeTypeAndValue;
import org.bouncycastle.asn1.x500.RDN;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
Expand Down Expand Up @@ -444,4 +449,29 @@ private void saveProperties() throws IOException {
FileUtilities.inheritParentPermissions(propsFile.toPath());
log.info("Successfully created SSL properties file: {}", propsFile);
}

public static boolean emailMatches(X509Certificate cert) {
return emailMatches(cert, false);
}

public static boolean emailMatches(X509Certificate cert, boolean quiet) {
try {
X500Name x500name = new JcaX509CertificateHolder(cert).getSubject();
RDN[] emailNames = x500name.getRDNs(BCStyle.E);
for(RDN emailName : emailNames) {
AttributeTypeAndValue first = emailName.getFirst();
if (first != null && first.getValue() != null && Constants.ABOUT_EMAIL.equals(first.getValue().toString())) {
if(!quiet) {
log.info("Email address {} found, assuming CertProvider is {}", Constants.ABOUT_EMAIL, ExpiryTask.CertProvider.INTERNAL);
}
return true;
}
}
}
catch(Exception ignore) {}
if(!quiet) {
log.info("Email address {} was not found. Assuming the certificate is manually installed, we won't try to renew it.", Constants.ABOUT_EMAIL);
}
return false;
}
}
16 changes: 1 addition & 15 deletions src/qz/installer/certificate/ExpiryTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -146,20 +146,6 @@ public static ExpiryState getExpiry(X509Certificate cert) {
return ExpiryState.VALID;
}

private static boolean emailMatches(X509Certificate cert) {
try {
X500Name x500name = new JcaX509CertificateHolder(cert).getSubject();
String email = x500name.getRDNs(BCStyle.E)[0].getFirst().getValue().toString();
if (Constants.ABOUT_EMAIL.equals(email)) {
log.info("Email address {} found, assuming CertProvider is {}", Constants.ABOUT_EMAIL, CertProvider.INTERNAL);
return true;
}
}
catch(Exception ignore) {}
log.info("Email address {} was not found. Assuming the certificate is manually installed, we won't try to renew it.", Constants.ABOUT_EMAIL);
return false;
}

public void schedule() {
schedule(DEFAULT_INITIAL_DELAY, DEFAULT_CHECK_FREQUENCY);
}
Expand All @@ -183,7 +169,7 @@ public CertProvider findCertProvider() {

public static CertProvider findCertProvider(X509Certificate cert) {
// Internal certs use CN=localhost, trust email instead
if (emailMatches(cert)) {
if (CertificateManager.emailMatches(cert)) {
return CertProvider.INTERNAL;
}

Expand Down
Loading

0 comments on commit ebd7ad0

Please sign in to comment.