view ui/createinstallerdialog.cpp @ 1119:5349e2354c48

(issue54) Merge branch runafterinstall There is now an NSIS Plugin that executes the Software after installation using COM in the shell of the current user. With the way over the shell there is no inheritance / token management required. As it is impossible to drop all privileges of a token granted by UAC and still be able to reelevate the Token again with another RunAs call later this round trip over the Shell was necessary.
author Andre Heinecke <andre.heinecke@intevation.de>
date Tue, 16 Sep 2014 19:48:22 +0200
parents 01128d63226d
children a162f4cbba75
line wrap: on
line source
/* Copyright (C) 2014 by Bundesamt für Sicherheit in der Informationstechnik
 * Software engineering by Intevation GmbH
 *
 * This file is Free Software under the GNU GPL (v>=2)
 * and comes with ABSOLUTELY NO WARRANTY!
 * See LICENSE.txt for details.
 */
#include "createinstallerdialog.h"
#include "sslhelp.h"

#include <QDebug>
#include <QTextEdit>
#include <QDir>
#include <QPushButton>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QLabel>
#include <QFileDialog>
#include <QSaveFile>
#include <QSettings>
#include <QStyle>
#include <QApplication>
#include <QMessageBox>
#include <QTemporaryDir>

#include <polarssl/pk.h>

/* Static information used in codesigning */
#ifndef SIGN_HASH
#define SIGN_HASH "sha256"
#endif
#ifndef SIGN_URL
#define SIGN_URL "http://wald.intevation.org/projects/trustbridge/"
#endif
#ifndef SIGN_PUBLISHER
#define SIGN_PUBLISHER "TrustBridge Test with ümlaut"
#endif

CreateInstallerDialog::CreateInstallerDialog(QMainWindow *parent) :
    QDialog(parent),
    mProgress(this),
    mInstallerPath(),
    mCurrentWorkingDir(NULL)
{
    QSettings settings;
    setWindowTitle(tr("Create binary installer"));
    setupGUI();
    resize(500, 250);
    mCertFile->setText(settings.value("CodeSignCert", QString()).toString());
    mBinaryFolder->setText(settings.value("LastBinaryFolder", QString()).toString());
    mSaveFile->setText(settings.value("LastBinOutputFolder", QString()).toString());

    connect(&mNSISProc, SIGNAL(finished(int, QProcess::ExitStatus)),
            this, SLOT(processFinished(int, QProcess::ExitStatus)));
    connect(&mNSISProc, SIGNAL(error(QProcess::ProcessError)),
            this, SLOT(processError(QProcess::ProcessError)));
}

void CreateInstallerDialog::setupGUI()
{
    /* Top level layout / widgets */
    QVBoxLayout *topLayout = new QVBoxLayout;
    QVBoxLayout *headerLayout = new QVBoxLayout;
    QHBoxLayout *headerSubLayout = new QHBoxLayout;
    QHBoxLayout *centerLayout = new QHBoxLayout;
    QHBoxLayout *bottomLayout = new QHBoxLayout;
    QVBoxLayout *labelLayout = new QVBoxLayout;
    QVBoxLayout *fieldLayout = new QVBoxLayout;
    QVBoxLayout *buttonLayout = new QVBoxLayout;

    QLabel *header = new QLabel("<h3>" + tr("Create binary installer") + "</h3>");
    QLabel *description = new QLabel(
        tr("Create and sign a TrustBridge binary installer."));
    headerSubLayout->insertSpacing(0, 40);
    headerSubLayout->addWidget(description);
    QFrame *headerSeparator = new QFrame();
    headerSeparator->setFrameShape(QFrame::HLine);
    headerSeparator->setFrameShadow(QFrame::Sunken);
    headerLayout->addWidget(header);
    headerLayout->addLayout(headerSubLayout);
    headerLayout->addWidget(headerSeparator);
    headerLayout->insertSpacing(4, 10);

    QLabel *archiveLabel = new QLabel(tr("Select binary folder:"));
    QLabel *certLabel = new QLabel(tr("Select code signing certificate:"));
    QLabel *saveLabel = new QLabel(tr("Select output folder:"));
    labelLayout->addWidget(archiveLabel);
    labelLayout->addWidget(certLabel);
    labelLayout->addWidget(saveLabel);

    mBinaryFolder = new QLineEdit();
    mCertFile = new QLineEdit();
    mSaveFile = new QLineEdit();
    fieldLayout->addWidget(mBinaryFolder);
    fieldLayout->addWidget(mCertFile);
    fieldLayout->addWidget(mSaveFile);

    QPushButton *archiveSelect = new QPushButton("...");
    connect(archiveSelect, SIGNAL(clicked()), this, SLOT(openFolderSelect()));
    archiveSelect->setFixedWidth(30);
    QPushButton *certSelect = new QPushButton("...");
    connect(certSelect, SIGNAL(clicked()), this, SLOT(openCertificateSelect()));
    certSelect->setFixedWidth(30);
    QPushButton *saveSelect = new QPushButton("...");
    connect(saveSelect, SIGNAL(clicked()), this, SLOT(openSaveLocation()));
    saveSelect->setFixedWidth(30);
    buttonLayout->addWidget(archiveSelect);
    buttonLayout->addWidget(certSelect);
    buttonLayout->addWidget(saveSelect);

    centerLayout->addLayout(labelLayout);
    centerLayout->addLayout(fieldLayout);
    centerLayout->addLayout(buttonLayout);

    QPushButton *create = new QPushButton(tr("Create installer"));
    connect(create, SIGNAL(clicked()), this, SLOT(createInstaller()));
    QPushButton *cancel = new QPushButton(tr("Cancel"));
    connect(cancel, SIGNAL(clicked()), this, SLOT(close()));
    bottomLayout->insertStretch(0, 10);
    bottomLayout->addWidget(create);
    bottomLayout->addWidget(cancel);

    QFrame *bottomSeparator = new QFrame();
    bottomSeparator->setFrameShape(QFrame::HLine);
    bottomSeparator->setFrameShadow(QFrame::Sunken);

    topLayout->addLayout(headerLayout);
    topLayout->addLayout(centerLayout);
    topLayout->insertStretch(2, 10);
    centerLayout->insertSpacing(3, 10);
    topLayout->addWidget(bottomSeparator);
    topLayout->addLayout(bottomLayout);

    setLayout(topLayout);

    mProgress.setWindowModality(Qt::WindowModal);
    mProgress.setCancelButton(0);
    mProgress.setRange(0,0);
    mProgress.setMinimumDuration(0);

    return;
}

void CreateInstallerDialog::openCertificateSelect()
{
    QSettings settings;
    QString certFile = QFileDialog::getOpenFileName(
        this, tr("Select certificate"),
        mCertFile->text().isEmpty() ? QDir::homePath() : mCertFile->text(),
        "*.pem *.der *.crt");
    settings.setValue("CodeSignCert", certFile);
    mCertFile->setText(certFile);
}

void CreateInstallerDialog::openFolderSelect()
{
    QSettings settings;
    QString archiveFolder = QFileDialog::getExistingDirectory(
        this, tr("Select binary folder"),
        mBinaryFolder->text().isEmpty() ? QDir::homePath() : mBinaryFolder->text());
    mBinaryFolder->setText(archiveFolder);
    settings.setValue("LastBinaryFolder", archiveFolder);
}

void CreateInstallerDialog::openSaveLocation()
{
    QSettings settings;
    QString saveFile = QFileDialog::getExistingDirectory(
        this, tr("Select target location"),
        mSaveFile->text().isEmpty() ? QDir::homePath() : mSaveFile->text());
    mSaveFile->setText(saveFile);
    settings.setValue("LastBinOutputFolder", saveFile);
}

void CreateInstallerDialog::showErrorMessage(const QString &msg)
{
    QMessageBox::warning(this, tr("Error!"), msg);
}

void CreateInstallerDialog::processFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
    if (mCurrentWorkingDir) {
        delete mCurrentWorkingDir;
        mCurrentWorkingDir = NULL;
    }
    FinishedDialog *fin = new FinishedDialog(0, tr("Created installer in %1.")
            .arg(mSaveFile->text()), mNSISProc.readAll(), false);
    qDebug() << "Finished: " << mNSISProc.readAll();
    mProgress.setLabelText(tr("Signing installer package..."));
    if (!signFile(mInstallerPath)) {
        showErrorMessage(tr("Failed to sign installer package."));
        QFile::remove(mInstallerPath);
    }
    mProgress.cancel();
    fin->show();
    close();
}

void CreateInstallerDialog::processError(QProcess::ProcessError error)
{
    if (mCurrentWorkingDir) {
        delete mCurrentWorkingDir;
        mCurrentWorkingDir = NULL;
    }
    qDebug() << "Error: " << mNSISProc.readAll();
    mProgress.cancel();
}

void CreateInstallerDialog::createInstaller()
{
    mProgress.setLabelText(tr("Creating installer package..."));
    QDir binDir(mBinaryFolder->text());
    QDir outDir(mSaveFile->text());
    if (mBinaryFolder->text().isEmpty() || !binDir.exists()) {
        showErrorMessage(tr("Please select an existing input folder."));
        return;
    }
    if (mCertFile->text().isEmpty()) {
        showErrorMessage(tr("Please select a codesigning certificate."));
        return;
    }
    if (mSaveFile->text().isEmpty() || !outDir.exists()) {
        showErrorMessage(tr("Please select a output folder."));
        return;
    }
    QSettings options(binDir.filePath("meta.ini"), QSettings::IniFormat);
    options.sync();
    QStringList keys = options.allKeys();
    if (options.status() != QSettings::NoError || keys.size() < 1) {
        showErrorMessage(tr("Folder %1 does not appear to contain a meta.ini")
                .arg(binDir.path()));
        return;
    }
    /* Sign the linux installer */
    QDir linuxDir(binDir.path() + "/linux");
    if (!linuxDir.exists()) {
        showErrorMessage(tr("Failed to find the directory for linux binaries: %1")
                .arg(linuxDir.path()));
        return;
    }
    QStringList nameFilter;
    nameFilter << "*.sh";
    QStringList candidates = linuxDir.entryList(nameFilter, QDir::Files | QDir::Readable);
    if (candidates.isEmpty()) {
        showErrorMessage(tr("Failed to find a readable *.sh file in: %1")
                .arg(linuxDir.path()));
        return;
    }

    foreach (const QString& candidate, candidates) {
        mProgress.setLabelText(tr("Signing Linux package..."));
        mProgress.cancel();

        bool x86arch;
        if (candidate.endsWith("-i386.sh")) {
            x86arch = true;
        } else if (candidate.endsWith("-amd64.sh")) {
            x86arch = false;
        } else {
            qDebug() << "Could not detrmine architecture of " << candidate;
            qDebug() << "Skipping.";
            continue;
        }

        QString outFileName = options.value("setupname", "TrustBridge-default.exe"
                ).toString().replace(".exe",
                    x86arch ? "-i386.sh" : "-amd64.sh").arg(QString());

        if (!appendTextSignatureToFile(linuxDir.path() + "/" + candidate,
                    outDir.path() + "/" + outFileName)) {
            showErrorMessage(tr("Failed to sign linux package: %1").arg(candidate));
            mProgress.close();
            return;
        }
    }

    /* The Windows installer */

    mCurrentWorkingDir = codesignBinaries(binDir.path() + "/windows");

    if (!mCurrentWorkingDir) {
        /* Error messages should have been shown by the codesign function */
        mProgress.close();
        return;
    }

    mProgress.setLabelText(tr("Creating NSIS package..."));

    /* Copy windows directory contents to tmpdir */
    QStringList arguments;
    mNSISProc.setProgram("makensis");
    mNSISProc.setProcessChannelMode(QProcess::MergedChannels);
    mNSISProc.setWorkingDirectory(outDir.path());
#ifdef Q_OS_WIN
    arguments << QString::fromLatin1("/Dfiles_dir=") + mCurrentWorkingDir->path().replace("/", "\\");
    arguments << "/Dpath_sep=\\";
    foreach (const QString &key, keys) {
        QString value = options.value(key, QString()).toString();
        if (key == "setupname") {
            value = value.arg(outDir.path().replace("/", "\\") + "\\");
            mInstallerPath = value;
        }
        arguments << QString::fromLatin1("/D%1=%2").arg(key, value);
    }
    arguments << QString(binDir.path() + "/trustbridge.nsi").replace("/", "\\");
#else
    arguments << QString::fromLatin1("-Dfiles_dir=") + mCurrentWorkingDir->path();
    arguments << "-Dpath_sep=/";
    foreach (const QString &key, keys) {
        QString value = options.value(key, QString()).toString();
        if (key == "setupname") {
            value = value.arg(outDir.path() + "/");
            mInstallerPath = value;
        }
        arguments << QString::fromLatin1("-D%1=%2").arg(key, value);
    }
    arguments << binDir.path() + "/trustbridge.nsi";
#endif


    QFileInfo nsiFile (binDir.path() + "/trustbridge.nsi");
    if (!nsiFile.exists()) {
        showErrorMessage(tr("Failed to find installer script at: %1 ").arg(nsiFile.filePath()));
    }

    qDebug() << "Starting makensis with arguments: " << arguments;
    mNSISProc.setArguments(arguments);
    mNSISProc.start();
    mProgress.show();

    if (!mNSISProc.waitForStarted() ||
        mNSISProc.state() == QProcess::NotRunning) {
        showErrorMessage(tr("Failed to start makensis.\n"
            "Please ensure that makensis is installed and in your PATH variable."));
    }
}

bool CreateInstallerDialog::signFile(QString filePath) {
    QProcess signProc;
    signProc.setProcessChannelMode(QProcess::MergedChannels);
    signProc.setProgram("osslsigncode");
    QStringList arguments;

    QSettings mySettings;

    QString publisher = mySettings.value("sign_publisher", SIGN_PUBLISHER).toString();
    QString url = mySettings.value("sign_url", SIGN_URL).toString();
    QString hash = mySettings.value("sign_hash", SIGN_HASH).toString();

    arguments << "sign" << "-certs" << mCertFile->text() << "-key"
              << mCertFile->text() << "-n" << publisher << "-i" <<
              url << "-h" << hash << "-in" << filePath << "-out" << filePath + ".signed";

    qDebug() << "Starting osslsigncode with arguments: " << arguments;
    signProc.setArguments(arguments);
    signProc.start();

    if (!signProc.waitForFinished(30000)) {
        qDebug() << "Signing takes longer then 30 seconds. Aborting.";
        return false;
    }

    if (signProc.exitStatus() != QProcess::NormalExit ||
        signProc.exitCode() != 0) {
        qDebug() << "Error process returned: " << signProc.exitCode();
        qDebug() << "Output: " << signProc.readAllStandardOutput();
        return false;
    }

    if (!QFile::remove(filePath)) {
        qDebug() << "Failed to remove file.";
        return false;
    }
    if (!QFile::copy(filePath + ".signed", filePath)) {
        qDebug() << "Failed to copy signed file in place.";
        return false;
    }
    if (!QFile::remove(filePath + ".signed")) {
        qDebug() << "Failed to remove signed.";
        return false;
    }
    return true;
}

bool copyPath(QString src, QString dst)
{
    QDir dir(src);
    if (! dir.exists()) {
        qDebug() << "Source directory does not exist.";
        return false;
    }

    foreach (QString d, dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
        QString dst_path = dst + QDir::separator() + d;
        dir.mkpath(dst_path);
        if (!copyPath(src+ QDir::separator() + d, dst_path)) {
            qDebug() << "Failed to copy subdirectory; " << d;
            return false;
        }
    }

    foreach (QString f, dir.entryList(QDir::Files)) {
        if (!QFile::copy(src + QDir::separator() + f, dst + QDir::separator() + f)) {
            qDebug() << "Failed to copy: " << f;
            return false;
        }
    }
    return true;
}

QTemporaryDir *CreateInstallerDialog::codesignBinaries(const QDir& sourceDir) {
    QTemporaryDir* target = new QTemporaryDir();
    /* Copy all files from the source dir to a temporary location */
    mProgress.setLabelText(tr("Signing binaries..."));

    mProgress.show();

    /* Copy the whole directory. */
    if (!copyPath(sourceDir.path(), target->path())) {
        qDebug() << "Copy failed.";
        showErrorMessage(tr("Failed to copy binaries to temporary location."));
        mProgress.cancel();
        return NULL;
    }
    /* Sign the top level .exe files */
    foreach (const QFileInfo& entry, sourceDir.entryInfoList()) {
        QString targetPath = target->path() + QString::fromLatin1("/") + entry.fileName();
        if (entry.fileName() == "." || entry.fileName() == "..") {
            continue;
        }
        if (entry.suffix() == "exe") {
            if (!signFile(targetPath)) {
                showErrorMessage(tr("Failed to sign binaries with osslsigncode.\n"
                            "Please check that %1 is a valid code signing certificate and that"
                            "osslsigncode can be found in the PATH.").arg(mCertFile->text()));
                mProgress.cancel();
                return NULL;
            }
        }
    }
    mProgress.cancel();
    return target;
}

bool CreateInstallerDialog::appendTextSignatureToFile(const QString& input,
                                                 const QString& output) {
    QFile inFile(input);
    pk_context pk;

    pk_init(&pk);
    int ret = pk_parse_keyfile(&pk, mCertFile->text().toLocal8Bit().constData(), "");

    if (ret != 0) {
        showErrorMessage(tr("Failed to load certificate: %1")
                .arg(getPolarSSLErrorMsg(ret)));
        pk_free(&pk);
        return false;
    }

    /* Check that it is a 3072 bit RSA key as specified */
    if (!pk.pk_info || pk_get_size(&pk) != 3072 ||
            pk.pk_info->type != POLARSSL_PK_RSA) {
        qDebug() << pk.pk_info->type << "type";
        qDebug() << POLARSSL_PK_RSA << "rsa";
        qDebug() << "size " << pk_get_size(&pk);
        showErrorMessage(tr("Only 3072 bit RSA keys are supported by the current format."));
        pk_free(&pk);
        return false;
    }

    if (!inFile.open(QIODevice::ReadOnly)) {
        showErrorMessage(tr("Failed to open input file: %1").arg(inFile.fileName()));
        pk_free(&pk);
        return false;
    }

    const QByteArray inputContent = inFile.readAll(); // Memory is cheap :)
    inFile.close();

    if (inputContent.isEmpty()) {
        showErrorMessage(tr("Failed to read input file: %1").arg(inFile.fileName()));
        pk_free(&pk);
        return false;
    }

    const QByteArray signature = rsaSignSHA256Hash(sha256sum(inputContent), &pk);

    pk_free(&pk);
    if (signature.size() != 3072 / 8) {
        qDebug() << "Signature creation returned signature of invalid size.";
        return false;
    }

    QSaveFile outFile(output);
    outFile.open(QIODevice::WriteOnly);
    outFile.write(inputContent);
    outFile.write("\r\nS:");
    outFile.write(signature.toBase64());
    outFile.write("\n");

    return outFile.commit();
}

FinishedDialog::FinishedDialog(QDialog *parent,
        QString msg, QString details, bool isErr):
    QDialog(parent)
{
    QVBoxLayout *topLayout = new QVBoxLayout;
    QHBoxLayout *buttonLayout = new QHBoxLayout;
    QLabel *msgLabel = new QLabel;
    QTextEdit *detailsWindow = new QTextEdit;

    detailsWindow->insertPlainText(details);
    detailsWindow->setReadOnly(true);
    detailsWindow->hide();

    if (!isErr) {
        setWindowTitle(tr("Successfully created installation package"));
        msgLabel->setPixmap(QApplication::style()->standardIcon(
                    QStyle::SP_MessageBoxInformation).pixmap(16, 16));
    } else {
        setWindowTitle(tr("Error!"));
        msgLabel->setPixmap(QApplication::style()->standardIcon(
                    QStyle::SP_MessageBoxCritical).pixmap(16, 16));
    }
    msgLabel->setText(msg);

    topLayout->addWidget(msgLabel);
    topLayout->addWidget(detailsWindow);
    QPushButton *detailsBtn = new QPushButton(tr("Details"));
    connect(detailsBtn, SIGNAL(clicked()), detailsWindow, SLOT(show()));
    buttonLayout->addWidget(detailsBtn);

    QPushButton *okBtn = new QPushButton(tr("OK"));
    okBtn->setIcon(QApplication::style()->standardIcon(QStyle::SP_DialogOkButton));
    connect(okBtn, SIGNAL(clicked()), this, SLOT(close()));
    buttonLayout->insertStretch(0, 100);
    buttonLayout->addWidget(okBtn);

    topLayout->addLayout(buttonLayout);
    setLayout(topLayout);
}

http://wald.intevation.org/projects/trustbridge/