/** \ingroup rpmcli
 * \file lib/rpmchecksig.c
 * Verify the signature of a package.
 */

#include "system.h"

#include <rpm/rpmlib.h>			/* RPMSIGTAG & related */
#include <rpm/rpmpgp.h>
#include <rpm/rpmcli.h>
#include <rpm/rpmfileutil.h>	/* rpmMkTemp() */
#include <rpm/rpmdb.h>
#include <rpm/rpmts.h>
#include <rpm/rpmlog.h>
#include <rpm/rpmstring.h>
#include <rpm/rpmkeyring.h>

#include "rpmio/digest.h"
#include "rpmio/rpmio_internal.h" 	/* fdSetBundle() */
#include "lib/rpmlead.h"
#include "lib/signature.h"

#include "debug.h"

int _print_pkts = 0;

static int doImport(rpmts ts, const char *fn, char *buf, ssize_t blen)
{
    char const * const pgpmark = "-----BEGIN PGP ";
    size_t marklen = strlen(pgpmark);
    int res = 0;
    int keyno = 1;
    char *start = strstr(buf, pgpmark);
    
    while (start) {
	uint8_t *pkt = NULL;
	size_t pktlen = 0;
	
	/* Read pgp packet. */
	if (pgpParsePkts(start, &pkt, &pktlen) == PGPARMOR_PUBKEY) {
	    /* Import pubkey packet(s). */
	    if (rpmtsImportPubkey(ts, pkt, pktlen) != RPMRC_OK) {
		rpmlog(RPMLOG_ERR, _("%s: key %d import failed.\n"), fn, keyno);
		res++;
	    }
	} else {
	    rpmlog(RPMLOG_ERR, _("%s: key %d not an armored public key.\n"),
		   fn, keyno);
	    res++;
	}
	
	/* See if there are more keys in the buffer */
	if (start + marklen < buf + blen) {
	    start = strstr(start + marklen, pgpmark);
	} else {
	    start = NULL;
	}

	keyno++;
	free(pkt);
    }
    return res;
}

int rpmcliImportPubkeys(rpmts ts, ARGV_const_t argv)
{
    int res = 0;
    for (ARGV_const_t arg = argv; arg && *arg; arg++) {
	const char *fn = *arg;
	uint8_t *buf = NULL;
	ssize_t blen = 0;
	char *t = NULL;
	int iorc;

	/* If arg looks like a keyid, then attempt keyserver retrieve. */
	if (rstreqn(fn, "0x", 2)) {
	    const char * s = fn + 2;
	    int i;
	    for (i = 0; *s && isxdigit(*s); s++, i++)
		{};
	    if (i == 8 || i == 16) {
		t = rpmExpand("%{_hkp_keyserver_query}", fn+2, NULL);
		if (t && *t != '%')
		    fn = t;
	    }
	}

	/* Read the file and try to import all contained keys */
	iorc = rpmioSlurp(fn, &buf, &blen);
	if (iorc || buf == NULL || blen < 64) {
	    rpmlog(RPMLOG_ERR, _("%s: import read failed(%d).\n"), fn, iorc);
	    res++;
	} else {
	    res += doImport(ts, fn, (char *)buf, blen);
	}
	
	free(t);
	free(buf);
    }
    return res;
}

/**
 * @todo If the GPG key was known available, the md5 digest could be skipped.
 */
static int readFile(FD_t fd, const char * fn, pgpDig dig,
		    rpmDigestBundle plbundle, rpmDigestBundle hdrbundle)
{
    unsigned char buf[4*BUFSIZ];
    ssize_t count;
    int rc = 1;
    Header h = NULL;

    /* Read the header from the package. */
    if ((h = headerRead(fd, HEADER_MAGIC_YES)) == NULL) {
	rpmlog(RPMLOG_ERR, _("%s: headerRead failed\n"), fn);
	goto exit;
    }

    if (headerIsEntry(h, RPMTAG_HEADERIMMUTABLE)) {
	struct rpmtd_s utd;
    
	if (!headerGet(h, RPMTAG_HEADERIMMUTABLE, &utd, HEADERGET_DEFAULT)){
	    rpmlog(RPMLOG_ERR, 
		    _("%s: Immutable header region could not be read. "
		    "Corrupted package?\n"), fn);
	    goto exit;
	}
	rpmDigestBundleUpdate(hdrbundle, rpm_header_magic, sizeof(rpm_header_magic));
	rpmDigestBundleUpdate(hdrbundle, utd.data, utd.count);
	rpmtdFreeData(&utd);
    }

    /* Read the payload from the package. */
    while ((count = Fread(buf, sizeof(buf[0]), sizeof(buf), fd)) > 0) {}
    if (count < 0) {
	rpmlog(RPMLOG_ERR, _("%s: Fread failed: %s\n"), fn, Fstrerror(fd));
	goto exit;
    }

    rc = 0;

exit:
    headerFree(h);
    return rc;
}

/* Parse the parameters from the OpenPGP packets that will be needed. */
/* XXX TODO: unify with similar parsePGP() in package.c */
static rpmRC parsePGP(rpmtd sigtd, const char *fn, pgpDig dig)
{
    rpmRC rc = RPMRC_FAIL;
    int debug = (_print_pkts & rpmIsDebug());
    if ((pgpPrtPkts(sigtd->data, sigtd->count, dig, debug) == 0) &&
	 (dig->signature.version == 3 || dig->signature.version == 4)) {
	rc = RPMRC_OK;
    } else {
	rpmlog(RPMLOG_ERR,
	    _("skipping package %s with unverifiable V%u signature\n"), fn,
	    dig->signature.version);
    }
    return rc;
}

/* 
 * Figure best available signature. 
 * XXX TODO: Similar detection in rpmReadPackageFile(), unify these.
 */
static rpmTagVal bestSig(Header sigh, int nosignatures, int nodigests)
{
    rpmTagVal sigtag = 0;
    if (sigtag == 0 && !nosignatures) {
	if (headerIsEntry(sigh, RPMSIGTAG_DSA))
	    sigtag = RPMSIGTAG_DSA;
	else if (headerIsEntry(sigh, RPMSIGTAG_RSA))
	    sigtag = RPMSIGTAG_RSA;
	else if (headerIsEntry(sigh, RPMSIGTAG_GPG))
	    sigtag = RPMSIGTAG_GPG;
	else if (headerIsEntry(sigh, RPMSIGTAG_PGP))
	    sigtag = RPMSIGTAG_PGP;
    }
    if (sigtag == 0 && !nodigests) {
	if (headerIsEntry(sigh, RPMSIGTAG_MD5))
	    sigtag = RPMSIGTAG_MD5;
	else if (headerIsEntry(sigh, RPMSIGTAG_SHA1))
	    sigtag = RPMSIGTAG_SHA1;	/* XXX never happens */
    }
    return sigtag;
}

static const char *sigtagname(rpmTagVal sigtag, int upper)
{
    const char *n = NULL;

    switch (sigtag) {
    case RPMSIGTAG_SIZE:
	n = (upper ? "SIZE" : "size");
	break;
    case RPMSIGTAG_SHA1:
	n = (upper ? "SHA1" : "sha1");
	break;
    case RPMSIGTAG_MD5:
	n = (upper ? "MD5" : "md5");
	break;
    case RPMSIGTAG_RSA:
	n = (upper ? "RSA" : "rsa");
	break;
    case RPMSIGTAG_PGP5:	/* XXX legacy */
    case RPMSIGTAG_PGP:
	n = (upper ? "(MD5) PGP" : "(md5) pgp");
	break;
    case RPMSIGTAG_DSA:
	n = (upper ? "(SHA1) DSA" : "(sha1) dsa");
	break;
    case RPMSIGTAG_GPG:
	n = (upper ? "GPG" : "gpg");
	break;
    default:
	n = (upper ? "?UnknownSigatureType?" : "???");
	break;
    }
    return n;
}

/* 
 * Format sigcheck result for output, appending the message spew to buf and
 * bad/missing keyids to keyprob.
 *
 * In verbose mode, just dump it all. Otherwise ok signatures
 * are dumped lowercase, bad sigs uppercase and for PGP/GPG
 * if misssing/untrusted key it's uppercase in parenthesis
 * and stash the key id as <SIGTYPE>#<keyid>. Pfft.
 */
static void formatResult(rpmTagVal sigtag, rpmRC sigres, const char *result,
			 int havekey, char **keyprob, char **buf)
{
    char *msg = NULL;
    if (rpmIsVerbose()) {
	rasprintf(&msg, "    %s", result);
    } else { 
	/* Check for missing / untrusted keys in result. */
	const char *signame = sigtagname(sigtag, (sigres != RPMRC_OK));
	
	if (havekey && (sigres == RPMRC_NOKEY || sigres == RPMRC_NOTTRUSTED)) {
	    const char *tempKey = strstr(result, "ey ID");
	    if (tempKey) {
		char keyid[sizeof(pgpKeyID_t) + 1];
		rstrlcpy(keyid, tempKey + 6, sizeof(keyid));
		rstrscat(keyprob, " ", signame, "#", keyid, NULL);
	    }
	}
	rasprintf(&msg, (*keyprob ? "(%s) " : "%s "), signame);
    }
    rstrcat(buf, msg);
    free(msg);
}

static int rpmpkgVerifySigs(rpmKeyring keyring, rpmQueryFlags flags,
			   FD_t fd, const char *fn)
{

    char *buf = NULL;
    char *missingKeys = NULL; 
    char *untrustedKeys = NULL;
    struct rpmtd_s sigtd;
    rpmTagVal sigtag;
    pgpDig dig = NULL;
    pgpDigParams sigp;
    Header sigh = NULL;
    HeaderIterator hi = NULL;
    char * msg = NULL;
    int res = 1; /* assume failure */
    int xx;
    rpmRC rc;
    int failed = 0;
    int nodigests = !(flags & VERIFY_DIGEST);
    int nosignatures = !(flags & VERIFY_SIGNATURE);
    rpmDigestBundle plbundle = rpmDigestBundleNew();
    rpmDigestBundle hdrbundle = rpmDigestBundleNew();

    rpmlead lead = rpmLeadNew();
    if ((rc = rpmLeadRead(fd, lead)) == RPMRC_OK) {
	const char *lmsg = NULL;
	rc = rpmLeadCheck(lead, &lmsg);
	if (rc != RPMRC_OK) 
	    rpmlog(RPMLOG_ERR, "%s: %s\n", fn, lmsg);
    }
    lead = rpmLeadFree(lead);

    if (rc != RPMRC_OK) {
	goto exit;
    }

    rc = rpmReadSignature(fd, &sigh, RPMSIGTYPE_HEADERSIG, &msg);
    switch (rc) {
    default:
	rpmlog(RPMLOG_ERR, _("%s: rpmReadSignature failed: %s"), fn,
		    (msg && *msg ? msg : "\n"));
	msg = _free(msg);
	goto exit;
	break;
    case RPMRC_OK:
	if (sigh == NULL) {
	    rpmlog(RPMLOG_ERR, _("%s: No signature available\n"), fn);
	    goto exit;
	}
	break;
    }
    msg = _free(msg);

    /* Grab a hint of what needs doing to avoid duplication. */
    sigtag = bestSig(sigh, nosignatures, nodigests);

    dig = pgpNewDig();
    sigp = &dig->signature;

    /* XXX RSA needs the hash_algo, so decode early. */
    if (sigtag == RPMSIGTAG_RSA || sigtag == RPMSIGTAG_PGP ||
		sigtag == RPMSIGTAG_DSA || sigtag == RPMSIGTAG_GPG) {
	xx = headerGet(sigh, sigtag, &sigtd, HEADERGET_DEFAULT);
	xx = pgpPrtPkts(sigtd.data, sigtd.count, dig, 0);
	rpmtdFreeData(&sigtd);
	/* XXX assume same hash_algo in header-only and header+payload */
	rpmDigestBundleAdd(plbundle, sigp->hash_algo, RPMDIGEST_NONE);
	rpmDigestBundleAdd(hdrbundle, sigp->hash_algo, RPMDIGEST_NONE);
    }

    if (headerIsEntry(sigh, RPMSIGTAG_PGP) ||
		      headerIsEntry(sigh, RPMSIGTAG_PGP5) ||
		      headerIsEntry(sigh, RPMSIGTAG_MD5)) {
	rpmDigestBundleAdd(plbundle, PGPHASHALGO_MD5, RPMDIGEST_NONE);
    }
    if (headerIsEntry(sigh, RPMSIGTAG_GPG)) {
	rpmDigestBundleAdd(plbundle, PGPHASHALGO_SHA1, RPMDIGEST_NONE);
    }

    /* always do sha1 hash of header */
    rpmDigestBundleAdd(hdrbundle, PGPHASHALGO_SHA1, RPMDIGEST_NONE);

    /* Read the file, generating digest(s) on the fly. */
    fdSetBundle(fd, plbundle);
    if (readFile(fd, fn, dig, plbundle, hdrbundle)) {
	goto exit;
    }

    rasprintf(&buf, "%s:%c", fn, (rpmIsVerbose() ? '\n' : ' ') );

    hi = headerInitIterator(sigh);
    for (; headerNext(hi, &sigtd) != 0; rpmtdFreeData(&sigtd)) {
	char *result = NULL;
	int havekey = 0;
	DIGEST_CTX ctx = NULL;
	if (sigtd.data == NULL) /* XXX can't happen */
	    continue;

	/* Clean up parameters from previous sigtag. */
	pgpCleanDig(dig);

	switch (sigtd.tag) {
	case RPMSIGTAG_GPG:
	case RPMSIGTAG_PGP5:	/* XXX legacy */
	case RPMSIGTAG_PGP:
	    havekey = 1;
	case RPMSIGTAG_RSA:
	case RPMSIGTAG_DSA:
	    if (nosignatures)
		 continue;
	    if (parsePGP(&sigtd, fn, dig) != RPMRC_OK) {
		goto exit;
	    }
	    ctx = rpmDigestBundleDupCtx(havekey ? plbundle : hdrbundle,
					dig->signature.hash_algo);
	    break;
	case RPMSIGTAG_SHA1:
	    if (nodigests)
		 continue;
	    ctx = rpmDigestBundleDupCtx(hdrbundle, PGPHASHALGO_SHA1);
	    break;
	case RPMSIGTAG_MD5:
	    if (nodigests)
		 continue;
	    ctx = rpmDigestBundleDupCtx(plbundle, PGPHASHALGO_MD5);
	    break;
	default:
	    continue;
	    break;
	}

	rc = rpmVerifySignature(keyring, &sigtd, dig, ctx, &result);
	rpmDigestFinal(ctx, NULL, NULL, 0);

	formatResult(sigtd.tag, rc, result, havekey, 
		     (rc == RPMRC_NOKEY ? &missingKeys : &untrustedKeys),
		     &buf);
	free(result);

	if (rc != RPMRC_OK) {
	    failed = 1;
	}

    }
    res = failed;

    if (rpmIsVerbose()) {
	rpmlog(RPMLOG_NOTICE, "%s", buf);
    } else {
	const char *ok = (failed ? _("NOT OK") : _("OK"));
	rpmlog(RPMLOG_NOTICE, "%s%s%s%s%s%s%s%s\n", buf, ok,
	       missingKeys ? _(" (MISSING KEYS:") : "",
	       missingKeys ? missingKeys : "",
	       missingKeys ? _(") ") : "",
	       untrustedKeys ? _(" (UNTRUSTED KEYS:") : "",
	       untrustedKeys ? untrustedKeys : "",
	       untrustedKeys ? _(")") : "");
    }
    free(missingKeys);
    free(untrustedKeys);

exit:
    free(buf);
    rpmDigestBundleFree(hdrbundle);
    rpmDigestBundleFree(plbundle);
    fdSetBundle(fd, NULL); /* XXX avoid double-free from fd close */
    sigh = rpmFreeSignature(sigh);
    hi = headerFreeIterator(hi);
    pgpFreeDig(dig);
    return res;
}

/* Wrapper around rpmkVerifySigs to preserve API */
int rpmVerifySignatures(QVA_t qva, rpmts ts, FD_t fd, const char * fn)
{
    int rc = 1; /* assume failure */
    if (ts && qva && fd && fn) {
	rpmKeyring keyring = rpmtsGetKeyring(ts, 1);
	rc = rpmpkgVerifySigs(keyring, qva->qva_flags, fd, fn);
    	rpmKeyringFree(keyring);
    }
    return rc;
}

int rpmcliVerifySignatures(rpmts ts, ARGV_const_t argv)
{
    const char * arg;
    int res = 0;
    rpmKeyring keyring = rpmtsGetKeyring(ts, 1);
    rpmVerifyFlags verifyFlags = (VERIFY_DIGEST|VERIFY_SIGNATURE);
    
    verifyFlags &= ~rpmcliQueryFlags;

    while ((arg = *argv++) != NULL) {
	FD_t fd = Fopen(arg, "r.ufdio");
	if (fd == NULL || Ferror(fd)) {
	    rpmlog(RPMLOG_ERR, _("%s: open failed: %s\n"), 
		     arg, Fstrerror(fd));
	    res++;
	} else if (rpmpkgVerifySigs(keyring, verifyFlags, fd, arg)) {
	    res++;
	}

	Fclose(fd);
	rpmdbCheckSignals();
    }
    rpmKeyringFree(keyring);
    return res;
}
