* [PATCH 0/4] Testing with patatt-0.2.0 from a clean env
@ 2021-05-12 15:19 Konstantin Ryabitsev
2021-05-12 15:19 ` [PATCH 1/4] Return a list of all sigs, not just goodsigs Konstantin Ryabitsev
` (3 more replies)
0 siblings, 4 replies; 5+ messages in thread
From: Konstantin Ryabitsev @ 2021-05-12 15:19 UTC (permalink / raw)
To: signatures
Merely testing from a clean env before the demo.
Konstantin Ryabitsev (4):
Return a list of all sigs, not just goodsigs
Return better result than just pass/fail
Support other git dirs as sources
Release as v0.2.0
README.rst | 22 +++----
man/patatt.5 | 2 +-
man/patatt.5.rst | 4 +-
patatt/__init__.py | 153 +++++++++++++++++++++++++++------------------
4 files changed, 107 insertions(+), 74 deletions(-)
--
2.25.1
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH 1/4] Return a list of all sigs, not just goodsigs
2021-05-12 15:19 [PATCH 0/4] Testing with patatt-0.2.0 from a clean env Konstantin Ryabitsev
@ 2021-05-12 15:19 ` Konstantin Ryabitsev
2021-05-12 15:19 ` [PATCH 2/4] Return better result than just pass/fail Konstantin Ryabitsev
` (2 subsequent siblings)
3 siblings, 0 replies; 5+ messages in thread
From: Konstantin Ryabitsev @ 2021-05-12 15:19 UTC (permalink / raw)
To: signatures
Instead of returning a list of goodsigs and throwing a ValidationError
if even one of them has failed, return a list of all sigs -- failed or
otherwise, and let the caller decide what to do with it.
Signed-off-by: Konstantin Ryabitsev <konstantin@linuxfoundation.org>
---
patatt/__init__.py | 48 ++++++++++++++++++++++------------------------
1 file changed, 23 insertions(+), 25 deletions(-)
diff --git a/patatt/__init__.py b/patatt/__init__.py
index 2f380e0..9ceaa29 100644
--- a/patatt/__init__.py
+++ b/patatt/__init__.py
@@ -42,7 +42,7 @@ DEFAULT_CONFIG = {
KEYCACHE = dict()
# My version
-__VERSION__ = '0.1.0'
+__VERSION__ = '0.2.0-dev'
MAX_SUPPORTED_FORMAT_VERSION = 1
@@ -841,19 +841,19 @@ def cmd_sign(cmdargs, config: dict) -> None:
def validate_message(msgdata: bytes, sources: list, trim_body: bool = False) -> list:
- errors = list()
- goodsigs = list()
- success = False
+ attestations = list()
pm = PatattMessage(msgdata)
if not pm.signed:
- errors.append('message is not signed')
- raise ValidationError('message is not signed', errors)
+ logger.debug('message is not signed')
+ return attestations
# Find all identities for which we have public keys
for ds in pm.get_sigs():
+ errors = list()
a = ds.get_field('a', decode=True)
i = ds.get_field('i', decode=True)
s = ds.get_field('s', decode=True)
+ t = ds.get_field('t', decode=True)
if not s:
s = 'default'
if a.startswith('ed25519'):
@@ -862,6 +862,7 @@ def validate_message(msgdata: bytes, sources: list, trim_body: bool = False) ->
algo = 'openpgp'
else:
errors.append('%s/%s Unknown algorigthm: %s' % (i, s, a))
+ attestations.append((False, i, t, None, a, errors))
continue
pkey = keysrc = None
@@ -874,21 +875,17 @@ def validate_message(msgdata: bytes, sources: list, trim_body: bool = False) ->
if not pkey and algo == 'ed25519':
errors.append('%s/%s no matching ed25519 key found' % (i, s))
+ attestations.append((False, i, t, None, algo, errors))
continue
try:
signtime = pm.validate(i, pkey, trim_body=trim_body)
- success = True
+ attestations.append((True, i, signtime, keysrc, algo, errors))
except ValidationError:
errors.append('failed to validate using %s' % keysrc)
- continue
-
- goodsigs.append((i, signtime, keysrc, algo))
+ attestations.append((False, i, t, keysrc, algo, errors))
- if not success:
- raise ValidationError('Failed to validate message', errors)
-
- return goodsigs
+ return attestations
def cmd_validate(cmdargs, config: dict):
@@ -922,19 +919,20 @@ def cmd_validate(cmdargs, config: dict):
allgood = True
for fn, msgdata in messages.items():
try:
- goodsigs = validate_message(msgdata, sources, trim_body=trim_body)
- for identity, signtime, keysrc, algo in goodsigs:
- logger.critical('PASS | %s | %s', identity, fn)
- if keysrc:
- logger.info(' | key: %s', keysrc)
+ attestations = validate_message(msgdata, sources, trim_body=trim_body)
+ for passing, identity, signtime, keysrc, algo, errors in attestations:
+ if passing:
+ logger.critical('PASS | %s | %s', identity, fn)
+ if keysrc:
+ logger.info(' | key: %s', keysrc)
+ else:
+ logger.info(' | key: default GnuPG keyring')
else:
- logger.info(' | key: default GnuPG keyring')
+ allgood = False
+ logger.critical('FAIL | %s | %s', identity, fn)
+ for error in errors:
+ logger.critical(' | %s', error)
- except ValidationError as ex:
- allgood = False
- logger.critical('FAIL | %s | %s', ex, fn)
- for error in ex.errors:
- logger.critical(' | %s', error)
except RuntimeError as ex:
allgood = False
logger.critical('ERR | err: %s | %s', ex, fn)
--
2.25.1
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH 2/4] Return better result than just pass/fail
2021-05-12 15:19 [PATCH 0/4] Testing with patatt-0.2.0 from a clean env Konstantin Ryabitsev
2021-05-12 15:19 ` [PATCH 1/4] Return a list of all sigs, not just goodsigs Konstantin Ryabitsev
@ 2021-05-12 15:19 ` Konstantin Ryabitsev
2021-05-12 15:19 ` [PATCH 3/4] Support other git dirs as sources Konstantin Ryabitsev
2021-05-12 15:19 ` [PATCH 4/4] Release as v0.2.0 Konstantin Ryabitsev
3 siblings, 0 replies; 5+ messages in thread
From: Konstantin Ryabitsev @ 2021-05-12 15:19 UTC (permalink / raw)
To: signatures
We want to pass some better information about why verification failed,
if only because "we don't have a key" is not nearly as bad as "we have a
key and it actively failed verification".
Signed-off-by: Konstantin Ryabitsev <konstantin@linuxfoundation.org>
---
patatt/__init__.py | 50 ++++++++++++++++++++++++++++++----------------
1 file changed, 33 insertions(+), 17 deletions(-)
diff --git a/patatt/__init__.py b/patatt/__init__.py
index 9ceaa29..be32e36 100644
--- a/patatt/__init__.py
+++ b/patatt/__init__.py
@@ -33,6 +33,13 @@ GPGBIN = 'gpg'
# Hardcoded defaults
DEVSIG_HDR = b'X-Developer-Signature'
DEVKEY_HDR = b'X-Developer-Key'
+
+# Result and severity levels
+RES_VALID = 0
+RES_NOKEY = 8
+RES_ERROR = 16
+RES_BADSIG = 32
+
REQ_HDRS = [b'from', b'subject']
DEFAULT_CONFIG = {
'keyringsrc': ['ref::.keys', 'ref::.local-keys', 'ref:refs/meta/keyring:'],
@@ -862,7 +869,7 @@ def validate_message(msgdata: bytes, sources: list, trim_body: bool = False) ->
algo = 'openpgp'
else:
errors.append('%s/%s Unknown algorigthm: %s' % (i, s, a))
- attestations.append((False, i, t, None, a, errors))
+ attestations.append((RES_ERROR, i, t, None, a, errors))
continue
pkey = keysrc = None
@@ -875,15 +882,15 @@ def validate_message(msgdata: bytes, sources: list, trim_body: bool = False) ->
if not pkey and algo == 'ed25519':
errors.append('%s/%s no matching ed25519 key found' % (i, s))
- attestations.append((False, i, t, None, algo, errors))
+ attestations.append((RES_NOKEY, i, t, None, algo, errors))
continue
try:
signtime = pm.validate(i, pkey, trim_body=trim_body)
- attestations.append((True, i, signtime, keysrc, algo, errors))
+ attestations.append((RES_VALID, i, signtime, keysrc, algo, errors))
except ValidationError:
errors.append('failed to validate using %s' % keysrc)
- attestations.append((False, i, t, keysrc, algo, errors))
+ attestations.append((RES_BADSIG, i, t, keysrc, algo, errors))
return attestations
@@ -916,29 +923,38 @@ def cmd_validate(cmdargs, config: dict):
else:
trim_body = False
- allgood = True
+ highest_err = 0
for fn, msgdata in messages.items():
try:
attestations = validate_message(msgdata, sources, trim_body=trim_body)
- for passing, identity, signtime, keysrc, algo, errors in attestations:
- if passing:
- logger.critical('PASS | %s | %s', identity, fn)
+ for result, identity, signtime, keysrc, algo, errors in attestations:
+ if result > highest_err:
+ highest_err = result
+
+ if result == RES_VALID:
+ logger.critical(' PASS | %s, %s', identity, fn)
if keysrc:
- logger.info(' | key: %s', keysrc)
+ logger.info(' | key: %s', keysrc)
else:
- logger.info(' | key: default GnuPG keyring')
+ logger.info(' | key: default GnuPG keyring')
+ elif result <= RES_NOKEY:
+ logger.critical(' NOKEY | %s, %s', identity, fn)
+ for error in errors:
+ logger.critical(' | %s', error)
+ elif result <= RES_ERROR:
+ logger.critical(' ERROR | %s, %s', identity, fn)
+ for error in errors:
+ logger.critical(' | %s', error)
else:
- allgood = False
- logger.critical('FAIL | %s | %s', identity, fn)
+ logger.critical('BADSIG | %s, %s', identity, fn)
for error in errors:
- logger.critical(' | %s', error)
+ logger.critical(' | %s', error)
except RuntimeError as ex:
- allgood = False
- logger.critical('ERR | err: %s | %s', ex, fn)
+ highest_err = RES_ERROR
+ logger.critical(' ERROR | err: %s | %s', ex, fn)
- if not allgood:
- sys.exit(1)
+ sys.exit(highest_err)
def cmd_genkey(cmdargs, config: dict) -> None:
--
2.25.1
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH 3/4] Support other git dirs as sources
2021-05-12 15:19 [PATCH 0/4] Testing with patatt-0.2.0 from a clean env Konstantin Ryabitsev
2021-05-12 15:19 ` [PATCH 1/4] Return a list of all sigs, not just goodsigs Konstantin Ryabitsev
2021-05-12 15:19 ` [PATCH 2/4] Return better result than just pass/fail Konstantin Ryabitsev
@ 2021-05-12 15:19 ` Konstantin Ryabitsev
2021-05-12 15:19 ` [PATCH 4/4] Release as v0.2.0 Konstantin Ryabitsev
3 siblings, 0 replies; 5+ messages in thread
From: Konstantin Ryabitsev @ 2021-05-12 15:19 UTC (permalink / raw)
To: signatures
We need to have a way to specify other git dirs as sources, so change
how our ref: locations work. Instead of:
ref:[refname]:[subpath]
we now have:
ref:[repopath]:[refname]:[subpath]
Additionally, add a way to deal with one level of symlinks.
Signed-off-by: Konstantin Ryabitsev <konstantin@linuxfoundation.org>
---
README.rst | 22 ++++++-------
man/patatt.5 | 2 +-
man/patatt.5.rst | 4 +--
patatt/__init__.py | 79 ++++++++++++++++++++++++++++------------------
4 files changed, 63 insertions(+), 44 deletions(-)
diff --git a/README.rst b/README.rst
index e0272b8..6cde177 100644
--- a/README.rst
+++ b/README.rst
@@ -368,12 +368,12 @@ check, via the ``keyringsrc`` setting (can be specified multiple
times and will be checked in the listed order)::
[patatt]
- # Empty ref means "use currently checked out ref"
- keyringsrc = ref::.keys
- # Use a dedicated ref called refs/meta/keyring
- keyringsrc = ref:refs/meta/keyring:
- # You can fetch other project's keyring into its own ref
- keyringsrc = ref:refs/meta/someone-elses-keyring:
+ # Empty ref means "use currently checked out ref in this repo"
+ keyringsrc = ref:::.keys
+ # Use a dedicated ref in this repo called refs/meta/keyring
+ keyringsrc = ref::refs/meta/keyring:
+ # Use a ref in a different repo
+ keyringsrc = ref:~/path/to/another/repo:refs/heads/main:.keys
# Use a regular dir on disk
keyringsrc = ~/git/pgpkeys/keyring
@@ -385,13 +385,13 @@ Any path on disk can be used for a keyring location, and some will
always be checked just in case. The following locations are added by
default::
- ref::.keys
- ref::.local-keys
- ref:refs/meta/keyring:
+ ref:::.keys
+ ref:::.local-keys
+ ref::refs/meta/keyring:
$XDG_DATA_HOME/patatt/public
-The "::" means "whatever ref is currently checked out", and
-$XDG_DATA_HOME usually points at ~/.local/share.
+The ":::" means "whatever ref is checked out in the current repo",
+and $XDG_DATA_HOME usually points at $HOME/.local/share.
Getting support and contributing patches
----------------------------------------
diff --git a/man/patatt.5 b/man/patatt.5
index 5e97753..70cea05 100644
--- a/man/patatt.5
+++ b/man/patatt.5
@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
-.TH PATATT 5 "2021-05-07" "0.1.0" ""
+.TH PATATT 5 "2021-05-11" "0.2.0" ""
.SH NAME
PATATT \- DKIM-like cryptographic patch attestation
.
diff --git a/man/patatt.5.rst b/man/patatt.5.rst
index f607ed9..2ab345c 100644
--- a/man/patatt.5.rst
+++ b/man/patatt.5.rst
@@ -5,10 +5,10 @@ DKIM-like cryptographic patch attestation
-----------------------------------------
:Author: mricon@kernel.org
-:Date: 2021-05-07
+:Date: 2021-05-11
:Copyright: The Linux Foundation and contributors
:License: MIT-0
-:Version: 0.1.0
+:Version: 0.2.0
:Manual section: 5
SYNOPSIS
diff --git a/patatt/__init__.py b/patatt/__init__.py
index be32e36..e547b28 100644
--- a/patatt/__init__.py
+++ b/patatt/__init__.py
@@ -41,9 +41,6 @@ RES_ERROR = 16
RES_BADSIG = 32
REQ_HDRS = [b'from', b'subject']
-DEFAULT_CONFIG = {
- 'keyringsrc': ['ref::.keys', 'ref::.local-keys', 'ref:refs/meta/keyring:'],
-}
# Quick cache for key info
KEYCACHE = dict()
@@ -598,9 +595,9 @@ def _run_command(cmdargs: list, stdin: bytes = None, env: Optional[dict] = None)
def git_run_command(gitdir: Optional[str], args: list, stdin: Optional[bytes] = None,
env: Optional[dict] = None) -> Tuple[int, bytes, bytes]:
if gitdir:
- env = {'GIT_DIR': gitdir}
-
- args = ['git', '--no-pager'] + args
+ args = ['git', '--git-dir', gitdir, '--no-pager'] + args
+ else:
+ args = ['git', '--no-pager'] + args
return _run_command(args, stdin=stdin, env=env)
@@ -688,36 +685,55 @@ def get_public_key(source: str, keytype: str, identity: str, selector: str) -> T
keypath = make_pkey_path(keytype, identity, selector)
logger.debug('Looking for %s in %s', keypath, source)
- if source.find('ref:') == 0:
- gittop = get_git_toplevel()
+ # ref:refs/heads/someref:in-repo/path
+ if source.startswith('ref:'):
+ # split by :
+ parts = source.split(':', 4)
+ if len(parts) < 4:
+ raise ConfigurationError('Invalid ref, must have at least 3 colons: %s' % source)
+ gittop = parts[1]
+ gitref = parts[2]
+ gitsub = parts[3]
+ if not gittop:
+ gittop = get_git_toplevel()
if not gittop:
raise KeyError('Not in a git tree, so cannot use a ref: source')
- # format is: ref:refspec:path
- # or it could omit the refspec, meaning "whatever the current ref"
- # but it should always have at least two ":"
- chunks = source.split(':', 2)
- if len(chunks) < 3:
- logger.debug('ref: sources must have refspec and path, e.g.: ref:refs/heads/master:.keys')
- raise ConfigurationError('Invalid ref: source: %s' % source)
+
+ gittop = os.path.expanduser(gittop)
+ if gittop.find('$') >= 0:
+ gittop = os.path.expandvars(gittop)
+ if os.path.isdir(os.path.join(gittop, '.git')):
+ gittop = os.path.join(gittop, '.git')
+
+ # it could omit the refspec, meaning "whatever the current ref"
# grab the key from a fully ref'ed path
- ref = chunks[1]
- pathtop = chunks[2]
- subpath = os.path.join(pathtop, keypath)
+ subpath = os.path.join(gitsub, keypath)
- if not ref:
+ if not gitref:
# What is our current ref?
- cmdargs = ['git', 'symbolic-ref', 'HEAD']
- ecode, out, err = _run_command(cmdargs)
+ cmdargs = ['symbolic-ref', 'HEAD']
+ ecode, out, err = git_run_command(gittop, cmdargs)
if ecode == 0:
- ref = out.decode().strip()
+ gitref = out.decode().strip()
+ if not gitref:
+ raise KeyError('Could not figure out current ref in %s' % gittop)
- cmdargs = ['git']
- keysrc = f'{ref}:{subpath}'
- cmdargs += ['show', keysrc]
- ecode, out, err = _run_command(cmdargs)
+ keysrc = f'{gitref}:{subpath}'
+ cmdargs = ['show', keysrc]
+ ecode, out, err = git_run_command(gittop, cmdargs)
if ecode == 0:
+ # Handle one level of symlinks
+ if out.find(b'\n') < 0 < out.find(b'/'):
+ # Check this path as well
+ linktgt = os.path.normpath(os.path.join(os.path.dirname(subpath), out.decode()))
+ keysrc = f'{gitref}:{linktgt}'
+ cmdargs = ['show', keysrc]
+ ecode, out, err = git_run_command(gittop, cmdargs)
+ if ecode == 0:
+ logger.debug('KEYSRC : %s (symlinked)', keysrc)
+ return out, 'ref:%s:%s' % (gittop, keysrc)
logger.debug('KEYSRC : %s', keysrc)
- return out, keysrc
+ return out, 'ref:%s:%s' % (gittop, keysrc)
# Does it exist on disk in gittop?
fullpath = os.path.join(gittop, subpath)
@@ -726,7 +742,7 @@ def get_public_key(source: str, keytype: str, identity: str, selector: str) -> T
logger.debug('KEYSRC : %s', fullpath)
return fh.read(), fullpath
- raise KeyError('Could not find %s in %s' % (subpath, ref))
+ raise KeyError('Could not find %s in %s:%s' % (subpath, gittop, gitref))
# It's a disk path, then
# Expand ~ and env vars
@@ -1084,8 +1100,11 @@ def command() -> None:
ch.setLevel(logging.CRITICAL)
logger.addHandler(ch)
- config = get_config_from_git(r'patatt\..*', section=_args.section,
- defaults=DEFAULT_CONFIG, multivals=['keyringsrc'])
+ config = get_config_from_git(r'patatt\..*', section=_args.section, multivals=['keyringsrc'])
+ # Append some extra keyring locations
+ if 'keyringsrc' not in config:
+ config['keyringsrc'] = list()
+ config['keyringsrc'] += ['ref:::.keys', 'ref:::.local-keys', 'ref::refs/meta/keyring:']
logger.debug('config: %s', config)
if 'func' not in _args:
--
2.25.1
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH 4/4] Release as v0.2.0
2021-05-12 15:19 [PATCH 0/4] Testing with patatt-0.2.0 from a clean env Konstantin Ryabitsev
` (2 preceding siblings ...)
2021-05-12 15:19 ` [PATCH 3/4] Support other git dirs as sources Konstantin Ryabitsev
@ 2021-05-12 15:19 ` Konstantin Ryabitsev
3 siblings, 0 replies; 5+ messages in thread
From: Konstantin Ryabitsev @ 2021-05-12 15:19 UTC (permalink / raw)
To: signatures
Time to cut a 0.2.0, though I expect 0.3.0 won't be too far behind.
Signed-off-by: Konstantin Ryabitsev <konstantin@linuxfoundation.org>
---
patatt/__init__.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/patatt/__init__.py b/patatt/__init__.py
index e547b28..373392d 100644
--- a/patatt/__init__.py
+++ b/patatt/__init__.py
@@ -46,7 +46,7 @@ REQ_HDRS = [b'from', b'subject']
KEYCACHE = dict()
# My version
-__VERSION__ = '0.2.0-dev'
+__VERSION__ = '0.2.0'
MAX_SUPPORTED_FORMAT_VERSION = 1
--
2.25.1
^ permalink raw reply related [flat|nested] 5+ messages in thread
end of thread, other threads:[~2021-05-12 15:19 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2021-05-12 15:19 [PATCH 0/4] Testing with patatt-0.2.0 from a clean env Konstantin Ryabitsev
2021-05-12 15:19 ` [PATCH 1/4] Return a list of all sigs, not just goodsigs Konstantin Ryabitsev
2021-05-12 15:19 ` [PATCH 2/4] Return better result than just pass/fail Konstantin Ryabitsev
2021-05-12 15:19 ` [PATCH 3/4] Support other git dirs as sources Konstantin Ryabitsev
2021-05-12 15:19 ` [PATCH 4/4] Release as v0.2.0 Konstantin Ryabitsev
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).