/* crlcache.c - LDAP access
* Copyright (C) 2002 Klarälvdalens Datakonsult AB
* Copyright (C) 2003, 2004, 2005, 2008 g10 Code GmbH
*
* This file is part of DirMngr.
*
* DirMngr is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* DirMngr is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see .
*/
/*
1. To keep track of the CRLs actually cached and to store the meta
information of the CRLs a simple record oriented text file is
used. Fields in the file are colon (':') separated and values
containing colons or linefeeds are percent escaped (e.g. a colon
itself is represented as "%3A").
The first field is a record type identifier, so that the file is
useful to keep track of other meta data too.
The name of the file is "DIR.txt".
1.1. Comment record
Field 1: Constant beginning with "#".
Other fields are not defined and such a record is simply
skipped during processing.
1.2. Version record
Field 1: Constant "v"
Field 2: Version number of this file. Must be 1.
This record must be the first non-comment record and
there shall only exist one record of this type.
1.3. CRL cache record
Field 1: Constant "c", "u" or "i".
A "c" or "u" indicate a valid cache entry, however
"u" requires that a user root certificate check needs
to be done.
An "i" indicates an invalid cache entry which should
not be used but still exists so that it can be
updated at NEXT_UPDATE.
Field 2: Hexadecimal encoded SHA-1 hash of the issuer DN using
uppercase letters.
Field 3: Issuer DN in RFC-2253 notation.
Field 4: URL used to retrieve the corresponding CRL.
Field 5: 15 character ISO timestamp with THIS_UPDATE.
Field 6: 15 character ISO timestamp with NEXT_UPDATE.
Field 7: Hexadecimal encoded MD-5 hash of the DB file to detect
accidental modified (i.e. deleted and created) cache files.
Field 8: optional CRL number as a hex string.
Field 9: AuthorityKeyID.issuer, each Name separated by 0x01
Field 10: AuthorityKeyID.serial
Field 11: Hex fingerprint of trust anchor if field 1 is 'u'.
2. Layout of the standard CRL Cache DB file:
We use records of variable length with this structure
n bytes Serialnumber (binary) used as key
thus there is no need to store the length explicitly with DB2.
1 byte Reason for revocation
(currently the KSBA reason flags are used)
15 bytes ISO date of revocation (e.g. 19980815T142000)
Note that there is no terminating 0 stored.
The filename used is the hexadecimal (using uppercase letters)
SHA-1 hash value of the issuer DN prefixed with a "crl-" and
suffixed with a ".db". Thus the length of the filename is 47.
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#ifndef HAVE_W32_SYSTEM
#include
#endif
#include "dirmngr.h"
#include "validate.h"
#include "certcache.h"
#include "crlcache.h"
#include "crlfetch.h"
#include "misc.h"
#include "cdb.h"
/* Change this whenever the format changes */
#define DBDIR_D "crls.d"
#define DBDIRFILE "DIR.txt"
#define DBDIRVERSION 1
/* The number of DB files we may have open at one time. We need to
limit this because there is no guarantee that the number of issuers
has a upper limit. We are currently using mmap, so it is a good
idea anyway to limit the number of opened cache files. */
#define MAX_OPEN_DB_FILES 5
#ifndef O_BINARY
# define O_BINARY 0
#endif
static const char oidstr_crlNumber[] = "2.5.29.20";
/* static const char oidstr_issuingDistributionPoint[] = "2.5.29.28"; */
static const char oidstr_authorityKeyIdentifier[] = "2.5.29.35";
/* Definition of one cached item. */
struct crl_cache_entry_s
{
struct crl_cache_entry_s *next;
int deleted; /* True if marked for deletion. */
int mark; /* Internally used by update_dir. */
unsigned int lineno;/* A 0 indicates a new entry. */
char *release_ptr; /* The actual allocated memory. */
char *url; /* Points into RELEASE_PTR. */
char *issuer; /* Ditto. */
char *issuer_hash; /* Ditto. */
char *dbfile_hash; /* MD5 sum of the cache file, points into RELEASE_PTR.*/
int invalid; /* Can't use this CRL. */
int user_trust_req; /* User supplied root certificate required. */
char *check_trust_anchor; /* Malloced fingerprint. */
ksba_isotime_t this_update;
ksba_isotime_t next_update;
ksba_isotime_t last_refresh; /* Use for the force_crl_refresh feature. */
char *crl_number;
char *authority_issuer;
char *authority_serialno;
struct cdb *cdb; /* The cache file handle or NULL if not open. */
unsigned int cdb_use_count; /* Current use count. */
unsigned int cdb_lru_count; /* Used for LRU purposes. */
int dbfile_checked; /* Set to true if the dbfile_hash value has
been checked one. */
};
/* Definition of the entire cache object. */
struct crl_cache_s
{
crl_cache_entry_t entries;
};
typedef struct crl_cache_s *crl_cache_t;
/* Prototypes. */
static crl_cache_entry_t find_entry (crl_cache_entry_t first,
const char *issuer_hash);
/* The currently loaded cache object. This is usually initialized
right at startup. */
static crl_cache_t current_cache;
/* Return the current cache object or bail out if it is has not yet
been initialized. */
static crl_cache_t
get_current_cache (void)
{
if (!current_cache)
log_fatal ("CRL cache has not yet been initialized\n");
return current_cache;
}
/*
Create ae directory if it does not yet exists. Returns on
success, or -1 on error.
*/
static int
create_directory_if_needed (const char *name)
{
gnupg_dir_t dir;
char *fname;
fname = make_filename (opt.homedir_cache, name, NULL);
dir = gnupg_opendir (fname);
if (!dir)
{
log_info (_("creating directory '%s'\n"), fname);
if (gnupg_mkdir (fname, "-rwx"))
{
int save_errno = errno;
log_error (_("error creating directory '%s': %s\n"),
fname, strerror (errno));
xfree (fname);
gpg_err_set_errno (save_errno);
return -1;
}
}
else
gnupg_closedir (dir);
xfree (fname);
return 0;
}
/* Remove all files from the cache directory. If FORCE is not true,
some sanity checks on the filenames are done. Return 0 if
everything went fine. */
static int
cleanup_cache_dir (int force)
{
char *dname = make_filename (opt.homedir_cache, DBDIR_D, NULL);
gnupg_dir_t dir;
gnupg_dirent_t de;
int problem = 0;
if (!force)
{ /* Very minor sanity checks. */
if (!strcmp (dname, "~/") || !strcmp (dname, "/" ))
{
log_error (_("ignoring database dir '%s'\n"), dname);
xfree (dname);
return -1;
}
}
dir = gnupg_opendir (dname);
if (!dir)
{
log_error (_("error reading directory '%s': %s\n"),
dname, strerror (errno));
xfree (dname);
return -1;
}
while ((de = gnupg_readdir (dir)))
{
if (strcmp (de->d_name, "." ) && strcmp (de->d_name, ".."))
{
char *cdbname = make_filename (dname, de->d_name, NULL);
int okay;
struct stat sbuf;
if (force)
okay = 1;
else
okay = (!gnupg_stat (cdbname, &sbuf) && S_ISREG (sbuf.st_mode));
if (okay)
{
log_info (_("removing cache file '%s'\n"), cdbname);
if (gnupg_remove (cdbname))
{
log_error ("failed to remove '%s': %s\n",
cdbname, strerror (errno));
problem = -1;
}
}
else
log_info (_("not removing file '%s'\n"), cdbname);
xfree (cdbname);
}
}
xfree (dname);
gnupg_closedir (dir);
return problem;
}
/* Read the next line from the file FP and return the line in an
malloced buffer. Return NULL on error or EOF. There is no
limitation os the line length. The trailing linefeed has been
removed, the function will read the last line of a file, even if
that is not terminated by a LF. */
static char *
next_line_from_file (estream_t fp, gpg_error_t *r_err)
{
char buf[300];
char *largebuf = NULL;
size_t buflen;
size_t len = 0;
unsigned char *p;
int c;
char *tmpbuf;
*r_err = 0;
p = buf;
buflen = sizeof buf - 1;
while ((c=es_getc (fp)) != EOF && c != '\n')
{
if (len >= buflen)
{
if (!largebuf)
{
buflen += 1024;
largebuf = xtrymalloc ( buflen + 1 );
if (!largebuf)
{
*r_err = gpg_error_from_syserror ();
return NULL;
}
memcpy (largebuf, buf, len);
}
else
{
buflen += 1024;
tmpbuf = xtryrealloc (largebuf, buflen + 1);
if (!tmpbuf)
{
*r_err = gpg_error_from_syserror ();
xfree (largebuf);
return NULL;
}
largebuf = tmpbuf;
}
p = largebuf;
}
p[len++] = c;
}
if (c == EOF && !len)
return NULL;
p[len] = 0;
if (largebuf)
tmpbuf = xtryrealloc (largebuf, len+1);
else
tmpbuf = xtrystrdup (buf);
if (!tmpbuf)
{
*r_err = gpg_error_from_syserror ();
xfree (largebuf);
}
return tmpbuf;
}
/* Release one cache entry. */
static void
release_one_cache_entry (crl_cache_entry_t entry)
{
if (entry)
{
if (entry->cdb)
{
int fd = cdb_fileno (entry->cdb);
cdb_free (entry->cdb);
xfree (entry->cdb);
if (close (fd))
log_error (_("error closing cache file: %s\n"), strerror(errno));
}
xfree (entry->release_ptr);
xfree (entry->check_trust_anchor);
xfree (entry);
}
}
/* Release the CACHE object. */
static void
release_cache (crl_cache_t cache)
{
crl_cache_entry_t entry, entry2;
if (!cache)
return;
for (entry = cache->entries; entry; entry = entry2)
{
entry2 = entry->next;
release_one_cache_entry (entry);
}
cache->entries = NULL;
xfree (cache);
}
/* Open the dir file FNAME or create a new one if it does not yet
exist. */
static estream_t
open_dir_file (const char *fname)
{
estream_t fp;
fp = es_fopen (fname, "r");
if (!fp)
{
log_error (_("failed to open cache dir file '%s': %s\n"),
fname, strerror (errno));
/* Make sure that the directory exists, try to create if otherwise. */
if (create_directory_if_needed (NULL)
|| create_directory_if_needed (DBDIR_D))
return NULL;
fp = es_fopen (fname, "w");
if (!fp)
{
log_error (_("error creating new cache dir file '%s': %s\n"),
fname, strerror (errno));
return NULL;
}
es_fprintf (fp, "v:%d:\n", DBDIRVERSION);
if (es_ferror (fp))
{
log_error (_("error writing new cache dir file '%s': %s\n"),
fname, strerror (errno));
es_fclose (fp);
return NULL;
}
if (es_fclose (fp))
{
log_error (_("error closing new cache dir file '%s': %s\n"),
fname, strerror (errno));
return NULL;
}
log_info (_("new cache dir file '%s' created\n"), fname);
fp = es_fopen (fname, "r");
if (!fp)
{
log_error (_("failed to re-open cache dir file '%s': %s\n"),
fname, strerror (errno));
return NULL;
}
}
return fp;
}
/* Helper for open_dir. */
static gpg_error_t
check_dir_version (estream_t *fpadr, const char *fname,
unsigned int *lineno,
int cleanup_on_mismatch)
{
char *line;
gpg_error_t lineerr = 0;
estream_t fp = *fpadr;
int created = 0;
retry:
while ((line = next_line_from_file (fp, &lineerr)))
{
++*lineno;
if (*line == 'v' && line[1] == ':')
break;
else if (*line != '#')
{
log_error (_("first record of '%s' is not the version\n"), fname);
xfree (line);
return gpg_error (GPG_ERR_CONFIGURATION);
}
xfree (line);
}
if (lineerr)
return lineerr;
/* The !line catches the case of an empty DIR file. We handle this
the same as a non-matching version. */
if (!line || strtol (line+2, NULL, 10) != DBDIRVERSION)
{
if (!created && cleanup_on_mismatch)
{
log_error (_("old version of cache directory - cleaning up\n"));
es_fclose (fp);
*fpadr = NULL;
if (!cleanup_cache_dir (1))
{
*lineno = 0;
fp = *fpadr = open_dir_file (fname);
if (!fp)
{
xfree (line);
return gpg_error (GPG_ERR_CONFIGURATION);
}
created = 1;
goto retry;
}
}
log_error (_("old version of cache directory - giving up\n"));
xfree (line);
return gpg_error (GPG_ERR_CONFIGURATION);
}
xfree (line);
return 0;
}
/* Open the dir file and read in all available information. Store
that in a newly allocated cache object and return that if
everything worked out fine. Create the cache directory and the dir
if it does not yet exist. Remove all files in that directory if
the version does not match. */
static gpg_error_t
open_dir (crl_cache_t *r_cache)
{
crl_cache_t cache;
char *fname;
char *line = NULL;
gpg_error_t lineerr = 0;
estream_t fp;
crl_cache_entry_t entry, *entrytail;
unsigned int lineno;
gpg_error_t err = 0;
int anyerr = 0;
cache = xtrycalloc (1, sizeof *cache);
if (!cache)
return gpg_error_from_syserror ();
fname = make_filename (opt.homedir_cache, DBDIR_D, DBDIRFILE, NULL);
lineno = 0;
fp = open_dir_file (fname);
if (!fp)
{
err = gpg_error (GPG_ERR_CONFIGURATION);
goto leave;
}
err = check_dir_version (&fp, fname, &lineno, 1);
if (err)
goto leave;
/* Read in all supported entries from the dir file. */
cache->entries = NULL;
entrytail = &cache->entries;
xfree (line);
while ((line = next_line_from_file (fp, &lineerr)))
{
int fieldno;
char *p, *endp;
lineno++;
if ( *line == 'c' || *line == 'u' || *line == 'i' )
{
entry = xtrycalloc (1, sizeof *entry);
if (!entry)
{
err = gpg_error_from_syserror ();
goto leave;
}
entry->lineno = lineno;
entry->release_ptr = line;
if (*line == 'i')
{
entry->invalid = atoi (line+1);
if (entry->invalid < 1)
entry->invalid = 1;
}
else if (*line == 'u')
entry->user_trust_req = 1;
for (fieldno=1, p = line; p; p = endp, fieldno++)
{
endp = strchr (p, ':');
if (endp)
*endp++ = '\0';
switch (fieldno)
{
case 1: /* record type */ break;
case 2: entry->issuer_hash = p; break;
case 3: entry->issuer = unpercent_string (p); break;
case 4: entry->url = unpercent_string (p); break;
case 5:
strncpy (entry->this_update, p, 15);
entry->this_update[15] = 0;
break;
case 6:
strncpy (entry->next_update, p, 15);
entry->next_update[15] = 0;
break;
case 7: entry->dbfile_hash = p; break;
case 8: if (*p) entry->crl_number = p; break;
case 9:
if (*p)
entry->authority_issuer = unpercent_string (p);
break;
case 10:
if (*p)
entry->authority_serialno = unpercent_string (p);
break;
case 11:
if (*p)
entry->check_trust_anchor = xtrystrdup (p);
break;
default:
if (*p)
log_info (_("extra field detected in crl record of "
"'%s' line %u\n"), fname, lineno);
break;
}
}
if (!entry->issuer_hash)
{
log_info (_("invalid line detected in '%s' line %u\n"),
fname, lineno);
xfree (entry);
entry = NULL;
}
else if (find_entry (cache->entries, entry->issuer_hash))
{
/* Fixme: The duplicate checking used is not very
effective for large numbers of issuers. */
log_info (_("duplicate entry detected in '%s' line %u\n"),
fname, lineno);
xfree (entry);
entry = NULL;
}
else
{
line = NULL;
*entrytail = entry;
entrytail = &entry->next;
}
}
else if (*line == '#')
;
else
log_info (_("unsupported record type in '%s' line %u skipped\n"),
fname, lineno);
if (line)
xfree (line);
}
if (lineerr)
{
err = lineerr;
log_error (_("error reading '%s': %s\n"), fname, gpg_strerror (err));
goto leave;
}
if (es_ferror (fp))
{
log_error (_("error reading '%s': %s\n"), fname, strerror (errno));
err = gpg_error (GPG_ERR_CONFIGURATION);
goto leave;
}
/* Now do some basic checks on the data. */
for (entry = cache->entries; entry; entry = entry->next)
{
assert (entry->lineno);
if (strlen (entry->issuer_hash) != 40)
{
anyerr++;
log_error (_("invalid issuer hash in '%s' line %u\n"),
fname, entry->lineno);
}
else if ( !*entry->issuer )
{
anyerr++;
log_error (_("no issuer DN in '%s' line %u\n"),
fname, entry->lineno);
}
else if ( check_isotime (entry->this_update)
|| check_isotime (entry->next_update))
{
anyerr++;
log_error (_("invalid timestamp in '%s' line %u\n"),
fname, entry->lineno);
}
/* Checks not leading to an immediate fail. */
if (strlen (entry->dbfile_hash) != 32)
log_info (_("WARNING: invalid cache file hash in '%s' line %u\n"),
fname, entry->lineno);
}
if (anyerr)
{
log_error (_("detected errors in cache dir file\n"));
log_info (_("please check the reason and manually delete that file\n"));
err = gpg_error (GPG_ERR_CONFIGURATION);
}
leave:
es_fclose (fp);
xfree (line);
xfree (fname);
if (err)
{
release_cache (cache);
cache = NULL;
}
*r_cache = cache;
return err;
}
static void
write_percented_string (const char *s, estream_t fp)
{
for (; *s; s++)
if (*s == ':')
es_fputs ("%3A", fp);
else if (*s == '\n')
es_fputs ("%0A", fp);
else if (*s == '\r')
es_fputs ("%0D", fp);
else
es_putc (*s, fp);
}
static void
write_dir_line_crl (estream_t fp, crl_cache_entry_t e)
{
if (e->invalid)
es_fprintf (fp, "i%d", e->invalid);
else if (e->user_trust_req)
es_putc ('u', fp);
else
es_putc ('c', fp);
es_putc (':', fp);
es_fputs (e->issuer_hash, fp);
es_putc (':', fp);
write_percented_string (e->issuer, fp);
es_putc (':', fp);
write_percented_string (e->url, fp);
es_putc (':', fp);
es_fwrite (e->this_update, 15, 1, fp);
es_putc (':', fp);
es_fwrite (e->next_update, 15, 1, fp);
es_putc (':', fp);
es_fputs (e->dbfile_hash, fp);
es_putc (':', fp);
if (e->crl_number)
es_fputs (e->crl_number, fp);
es_putc (':', fp);
if (e->authority_issuer)
write_percented_string (e->authority_issuer, fp);
es_putc (':', fp);
if (e->authority_serialno)
es_fputs (e->authority_serialno, fp);
es_putc (':', fp);
if (e->check_trust_anchor && e->user_trust_req)
es_fputs (e->check_trust_anchor, fp);
es_putc ('\n', fp);
}
/* Update the current dir file using the cache. */
static gpg_error_t
update_dir (crl_cache_t cache)
{
char *fname = NULL;
char *tmpfname = NULL;
char *line = NULL;
gpg_error_t lineerr = 0;
estream_t fp;
estream_t fpout = NULL;
crl_cache_entry_t e;
unsigned int lineno;
gpg_error_t err = 0;
fname = make_filename (opt.homedir_cache, DBDIR_D, DBDIRFILE, NULL);
/* Fixme: Take an update file lock here. */
for (e= cache->entries; e; e = e->next)
e->mark = 1;
lineno = 0;
fp = es_fopen (fname, "r");
if (!fp)
{
err = gpg_error_from_errno (errno);
log_error (_("failed to open cache dir file '%s': %s\n"),
fname, strerror (errno));
goto leave;
}
err = check_dir_version (&fp, fname, &lineno, 0);
if (err)
goto leave;
es_rewind (fp);
lineno = 0;
/* Create a temporary DIR file. */
{
char *tmpbuf, *p;
const char *nodename;
#ifndef HAVE_W32_SYSTEM
struct utsname utsbuf;
#endif
#ifdef HAVE_W32_SYSTEM
nodename = "unknown";
#else
if (uname (&utsbuf))
nodename = "unknown";
else
nodename = utsbuf.nodename;
#endif
gpgrt_asprintf (&tmpbuf, "DIR-tmp-%s-%u-%p.txt.tmp",
nodename, (unsigned int)getpid (), &tmpbuf);
if (!tmpbuf)
{
err = gpg_error_from_errno (errno);
log_error (_("failed to create temporary cache dir file '%s': %s\n"),
tmpfname, strerror (errno));
goto leave;
}
for (p=tmpbuf; *p; p++)
if (*p == '/')
*p = '.';
tmpfname = make_filename (opt.homedir_cache, DBDIR_D, tmpbuf, NULL);
xfree (tmpbuf);
}
fpout = es_fopen (tmpfname, "w");
if (!fpout)
{
err = gpg_error_from_errno (errno);
log_error (_("failed to create temporary cache dir file '%s': %s\n"),
tmpfname, strerror (errno));
goto leave;
}
while ((line = next_line_from_file (fp, &lineerr)))
{
lineno++;
if (*line == 'c' || *line == 'u' || *line == 'i')
{
/* Extract the issuer hash field. */
char *fieldp, *endp;
fieldp = strchr (line, ':');
endp = fieldp? strchr (++fieldp, ':') : NULL;
if (endp)
{
/* There should be no percent within the issuer hash
field, thus we can compare it pretty easily. */
*endp = 0;
e = find_entry ( cache->entries, fieldp);
*endp = ':'; /* Restore original line. */
if (e && e->deleted)
{
/* Marked for deletion, so don't write it. */
e->mark = 0;
}
else if (e)
{
/* Yep, this is valid entry we know about; write it out */
write_dir_line_crl (fpout, e);
e->mark = 0;
}
else
{ /* We ignore entries we don't have in our cache
because they may have been added in the meantime
by other instances of dirmngr. */
es_fprintf (fpout, "# Next line added by "
"another process; our pid is %lu\n",
(unsigned long)getpid ());
es_fputs (line, fpout);
es_putc ('\n', fpout);
}
}
else
{
es_fputs ("# Invalid line detected: ", fpout);
es_fputs (line, fpout);
es_putc ('\n', fpout);
}
}
else
{
/* Write out all non CRL lines as they are. */
es_fputs (line, fpout);
es_putc ('\n', fpout);
}
xfree (line);
}
if (!es_ferror (fp) && !es_ferror (fpout) && !lineerr)
{
/* Write out the remaining entries. */
for (e= cache->entries; e; e = e->next)
if (e->mark)
{
if (!e->deleted)
write_dir_line_crl (fpout, e);
e->mark = 0;
}
}
if (lineerr)
{
err = lineerr;
log_error (_("error reading '%s': %s\n"), fname, gpg_strerror (err));
goto leave;
}
if (es_ferror (fp))
{
err = gpg_error_from_errno (errno);
log_error (_("error reading '%s': %s\n"), fname, strerror (errno));
}
if (es_ferror (fpout))
{
err = gpg_error_from_errno (errno);
log_error (_("error writing '%s': %s\n"), tmpfname, strerror (errno));
}
if (err)
goto leave;
/* Rename the files. */
es_fclose (fp);
fp = NULL;
if (es_fclose (fpout))
{
err = gpg_error_from_errno (errno);
log_error (_("error closing '%s': %s\n"), tmpfname, strerror (errno));
goto leave;
}
fpout = NULL;
#ifdef HAVE_W32_SYSTEM
/* No atomic mv on W32 systems. */
gnupg_remove (fname);
#endif
if (rename (tmpfname, fname))
{
err = gpg_error_from_errno (errno);
log_error (_("error renaming '%s' to '%s': %s\n"),
tmpfname, fname, strerror (errno));
goto leave;
}
leave:
/* Fixme: Relinquish update lock. */
xfree (line);
es_fclose (fp);
xfree (fname);
if (fpout)
{
es_fclose (fpout);
if (err && tmpfname)
gnupg_remove (tmpfname);
}
xfree (tmpfname);
return err;
}
/* Create the filename for the cache file from the 40 byte ISSUER_HASH
string. Caller must release the return string. */
static char *
make_db_file_name (const char *issuer_hash)
{
char bname[50];
assert (strlen (issuer_hash) == 40);
memcpy (bname, "crl-", 4);
memcpy (bname + 4, issuer_hash, 40);
strcpy (bname + 44, ".db");
return make_filename (opt.homedir_cache, DBDIR_D, bname, NULL);
}
/* Hash the file FNAME and return the MD5 digest in MD5BUFFER. The
caller must allocate MD%buffer wityh at least 16 bytes. Returns 0
on success. */
static int
hash_dbfile (const char *fname, unsigned char *md5buffer)
{
estream_t fp;
char *buffer;
size_t n;
gcry_md_hd_t md5;
gpg_error_t err;
buffer = xtrymalloc (65536);
fp = buffer? es_fopen (fname, "rb") : NULL;
if (!fp)
{
log_error (_("can't hash '%s': %s\n"), fname, strerror (errno));
xfree (buffer);
return -1;
}
err = gcry_md_open (&md5, GCRY_MD_MD5, 0);
if (err)
{
log_error (_("error setting up MD5 hash context: %s\n"),
gpg_strerror (err));
xfree (buffer);
es_fclose (fp);
return -1;
}
/* We better hash some information about the cache file layout in. */
sprintf (buffer, "%.100s/%.100s:%d", DBDIR_D, DBDIRFILE, DBDIRVERSION);
gcry_md_write (md5, buffer, strlen (buffer));
for (;;)
{
n = es_fread (buffer, 1, 65536, fp);
if (n < 65536 && es_ferror (fp))
{
log_error (_("error hashing '%s': %s\n"), fname, strerror (errno));
xfree (buffer);
es_fclose (fp);
gcry_md_close (md5);
return -1;
}
if (!n)
break;
gcry_md_write (md5, buffer, n);
}
es_fclose (fp);
xfree (buffer);
gcry_md_final (md5);
memcpy (md5buffer, gcry_md_read (md5, GCRY_MD_MD5), 16);
gcry_md_close (md5);
return 0;
}
/* Compare the file FNAME against the dexified MD5 hash MD5HASH and
return 0 if they match. */
static int
check_dbfile (const char *fname, const char *md5hexvalue)
{
unsigned char buffer1[16], buffer2[16];
if (strlen (md5hexvalue) != 32)
{
log_error (_("invalid formatted checksum for '%s'\n"), fname);
return -1;
}
unhexify (buffer1, md5hexvalue);
if (hash_dbfile (fname, buffer2))
return -1;
return memcmp (buffer1, buffer2, 16);
}
/* Open the cache file for ENTRY. This function implements a caching
strategy and might close unused cache files. It is required to use
unlock_db_file after using the file. */
static struct cdb *
lock_db_file (crl_cache_t cache, crl_cache_entry_t entry)
{
char *fname;
int fd;
int open_count;
crl_cache_entry_t e;
if (entry->cdb)
{
entry->cdb_use_count++;
return entry->cdb;
}
for (open_count = 0, e = cache->entries; e; e = e->next)
{
if (e->cdb)
open_count++;
/* log_debug ("CACHE: cdb=%p use_count=%u lru_count=%u\n", */
/* e->cdb,e->cdb_use_count,e->cdb_lru_count); */
}
/* If there are too many file open, find the least recent used DB
file and close it. Note that for Pth thread safeness we need to
use a loop here. */
while (open_count >= MAX_OPEN_DB_FILES )
{
crl_cache_entry_t last_e = NULL;
unsigned int last_lru = (unsigned int)(-1);
for (e = cache->entries; e; e = e->next)
if (e->cdb && !e->cdb_use_count && e->cdb_lru_count < last_lru)
{
last_lru = e->cdb_lru_count;
last_e = e;
}
if (!last_e)
{
log_error (_("too many open cache files; can't open anymore\n"));
return NULL;
}
/* log_debug ("CACHE: closing file at cdb=%p\n", last_e->cdb); */
fd = cdb_fileno (last_e->cdb);
cdb_free (last_e->cdb);
xfree (last_e->cdb);
last_e->cdb = NULL;
if (close (fd))
log_error (_("error closing cache file: %s\n"), strerror(errno));
open_count--;
}
fname = make_db_file_name (entry->issuer_hash);
if (opt.verbose)
log_info (_("opening cache file '%s'\n"), fname );
if (!entry->dbfile_checked)
{
if (!check_dbfile (fname, entry->dbfile_hash))
entry->dbfile_checked = 1;
/* Note, in case of an error we don't print an error here but
let require the caller to do that check. */
}
entry->cdb = xtrycalloc (1, sizeof *entry->cdb);
if (!entry->cdb)
{
xfree (fname);
return NULL;
}
fd = gnupg_open (fname, O_RDONLY | O_BINARY, 0);
if (fd == -1)
{
log_error (_("error opening cache file '%s': %s\n"),
fname, strerror (errno));
xfree (entry->cdb);
entry->cdb = NULL;
xfree (fname);
return NULL;
}
if (cdb_init (entry->cdb, fd))
{
log_error (_("error initializing cache file '%s' for reading: %s\n"),
fname, strerror (errno));
xfree (entry->cdb);
entry->cdb = NULL;
close (fd);
xfree (fname);
return NULL;
}
xfree (fname);
entry->cdb_use_count = 1;
entry->cdb_lru_count = 0;
return entry->cdb;
}
/* Unlock a cache file, so that it can be reused. */
static void
unlock_db_file (crl_cache_t cache, crl_cache_entry_t entry)
{
if (!entry->cdb)
log_error (_("calling unlock_db_file on a closed file\n"));
else if (!entry->cdb_use_count)
log_error (_("calling unlock_db_file on an unlocked file\n"));
else
{
entry->cdb_use_count--;
entry->cdb_lru_count++;
}
/* If the entry was marked for deletion in the meantime do it now.
We do this for the sake of Pth thread safeness. */
if (!entry->cdb_use_count && entry->deleted)
{
crl_cache_entry_t eprev, enext;
enext = entry->next;
for (eprev = cache->entries;
eprev && eprev->next != entry; eprev = eprev->next)
;
assert (eprev);
if (eprev == cache->entries)
cache->entries = enext;
else
eprev->next = enext;
/* FIXME: Do we leak ENTRY? */
}
}
/* Find ISSUER_HASH in our cache FIRST. This may be used to enumerate
the linked list we use to keep the CRLs of an issuer. */
static crl_cache_entry_t
find_entry (crl_cache_entry_t first, const char *issuer_hash)
{
while (first && (first->deleted || strcmp (issuer_hash, first->issuer_hash)))
first = first->next;
return first;
}
/* Create a new CRL cache. This function is usually called only once.
never fail. */
void
crl_cache_init(void)
{
crl_cache_t cache = NULL;
gpg_error_t err;
if (current_cache)
{
log_error ("crl cache has already been initialized - not doing twice\n");
return;
}
err = open_dir (&cache);
if (err)
log_fatal (_("failed to create a new cache object: %s\n"),
gpg_strerror (err));
current_cache = cache;
}
/* Remove the cache information and all its resources. Note that we
still keep the cache on disk. */
void
crl_cache_deinit (void)
{
if (current_cache)
{
release_cache (current_cache);
current_cache = NULL;
}
}
/* Delete the cache from disk and memory. Return 0 on success.*/
int
crl_cache_flush (void)
{
int rc;
crl_cache_deinit ();
rc = cleanup_cache_dir (0)? -1 : 0;
crl_cache_init ();
return rc;
}
/* Check whether the certificate identified by ISSUER_HASH and
SN/SNLEN is valid; i.e. not listed in our cache. With
FORCE_REFRESH set to true, a new CRL will be retrieved even if the
cache has not yet expired. We use a 30 minutes threshold here so
that invoking this function several times won't load the CRL over
and over. */
static crl_cache_result_t
cache_isvalid (ctrl_t ctrl, const char *issuer_hash,
const unsigned char *sn, size_t snlen,
int force_refresh)
{
crl_cache_t cache = get_current_cache ();
crl_cache_result_t retval;
struct cdb *cdb;
int rc;
crl_cache_entry_t entry;
gnupg_isotime_t current_time;
size_t n;
(void)ctrl;
entry = find_entry (cache->entries, issuer_hash);
if (!entry)
{
log_info (_("no CRL available for issuer id %s\n"), issuer_hash );
return CRL_CACHE_DONTKNOW;
}
gnupg_get_isotime (current_time);
if (strcmp (entry->next_update, current_time) < 0 )
{
log_info (_("cached CRL for issuer id %s too old; update required\n"),
issuer_hash);
return CRL_CACHE_DONTKNOW;
}
if (force_refresh)
{
gnupg_isotime_t tmptime;
if (*entry->last_refresh)
{
gnupg_copy_time (tmptime, entry->last_refresh);
add_seconds_to_isotime (tmptime, 30 * 60);
if (strcmp (tmptime, current_time) < 0 )
{
log_info (_("force-crl-refresh active and %d minutes passed for"
" issuer id %s; update required\n"),
30, issuer_hash);
return CRL_CACHE_DONTKNOW;
}
}
else
{
log_info (_("force-crl-refresh active for"
" issuer id %s; update required\n"),
issuer_hash);
return CRL_CACHE_DONTKNOW;
}
}
if (entry->invalid)
{
log_info (_("available CRL for issuer ID %s can't be used\n"),
issuer_hash);
return CRL_CACHE_CANTUSE;
}
cdb = lock_db_file (cache, entry);
if (!cdb)
return CRL_CACHE_DONTKNOW; /* Hmmm, not the best error code. */
if (!entry->dbfile_checked)
{
log_error (_("cached CRL for issuer id %s tampered; we need to update\n")
, issuer_hash);
unlock_db_file (cache, entry);
return CRL_CACHE_DONTKNOW;
}
rc = cdb_find (cdb, sn, snlen);
if (rc == 1)
{
n = cdb_datalen (cdb);
if (n != 16)
{
log_error (_("WARNING: invalid cache record length for S/N "));
log_printf ("0x");
log_printhex (sn, snlen, "");
}
else if (opt.verbose)
{
unsigned char record[16];
char *tmp = hexify_data (sn, snlen, 1);
if (cdb_read (cdb, record, n, cdb_datapos (cdb)))
log_error (_("problem reading cache record for S/N %s: %s\n"),
tmp, strerror (errno));
else
log_info (_("S/N %s is not valid; reason=%02X date=%.15s\n"),
tmp, *record, record+1);
xfree (tmp);
}
retval = CRL_CACHE_INVALID;
}
else if (!rc)
{
if (opt.verbose)
{
char *serialno = hexify_data (sn, snlen, 1);
log_info (_("S/N %s is valid, it is not listed in the CRL\n"),
serialno );
xfree (serialno);
}
retval = CRL_CACHE_VALID;
}
else
{
log_error (_("error getting data from cache file: %s\n"),
strerror (errno));
retval = CRL_CACHE_DONTKNOW;
}
if (entry->user_trust_req
&& (retval == CRL_CACHE_VALID || retval == CRL_CACHE_INVALID))
{
if (!entry->check_trust_anchor)
{
log_error ("inconsistent data on user trust check\n");
retval = CRL_CACHE_CANTUSE;
}
else if (get_istrusted_from_client (ctrl, entry->check_trust_anchor))
{
if (opt.verbose)
log_info ("no system trust and client does not trust either\n");
retval = CRL_CACHE_CANTUSE;
}
else
{
/* Okay, the CRL is considered valid by the client and thus
we can return the result as is. */
}
}
unlock_db_file (cache, entry);
return retval;
}
/* Check whether the certificate identified by ISSUER_HASH and
SERIALNO is valid; i.e. not listed in our cache. With
FORCE_REFRESH set to true, a new CRL will be retrieved even if the
cache has not yet expired. We use a 30 minutes threshold here so
that invoking this function several times won't load the CRL over
and over. */
crl_cache_result_t
crl_cache_isvalid (ctrl_t ctrl, const char *issuer_hash, const char *serialno,
int force_refresh)
{
crl_cache_result_t result;
unsigned char snbuf_buffer[50];
unsigned char *snbuf;
size_t n;
n = strlen (serialno)/2+1;
if (n < sizeof snbuf_buffer - 1)
snbuf = snbuf_buffer;
else
{
snbuf = xtrymalloc (n);
if (!snbuf)
return CRL_CACHE_DONTKNOW;
}
n = unhexify (snbuf, serialno);
result = cache_isvalid (ctrl, issuer_hash, snbuf, n, force_refresh);
if (snbuf != snbuf_buffer)
xfree (snbuf);
return result;
}
/* Check whether the certificate CERT is valid; i.e. not listed in our
cache. With FORCE_REFRESH set to true, a new CRL will be retrieved
even if the cache has not yet expired. We use a 30 minutes
threshold here so that invoking this function several times won't
load the CRL over and over. */
gpg_error_t
crl_cache_cert_isvalid (ctrl_t ctrl, ksba_cert_t cert,
int force_refresh)
{
gpg_error_t err;
crl_cache_result_t result;
unsigned char issuerhash[20];
char issuerhash_hex[41];
ksba_sexp_t serial;
unsigned char *sn;
size_t snlen;
char *endp, *tmp;
int i;
/* Compute the hash value of the issuer name. */
tmp = ksba_cert_get_issuer (cert, 0);
if (!tmp)
{
log_error ("oops: issuer missing in certificate\n");
return gpg_error (GPG_ERR_INV_CERT_OBJ);
}
gcry_md_hash_buffer (GCRY_MD_SHA1, issuerhash, tmp, strlen (tmp));
xfree (tmp);
for (i=0,tmp=issuerhash_hex; i < 20; i++, tmp += 2)
sprintf (tmp, "%02X", issuerhash[i]);
/* Get the serial number. */
serial = ksba_cert_get_serial (cert);
if (!serial)
{
log_error ("oops: S/N missing in certificate\n");
return gpg_error (GPG_ERR_INV_CERT_OBJ);
}
sn = serial;
if (*sn != '(')
{
log_error ("oops: invalid S/N\n");
xfree (serial);
return gpg_error (GPG_ERR_INV_CERT_OBJ);
}
sn++;
snlen = strtoul (sn, &endp, 10);
sn = endp;
if (*sn != ':')
{
log_error ("oops: invalid S/N\n");
xfree (serial);
return gpg_error (GPG_ERR_INV_CERT_OBJ);
}
sn++;
/* Check the cache. */
result = cache_isvalid (ctrl, issuerhash_hex, sn, snlen, force_refresh);
switch (result)
{
case CRL_CACHE_VALID:
err = 0;
break;
case CRL_CACHE_INVALID:
err = gpg_error (GPG_ERR_CERT_REVOKED);
break;
case CRL_CACHE_DONTKNOW:
err = gpg_error (GPG_ERR_NO_CRL_KNOWN);
break;
case CRL_CACHE_CANTUSE:
err = gpg_error (GPG_ERR_NO_CRL_KNOWN);
break;
default:
log_fatal ("cache_isvalid returned invalid status code %d\n", result);
}
xfree (serial);
return err;
}
/* Return the hash algorithm's algo id from its name given in the
* non-null termnated string in (buffer,buflen). Returns 0 on failure
* or if the algo is not known. */
static int
hash_algo_from_buffer (const void *buffer, size_t buflen)
{
char *string;
int algo;
string = xtrymalloc (buflen + 1);
if (!string)
{
log_error (_("out of core\n"));
return 0;
}
memcpy (string, buffer, buflen);
string[buflen] = 0;
algo = gcry_md_map_name (string);
if (!algo)
log_error ("unknown digest algorithm '%s' used in certificate\n", string);
xfree (string);
return algo;
}
/* Return an unsigned integer from the non-null termnated string
* (buffer,buflen). Returns 0 on failure. */
static unsigned int
uint_from_buffer (const void *buffer, size_t buflen)
{
char *string;
unsigned int val;
string = xtrymalloc (buflen + 1);
if (!string)
{
log_error (_("out of core\n"));
return 0;
}
memcpy (string, buffer, buflen);
string[buflen] = 0;
val = strtoul (string, NULL, 10);
xfree (string);
return val;
}
/* Prepare a hash context for the signature verification. Input is
the CRL and the output is the hash context MD as well as the uses
algorithm identifier ALGO. */
static gpg_error_t
start_sig_check (ksba_crl_t crl, gcry_md_hd_t *md, int *algo, int *use_pss)
{
gpg_error_t err;
const char *algoid;
*use_pss = 0;
algoid = ksba_crl_get_digest_algo (crl);
if (algoid && !strcmp (algoid, "1.2.840.113549.1.1.10"))
{
/* Parse rsaPSS parameter. */
gcry_buffer_t ioarray[1] = { {0} };
ksba_sexp_t pssparam;
size_t n;
gcry_sexp_t psssexp;
pssparam = ksba_crl_get_sig_val (crl);
n = gcry_sexp_canon_len (pssparam, 0, NULL, NULL);
if (!n)
{
ksba_free (pssparam);
log_error (_("got an invalid S-expression from libksba\n"));
return gpg_error (GPG_ERR_INV_SEXP);
}
err = gcry_sexp_sscan (&psssexp, NULL, pssparam, n);
ksba_free (pssparam);
if (err)
{
log_error (_("converting S-expression failed: %s\n"),
gcry_strerror (err));
return err;
}
err = gcry_sexp_extract_param (psssexp, "sig-val",
"&'hash-algo'", ioarray, NULL);
gcry_sexp_release (psssexp);
if (err)
{
log_error ("extracting params from PSS failed: %s\n",
gpg_strerror (err));
return err;
}
*algo = hash_algo_from_buffer (ioarray[0].data, ioarray[0].len);
xfree (ioarray[0].data);
*use_pss = 1;
}
else
*algo = gcry_md_map_name (algoid);
if (!*algo)
{
log_error (_("unknown hash algorithm '%s'\n"), algoid? algoid:"?");
return gpg_error (GPG_ERR_DIGEST_ALGO);
}
err = gcry_md_open (md, *algo, 0);
if (err)
{
log_error (_("gcry_md_open for algorithm %d failed: %s\n"),
*algo, gcry_strerror (err));
return err;
}
if (DBG_HASHING)
gcry_md_debug (*md, "hash.cert");
ksba_crl_set_hash_function (crl, HASH_FNC, *md);
return 0;
}
/* Finish a hash context and verify the signature. This function
should return 0 on a good signature, GPG_ERR_BAD_SIGNATURE if the
signature does not verify or any other error code. CRL is the CRL
object we are working on, MD the hash context and ISSUER_CERT the
certificate of the CRL issuer. This function takes ownership of MD. */
static gpg_error_t
finish_sig_check (ksba_crl_t crl, gcry_md_hd_t md, int algo,
ksba_cert_t issuer_cert, int use_pss)
{
gpg_error_t err;
ksba_sexp_t sigval = NULL, pubkey = NULL;
size_t n;
gcry_sexp_t s_sig = NULL, s_hash = NULL, s_pkey = NULL;
unsigned int saltlen = 0; /* (used only with use_pss) */
/* This also stops debugging on the MD. */
gcry_md_final (md);
/* Get and convert the signature value. */
sigval = ksba_crl_get_sig_val (crl);
n = gcry_sexp_canon_len (sigval, 0, NULL, NULL);
if (!n)
{
log_error (_("got an invalid S-expression from libksba\n"));
err = gpg_error (GPG_ERR_INV_SEXP);
goto leave;
}
err = gcry_sexp_sscan (&s_sig, NULL, sigval, n);
if (err)
{
log_error (_("converting S-expression failed: %s\n"),
gcry_strerror (err));
goto leave;
}
if (use_pss)
{
/* Parse rsaPSS parameter which we should find in S_SIG. */
gcry_buffer_t ioarray[2] = { {0}, {0} };
ksba_sexp_t pssparam;
gcry_sexp_t psssexp;
int hashalgo;
pssparam = ksba_crl_get_sig_val (crl);
n = gcry_sexp_canon_len (pssparam, 0, NULL, NULL);
if (!n)
{
ksba_free (pssparam);
log_error (_("got an invalid S-expression from libksba\n"));
err = gpg_error (GPG_ERR_INV_SEXP);
goto leave;
}
err = gcry_sexp_sscan (&psssexp, NULL, pssparam, n);
ksba_free (pssparam);
if (err)
{
log_error (_("converting S-expression failed: %s\n"),
gcry_strerror (err));
goto leave;
}
err = gcry_sexp_extract_param (psssexp, "sig-val",
"&'hash-algo''salt-length'",
ioarray+0, ioarray+1, NULL);
gcry_sexp_release (psssexp);
if (err)
{
log_error ("extracting params from PSS failed: %s\n",
gpg_strerror (err));
goto leave;
}
hashalgo = hash_algo_from_buffer (ioarray[0].data, ioarray[0].len);
saltlen = uint_from_buffer (ioarray[1].data, ioarray[1].len);
xfree (ioarray[0].data);
xfree (ioarray[1].data);
if (hashalgo != algo)
{
log_error ("hash algo mismatch: %d announced but %d used\n",
algo, hashalgo);
return gpg_error (GPG_ERR_INV_CRL);
}
/* Add some restrictions; see ../sm/certcheck.c for details. */
switch (algo)
{
case GCRY_MD_SHA1:
case GCRY_MD_SHA256:
case GCRY_MD_SHA384:
case GCRY_MD_SHA512:
case GCRY_MD_SHA3_256:
case GCRY_MD_SHA3_384:
case GCRY_MD_SHA3_512:
break;
default:
log_error ("PSS hash algorithm '%s' rejected\n",
gcry_md_algo_name (algo));
return gpg_error (GPG_ERR_DIGEST_ALGO);
}
if (gcry_md_get_algo_dlen (algo) != saltlen)
{
log_error ("PSS hash algorithm '%s' rejected due to salt length %u\n",
gcry_md_algo_name (algo), saltlen);
return gpg_error (GPG_ERR_DIGEST_ALGO);
}
}
/* Get and convert the public key for the issuer certificate. */
if (DBG_X509)
dump_cert ("crl_issuer_cert", issuer_cert);
pubkey = ksba_cert_get_public_key (issuer_cert);
n = gcry_sexp_canon_len (pubkey, 0, NULL, NULL);
if (!n)
{
log_error (_("got an invalid S-expression from libksba\n"));
err = gpg_error (GPG_ERR_INV_SEXP);
goto leave;
}
err = gcry_sexp_sscan (&s_pkey, NULL, pubkey, n);
if (err)
{
log_error (_("converting S-expression failed: %s\n"),
gcry_strerror (err));
goto leave;
}
/* Create an S-expression with the actual hash value. */
if (use_pss)
{
err = gcry_sexp_build (&s_hash, NULL,
"(data (flags pss)"
"(hash %s %b)"
"(salt-length %u))",
hash_algo_to_string (algo),
(int)gcry_md_get_algo_dlen (algo),
gcry_md_read (md, algo),
saltlen);
}
else
{
err = gcry_sexp_build (&s_hash, NULL,
"(data(flags pkcs1)(hash %s %b))",
hash_algo_to_string (algo),
(int)gcry_md_get_algo_dlen (algo),
gcry_md_read (md, algo));
}
if (err)
{
log_error (_("creating S-expression failed: %s\n"), gcry_strerror (err));
goto leave;
}
/* Pass this on to the signature verification. */
err = gcry_pk_verify (s_sig, s_hash, s_pkey);
if (DBG_X509)
log_debug ("gcry_pk_verify: %s\n", gpg_strerror (err));
leave:
xfree (sigval);
xfree (pubkey);
gcry_sexp_release (s_sig);
gcry_sexp_release (s_hash);
gcry_sexp_release (s_pkey);
gcry_md_close (md);
return err;
}
/* Call this to match a start_sig_check that can not be completed
normally. Takes ownership of MD if MD is not NULL. */
static void
abort_sig_check (ksba_crl_t crl, gcry_md_hd_t md)
{
(void)crl;
if (md)
gcry_md_close (md);
}
/* Workhorse of the CRL loading machinery. The CRL is read using the
CRL object and stored in the data base file DB with the name FNAME
(only used for printing error messages). That DB should be a
temporary one and not the actual one. If the function fails the
caller should delete this temporary database file. CTRL is
required to retrieve certificates using the general dirmngr
callback service. R_CRLISSUER returns an allocated string with the
crl-issuer DN, THIS_UPDATE and NEXT_UPDATE are filled with the
corresponding data from the CRL. Note that these values might get
set even if the CRL processing fails at a later step; thus the
caller should free *R_ISSUER even if the function returns with an
error. R_TRUST_ANCHOR is set on exit to NULL or a string with the
hexified fingerprint of the root certificate, if checking this
certificate for trustiness is required.
*/
static int
crl_parse_insert (ctrl_t ctrl, ksba_crl_t crl,
struct cdb_make *cdb, const char *fname,
char **r_crlissuer,
ksba_isotime_t thisupdate, ksba_isotime_t nextupdate,
char **r_trust_anchor)
{
gpg_error_t err;
ksba_stop_reason_t stopreason;
ksba_cert_t crlissuer_cert = NULL;
gcry_md_hd_t md = NULL;
int algo = 0;
int use_pss = 0;
size_t n;
(void)fname;
*r_crlissuer = NULL;
*thisupdate = *nextupdate = 0;
*r_trust_anchor = NULL;
/* Start of the KSBA parser loop. */
do
{
err = ksba_crl_parse (crl, &stopreason);
if (err)
{
log_error (_("ksba_crl_parse failed: %s\n"), gpg_strerror (err) );
goto failure;
}
switch (stopreason)
{
case KSBA_SR_BEGIN_ITEMS:
{
err = start_sig_check (crl, &md, &algo, &use_pss);
if (err)
goto failure;
err = ksba_crl_get_update_times (crl, thisupdate, nextupdate);
if (err)
{
log_error (_("error getting update times of CRL: %s\n"),
gpg_strerror (err));
err = gpg_error (GPG_ERR_INV_CRL);
goto failure;
}
if (opt.verbose || !*nextupdate)
log_info (_("update times of this CRL: this=%s next=%s\n"),
thisupdate, nextupdate);
if (!*nextupdate)
{
log_info (_("nextUpdate not given; "
"assuming a validity period of one day\n"));
gnupg_copy_time (nextupdate, thisupdate);
add_seconds_to_isotime (nextupdate, 86400);
}
}
break;
case KSBA_SR_GOT_ITEM:
{
ksba_sexp_t serial;
const unsigned char *p;
ksba_isotime_t rdate;
ksba_crl_reason_t reason;
int rc;
unsigned char record[1+15];
err = ksba_crl_get_item (crl, &serial, rdate, &reason);
if (err)
{
log_error (_("error getting CRL item: %s\n"),
gpg_strerror (err));
err = gpg_error (GPG_ERR_INV_CRL);
ksba_free (serial);
goto failure;
}
p = serial_to_buffer (serial, &n);
if (!p)
BUG ();
record[0] = (reason & 0xff);
memcpy (record+1, rdate, 15);
rc = cdb_make_add (cdb, p, n, record, 1+15);
if (rc)
{
err = gpg_error_from_errno (errno);
log_error (_("error inserting item into "
"temporary cache file: %s\n"),
strerror (errno));
goto failure;
}
ksba_free (serial);
}
break;
case KSBA_SR_END_ITEMS:
break;
case KSBA_SR_READY:
{
char *crlissuer;
ksba_name_t authid;
ksba_sexp_t authidsn;
ksba_sexp_t keyid;
/* We need to look for the issuer only after having read
all items. The issuer itselfs comes before the items
but the optional authorityKeyIdentifier comes after the
items. */
err = ksba_crl_get_issuer (crl, &crlissuer);
if( err )
{
log_error (_("no CRL issuer found in CRL: %s\n"),
gpg_strerror (err) );
err = gpg_error (GPG_ERR_INV_CRL);
goto failure;
}
/* Note: This should be released by ksba_free, not xfree.
May need a memory reallocation dance. */
*r_crlissuer = crlissuer; /* (Do it here so we don't need
to free it later) */
if (!ksba_crl_get_auth_key_id (crl, &keyid, &authid, &authidsn))
{
const char *s;
if (opt.verbose)
log_info (_("locating CRL issuer certificate by "
"authorityKeyIdentifier\n"));
s = ksba_name_enum (authid, 0);
if (s && *authidsn)
crlissuer_cert = find_cert_bysn (ctrl, s, authidsn);
if (!crlissuer_cert && keyid)
crlissuer_cert = find_cert_bysubject (ctrl,
crlissuer, keyid);
if (!crlissuer_cert)
{
log_info ("CRL issuer certificate ");
if (keyid)
{
log_printf ("{");
dump_serial (keyid);
log_printf ("} ");
}
if (authidsn)
{
log_printf ("(#");
dump_serial (authidsn);
log_printf ("/");
dump_string (s);
log_printf (") ");
}
log_printf ("not found\n");
}
ksba_name_release (authid);
xfree (authidsn);
xfree (keyid);
}
else
crlissuer_cert = find_cert_bysubject (ctrl, crlissuer, NULL);
err = 0;
if (!crlissuer_cert)
{
err = gpg_error (GPG_ERR_MISSING_CERT);
goto failure;
}
err = finish_sig_check (crl, md, algo, crlissuer_cert, use_pss);
md = NULL; /* Closed. */
if (err)
{
log_error (_("CRL signature verification failed: %s\n"),
gpg_strerror (err));
goto failure;
}
err = validate_cert_chain (ctrl, crlissuer_cert, NULL,
(VALIDATE_FLAG_TRUST_CONFIG
| VALIDATE_FLAG_CRL
| VALIDATE_FLAG_RECURSIVE),
r_trust_anchor);
if (err)
{
log_error (_("error checking validity of CRL "
"issuer certificate: %s\n"),
gpg_strerror (err));
goto failure;
}
}
break;
default:
log_debug ("crl_parse_insert: unknown stop reason\n");
err = gpg_error (GPG_ERR_BUG);
goto failure;
}
}
while (stopreason != KSBA_SR_READY);
assert (!err);
failure:
abort_sig_check (crl, md);
ksba_cert_release (crlissuer_cert);
return err;
}
/* Return the crlNumber extension as an allocated hex string or NULL
if there is none. */
static char *
get_crl_number (ksba_crl_t crl)
{
gpg_error_t err;
ksba_sexp_t number;
char *string;
err = ksba_crl_get_crl_number (crl, &number);
if (err)
return NULL;
string = serial_hex (number);
ksba_free (number);
return string;
}
/* Return the authorityKeyIdentifier or NULL if it is not available.
The issuer name may consists of several parts - they are delimted by
0x01. */
static char *
get_auth_key_id (ksba_crl_t crl, char **serialno)
{
gpg_error_t err;
ksba_name_t name;
ksba_sexp_t sn;
int idx;
const char *s;
char *string;
size_t length;
*serialno = NULL;
err = ksba_crl_get_auth_key_id (crl, NULL, &name, &sn);
if (err)
return NULL;
*serialno = serial_hex (sn);
ksba_free (sn);
if (!name)
return xstrdup ("");
length = 0;
for (idx=0; (s = ksba_name_enum (name, idx)); idx++)
{
char *p = ksba_name_get_uri (name, idx);
length += strlen (p?p:s) + 1;
xfree (p);
}
string = xtrymalloc (length+1);
if (string)
{
*string = 0;
for (idx=0; (s = ksba_name_enum (name, idx)); idx++)
{
char *p = ksba_name_get_uri (name, idx);
if (*string)
strcat (string, "\x01");
strcat (string, p?p:s);
xfree (p);
}
}
ksba_name_release (name);
return string;
}
/* Insert the CRL retrieved using URL into the cache specified by
CACHE. The CRL itself will be read from the stream FP and is
expected in binary format.
Called by:
crl_cache_load
cmd_loadcrl
--load-crl
crl_cache_reload_crl
cmd_isvalid
cmd_checkcrl
cmd_loadcrl
--fetch-crl
*/
gpg_error_t
crl_cache_insert (ctrl_t ctrl, const char *url, ksba_reader_t reader)
{
crl_cache_t cache = get_current_cache ();
gpg_error_t err, err2;
ksba_crl_t crl;
char *fname = NULL;
char *newfname = NULL;
struct cdb_make cdb;
int fd_cdb = -1;
char *issuer = NULL;
char *issuer_hash = NULL;
ksba_isotime_t thisupdate, nextupdate;
crl_cache_entry_t entry = NULL;
crl_cache_entry_t e;
gnupg_isotime_t current_time;
char *checksum = NULL;
int invalidate_crl = 0;
int idx;
const char *oid;
int critical;
char *trust_anchor = NULL;
/* FIXME: We should acquire a mutex for the URL, so that we don't
simultaneously enter the same CRL twice. However this needs to be
interweaved with the checking function.*/
err2 = 0;
err = ksba_crl_new (&crl);
if (err)
{
log_error (_("ksba_crl_new failed: %s\n"), gpg_strerror (err));
goto leave;
}
err = ksba_crl_set_reader (crl, reader);
if ( err )
{
log_error (_("ksba_crl_set_reader failed: %s\n"), gpg_strerror (err));
goto leave;
}
/* Create a temporary cache file to load the CRL into. */
{
char *tmpfname, *p;
const char *nodename;
#ifndef HAVE_W32_SYSTEM
struct utsname utsbuf;
#endif
#ifdef HAVE_W32_SYSTEM
nodename = "unknown";
#else
if (uname (&utsbuf))
nodename = "unknown";
else
nodename = utsbuf.nodename;
#endif
gpgrt_asprintf (&tmpfname, "crl-tmp-%s-%u-%p.db.tmp",
nodename, (unsigned int)getpid (), &tmpfname);
if (!tmpfname)
{
err = gpg_error_from_syserror ();
goto leave;
}
for (p=tmpfname; *p; p++)
if (*p == '/')
*p = '.';
fname = make_filename (opt.homedir_cache, DBDIR_D, tmpfname, NULL);
xfree (tmpfname);
if (!gnupg_remove (fname))
log_info (_("removed stale temporary cache file '%s'\n"), fname);
else if (errno != ENOENT)
{
err = gpg_error_from_syserror ();
log_error (_("problem removing stale temporary cache file '%s': %s\n"),
fname, gpg_strerror (err));
goto leave;
}
}
fd_cdb = gnupg_open (fname, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, 0644);
if (fd_cdb == -1)
{
err = gpg_error_from_errno (errno);
log_error (_("error creating temporary cache file '%s': %s\n"),
fname, strerror (errno));
goto leave;
}
cdb_make_start(&cdb, fd_cdb);
err = crl_parse_insert (ctrl, crl, &cdb, fname,
&issuer, thisupdate, nextupdate, &trust_anchor);
if (err)
{
log_error (_("crl_parse_insert failed: %s\n"), gpg_strerror (err));
/* Error in cleanup ignored. */
cdb_make_finish (&cdb);
goto leave;
}
/* Finish the database. */
if (cdb_make_finish (&cdb))
{
err = gpg_error_from_errno (errno);
log_error (_("error finishing temporary cache file '%s': %s\n"),
fname, strerror (errno));
goto leave;
}
if (close (fd_cdb))
{
err = gpg_error_from_errno (errno);
log_error (_("error closing temporary cache file '%s': %s\n"),
fname, strerror (errno));
goto leave;
}
fd_cdb = -1;
/* Create a checksum. */
{
unsigned char md5buf[16];
if (hash_dbfile (fname, md5buf))
{
err = gpg_error (GPG_ERR_CHECKSUM);
goto leave;
}
checksum = hexify_data (md5buf, 16, 0);
}
/* Check whether that new CRL is still not expired. */
gnupg_get_isotime (current_time);
if (strcmp (nextupdate, current_time) < 0 )
{
if (opt.force)
log_info (_("WARNING: new CRL still too old; it expired on %s "
"- loading anyway\n"), nextupdate);
else
{
log_error (_("new CRL still too old; it expired on %s\n"),
nextupdate);
if (!err2)
err2 = gpg_error (GPG_ERR_CRL_TOO_OLD);
invalidate_crl |= 1;
}
}
/* Check for unknown critical extensions. */
for (idx=0; !(err=ksba_crl_get_extension (crl, idx, &oid, &critical,
NULL, NULL)); idx++)
{
if (!critical
|| !strcmp (oid, oidstr_authorityKeyIdentifier)
|| !strcmp (oid, oidstr_crlNumber) )
continue;
log_error (_("unknown critical CRL extension %s\n"), oid);
if (!err2)
err2 = gpg_error (GPG_ERR_INV_CRL);
invalidate_crl |= 2;
}
if (gpg_err_code (err) == GPG_ERR_EOF
|| gpg_err_code (err) == GPG_ERR_NO_DATA )
err = 0;
if (err)
{
log_error (_("error reading CRL extensions: %s\n"), gpg_strerror (err));
err = gpg_error (GPG_ERR_INV_CRL);
}
/* Create an hex encoded SHA-1 hash of the issuer DN to be
used as the key for the cache. */
issuer_hash = hashify_data (issuer, strlen (issuer));
/* Create an ENTRY. */
entry = xtrycalloc (1, sizeof *entry);
if (!entry)
{
err = gpg_error_from_syserror ();
goto leave;
}
entry->release_ptr = xtrymalloc (strlen (issuer_hash) + 1
+ strlen (issuer) + 1
+ strlen (url) + 1
+ strlen (checksum) + 1);
if (!entry->release_ptr)
{
err = gpg_error_from_syserror ();
xfree (entry);
entry = NULL;
goto leave;
}
entry->issuer_hash = entry->release_ptr;
entry->issuer = stpcpy (entry->issuer_hash, issuer_hash) + 1;
entry->url = stpcpy (entry->issuer, issuer) + 1;
entry->dbfile_hash = stpcpy (entry->url, url) + 1;
strcpy (entry->dbfile_hash, checksum);
gnupg_copy_time (entry->this_update, thisupdate);
gnupg_copy_time (entry->next_update, nextupdate);
gnupg_copy_time (entry->last_refresh, current_time);
entry->crl_number = get_crl_number (crl);
entry->authority_issuer = get_auth_key_id (crl, &entry->authority_serialno);
entry->invalid = invalidate_crl;
entry->user_trust_req = !!trust_anchor;
entry->check_trust_anchor = trust_anchor;
trust_anchor = NULL;
/* Check whether we already have an entry for this issuer and mark
it as deleted. We better use a loop, just in case duplicates got
somehow into the list. */
for (e = cache->entries; (e=find_entry (e, entry->issuer_hash)); e = e->next)
e->deleted = 1;
/* Rename the temporary DB to the real name. */
newfname = make_db_file_name (entry->issuer_hash);
if (opt.verbose)
log_info (_("creating cache file '%s'\n"), newfname);
/* Just in case close unused matching files. Actually we need this
only under Windows but saving file descriptors is never bad. */
{
int any;
do
{
any = 0;
for (e = cache->entries; e; e = e->next)
if (!e->cdb_use_count && e->cdb
&& !strcmp (e->issuer_hash, entry->issuer_hash))
{
int fd = cdb_fileno (e->cdb);
cdb_free (e->cdb);
xfree (e->cdb);
e->cdb = NULL;
if (close (fd))
log_error (_("error closing cache file: %s\n"),
strerror(errno));
any = 1;
break;
}
}
while (any);
}
#ifdef HAVE_W32_SYSTEM
gnupg_remove (newfname);
#endif
if (rename (fname, newfname))
{
err = gpg_error_from_syserror ();
log_error (_("problem renaming '%s' to '%s': %s\n"),
fname, newfname, gpg_strerror (err));
goto leave;
}
xfree (fname); fname = NULL; /*(let the cleanup code not try to remove it)*/
/* Link the new entry in. */
entry->next = cache->entries;
cache->entries = entry;
entry = NULL;
err = update_dir (cache);
if (err)
{
log_error (_("updating the DIR file failed - "
"cache entry will get lost with the next program start\n"));
err = 0; /* Keep on running. */
}
leave:
release_one_cache_entry (entry);
if (fd_cdb != -1)
close (fd_cdb);
if (fname)
{
gnupg_remove (fname);
xfree (fname);
}
xfree (newfname);
ksba_crl_release (crl);
xfree (issuer);
xfree (issuer_hash);
xfree (checksum);
xfree (trust_anchor);
return err ? err : err2;
}
/* Print one cached entry E in a human readable format to stream
FP. Return 0 on success. */
static gpg_error_t
list_one_crl_entry (crl_cache_t cache, crl_cache_entry_t e, estream_t fp)
{
struct cdb_find cdbfp;
struct cdb *cdb;
int rc;
int warn = 0;
const unsigned char *s;
es_fputs ("--------------------------------------------------------\n", fp );
es_fprintf (fp, _("Begin CRL dump (retrieved via %s)\n"), e->url );
es_fprintf (fp, " Issuer:\t%s\n", e->issuer );
es_fprintf (fp, " Issuer Hash:\t%s\n", e->issuer_hash );
es_fprintf (fp, " This Update:\t%s\n", e->this_update );
es_fprintf (fp, " Next Update:\t%s\n", e->next_update );
es_fprintf (fp, " CRL Number :\t%s\n", e->crl_number? e->crl_number: "none");
es_fprintf (fp, " AuthKeyId :\t%s\n",
e->authority_serialno? e->authority_serialno:"none");
if (e->authority_serialno && e->authority_issuer)
{
es_fputs (" \t", fp);
for (s=e->authority_issuer; *s; s++)
if (*s == '\x01')
es_fputs ("\n \t", fp);
else
es_putc (*s, fp);
es_putc ('\n', fp);
}
es_fprintf (fp, " Trust Check:\t%s\n",
!e->user_trust_req? "[system]" :
e->check_trust_anchor? e->check_trust_anchor:"[missing]");
if ((e->invalid & 1))
es_fprintf (fp, _(" ERROR: The CRL will not be used "
"because it was still too old after an update!\n"));
if ((e->invalid & 2))
es_fprintf (fp, _(" ERROR: The CRL will not be used "
"due to an unknown critical extension!\n"));
if ((e->invalid & ~3))
es_fprintf (fp, _(" ERROR: The CRL will not be used\n"));
cdb = lock_db_file (cache, e);
if (!cdb)
return gpg_error (GPG_ERR_GENERAL);
if (!e->dbfile_checked)
es_fprintf (fp, _(" ERROR: This cached CRL may have been tampered with!\n"));
es_putc ('\n', fp);
rc = cdb_findinit (&cdbfp, cdb, NULL, 0);
while (!rc && (rc=cdb_findnext (&cdbfp)) > 0 )
{
unsigned char keyrecord[256];
unsigned char record[16];
int reason;
int any = 0;
cdbi_t n;
cdbi_t i;
rc = 0;
n = cdb_datalen (cdb);
if (n != 16)
{
log_error (_(" WARNING: invalid cache record length\n"));
warn = 1;
continue;
}
if (cdb_read (cdb, record, n, cdb_datapos (cdb)))
{
log_error (_("problem reading cache record: %s\n"),
strerror (errno));
warn = 1;
continue;
}
n = cdb_keylen (cdb);
if (n > sizeof keyrecord)
n = sizeof keyrecord;
if (cdb_read (cdb, keyrecord, n, cdb_keypos (cdb)))
{
log_error (_("problem reading cache key: %s\n"), strerror (errno));
warn = 1;
continue;
}
reason = *record;
es_fputs (" ", fp);
for (i = 0; i < n; i++)
es_fprintf (fp, "%02X", keyrecord[i]);
es_fputs (":\t reasons( ", fp);
if (reason & KSBA_CRLREASON_UNSPECIFIED)
es_fputs( "unspecified ", fp ), any = 1;
if (reason & KSBA_CRLREASON_KEY_COMPROMISE )
es_fputs( "key_compromise ", fp ), any = 1;
if (reason & KSBA_CRLREASON_CA_COMPROMISE )
es_fputs( "ca_compromise ", fp ), any = 1;
if (reason & KSBA_CRLREASON_AFFILIATION_CHANGED )
es_fputs( "affiliation_changed ", fp ), any = 1;
if (reason & KSBA_CRLREASON_SUPERSEDED )
es_fputs( "superseded", fp ), any = 1;
if (reason & KSBA_CRLREASON_CESSATION_OF_OPERATION )
es_fputs( "cessation_of_operation", fp ), any = 1;
if (reason & KSBA_CRLREASON_CERTIFICATE_HOLD )
es_fputs( "certificate_hold", fp ), any = 1;
if (reason && !any)
es_fputs( "other", fp );
es_fprintf (fp, ") rdate: %.15s\n", record+1);
}
if (rc)
log_error (_("error reading cache entry from db: %s\n"), strerror (rc));
unlock_db_file (cache, e);
es_fprintf (fp, _("End CRL dump\n") );
es_putc ('\n', fp);
return (rc||warn)? gpg_error (GPG_ERR_GENERAL) : 0;
}
/* Print the contents of the CRL CACHE in a human readable format to
stream FP. */
gpg_error_t
crl_cache_list (estream_t fp)
{
crl_cache_t cache = get_current_cache ();
crl_cache_entry_t entry;
gpg_error_t err = 0;
for (entry = cache->entries;
entry && !entry->deleted && !err;
entry = entry->next )
err = list_one_crl_entry (cache, entry, fp);
return err;
}
/* Load the CRL containing the file named FILENAME into our CRL cache. */
gpg_error_t
crl_cache_load (ctrl_t ctrl, const char *filename)
{
gpg_error_t err;
estream_t fp;
ksba_reader_t reader;
fp = es_fopen (filename, "rb");
if (!fp)
{
err = gpg_error_from_errno (errno);
log_error (_("can't open '%s': %s\n"), filename, strerror (errno));
return err;
}
err = create_estream_ksba_reader (&reader, fp);
if (!err)
{
err = crl_cache_insert (ctrl, filename, reader);
ksba_reader_release (reader);
}
es_fclose (fp);
return err;
}
/* Locate the corresponding CRL for the certificate CERT, read and
verify the CRL and store it in the cache. */
gpg_error_t
crl_cache_reload_crl (ctrl_t ctrl, ksba_cert_t cert)
{
gpg_error_t err;
ksba_reader_t reader = NULL;
char *issuer = NULL;
ksba_name_t distpoint = NULL;
ksba_name_t issuername = NULL;
char *distpoint_uri = NULL;
int any_dist_point = 0;
int seq;
gpg_error_t last_err = 0;
/* Loop over all distribution points, get the CRLs and put them into
the cache. */
if (opt.verbose)
log_info ("checking distribution points\n");
seq = 0;
while (xfree (distpoint), xfree (issuername),
!(err = ksba_cert_get_crl_dist_point (cert, seq++,
&distpoint,
&issuername, NULL )))
{
int name_seq;
if (!distpoint && !issuername)
{
if (opt.verbose)
log_info ("no issuer name and no distribution point\n");
break; /* Not allowed; i.e. an invalid certificate. We give
up here and hope that the default method returns a
suitable CRL. */
}
/* Get the URIs. We do this in a loop to iterate over all names
in the crlDP. */
for (name_seq=0; ksba_name_enum (distpoint, name_seq); name_seq++)
{
xfree (distpoint_uri);
distpoint_uri = ksba_name_get_uri (distpoint, name_seq);
if (!distpoint_uri)
continue;
if (!strncmp (distpoint_uri, "ldap:", 5)
|| !strncmp (distpoint_uri, "ldaps:", 6))
{
if (opt.ignore_ldap_dp)
continue;
}
else if (!strncmp (distpoint_uri, "http:", 5)
|| !strncmp (distpoint_uri, "https:", 6))
{
if (opt.ignore_http_dp)
continue;
}
else
continue; /* Skip unknown schemes. */
any_dist_point = 1;
if (opt.verbose)
log_info ("fetching CRL from '%s'\n", distpoint_uri);
crl_close_reader (reader);
err = crl_fetch (ctrl, distpoint_uri, &reader);
if (err)
{
log_error (_("crl_fetch via DP failed: %s\n"),
gpg_strerror (err));
last_err = err;
continue; /* with the next name. */
}
if (opt.verbose)
log_info ("inserting CRL (reader %p)\n", reader);
err = crl_cache_insert (ctrl, distpoint_uri, reader);
if (err)
{
log_error (_("crl_cache_insert via DP failed: %s\n"),
gpg_strerror (err));
last_err = err;
continue; /* with the next name. */
}
goto leave; /* Ready - we got the CRL. */
}
}
if (gpg_err_code (err) == GPG_ERR_EOF)
err = 0;
if (!err && last_err)
{
err = last_err;
goto leave;
}
/* If we did not found any distpoint, try something reasonable. */
if (!any_dist_point )
{
if (opt.verbose)
log_info ("no distribution point - trying issuer name\n");
issuer = ksba_cert_get_issuer (cert, 0);
if (!issuer)
{
log_error ("oops: issuer missing in certificate\n");
err = gpg_error (GPG_ERR_INV_CERT_OBJ);
goto leave;
}
if (opt.verbose)
log_info ("fetching CRL from default location\n");
crl_close_reader (reader);
err = crl_fetch_default (ctrl, issuer, &reader);
if (err)
{
log_error ("crl_fetch via issuer failed: %s\n",
gpg_strerror (err));
goto leave;
}
if (opt.verbose)
log_info ("inserting CRL (reader %p)\n", reader);
err = crl_cache_insert (ctrl, "default location(s)", reader);
if (err)
{
log_error (_("crl_cache_insert via issuer failed: %s\n"),
gpg_strerror (err));
goto leave;
}
}
leave:
crl_close_reader (reader);
xfree (distpoint_uri);
ksba_name_release (distpoint);
ksba_name_release (issuername);
ksba_free (issuer);
return err;
}