view ui/createinstallerdialog.cpp @ 1395:a2574a029322

Fix Base 64 signature size calculation. If the signature byte size is not equally dividable by three the base 64 encoding needs three additional bytes. The value is now fixed to avoid such errors in the future.
author Andre Heinecke <andre.heinecke@intevation.de>
date Mon, 26 Jan 2015 13:17:32 +0100
parents f3e2df6b49ba
children
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 "pubkey.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 "https://www.trustbridge.de"
#endif
#ifndef SIGN_PUBLISHER
#define SIGN_PUBLISHER "Bundesamt für Sicherheit in der Informationstechnik"
#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;
    }

    mProgress.setLabelText(tr("Signing installer package..."));
    if (!signFile(mInstallerPath)) {
        showErrorMessage(tr("Failed to sign installer package."));
        QFile::remove(mInstallerPath);
        mProgress.cancel();
    } else {
        mProgress.setLabelText(tr("Calculating checksums..."));
        QString checksums = QString::fromLatin1("<br/><h3>") + tr("Checksums:") + "</h3>";
        QDir outDir(mSaveFile->text());
        bool checksumErr = false;
        QStringList filters;
        filters << "TrustBridge-*.sh" << "TrustBridge-*.exe";
        qDebug() << "Entries: " << outDir.entryList(filters);
        qDebug() << "Entries unfiltered: " << outDir.entryList();
        foreach (const QString &file, outDir.entryList(filters)) {
            QFile f(outDir.filePath(file));
            if (!f.open(QIODevice::ReadOnly)) {
                showErrorMessage (tr("Failed to open file \"%1\".").arg(file));
                checksumErr = true;
                break;
            }
            const QByteArray fileData = f.readAll();
            if (fileData.isEmpty()) {
                showErrorMessage (tr("Failed to read file \"%1\".").arg(file));
                checksumErr = true;
                break;
            }
            const QByteArray theSha256sum = sha256sum(fileData);
            const QByteArray theSha1sum = sha1sum(fileData);
            if (theSha1sum.isEmpty() || theSha256sum.isEmpty()) {
                showErrorMessage (tr("Failed to calculate checksums for \"%1\".").arg(file));
                checksumErr = true;
                break;
            }
            checksums += QString::fromLatin1("<br/><b>%1:</b><br/><pre>").arg(file);
            checksums += "    SHA1: ";
            for (int i=0; i < theSha1sum.size(); i++) {
                checksums += QString("%1").arg(
                    (unsigned char)(theSha1sum[i]), 0, 16).rightJustified(2, '0');
            }

            checksums += "\n    SHA256: ";
            for (int i=0; i < theSha256sum.size(); i++) {
                checksums += QString("%1").arg(
                    (unsigned char)(theSha256sum[i]), 0, 16).rightJustified(2, '0');
            }
            checksums += "</pre>";

        }

        mProgress.cancel();
        if (!checksumErr) {
            FinishedDialog *fin = new FinishedDialog(0, tr("Successfully created the installation packages in \"%1\".")
                    .arg(mSaveFile->text()) + checksums, mNSISProc.readAll(), false);
            qDebug() << "Finished: " << mNSISProc.readAll();
            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 an RSA key that matches the size */
    if (!pk.pk_info || pk_get_size(&pk) != TRUSTBRIDGE_RSA_CODESIGN_SIZE ||
            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 %1 bit RSA keys are supported by the current format.").arg(
                    TRUSTBRIDGE_RSA_CODESIGN_SIZE));
        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() != TRUSTBRIDGE_RSA_CODESIGN_SIZE / 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);
    msgLabel->setTextInteractionFlags(
        Qt::TextSelectableByMouse |
        Qt::TextSelectableByKeyboard);

    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/