فهرست منبع

HPCC-25130 Refactor code signing source to prepare for K8s related changes

Signed-off-by: Shamser Ahmed <shamser.ahmed@lexisnexis.com>
Shamser Ahmed 4 سال پیش
والد
کامیت
2faa6c7503

+ 1 - 0
ecl/hql/CMakeLists.txt

@@ -124,6 +124,7 @@ include_directories (
          ./../../ecl/hql 
          ./../../testing/unittests
          ./../../system/security/zcrypt
+         ${HPCC_SOURCE_DIR}/system/codesigner
     )
 
 if (WIN32)

+ 3 - 3
ecl/hql/hqlmanifest.cpp

@@ -18,6 +18,7 @@
 #include "hql.hpp"
 #include "hqlutil.hpp"
 #include "hqlmanifest.hpp"
+#include "codesigner.hpp"
 
 //-------------------------------------------------------------------------------------------------------------------
 // Process manifest resources.
@@ -34,12 +35,11 @@ public:
             const char *xml = fileContents.str();
             StringBuffer body;
             // Check for signature
-            if (startsWith(fileContents, "-----BEGIN PGP SIGNED MESSAGE-----"))
+            if (queryCodeSigner().hasSignature(fileContents))
             {
                 // Note - we do not check the signature here - we are creating an archive, and typically that means we
                 // are on the client machine, while the signature can only be checked on the server where the keys are installed.
-                stripSignature(body, fileContents);
-                xml = body.str();
+                xml = queryCodeSigner().stripSignature(fileContents, body).str();
                 isSigned = true;
             }
             manifest.setown(createPTreeFromXMLString(xml));

+ 6 - 1
ecl/hql/hqlparse.cpp

@@ -29,6 +29,7 @@
 #define YY_NO_UNISTD_H
 #include "hqllex.hpp"
 #include "eclrtl.hpp"
+#include "codesigner.hpp"
 
 //#define TIMING_DEBUG
 
@@ -548,7 +549,11 @@ void HqlLex::checkSignature(const attribute & dummyToken)
         return;
     try
     {
-        yyParser->gpgSignature.setown(::checkSignature(text->length(), text->getText()));
+        StringBuffer signer;
+        if (!queryCodeSigner().verifySignature(text->getText(), signer))
+            throw makeStringExceptionV(MSGAUD_user, CODESIGNER_ERR_VERIFY, "Code sign verify: signature not verified");
+
+        yyParser->gpgSignature.setown(createExprAttribute(_signed_Atom,createConstant(signer.str())));
         yyParser->inSignedModule = true;
     }
     catch (IException *e)

+ 0 - 73
ecl/hql/hqlutil.cpp

@@ -10570,76 +10570,3 @@ const RtlTypeInfo *buildRtlType(IRtlFieldTypeDeserializer &deserializer, ITypeIn
 
     return deserializer.addType(info, type);
 }
-
-extern HQL_API IHqlExpression *checkSignature(unsigned fileSize, const char *fileContents)
-{
-    Owned<IPipeProcess> pipe = createPipeProcess();
-    if (!pipe->run("gpg", "gpg --verify -", ".", true, false, true, 0, false))
-        throw makeStringException(0, "Signature could not be checked because gpg was not found");
-    pipe->write(fileSize, fileContents);
-    pipe->closeInput();
-    unsigned retcode = pipe->wait();
-
-    StringBuffer buf;
-    Owned<ISimpleReadStream> pipeReader = pipe->getErrorStream();
-    readSimpleStream(buf, *pipeReader);
-    DBGLOG("GPG %d %s", retcode, buf.str());
-    if (retcode)
-    {
-        StringArray allErrs;
-        allErrs.appendList(buf, "\n");
-        ForEachItemInRev(idx, allErrs)
-        {
-            if (strlen(allErrs.item(idx)))
-                throw makeStringExceptionV(0, "gpg error: gpg returned %d: %s", retcode, allErrs.item(idx));
-        }
-        throw makeStringExceptionV(0, "gpg error: gpg returned %d", retcode);
-    }
-    else
-    {
-        const char * sigprefix = "Good signature from \"";
-        const char * const s = buf.str();
-        const char * match = strstr(s, sigprefix);
-        if (match)
-        {
-            match += strlen(sigprefix);
-            const char * const end = strchr(match, '\"');
-            if (end)
-            {
-                buf.setLength(end-s);
-                const char * sig = buf.str() + (match-s);
-                return createExprAttribute(_signed_Atom,createConstant(sig));
-            }
-        }
-        throw makeStringExceptionV(0, "gpg error: gpg response not recognised");
-    }
-}
-
-extern HQL_API StringBuffer &stripSignature(StringBuffer &out, const char *fileContents)
-{
-    assertex(startsWith(fileContents, "-----BEGIN PGP SIGNED MESSAGE-----"));
-    // Look for first blank line
-    const char *head = fileContents;
-    while ((head = strchr(head, '\n')) != nullptr)
-    {
-        head++;
-        if (*head=='\n')
-        {
-            head++;
-            break;
-        }
-        else if (*head=='\r' && head[1]=='\n')
-        {
-            head += 2;
-            break;
-        }
-    }
-    if (!head)
-        throw makeStringException(0, "End of PGP header not found");
-    // Now look for signature
-    const char *tail = strstr(head, "-----BEGIN PGP SIGNATURE-----");
-    if (!tail)
-        throw makeStringException(0, "PGP signature not found");
-    return out.append(tail-head, head);
-}
-

+ 0 - 24
ecl/hql/hqlutil.hpp

@@ -251,30 +251,6 @@ extern HQL_API IHqlExpression * createTransformForField(IHqlExpression * field,
 extern HQL_API IHqlExpression * convertScalarToRow(IHqlExpression * value, ITypeInfo * fieldType);
 extern HQL_API bool splitResultValue(SharedHqlExpr & dataset, SharedHqlExpr & attribute, IHqlExpression * value);
 
-/**
- * Check whether GPG signed file is valid.
- *
- * @param fileSize      Size of file contents
- * @param fileContents  File contents
- *
- * @return             _signed_ attribute containing signature
- *
- * Will throw exception if signature cannot be verified.
- */
-
-extern HQL_API IHqlExpression * checkSignature(unsigned fileSize, const char *fileContents);
-
-/**
- * Strip GPG signature from file
- *
- * @param fileContents  File contents
- * @param out           Buffer for returned value
- *
- * @return              Reference to out
- */
-
-extern HQL_API StringBuffer &stripSignature(StringBuffer &out, const char *fileContents);
-
 //Is 'expr' really dependent on a parameter - expr->isFullyBound() can give false negatives.
 extern HQL_API bool isDependentOnParameter(IHqlExpression * expr);
 extern HQL_API bool isTimed(IHqlExpression * expr);

+ 1 - 0
ecl/hqlcpp/CMakeLists.txt

@@ -121,6 +121,7 @@ include_directories (
          ./../../common/thorhelper
          ./../../rtl/eclrtl
          ./../../rtl/include
+         ${HPCC_SOURCE_DIR}/system/codesigner
     )
 
 IF (NOT WIN32)

+ 13 - 8
ecl/hqlcpp/hqlres.cpp

@@ -24,6 +24,7 @@
 #include "jexcept.hpp"
 #include "hqlcerrors.hpp"
 #include "thorplugin.hpp"
+#include "codesigner.hpp"
 
 #define BIGSTRING_BASE 101
 #define MANIFEST_BASE 1000
@@ -174,13 +175,15 @@ void ResourceManager::addManifestFile(const char *filename, ICodegenContextCallb
     bool isSigned = false;
     const char *useContents = fileContents;
     // Check for signature
-    if (startsWith(fileContents, "-----BEGIN PGP SIGNED MESSAGE-----"))
+    if (queryCodeSigner().hasSignature(fileContents))
     {
         try
         {
-            OwnedHqlExpr sig = checkSignature(fileContents.length(), fileContents.str());
-            useContents = stripSignature(strippedFileContents, fileContents).str();
-            isSigned = true;
+            StringBuffer signer;
+            isSigned = queryCodeSigner().verifySignature(fileContents, signer);
+            if (!isSigned)
+                throw makeStringExceptionV(MSGAUD_user, CODESIGNER_ERR_VERIFY, "Code sign verify: signature not verified");
+            useContents = queryCodeSigner().stripSignature(fileContents, strippedFileContents).str();
         }
         catch (IException *E)
         {
@@ -313,13 +316,15 @@ void ResourceManager::addManifestsFromArchive(IPropertyTree *archive, ICodegenCo
         const char *xml = manifestContents;
         bool isSigned = false;
         // Check for signature
-        if (startsWith(xml, "-----BEGIN PGP SIGNED MESSAGE-----"))
+        if (queryCodeSigner().hasSignature(xml))
         {
             try
             {
-                OwnedHqlExpr sig = checkSignature(manifestContents.length(), manifestContents.str());
-                xml = stripSignature(strippedManifestContents, manifestContents).str();
-                isSigned = true;
+                StringBuffer signer;
+                isSigned = queryCodeSigner().verifySignature(manifestContents, signer);
+                if (!isSigned)
+                    throw makeStringExceptionV(0, "Code sign verify: signature not verified");
+                xml = queryCodeSigner().stripSignature(manifestContents, strippedManifestContents).str();
             }
             catch (IException *E)
             {

+ 1 - 0
esp/services/ws_codesign/CMakeLists.txt

@@ -58,6 +58,7 @@ include_directories (
          ${CMAKE_BINARY_DIR}/oss
          ${CMAKE_BINARY_DIR}/esp/services/ws_codesign
          ${HPCC_SOURCE_DIR}/common/thorhelper
+         ${HPCC_SOURCE_DIR}/system/codesigner
     )
 
 ADD_DEFINITIONS( -D_USRDLL )

+ 19 - 117
esp/services/ws_codesign/ws_codesignService.cpp

@@ -17,6 +17,7 @@
 
 #include "ws_codesignService.hpp"
 #include "jutil.hpp"
+#include "codesigner.hpp"
 
 Cws_codesignEx::Cws_codesignEx()
 {
@@ -36,18 +37,11 @@ void Cws_codesignEx::init(IPropertyTree *cfg, const char *process, const char *s
     m_serviceCfg.setown(cfg->getPropTree(xpath.str()));
 }
 
-void Cws_codesignEx::clearPassphrase(const char* key)
-{
-    StringBuffer output, errmsg;
-    VStringBuffer cmd("gpg-connect-agent \"clear_passphrase --mode=normal %s\" /bye", key);
-    runExternalCommand(output, errmsg, cmd.str(), nullptr);
-}
-
 bool Cws_codesignEx::onSign(IEspContext &context, IEspSignRequest &req, IEspSignResponse &resp)
 {
     resp.setRetCode(-1);
 
-    StringBuffer userid(req.getUserID());
+    StringBuffer userid(req.getUserID()), signedText;
     userid.trim();
     const char* text = req.getText();
     if (userid.length() == 0 || !text || !*text)
@@ -55,131 +49,39 @@ bool Cws_codesignEx::onSign(IEspContext &context, IEspSignRequest &req, IEspSign
         resp.setErrMsg("Please provide both UserID and Text");
         return false;
     }
-
-    if (strstr(userid.str(), "\""))
-    {
-        resp.setErrMsg("Invalid UserID");
-        return false;
-    }
-
-    StringBuffer cmd, output, errmsg;
-
-    int ret = runExternalCommand(output, errmsg, "gpg --version", nullptr);
-    if (ret != 0)
-        throw MakeStringException(-1, "Error running gpg: %s", errmsg.str());
-    bool isGPGv1 = strstr(output.str(), "gpg (GnuPG) 1.");
-
-    output.clear();
-    errmsg.clear();
-    if (isGPGv1)
-        cmd.appendf("gpg --list-secret-keys \"=%s\"", userid.str()); // = means exact match
-    else
-        cmd.appendf("gpg --list-secret-keys --with-keygrip \"=%s\"", userid.str()); // = means exact match
-    ret = runExternalCommand(output, errmsg, cmd.str(), nullptr);
-    if (ret != 0 || strstr(output.str(), userid.str()) == nullptr)
-    {
-        resp.setErrMsg("Key not found");
-        return false;
-    }
-
-    StringBuffer keygrip;
-    if (!isGPGv1)
+    try
     {
-        auto kgptr = strstr(output.str(), "Keygrip = ");
-        if (kgptr)
-            keygrip.append(40, kgptr+10);
-
-        if (keygrip.length() > 0)
-            clearPassphrase(keygrip.str());
+        queryCodeSigner().sign(text, userid.str(), req.getKeyPass(), signedText);
     }
-
-    output.clear();
-    errmsg.clear();
-    cmd.clear().appendf("gpg --clearsign -u \"%s\" --yes --batch --passphrase-fd 0", userid.str());
-    if (!isGPGv1)
-        cmd.append(" --pinentry-mode loopback");
-    VStringBuffer input("%s\n", req.getKeyPass());
-    input.append(text);
-    ret = runExternalCommand(output, errmsg, cmd.str(), input.str());
-    if (ret != 0 || output.length() == 0)
+    catch (IException *e)
     {
-        UERRLOG("gpg clearsign error: [%d] %s\nOutput: n%s", ret, errmsg.str(), output.str());
-        resp.setErrMsg("Failed to sign text, please check service log for details");
+        StringBuffer msg;
+        e->errorMessage(msg);
+        resp.setRetCode(e->errorCode());
+        resp.setErrMsg(msg);
+        e->Release();
         return false;
     }
 
     resp.setRetCode(0);
-    resp.setSignedText(output.str());
-
-    if (!isGPGv1 && keygrip.length() > 0)
-        clearPassphrase(keygrip.str());
+    resp.setSignedText(signedText.str());
 
     return true;
 }
 
-const char* skipn(const char* str, char c, int n)
-{
-    for (int i = 0; i < n && str && *str; i++)
-    {
-        str = strchr(str, c);
-        if (!str)
-            break;
-        str++;
-    }
-    return str;
-}
-
 bool Cws_codesignEx::onListUserIDs(IEspContext &context, IEspListUserIDsRequest &req, IEspListUserIDsResponse &resp)
 {
-    StringBuffer output, errmsg;
-
-    int ret = runExternalCommand(output, errmsg, "gpg --version", nullptr);
-    if (ret != 0)
-        throw MakeStringException(-1, "Error running gpg: %s", errmsg.str());
-    bool isGPGv1 = strstr(output.str(), "gpg (GnuPG) 1.");
-
-    const char* START = "\nuid:";
-    if (isGPGv1)
-        START = "\nsec:";
-    int startlen = strlen(START);
-    const int SKIP = 8;
-    output.clear().append("\n");
-    errmsg.clear();
-    ret = runExternalCommand(output, errmsg, "gpg --list-secret-keys --with-colon", nullptr);
-    if (ret != 0)
-        throw MakeStringException(-1, "Error running gpg: %s", errmsg.str());
-    const char* line = output.str();
-    StringArray uids;
-    while (line && *line)
+    StringArray userIds;
+    try
     {
-        line = strstr(line, START);
-        if (!line)
-            break;
-        line += startlen;
-        line = skipn(line, ':', SKIP);
-        if (!line || !*line)
-            break;
-        const char* uid_s = line;
-        while (*line != '\0' && *line != ':')
-            line++;
-        if (line > uid_s)
-        {
-            StringBuffer uid(line - uid_s, uid_s);
-            uid.trim();
-            if (uid.length() > 0)
-                uids.append(uid.str());
-        }
+        queryCodeSigner().getUserIds(userIds);
     }
-    uids.sortAscii(false);
-    const char* current = "";
-    StringArray& respuserids = resp.getUserIDs();
-    for (int i = 0; i < uids.length(); i++)
+    catch (IException *e)
     {
-        if (strcmp(uids.item(i), current) != 0)
-        {
-            current = uids.item(i);
-            respuserids.append(current);
-        }
+        e->Release();
+        return false;
     }
+
+    resp.setUserIDs(userIds);
     return true;
 }

+ 0 - 1
esp/services/ws_codesign/ws_codesignService.hpp

@@ -24,7 +24,6 @@ class Cws_codesignEx : public Cws_codesign
 {
 private:
     Owned<IPropertyTree> m_serviceCfg;
-    void clearPassphrase(const char* key);
 public:
     IMPLEMENT_IINTERFACE
 

+ 23 - 0
system/codesigner/codesigner.cpp

@@ -0,0 +1,23 @@
+/*##############################################################################
+
+    HPCC SYSTEMS software Copyright (C) 2021 HPCC Systems®.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+############################################################################## */
+
+#include "gpgcodesigner.hpp"
+
+extern jlib_decl ICodeSigner &queryCodeSigner()
+{
+    return queryGpgCodeSigner();
+}

+ 42 - 0
system/codesigner/codesigner.hpp

@@ -0,0 +1,42 @@
+/*##############################################################################
+
+    HPCC SYSTEMS software Copyright (C) 2021 HPCC Systems®.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+############################################################################## */
+
+#ifndef CODESIGN_HPP
+#define CODESIGN_HPP
+
+#include "jlib.hpp"
+#include "errorlist.h"
+
+#define CODESIGNER_ERR_GPG         CODESIGNER_ERROR_START
+#define CODESIGNER_ERR_BADUSERID   CODESIGNER_ERROR_START+1
+#define CODESIGNER_ERR_KEYNOTFOUND CODESIGNER_ERROR_START+2
+#define CODESIGNER_ERR_SIGN        CODESIGNER_ERROR_START+3
+#define CODESIGNER_ERR_VERIFY      CODESIGNER_ERROR_START+4
+#define CODESIGNER_ERR_LISTKEYS    CODESIGNER_ERROR_START+5
+
+interface ICodeSigner
+{
+    virtual void sign(const char * text, const char * userId, const char * passphrase, StringBuffer & signedText) = 0;
+    virtual bool verifySignature(const char *text, StringBuffer &signer) = 0;
+    virtual bool hasSignature(const char *text) const = 0;
+    virtual StringBuffer &stripSignature(const char *text, StringBuffer &unsignedText) const = 0;
+    virtual StringArray &getUserIds(StringArray &userIds) = 0;
+};
+
+extern jlib_decl ICodeSigner &queryCodeSigner();
+
+#endif

+ 356 - 0
system/codesigner/gpgcodesigner.cpp

@@ -0,0 +1,356 @@
+/*##############################################################################
+
+    HPCC SYSTEMS software Copyright (C) 2021 HPCC Systems®.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+############################################################################## */
+
+#include "jlib.hpp"
+#include "jexcept.hpp"
+#include "jlog.hpp"
+#include "gpgcodesigner.hpp"
+#include "atomic"
+
+/**
+ * Encapsulate the gpg operations used for code signing
+ * 
+ * Note:
+ * - One global instance of this class is sufficient
+ * - the member functions in class is thread safe
+ * - there are no special requirements for this objects destructions
+ */
+class GpgCodeSigner : implements ICodeSigner
+{
+    virtual void sign(const char * text, const char * userId, const char * passphrase, StringBuffer & signedText) override;
+    virtual bool verifySignature(const char * text, StringBuffer & signer) override;
+    virtual bool hasSignature(const char * text) const override;
+    virtual StringBuffer &stripSignature(const char * text, StringBuffer & unsignedText) const override;
+    virtual StringArray &getUserIds(StringArray & userIds) override;
+private:
+    void initGpg(void);
+    bool getKeyGrip(const char * user, StringBuffer & keygrip);
+    void clearPassphrase(const char * key);
+    CriticalSection crit;
+    std::atomic<bool> isGpgV1{false};
+    std::atomic<bool> isInitialized{false};
+    static constexpr const char* signatureMsgHeader = "-----BEGIN PGP SIGNED MESSAGE-----";
+    static constexpr const char* signatureBegin = "-----BEGIN PGP SIGNATURE-----";
+};
+
+static GpgCodeSigner gpgCodeSigner;
+
+extern jlib_decl ICodeSigner &queryGpgCodeSigner()
+{
+    return gpgCodeSigner;
+}
+
+/**
+ * Initialize GpgCodeSigner
+ * - confirm gpg is installed and working
+ * - check gpg version
+ */
+void GpgCodeSigner::initGpg(void)
+{
+    if (isInitialized) return;
+    CriticalBlock block(crit);
+    if (isInitialized) return;
+
+    StringBuffer cmd, output, errmsg;
+    int ret = runExternalCommand(output, errmsg, "gpg --version", nullptr);
+    if (ret != 0)
+        throw makeStringExceptionV(MSGAUD_operator, CODESIGNER_ERR_GPG, "Error running gpg: %s", errmsg.str());
+    isGpgV1 = strstr(output.str(), "gpg (GnuPG) 1.");
+    isInitialized = true;
+}
+
+/**
+ * Sign a block of text with a pgp signature - used of code signing
+ *
+ * @param text          Text block to sign
+ * @param userId        The user id with which to sign the text - must match the user id in the keys
+ * @param passphrase    The passphrase for the userId
+ * @param signedText    Returned signed text which includes text block wrapped in armour and signature
+ *
+ * @return              Reference to signedText
+ * 
+ * Exceptions:
+ * - CODESIGNER_ERR_BADUSERID - Invalid user id
+ * - CODESIGNER_ERR_KEYNOTFOUND - User id not in key list
+ * - CODESIGNER_ERR_SIGN - Signing failed: bad or missing passphrase
+ */
+void GpgCodeSigner::sign(const char * text, const char * userId, const char * passphrase, StringBuffer & signedText)
+{
+    initGpg();
+
+    if (strchr(userId, '\"')!=nullptr || strlen(userId) > 2000)
+        throw makeStringExceptionV(MSGAUD_user, CODESIGNER_ERR_BADUSERID, "Invalid user id: %s", userId);
+
+    StringBuffer keygrip;
+    if (!isGpgV1)
+    {
+        if (!getKeyGrip(userId, keygrip) || keygrip.length()==0)
+            throw makeStringExceptionV(MSGAUD_user, CODESIGNER_ERR_KEYNOTFOUND, "Key for user not found: %s", userId);
+        clearPassphrase(keygrip);
+    }
+    StringBuffer cmd, errmsg;
+    cmd.setf("gpg --clearsign -u \"%s\" --yes --batch --passphrase-fd 0", userId);
+    if (!isGpgV1)
+        cmd.append(" --pinentry-mode loopback");
+    VStringBuffer input("%s\n", passphrase);
+    input.append(text);
+
+    int ret = runExternalCommand(signedText, errmsg, cmd.str(), input.str());
+    if (ret != 0 || signedText.length() == 0)
+    {
+        if (strstr(errmsg.str(),"No passphrase given")!=nullptr)
+            errmsg.set("Passphrase required");
+        else if (strstr(errmsg.str(),"Bad passphrase")!=nullptr)
+            errmsg.set("Invalid passphrase");
+        throw makeStringExceptionV(MSGAUD_user, CODESIGNER_ERR_SIGN, "Code sign failed: %s", errmsg.str());
+    }
+
+    if (!isGpgV1)
+        clearPassphrase(keygrip.str());
+}
+
+/**
+ * Check signature of signed block
+ *
+ * @param text          Block of signed text
+ * @param signer        The user id of signer
+ *
+ * @return              True if valid signature
+ *                      False if invalid signature
+ * 
+ * Exceptions:
+ * - CODESIGNER_ERR_VERIFY - gpg verify could not be executed
+ */
+bool GpgCodeSigner::verifySignature(const char * text, StringBuffer & signer)
+{
+    initGpg();
+    Owned<IPipeProcess> pipe = createPipeProcess();
+    if (!pipe->run("gpg", "gpg --verify -", ".", true, false, true, 0, false))
+        throw makeStringExceptionV(MSGAUD_user, CODESIGNER_ERR_VERIFY, "Code sign verify failed (gpg --verify failed)");
+    pipe->write(strlen(text), text);
+    pipe->closeInput();
+    unsigned retcode = pipe->wait();
+    if (retcode)
+        throw makeStringExceptionV(MSGAUD_user, CODESIGNER_ERR_VERIFY, "Code sign verify failed");
+
+    StringBuffer buf;
+    Owned<ISimpleReadStream> pipeReader = pipe->getErrorStream();
+    readSimpleStream(buf, *pipeReader);
+    const char * sigprefix = "Good signature from \"";
+    const char * const s = buf.str();
+    const char * match = strstr(s, sigprefix);
+    if (match)
+    {
+        match += strlen(sigprefix);
+        const char * const end = strchr(match, '\"');
+        if (end)
+        {
+            signer.append(end-match, match);
+            return true;
+        }
+    }
+    return false;
+}
+
+/**
+ * Check if a text blockt has the header of a signed block
+ *
+ * @param text          Block of signed text/unsigned text
+ *
+ * @return              True if it has a signature header
+ *                      False otherwise
+ */
+bool GpgCodeSigner::hasSignature(const char * text) const
+{
+    return startsWith(text, signatureMsgHeader);
+}
+
+/**
+ * Remove text armour and signature from signed text block (if it exists)
+ *
+ * @param text          Block of signed text/unsigned text
+ * @param unsignedText  Unsigned text block return 
+ *
+ * @return              Reference to unsignedText
+ */
+StringBuffer &GpgCodeSigner::stripSignature(const char * text, StringBuffer & unsignedText) const
+{
+    if (!hasSignature(text))  // no signature -> return unchanged
+        return unsignedText.set(text);
+
+    const char *head = text;
+    head += strlen(signatureMsgHeader);     // skip header
+    while ((head = strchr(head, '\n')) != nullptr)
+    {
+        head++;
+        if (*head=='\n')
+        {
+            head++;
+            break;
+        }
+        else if (*head=='\r' && head[1]=='\n')
+        {
+            head += 2;
+            break;
+        }
+    }
+
+    if (!head)
+        return unsignedText.set(text);
+
+    const char *tail = strstr(head, signatureBegin);
+    if (!tail)
+        return unsignedText.set(text);
+    return unsignedText.append(tail-head, head);
+}
+
+/**
+ * Skips the specified character a specified number of times
+ *
+ * @param str           input text
+ * @param c             character to skip
+ * @param n             number of matching characters to skip
+ *
+ * @return              Pointer to position in text after the specfied number of skips
+ */
+const char* skipn(const char * str, char c, int n)
+{
+    for (int i = 0; i < n && str && *str; i++)
+    {
+        str = strchr(str, c);
+        if (!str)
+            break;
+        str++;
+    }
+    return str;
+}
+
+/**
+ * A list of the user ids
+ *
+ * @param userIds       Used to return the user ids
+ *
+ * @return              referenced to userIds
+ * 
+ * Exceptions:
+ * - CODESIGNER_ERR_LISTKEYS - gpg list keys could not be executed
+ */
+StringArray &GpgCodeSigner::getUserIds(StringArray & userIds)
+{
+    initGpg();
+    StringBuffer errmsg, output("\n");
+    int ret = runExternalCommand(output, errmsg, "gpg --list-secret-keys --with-colon", nullptr);
+    if (ret != 0)
+        throw makeStringExceptionV(MSGAUD_user, CODESIGNER_ERR_LISTKEYS, "list secret keys failed: %s", errmsg.str());
+
+    const char* START = "\nuid:";
+    if (isGpgV1)
+        START = "\nsec:";
+    int startlen = strlen(START);
+    const int SKIP = 8;
+    const char* line = output.str();
+    StringArray uids;
+    while (line && *line)
+    {
+        line = strstr(line, START);
+        if (!line)
+            break;
+        line += startlen;
+        line = skipn(line, ':', SKIP);
+        if (!*line)
+            break;
+        const char* uid_s = line;
+        while (*line != '\0' && *line != ':')
+            line++;
+        if (line > uid_s)
+        {
+            StringBuffer uid(line - uid_s, uid_s);
+            uid.trim();
+            if (uid.length() > 0)
+                uids.append(uid.str());
+        }
+    }
+    uids.sortAscii(false);
+    const char* current = "";
+    for (unsigned i = 0; i < uids.length(); i++)
+    {
+        if (strcmp(uids.item(i), current) != 0)
+        {
+            current = uids.item(i);
+            userIds.append(current);
+        }
+    }
+
+    return userIds;
+}
+
+/**
+ * A list of the user ids
+ *
+ * @param userId        User id to match
+ * @param keygrip       Returned keygrip of match user
+ *
+ * @return              True if matching key grip found
+ *                      False otherwise
+ * 
+ * Exceptions:
+ * - CODESIGNER_ERR_LISTKEYS - gpg list keys could not be executed
+ */
+bool GpgCodeSigner::getKeyGrip(const char * userId, StringBuffer & keygrip)
+{
+    initGpg();
+    keygrip.clear();
+
+    StringBuffer cmd;
+    if (isGpgV1)
+        cmd.appendf("gpg --list-secret-keys \"=%s\"", userId); // = means exact match
+    else
+        cmd.appendf("gpg --list-secret-keys --with-keygrip \"=%s\"", userId); // = means exact match
+
+    StringBuffer output, errmsg;
+    int ret = runExternalCommand(output, errmsg, cmd.str(), nullptr);
+    if (ret != 0)
+    {
+        if (strstr(errmsg.str(), "No secret key")==nullptr)
+            throw makeStringExceptionV(MSGAUD_user, CODESIGNER_ERR_LISTKEYS, "List keys failed: %s (%d)", errmsg.str(), ret);
+        return false;
+    }
+
+    if(strstr(output.str(), userId) == nullptr)
+        return false;
+    auto kgptr = strstr(output.str(), "Keygrip = ");
+    if (kgptr)
+        keygrip.append(40, kgptr+10);
+    else
+        return false;
+    return true;
+}
+
+/**
+ * Clear the passphrase cached with agent of specified user
+ *
+ * @param key           Keygrip of user
+ *
+ * Note: this may fail silently
+ */
+void GpgCodeSigner::clearPassphrase(const char * key)
+{
+    initGpg();
+    StringBuffer output, errmsg;
+    VStringBuffer cmd("gpg-connect-agent \"clear_passphrase --mode=normal %s\" /bye", key);
+    runExternalCommand(output, errmsg, cmd.str(), nullptr);
+}

+ 25 - 0
system/codesigner/gpgcodesigner.hpp

@@ -0,0 +1,25 @@
+/*##############################################################################
+
+    HPCC SYSTEMS software Copyright (C) 2021 HPCC Systems®.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+############################################################################## */
+
+#ifndef GPGCODESIGNER_HPP
+#define GPGCODESIGNER_HPP
+
+#include "codesigner.hpp"
+
+extern jlib_decl ICodeSigner &queryGpgCodeSigner();
+
+#endif

+ 3 - 0
system/include/errorlist.h

@@ -114,5 +114,8 @@
 
 #define WORKUNIT_ANALYZER_START 31000
 #define WORKUNIT_ANALYZER_END   31999
+
+#define CODESIGNER_ERROR_START  32000
+#define CODESIGNER_ERROR_END    32999
 #endif
 

+ 3 - 0
system/jlib/CMakeLists.txt

@@ -95,10 +95,13 @@ set (    SRCS
          junicode.cpp 
          jutil.cpp 
          ${HPCC_SOURCE_DIR}/system/globalid/lnuid.cpp
+         ${HPCC_SOURCE_DIR}/system/codesigner/codesigner.cpp
+         ${HPCC_SOURCE_DIR}/system/codesigner/gpgcodesigner.cpp
          sourcedoc.xml
     )
 
 set (    INCLUDES
+        ${HPCC_SOURCE_DIR}/system/codesigner/codesigner.hpp
         jaio.hpp
         jarray.hpp
         jatomic.hpp