commit 18357cb1370c41825ea139b1e8b62d515b29146f
parent bfd5f5318e94d212bfd52fb4bc69bd84c2e332e5
Author: lash <dev@holbrook.no>
Date: Fri, 7 Nov 2025 04:31:31 +0000
Enable signing of entries
Diffstat:
5 files changed, 67 insertions(+), 1 deletion(-)
diff --git a/feedwarrior/cmd/add.py b/feedwarrior/cmd/add.py
@@ -11,6 +11,7 @@ import gzip
import tempfile
import base64
import uuid
+import getpass
# local imports
import feedwarrior
@@ -18,6 +19,7 @@ from feedwarrior import entry as feedentry
from feedwarrior.adapters import fileadapter
from feedwarrior.entry import extension
from feedwarrior.common import task_ids_to_uuids, check_task_uuids
+from feedwarrior.crypto import PGPSigner
logg = logging.getLogger()
@@ -30,6 +32,7 @@ def parse_args(argparser):
argparser.add_argument('--task-id', dest='task_id', type=int, action='append', help='add taskwarrior task id relations translated to uuis (cannot be used with --task-uuid')
argparser.add_argument('--task-uuid', dest='task_uuid', type=str, action='append', help='add taskwarrior task uuid relations (cannot be used with --task-id')
argparser.add_argument('-s', type=str, help='entry subject')
+ argparser.add_argument('-y', '--sign', dest='y', action='store_true', help='sign entry with pgp key')
return True
@@ -39,6 +42,10 @@ def check_args(args):
# TODO: move logic to package to get symmetry with the show.py logic
def execute(config, feed, args):
+ signer = None
+ if args.y:
+ signer = PGPSigner()
+
task_uuids = []
if args.task_id != None:
task_uuids += task_ids_to_uuids(config.task_dir, args.task_id)
@@ -88,8 +95,14 @@ def execute(config, feed, args):
uu = str(entry.uuid)
logg.debug('adding entry {}'.format(uu))
+ if args.y:
+ passphrase = getpass.getpass('passphrase: ')
+ if len(passphrase) == 0:
+ passphrase = None
+ signer.sign(entry, passphrase=passphrase)
+
fa = fileadapter(config.data_dir, feed.uuid)
fa.put(entry.uuid, entry, compress=args.z)
-
+
feed.add(entry)
return str(entry.uuid)
diff --git a/feedwarrior/cmd/entry.py b/feedwarrior/cmd/entry.py
@@ -5,6 +5,7 @@ import logging
import uuid
import json
import gzip
+import getpass
# local imports
import feedwarrior
@@ -12,6 +13,7 @@ from feedwarrior import entry as feedentry
from feedwarrior.adapters import fileadapter
from feedwarrior.common import task_ids_to_uuids, check_task_uuids
from feedwarrior.error import NotMultipartError
+from feedwarrior.crypto import PGPSigner
logg = logging.getLogger()
@@ -20,6 +22,7 @@ def parse_args(argparser):
argparser.add_argument('-z', action='store_true', help='compress entry with gzip')
argparser.add_argument('--task-id', dest='task_id', type=int, action='append', help='add taskwarrior task id relations translated to uuis (cannot be used with --task-uuid')
argparser.add_argument('--task-uuid', dest='task_uuid', type=str, action='append', help='add taskwarrior task uuid relations (cannot be used with --task-id')
+ argparser.add_argument('-y', '--sign', dest='y', action='store_true', help='sign entry with pgp key')
argparser.add_argument('path', help='multipart file to use for content')
return True
@@ -30,6 +33,10 @@ def check_args(args):
# TODO: move logic to package to get symmetry with the show.py logic
def execute(config, feed, args):
+ signer = None
+ if args.y:
+ signer = PGPSigner()
+
task_uuids = []
if args.task_id != None:
task_uuids += task_ids_to_uuids(config.task_dir, args.task_id)
@@ -51,6 +58,12 @@ def execute(config, feed, args):
uu = str(entry.uuid)
logg.debug('adding entry {}'.format(uu))
+ if args.y:
+ passphrase = getpass.getpass('passphrase: ')
+ if len(passphrase) == 0:
+ passphrase = None
+ signer.sign(entry, passphrase=passphrase)
+
fa = fileadapter(config.data_dir, feed.uuid)
fa.put(entry.uuid, entry, compress=args.z)
diff --git a/feedwarrior/crypto.py b/feedwarrior/crypto.py
@@ -0,0 +1,30 @@
+import logging
+
+import gnupg
+
+logg = logging.getLogger(__name__)
+logging.getLogger('gnupg').setLevel(logging.ERROR)
+#logging.getLogger('gnupg').setLevel(logging.DEBUG)
+
+
+class PGPSigner:
+
+ def __init__(self, home_dir=None, default_key=None, passphrase=None, use_agent=False, skip_verify=False):
+ self.gpg = gnupg.GPG(gnupghome=home_dir)
+ self.default_key = default_key
+ self.passphrase = passphrase
+ self.use_agent = use_agent
+ self.sign_material = None
+ self.pre_buffer = []
+ self.__skip_verify = skip_verify
+
+
+ def sign(self, entry, passphrase=None): # msg = IssueMessage object
+ if passphrase == None:
+ passphrase = self.passphrase
+ s = entry.serialize()
+ sig = self.gpg.sign(s['payload'], keyid=self.default_key, detach=True, passphrase=passphrase)
+ if sig.status != 'signature created':
+ logg.error('signature failure: ' + sig.status)
+ raise SignError()
+ entry.set_signature(str(sig))
diff --git a/feedwarrior/entry.py b/feedwarrior/entry.py
@@ -32,6 +32,7 @@ class entry:
self.uuid = uu
self.message = message
self.extensions = {}
+ self.signature = None
def add_extension(self, k, v):
@@ -65,9 +66,14 @@ class entry:
'uuid': str(self.uuid),
'timestamp': int(ts),
'payload': self.message.as_string(),
+ 'signature': repr(self.signature),
}
+ def set_signature(self, v):
+ self.signature = v
+
+
def to_multipart_file(path):
#d = tempfile.TemporaryDirectory()
#t = tempfile.NamedTemporaryFile(mode='w+', dir=d.name)
diff --git a/feedwarrior/error.py b/feedwarrior/error.py
@@ -1,2 +1,6 @@
class NotMultipartError(Exception):
pass
+
+
+class SignError(Exception):
+ pass