view ui/createinstallerdialog.cpp @ 1368:41cf49df007d

(issue179) Add signature timestamp in linux installer packages
author Andre Heinecke <andre.heinecke@intevation.de>
date Mon, 24 Nov 2014 14:04:34 +0100
parents 137a0686de7b
children c8a6a3e6bdeb
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 <QDateTime>

#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("/", "\\");
    QString resourcedir = binDir.path() + "/resources";
    arguments << QString::fromLatin1("/Dplugin_dir=") + resourcedir.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 << QString::fromLatin1("-Dplugin_dir=") + binDir.path() + "/resources";
    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);
    QStringList candidates;
    candidates << "osslsigncode"
        << QDir::toNativeSeparators(QCoreApplication::applicationDirPath() + "/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);
    bool once_successful = false;
    foreach (const QString &prog, candidates) {
        signProc.setProgram(prog);
        signProc.start();

        if (!signProc.waitForFinished(30000)) {
            continue;
        }

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

    if (!once_successful) {
        qDebug() << "Failed to execute all osslsigncode candidates.";
        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) {
        if (pk.pk_info) {
            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;
    }

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


    /* Append the current date time to the signed data so that it is also signed.
     * Until 2106 qt will probably return an 64 bit int on toTime_t. If not
     * I'm sorry. */
    QString sign_dt = QString("%1").arg(QDateTime::currentDateTime().toTime_t());
    inputContent.replace("###SIGNATURE_DATE###", sign_dt.toLocal8Bit().constData());
    inputContent.append("\r\nS_DT:" + sign_dt);

    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/