tools: Extend gpg-check-pattern.

* tools/gpg-check-pattern.c: Major rewrite.
--
Signed-off-by: Werner Koch <wk@gnupg.org>

Here is a simple pattern file:

====================
# Pattern to reject passwords which do not comply to
#   - at least 1 uppercase letter
#   - at least 1 lowercase letter
#   - at least one number
#   - at least one special character
# and a few extra things to show the reject mode

# Reject is the default mode, ignore case is the default
#[reject]
#[icase]

# If the password starts with "foo" (case insensitive) it is rejected.
/foo.*/

[case]

# If the password starts with "bar" (case sensitive) it is rejected.
/bar.*/

# Switch to accept mode: Only if all patterns up to the next "accept"
# or "reject" tag or EOF match, the password is accepted.  Otherwise
# the password is rejected.

[accept]

/[A-Z]+/
/[a-z]+/
/[0-9]+/
/[^A-Za-z0-9]+/
=================

Someone™ please write regression tests.
This commit is contained in:
Werner Koch 2021-07-29 11:25:06 +02:00
parent 5c8124b8b9
commit 73c03e0232
No known key found for this signature in database
GPG Key ID: E3FDFF218E45B72B
2 changed files with 207 additions and 25 deletions

View File

@ -2081,6 +2081,51 @@ gpgtar --list-archive test1
@command{gpg-check-pattern} checks a passphrase given on stdin against
a specified pattern file.
The pattern file is line based with comment lines beginning on the
@emph{first} position with a @code{#}. Empty lines and lines with
only white spaces are ignored. The actual pattern lines may either be
verbatim string pattern and match as they are (trailing spaces are
ignored) or extended regular expressions indicated by a @code{/} or
@code{!/} in the first column and terminated by another @code{/} or
end of line. If a regular expression starts with @code{!/} the match
result is reversed. By default all comparisons are case insensitive.
Tag lines may be used to further control the operation of this tool.
The currently defined tags are:
@table @code
@item [icase]
Switch to case insensitive comparison for all further patterns. This
is the default.
@item [case]
Switch to case sensitive comparison for all further patterns.
@item [reject]
Switch to reject mode. This is the default mode.
@item [accept]
Switch to accept mode.
@end table
In the future more tags may be introduced and thus it is advisable not to
start a plain pattern string with an open bracket. The tags must be
given verbatim on the line with no spaces to the left or any non white
space characters to the right.
In reject mode the program exits on the first match with an exit code
of 1 (failure). If at the end of the pattern list the reject mode is
still active the program exits with code 0 (success).
In accept mode blocks of patterns are used. A block starts at the
next pattern after an "accept" tag and ends with the last pattern
before the next "accept" or "reject" tag or at the end of the pattern
list. If all patterns in a block match the program exits with an exit
code of 0 (success). If any pattern in a block do not match the next
pattern block is evaluated. If at the end of the pattern list the
accept mode is still active the program exits with code 1 (failure).
@mansect options
@noindent
@ -2102,6 +2147,6 @@ Input is expected to be null delimited.
@mansect see also
@ifset isman
@command{gpg}(1),
@command{gpg-agent}(1),
@end ifset
@include see-also-note.texi

View File

@ -1,4 +1,5 @@
/* gpg-check-pattern.c - A tool to check passphrases against pattern.
* Copyright (C) 2021 g10 Code GmbH
* Copyright (C) 2007 Free Software Foundation, Inc.
*
* This file is part of GnuPG.
@ -26,7 +27,6 @@
#include <stdarg.h>
#include <string.h>
#include <errno.h>
#include <assert.h>
#ifdef HAVE_LOCALE_H
# include <locale.h>
#endif
@ -50,11 +50,7 @@
enum cmd_and_opt_values
{ aNull = 0,
oVerbose = 'v',
oArmor = 'a',
oPassphrase = 'P',
oProtect = 'p',
oUnprotect = 'u',
oNull = '0',
oNoVerbose = 500,
@ -101,6 +97,10 @@ struct pattern_s
{
int type;
unsigned int lineno; /* Line number of the pattern file. */
unsigned int newblock; /* First pattern in a new block. */
unsigned int icase:1; /* Case insensitive match. */
unsigned int accept:1; /* In accept mode. */
unsigned int reverse:1; /* Reverse the outcome of a regexp match. */
union {
struct {
const char *string; /* Pointer to the actual string (nul termnated). */
@ -200,7 +200,7 @@ main (int argc, char **argv )
gpgrt_usage (1);
/* We read the entire pattern file into our memory and parse it
using a separate function. This allows us to eventual do the
using a separate function. This allows us to eventually do the
reading while running setuid so that the pattern file can be
hidden from regular users. I am not sure whether this makes
sense, but lets be prepared for it. */
@ -219,7 +219,7 @@ main (int argc, char **argv )
#endif
process (stdin, patternarray);
return log_get_errorcount(0)? 1 : 0;
return 4; /*NOTREACHED*/
}
@ -310,6 +310,7 @@ get_regerror (int errcode, regex_t *compiled)
return buffer;
}
/* Parse the pattern given in the memory aread DATA/DATALEN and return
a new pattern array. The end of the array is indicated by a NULL
entry. On error an error message is printed and the function
@ -324,6 +325,9 @@ parse_pattern_file (char *data, size_t datalen)
pattern_t *array;
size_t arraysize, arrayidx;
unsigned int lineno = 0;
unsigned int icase_mode = 1;
unsigned int accept_mode = 0;
unsigned int newblock = 1; /* The first implict block. */
/* Estimate the number of entries by counting the non-comment lines. */
arraysize = 0;
@ -349,7 +353,7 @@ parse_pattern_file (char *data, size_t datalen)
}
else
p2 = p + datalen;
assert (!*p2);
log_assert (!*p2);
p2--;
while (isascii (*p) && isspace (*p))
p++;
@ -359,23 +363,57 @@ parse_pattern_file (char *data, size_t datalen)
*p2-- = 0;
if (!*p)
continue;
assert (arrayidx < arraysize);
if (!strcmp (p, "[case]"))
{
icase_mode = 0;
continue;
}
if (!strcmp (p, "[icase]"))
{
icase_mode = 1;
continue;
}
if (!strcmp (p, "[accept]"))
{
accept_mode = 1;
newblock = 1;
continue;
}
if (!strcmp (p, "[reject]"))
{
accept_mode = 0;
newblock = 1;
continue;
}
log_assert (arrayidx < arraysize);
array[arrayidx].lineno = lineno;
if (*p == '/')
array[arrayidx].icase = icase_mode;
array[arrayidx].accept = accept_mode;
array[arrayidx].reverse = 0;
array[arrayidx].newblock = newblock;
newblock = 0;
if (*p == '/' || (*p == '!' && p[1] == '/'))
{
int rerr;
int reverse;
reverse = (*p == '!');
p++;
if (reverse)
p++;
array[arrayidx].type = PAT_REGEX;
if (*p && p[strlen(p)-1] == '/')
p[strlen(p)-1] = 0; /* Remove optional delimiter. */
array[arrayidx].u.r.regex = xcalloc (1, sizeof (regex_t));
array[arrayidx].reverse = reverse;
rerr = regcomp (array[arrayidx].u.r.regex, p,
REG_ICASE|REG_EXTENDED);
(array[arrayidx].icase? REG_ICASE:0)|REG_EXTENDED);
if (rerr)
{
char *rerrbuf = get_regerror (rerr, array[arrayidx].u.r.regex);
log_error ("invalid r.e. at line %u: %s\n", lineno, rerrbuf);
log_error ("invalid regexp at line %u: %s\n", lineno, rerrbuf);
xfree (rerrbuf);
if (!opt.checkonly)
exit (1);
@ -383,25 +421,44 @@ parse_pattern_file (char *data, size_t datalen)
}
else
{
if (*p == '[')
{
static int shown;
if (!shown)
{
log_info ("future warning: do no start a string with '['"
" but use a regexp (line %u)\n", lineno);
shown = 1;
}
}
array[arrayidx].type = PAT_STRING;
array[arrayidx].u.s.string = p;
array[arrayidx].u.s.length = strlen (p);
}
arrayidx++;
}
assert (arrayidx < arraysize);
log_assert (arrayidx < arraysize);
array[arrayidx].type = PAT_NULL;
if (lineno && newblock)
log_info ("warning: pattern list ends with a singleton"
" accept or reject tag\n");
return array;
}
/* Check whether string macthes any of the pattern in PATARRAY and
/* Check whether string matches any of the pattern in PATARRAY and
returns the matching pattern item or NULL. */
static pattern_t *
match_p (const char *string, pattern_t *patarray)
{
pattern_t *pat;
int match;
int accept_match; /* Tracks matchinf state in an accept block. */
int accept_skip; /* Skip remaining patterns in an accept block. */
if (!*string)
{
@ -410,30 +467,84 @@ match_p (const char *string, pattern_t *patarray)
return NULL;
}
accept_match = 0;
accept_skip = 0;
for (pat = patarray; pat->type != PAT_NULL; pat++)
{
match = 0;
if (pat->newblock)
accept_match = accept_skip = 0;
if (pat->type == PAT_STRING)
{
if (!strcasecmp (pat->u.s.string, string))
return pat;
if (pat->icase)
{
if (!strcasecmp (pat->u.s.string, string))
match = 1;
}
else
{
if (!strcmp (pat->u.s.string, string))
match = 1;
}
}
else if (pat->type == PAT_REGEX)
{
int rerr;
rerr = regexec (pat->u.r.regex, string, 0, NULL, 0);
if (pat->reverse)
{
if (!rerr)
rerr = REG_NOMATCH;
else if (rerr == REG_NOMATCH)
rerr = 0;
}
if (!rerr)
return pat;
match = 1;
else if (rerr != REG_NOMATCH)
{
char *rerrbuf = get_regerror (rerr, pat->u.r.regex);
log_error ("matching r.e. failed: %s\n", rerrbuf);
log_error ("matching regexp failed: %s\n", rerrbuf);
xfree (rerrbuf);
return pat; /* Better indicate a match on error. */
if (pat->accept)
match = 0; /* Better indicate no match on error. */
else
match = 1; /* Better indicate a match on error. */
}
}
else
BUG ();
if (pat->accept)
{
/* Accept mode: all patterns in the accept block must match.
* Thus we need to check whether the next pattern has a
* transition and act only then. */
if (match && !accept_skip)
accept_match = 1;
else
{
accept_match = 0;
accept_skip = 1;
}
if (pat[1].type == PAT_NULL || pat[1].newblock)
{
/* Transition detected. Note that this also handles the
* end of pattern loop case. */
if (accept_match)
return pat;
/* The next is not really but we do it for clarity. */
accept_match = accept_skip = 0;
}
}
else /* Reject mode: Return true on the first match. */
{
if (match)
return pat;
}
}
return NULL;
}
@ -449,6 +560,7 @@ process (FILE *fp, pattern_t *patarray)
int c;
unsigned long lineno = 0;
pattern_t *pat;
int last_is_accept;
idx = 0;
c = 0;
@ -468,17 +580,28 @@ process (FILE *fp, pattern_t *patarray)
pat = match_p (buffer, patarray);
if (pat)
{
/* Note that the accept mode works correctly only with
* one input line. */
if (opt.verbose)
log_error ("input line %lu matches pattern at line %u"
" - rejected\n",
lineno, pat->lineno);
exit (1);
log_info ("input line %lu matches pattern at line %u"
" - %s\n",
lineno, pat->lineno,
pat->accept? "accepted":"rejected");
}
idx = 0;
wipememory (buffer, sizeof buffer);
if (pat)
{
if (pat->accept)
exit (0);
else
exit (1);
}
}
else
idx++;
}
wipememory (buffer, sizeof buffer);
if (c != EOF)
{
log_error ("input line %lu too long - rejected\n", lineno+1);
@ -490,6 +613,20 @@ process (FILE *fp, pattern_t *patarray)
lineno+1, strerror (errno));
exit (1);
}
/* Check last pattern to see whether we are in accept mode. */
last_is_accept = 0;
for (pat = patarray; pat->type != PAT_NULL; pat++)
last_is_accept = pat->accept;
if (opt.verbose)
log_info ("no input line matches the pattern - accepted\n");
log_info ("no input line matches the pattern - %s\n",
last_is_accept? "rejected":"accepted");
if (log_get_errorcount(0))
exit (2); /* Ooops - reject. */
else if (last_is_accept)
exit (1); /* Reject */
else
exit (0); /* Accept */
}