gpgcodesigner.cpp 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. /*##############################################################################
  2. HPCC SYSTEMS software Copyright (C) 2021 HPCC Systems®.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. ############################################################################## */
  13. #include "jlib.hpp"
  14. #include "jexcept.hpp"
  15. #include "jlog.hpp"
  16. #include "jfile.hpp"
  17. #include "jsecrets.hpp"
  18. #include "gpgcodesigner.hpp"
  19. #include "atomic"
  20. /**
  21. * Encapsulate the gpg operations used for code signing
  22. *
  23. * Note:
  24. * - One global instance of this class is sufficient
  25. * - the member functions in class is thread safe
  26. * - there are no special requirements for this objects destructions
  27. */
  28. class GpgCodeSigner : implements ICodeSigner
  29. {
  30. virtual void initForContainer();
  31. virtual void sign(const char * text, const char * userId, const char * passphrase, StringBuffer & signedText) override;
  32. virtual bool verifySignature(const char * text, StringBuffer & signer) override;
  33. virtual bool hasSignature(const char * text) const override;
  34. virtual StringBuffer &stripSignature(const char * text, StringBuffer & unsignedText) const override;
  35. virtual StringArray &getUserIds(StringArray & userIds) override;
  36. private:
  37. void initGpg(void);
  38. bool importKey(const char *key, const char *passphrase);
  39. void importKeysFromSecret(const char * cat, const char *type);
  40. void importSigningKeysFromSecrets();
  41. void importVerifyKeysFromSecrets();
  42. bool getKeyGrip(const char * user, StringBuffer & keygrip);
  43. void clearPassphrase(const char * key);
  44. CriticalSection crit;
  45. std::atomic<bool> isGpgV1{false};
  46. std::atomic<bool> isInitialized{false};
  47. std::atomic<bool> getSignKeysFromSecrets {false};
  48. std::atomic<bool> getVerifyKeysFromSecrets {false};
  49. StringBuffer gpgOptions;
  50. StringBuffer gpgHomeDir;
  51. bool createNewGpgHomeDir = false;
  52. static constexpr const char* signatureMsgHeader = "-----BEGIN PGP SIGNED MESSAGE-----";
  53. static constexpr const char* signatureBegin = "-----BEGIN PGP SIGNATURE-----";
  54. };
  55. static GpgCodeSigner gpgCodeSigner;
  56. extern jlib_decl ICodeSigner &queryGpgCodeSigner()
  57. {
  58. return gpgCodeSigner;
  59. }
  60. /**
  61. * Initialize GpgCodeSigner
  62. * - confirm gpg is installed and working
  63. * - check gpg version
  64. */
  65. void GpgCodeSigner::initGpg(void)
  66. {
  67. if (isInitialized) return;
  68. CriticalBlock block(crit);
  69. if (isInitialized) return;
  70. StringBuffer cmd, output, errmsg;
  71. int ret = runExternalCommand(output, errmsg, "gpg --version", nullptr);
  72. if (ret != 0)
  73. throw makeStringExceptionV(MSGAUD_operator, CODESIGNER_ERR_GPG, "Error running gpg: %s", errmsg.str());
  74. isGpgV1 = strstr(output.str(), "gpg (GnuPG) 1.");
  75. if (createNewGpgHomeDir)
  76. {
  77. try
  78. {
  79. Owned<IFile> dir = createIFile(gpgHomeDir);
  80. dir->createDirectory();
  81. }
  82. catch (IException *e)
  83. {
  84. OERRLOG("Create directory failed: %s", gpgHomeDir.str());
  85. e->Release();
  86. }
  87. createNewGpgHomeDir = false;
  88. }
  89. isInitialized = true;
  90. }
  91. /**
  92. * Import a gpg key
  93. *
  94. * @param key The gpg key
  95. * @param passphrase The passphrase for key (optional)
  96. *
  97. * @return True if successfully imported
  98. * False if import failed
  99. */
  100. bool GpgCodeSigner::importKey(const char *key, const char *passphrase)
  101. {
  102. initGpg();
  103. VStringBuffer cmd("gpg %s --batch --passphrase-fd 0 --import ", gpgOptions.str());
  104. StringBuffer output, errmsg;
  105. VStringBuffer input("%s\n", passphrase);
  106. input.append(key);
  107. int ret = runExternalCommand(output, errmsg, cmd.str(), input);
  108. if (ret != 0)
  109. {
  110. OERRLOG("External command failed: %s", errmsg.str());
  111. return false;
  112. }
  113. return true;
  114. }
  115. /**
  116. * Import a key from secrets
  117. *
  118. * @param key Secrets category
  119. * @param keytype Type of key: public or private
  120. */
  121. void GpgCodeSigner::importKeysFromSecret(const char * cat, const char *keytype)
  122. {
  123. unsigned importCount = 0;
  124. unsigned failCount = 0;
  125. for (int keyentry = 1; ; keyentry++)
  126. {
  127. VStringBuffer keysecretname("gpg-%s-key-%d", keytype, keyentry);
  128. Owned<IPropertyTree> secretKey = getSecret(cat, keysecretname.str());
  129. if (secretKey)
  130. {
  131. StringBuffer gpgKey;
  132. if (secretKey->getProp(keytype, gpgKey))
  133. {
  134. StringBuffer passphrase;
  135. secretKey->getProp("passphrase", passphrase);
  136. if (importKey(gpgKey, passphrase))
  137. ++importCount;
  138. else
  139. ++failCount;
  140. }
  141. }
  142. else
  143. {
  144. break; // finished importing keys
  145. }
  146. }
  147. if (failCount)
  148. OERRLOG("Keys imported from %s/gpg-%s-key-* failed: %u (succeeded %u)", cat, keytype, failCount, importCount);
  149. else
  150. DBGLOG("Keys imported from %s/gpg-%s-key-*: %u", cat, keytype, importCount);
  151. }
  152. /**
  153. * Imports signing keys from secrets
  154. * - Imports takes place just once. All subsequant calls ignored.
  155. * - (this may be called before every operation so keys imported only if needed)
  156. */
  157. void GpgCodeSigner::importSigningKeysFromSecrets()
  158. {
  159. if (!getSignKeysFromSecrets) return;
  160. CriticalBlock block(crit);
  161. if (!getSignKeysFromSecrets) return;
  162. importKeysFromSecret("codeSign","private");
  163. getSignKeysFromSecrets = false;
  164. }
  165. /**
  166. * Imports verifying keys from secrets
  167. * - Imports takes place just once. All subsequant calls ignored.
  168. * - (this may be called before every operation so keys imported only if needed)
  169. */
  170. void GpgCodeSigner::importVerifyKeysFromSecrets()
  171. {
  172. if (!getVerifyKeysFromSecrets) return;
  173. CriticalBlock block(crit);
  174. if (!getVerifyKeysFromSecrets) return;
  175. importKeysFromSecret("codeVerify","public");
  176. getVerifyKeysFromSecrets = false;
  177. }
  178. /**
  179. * Initialize gpg code signer for containers
  180. * - uses current directory for gpg instead of home directory (which may not exist)
  181. */
  182. void GpgCodeSigner::initForContainer()
  183. {
  184. // Processes running in containers may be owned by a user without a home directory
  185. // so use a directory under the current directory (create it later on 1st use)
  186. appendCurrentDirectory(gpgHomeDir, false);
  187. addPathSepChar(gpgHomeDir).append("gnugpg");
  188. gpgOptions.appendf(" --homedir %s", gpgHomeDir.str());
  189. createNewGpgHomeDir = true;
  190. getSignKeysFromSecrets = true;
  191. getVerifyKeysFromSecrets = true;
  192. }
  193. /**
  194. * Sign a block of text with a pgp signature - used of code signing
  195. *
  196. * @param text Text block to sign
  197. * @param userId The user id with which to sign the text - must match the user id in the keys
  198. * @param passphrase The passphrase for the userId
  199. * @param signedText Returned signed text which includes text block wrapped in armour and signature
  200. *
  201. * @return Reference to signedText
  202. *
  203. * Exceptions:
  204. * - CODESIGNER_ERR_BADUSERID - Invalid user id
  205. * - CODESIGNER_ERR_KEYNOTFOUND - User id not in key list
  206. * - CODESIGNER_ERR_SIGN - Signing failed: bad or missing passphrase
  207. */
  208. void GpgCodeSigner::sign(const char * text, const char * userId, const char * passphrase, StringBuffer & signedText)
  209. {
  210. initGpg();
  211. if (strchr(userId, '\"')!=nullptr || strlen(userId) > 2000)
  212. throw makeStringExceptionV(MSGAUD_user, CODESIGNER_ERR_BADUSERID, "Invalid user id: %s", userId);
  213. importSigningKeysFromSecrets();
  214. StringBuffer keygrip;
  215. if (!isGpgV1)
  216. {
  217. if (!getKeyGrip(userId, keygrip) || keygrip.length()==0)
  218. throw makeStringExceptionV(MSGAUD_user, CODESIGNER_ERR_KEYNOTFOUND, "Key for user not found: %s", userId);
  219. clearPassphrase(keygrip);
  220. }
  221. StringBuffer cmd, errmsg;
  222. cmd.setf("gpg %s --clearsign -u \"%s\" --yes --batch --passphrase-fd 0", gpgOptions.str(), userId);
  223. if (!isGpgV1)
  224. cmd.append(" --pinentry-mode loopback");
  225. VStringBuffer input("%s\n", passphrase);
  226. input.append(text);
  227. int ret = runExternalCommand(signedText, errmsg, cmd.str(), input.str());
  228. if (ret != 0 || signedText.length() == 0)
  229. {
  230. if (strstr(errmsg.str(),"No passphrase given")!=nullptr)
  231. errmsg.set("Passphrase required");
  232. else if (strstr(errmsg.str(),"Bad passphrase")!=nullptr)
  233. errmsg.set("Invalid passphrase");
  234. throw makeStringExceptionV(MSGAUD_user, CODESIGNER_ERR_SIGN, "Code sign failed: %s", errmsg.str());
  235. }
  236. if (!isGpgV1)
  237. clearPassphrase(keygrip.str());
  238. }
  239. /**
  240. * Check signature of signed block
  241. *
  242. * @param text Block of signed text
  243. * @param signer The user id of signer
  244. *
  245. * @return True if valid signature
  246. * False if invalid signature
  247. *
  248. * Exceptions:
  249. * - CODESIGNER_ERR_VERIFY - gpg verify could not be executed
  250. */
  251. bool GpgCodeSigner::verifySignature(const char * text, StringBuffer & signer)
  252. {
  253. initGpg();
  254. importVerifyKeysFromSecrets();
  255. Owned<IPipeProcess> pipe = createPipeProcess();
  256. VStringBuffer cmd("gpg %s --verify -", gpgOptions.str());
  257. if (!pipe->run("gpg", cmd.str(), ".", true, false, true, 0, false))
  258. throw makeStringExceptionV(MSGAUD_user, CODESIGNER_ERR_VERIFY, "Code sign verify failed (gpg --verify failed)");
  259. pipe->write(strlen(text), text);
  260. pipe->closeInput();
  261. unsigned retcode = pipe->wait();
  262. if (retcode)
  263. throw makeStringExceptionV(MSGAUD_user, CODESIGNER_ERR_VERIFY, "Code sign verify failed");
  264. StringBuffer buf;
  265. Owned<ISimpleReadStream> pipeReader = pipe->getErrorStream();
  266. readSimpleStream(buf, *pipeReader);
  267. const char * sigprefix = "Good signature from \"";
  268. const char * const s = buf.str();
  269. const char * match = strstr(s, sigprefix);
  270. if (match)
  271. {
  272. match += strlen(sigprefix);
  273. const char * const end = strchr(match, '\"');
  274. if (end)
  275. {
  276. signer.append(end-match, match);
  277. return true;
  278. }
  279. }
  280. return false;
  281. }
  282. /**
  283. * Check if a text blockt has the header of a signed block
  284. *
  285. * @param text Block of signed text/unsigned text
  286. *
  287. * @return True if it has a signature header
  288. * False otherwise
  289. */
  290. bool GpgCodeSigner::hasSignature(const char * text) const
  291. {
  292. return startsWith(text, signatureMsgHeader);
  293. }
  294. /**
  295. * Remove text armour and signature from signed text block (if it exists)
  296. *
  297. * @param text Block of signed text/unsigned text
  298. * @param unsignedText Unsigned text block return
  299. *
  300. * @return Reference to unsignedText
  301. */
  302. StringBuffer &GpgCodeSigner::stripSignature(const char * text, StringBuffer & unsignedText) const
  303. {
  304. if (!hasSignature(text)) // no signature -> return unchanged
  305. return unsignedText.set(text);
  306. const char *head = text;
  307. head += strlen(signatureMsgHeader); // skip header
  308. while ((head = strchr(head, '\n')) != nullptr)
  309. {
  310. head++;
  311. if (*head=='\n')
  312. {
  313. head++;
  314. break;
  315. }
  316. else if (*head=='\r' && head[1]=='\n')
  317. {
  318. head += 2;
  319. break;
  320. }
  321. }
  322. if (!head)
  323. return unsignedText.set(text);
  324. const char *tail = strstr(head, signatureBegin);
  325. if (!tail)
  326. return unsignedText.set(text);
  327. return unsignedText.append(tail-head, head);
  328. }
  329. /**
  330. * Skips the specified character a specified number of times
  331. *
  332. * @param str input text
  333. * @param c character to skip
  334. * @param n number of matching characters to skip
  335. *
  336. * @return Pointer to position in text after the specfied number of skips
  337. */
  338. const char* skipn(const char * str, char c, int n)
  339. {
  340. for (int i = 0; i < n && str && *str; i++)
  341. {
  342. str = strchr(str, c);
  343. if (!str)
  344. break;
  345. str++;
  346. }
  347. return str;
  348. }
  349. /**
  350. * A list of the user ids
  351. *
  352. * @param userIds Used to return the user ids
  353. *
  354. * @return referenced to userIds
  355. *
  356. * Exceptions:
  357. * - CODESIGNER_ERR_LISTKEYS - gpg list keys could not be executed
  358. */
  359. StringArray &GpgCodeSigner::getUserIds(StringArray & userIds)
  360. {
  361. initGpg();
  362. importSigningKeysFromSecrets();
  363. StringBuffer errmsg, output("\n");
  364. VStringBuffer cmd("gpg %s --list-secret-keys --with-colon", gpgOptions.str());
  365. int ret = runExternalCommand(output, errmsg, cmd.str(), nullptr);
  366. if (ret != 0)
  367. {
  368. IERRLOG("list secret keys failed: %s", errmsg.str());
  369. throw makeStringExceptionV(MSGAUD_user, CODESIGNER_ERR_LISTKEYS, "list secret keys failed: %s", errmsg.str());
  370. }
  371. const char* START = "\nuid:";
  372. if (isGpgV1)
  373. START = "\nsec:";
  374. int startlen = strlen(START);
  375. const int SKIP = 8;
  376. const char* line = output.str();
  377. StringArray uids;
  378. while (line && *line)
  379. {
  380. line = strstr(line, START);
  381. if (!line)
  382. break;
  383. line += startlen;
  384. line = skipn(line, ':', SKIP);
  385. if (!*line)
  386. break;
  387. const char* uid_s = line;
  388. while (*line != '\0' && *line != ':')
  389. line++;
  390. if (line > uid_s)
  391. {
  392. StringBuffer uid(line - uid_s, uid_s);
  393. uid.trim();
  394. if (uid.length() > 0)
  395. uids.append(uid.str());
  396. }
  397. }
  398. uids.sortAscii(false);
  399. const char* current = "";
  400. for (unsigned i = 0; i < uids.length(); i++)
  401. {
  402. if (strcmp(uids.item(i), current) != 0)
  403. {
  404. current = uids.item(i);
  405. userIds.append(current);
  406. }
  407. }
  408. return userIds;
  409. }
  410. /**
  411. * A list of the user ids
  412. *
  413. * @param userId User id to match
  414. * @param keygrip Returned keygrip of match user
  415. *
  416. * @return True if matching key grip found
  417. * False otherwise
  418. *
  419. * Exceptions:
  420. * - CODESIGNER_ERR_LISTKEYS - gpg list keys could not be executed
  421. */
  422. bool GpgCodeSigner::getKeyGrip(const char * userId, StringBuffer & keygrip)
  423. {
  424. initGpg();
  425. keygrip.clear();
  426. StringBuffer cmd;
  427. if (isGpgV1)
  428. cmd.appendf("gpg %s --list-secret-keys \"=%s\"", gpgOptions.str(), userId); // = means exact match
  429. else
  430. cmd.appendf("gpg %s --list-secret-keys --with-keygrip \"=%s\"", gpgOptions.str(), userId); // = means exact match
  431. StringBuffer output, errmsg;
  432. int ret = runExternalCommand(output, errmsg, cmd.str(), nullptr);
  433. if (ret != 0)
  434. {
  435. if (strstr(errmsg.str(), "No secret key")==nullptr)
  436. throw makeStringExceptionV(MSGAUD_user, CODESIGNER_ERR_LISTKEYS, "List keys failed: %s (%d)", errmsg.str(), ret);
  437. return false;
  438. }
  439. if(strstr(output.str(), userId) == nullptr)
  440. return false;
  441. auto kgptr = strstr(output.str(), "Keygrip = ");
  442. if (kgptr)
  443. keygrip.append(40, kgptr+10);
  444. else
  445. return false;
  446. return true;
  447. }
  448. /**
  449. * Clear the passphrase cached with agent of specified user
  450. *
  451. * @param key Keygrip of user
  452. *
  453. * Note: this may fail silently
  454. */
  455. void GpgCodeSigner::clearPassphrase(const char * key)
  456. {
  457. initGpg();
  458. StringBuffer output, errmsg;
  459. VStringBuffer cmd("gpg-connect-agent %s \"clear_passphrase --mode=normal %s\" /bye", gpgOptions.str(), key);
  460. runExternalCommand(output, errmsg, cmd.str(), nullptr);
  461. }