aboutsummaryrefslogtreecommitdiff
path: root/ks_token.c
diff options
context:
space:
mode:
Diffstat (limited to 'ks_token.c')
-rw-r--r--ks_token.c665
1 files changed, 665 insertions, 0 deletions
diff --git a/ks_token.c b/ks_token.c
new file mode 100644
index 0000000..1676bc0
--- /dev/null
+++ b/ks_token.c
@@ -0,0 +1,665 @@
+/*
+ * ks_token.c
+ * ----------
+ * Keystore implementation in flash memory.
+ *
+ * Authors: Rob Austein, Fredrik Thulin
+ * Copyright (c) 2015-2017, NORDUnet A/S All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * - Neither the name of the NORDUnet nor the names of its contributors may
+ * be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+ * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+ * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*
+ * This keystore driver operates over bare flash, versus over a flash file
+ * system or flash translation layer. The block size is large enough to
+ * hold an AES-keywrapped 4096-bit RSA key. Any remaining space in the key
+ * block may be used to store attributes (opaque TLV blobs). If the
+ * attributes overflow the key block, additional blocks may be added, but
+ * no attribute may exceed the block size.
+ */
+
+#include <stddef.h>
+#include <string.h>
+#include <assert.h>
+
+#include "hal.h"
+#include "hal_internal.h"
+#include "ks.h"
+
+#include "last_gasp_pin_internal.h"
+
+#define HAL_OK CMSIS_HAL_OK
+#include "stm-keystore.h"
+#undef HAL_OK
+
+#ifndef KS_TOKEN_CACHE_SIZE
+#define KS_TOKEN_CACHE_SIZE 4
+#endif
+
+#if HAL_KS_BLOCK_SIZE % KEYSTORE_SUBSECTOR_SIZE != 0
+#error Keystore block size is not a multiple of flash subsector size
+#endif
+
+#define NUM_FLASH_BLOCKS ((KEYSTORE_NUM_SUBSECTORS * KEYSTORE_SUBSECTOR_SIZE) / HAL_KS_BLOCK_SIZE)
+#define SUBSECTORS_PER_BLOCK (HAL_KS_BLOCK_SIZE / KEYSTORE_SUBSECTOR_SIZE)
+
+/*
+ * Keystore database.
+ */
+
+typedef struct {
+ hal_ks_t ks; /* Must be first (C "subclassing") */
+ hal_ks_pin_t wheel_pin;
+ hal_ks_pin_t so_pin;
+ hal_ks_pin_t user_pin;
+} ks_token_db_t;
+
+/*
+ * This is a bit silly, but it's safe enough, and it lets us avoid a
+ * nasty mess of forward references.
+ */
+
+#define db ((ks_token_db_t * const) hal_ks_token)
+
+/*
+ * Calculate offset of the block in the flash address space.
+ */
+
+static inline uint32_t ks_token_offset(const unsigned blockno)
+{
+ return blockno * HAL_KS_BLOCK_SIZE;
+}
+
+/*
+ * Read a flash block.
+ *
+ * Flash read on the Alpha is slow enough that it pays to check the
+ * first page before reading the rest of the block.
+ */
+
+static hal_error_t ks_token_read(hal_ks_t *ks, const unsigned blockno, hal_ks_block_t *block)
+{
+ if (ks != hal_ks_token || block == NULL || blockno >= NUM_FLASH_BLOCKS || sizeof(*block) != HAL_KS_BLOCK_SIZE)
+ return HAL_ERROR_IMPOSSIBLE;
+
+ if (keystore_read_data(ks_token_offset(blockno),
+ block->bytes,
+ KEYSTORE_PAGE_SIZE) != CMSIS_HAL_OK)
+ return HAL_ERROR_KEYSTORE_ACCESS;
+
+ switch (hal_ks_block_get_type(block)) {
+ case HAL_KS_BLOCK_TYPE_ERASED:
+ case HAL_KS_BLOCK_TYPE_ZEROED:
+ return HAL_OK;
+ case HAL_KS_BLOCK_TYPE_KEY:
+ case HAL_KS_BLOCK_TYPE_PIN:
+ break;
+ default:
+ return HAL_ERROR_KEYSTORE_BAD_BLOCK_TYPE;
+ }
+
+ switch (hal_ks_block_get_status(block)) {
+ case HAL_KS_BLOCK_STATUS_LIVE:
+ case HAL_KS_BLOCK_STATUS_TOMBSTONE:
+ break;
+ default:
+ return HAL_ERROR_KEYSTORE_BAD_BLOCK_TYPE;
+ }
+
+ if (keystore_read_data(ks_token_offset(blockno) + KEYSTORE_PAGE_SIZE,
+ block->bytes + KEYSTORE_PAGE_SIZE,
+ sizeof(*block) - KEYSTORE_PAGE_SIZE) != CMSIS_HAL_OK)
+ return HAL_ERROR_KEYSTORE_ACCESS;
+
+ if (hal_ks_block_calculate_crc(block) != block->header.crc)
+ return HAL_ERROR_KEYSTORE_BAD_CRC;
+
+ return HAL_OK;
+}
+
+/*
+ * Convert a live block into a tombstone. Caller is responsible for
+ * making sure that the block being converted is valid; since we don't
+ * need to update the CRC for this, we just modify the first page.
+ */
+
+static hal_error_t ks_token_deprecate(hal_ks_t *ks, const unsigned blockno)
+{
+ if (ks != hal_ks_token || blockno >= NUM_FLASH_BLOCKS)
+ return HAL_ERROR_IMPOSSIBLE;
+
+ uint8_t page[KEYSTORE_PAGE_SIZE];
+ hal_ks_block_header_t *header = (void *) page;
+ uint32_t offset = ks_token_offset(blockno);
+
+ if (keystore_read_data(offset, page, sizeof(page)) != CMSIS_HAL_OK)
+ return HAL_ERROR_KEYSTORE_ACCESS;
+
+ header->block_status = HAL_KS_BLOCK_STATUS_TOMBSTONE;
+
+ if (keystore_write_data(offset, page, sizeof(page)) != CMSIS_HAL_OK)
+ return HAL_ERROR_KEYSTORE_ACCESS;
+
+ return HAL_OK;
+}
+
+/*
+ * Zero (not erase) a flash block. Just need to zero the first page.
+ */
+
+static hal_error_t ks_token_zero(hal_ks_t *ks, const unsigned blockno)
+{
+ if (ks != hal_ks_token || blockno >= NUM_FLASH_BLOCKS)
+ return HAL_ERROR_IMPOSSIBLE;
+
+ uint8_t page[KEYSTORE_PAGE_SIZE] = {0};
+
+ if (keystore_write_data(ks_token_offset(blockno), page, sizeof(page)) != CMSIS_HAL_OK)
+ return HAL_ERROR_KEYSTORE_ACCESS;
+
+ return HAL_OK;
+}
+
+/*
+ * Erase a flash block. Also see ks_token_erase_maybe(), below.
+ */
+
+static hal_error_t ks_token_erase(hal_ks_t *ks, const unsigned blockno)
+{
+ if (ks != hal_ks_token || blockno >= NUM_FLASH_BLOCKS)
+ return HAL_ERROR_IMPOSSIBLE;
+
+ unsigned subsector = blockno * SUBSECTORS_PER_BLOCK;
+ const unsigned end = (blockno + 1) * SUBSECTORS_PER_BLOCK;
+
+ do {
+ if (keystore_erase_subsector(subsector) != CMSIS_HAL_OK)
+ return HAL_ERROR_KEYSTORE_ACCESS;
+ } while (++subsector < end);
+
+ return HAL_OK;
+}
+
+/*
+ * Erase a flash block if it hasn't already been erased.
+ * May not be necessary, trying to avoid unnecessary wear.
+ *
+ * Unclear whether there's any sane reason why this needs to be
+ * constant time, given how slow erasure is. But side channel attacks
+ * can be tricky things, and it's theoretically possible that we could
+ * leak information about, eg, key length, so we do constant time.
+ */
+
+static hal_error_t ks_token_erase_maybe(hal_ks_t *ks, const unsigned blockno)
+{
+ if (ks != hal_ks_token || blockno >= NUM_FLASH_BLOCKS)
+ return HAL_ERROR_IMPOSSIBLE;
+
+ uint8_t mask = 0xFF;
+
+ for (uint32_t a = ks_token_offset(blockno); a < ks_token_offset(blockno + 1); a += KEYSTORE_PAGE_SIZE) {
+ uint8_t page[KEYSTORE_PAGE_SIZE];
+ if (keystore_read_data(a, page, sizeof(page)) != CMSIS_HAL_OK)
+ return HAL_ERROR_KEYSTORE_ACCESS;
+ for (int i = 0; i < KEYSTORE_PAGE_SIZE; i++)
+ mask &= page[i];
+ }
+
+ return mask == 0xFF ? HAL_OK : ks_token_erase(ks, blockno);
+}
+
+/*
+ * Write a flash block, calculating CRC when appropriate.
+ */
+
+static hal_error_t ks_token_write(hal_ks_t *ks, const unsigned blockno, hal_ks_block_t *block)
+{
+ if (ks != hal_ks_token || block == NULL || blockno >= NUM_FLASH_BLOCKS || sizeof(*block) != HAL_KS_BLOCK_SIZE)
+ return HAL_ERROR_IMPOSSIBLE;
+
+ hal_error_t err = ks_token_erase_maybe(ks, blockno);
+
+ if (err != HAL_OK)
+ return err;
+
+ switch (hal_ks_block_get_type(block)) {
+ case HAL_KS_BLOCK_TYPE_KEY:
+ case HAL_KS_BLOCK_TYPE_PIN:
+ block->header.crc = hal_ks_block_calculate_crc(block);
+ break;
+ default:
+ break;
+ }
+
+ if (keystore_write_data(ks_token_offset(blockno), block->bytes, sizeof(*block)) != CMSIS_HAL_OK)
+ return HAL_ERROR_KEYSTORE_ACCESS;
+
+ return HAL_OK;
+}
+
+/*
+ * The token keystore doesn't implement per-session objects, so these are no-ops.
+ */
+
+static hal_error_t ks_token_set_owner(hal_ks_t *ks,
+ const unsigned blockno,
+ const hal_client_handle_t client,
+ const hal_session_handle_t session)
+{
+ return HAL_OK;
+}
+
+static hal_error_t ks_token_test_owner(hal_ks_t *ks,
+ const unsigned blockno,
+ const hal_client_handle_t client,
+ const hal_session_handle_t session)
+{
+ return HAL_OK;
+}
+
+static hal_error_t ks_token_copy_owner(hal_ks_t *ks,
+ const unsigned source,
+ const unsigned target)
+{
+ return HAL_OK;
+}
+
+static hal_error_t ks_token_logout(hal_ks_t *ks,
+ hal_client_handle_t client)
+{
+ return HAL_OK;
+}
+
+/*
+ * Forward reference.
+ */
+
+static hal_error_t fetch_pin_block(unsigned *b, hal_ks_block_t **block);
+
+/*
+ * Initialize keystore.
+ */
+
+static hal_error_t ks_token_init(hal_ks_t *ks, const int alloc)
+{
+ if (ks != hal_ks_token)
+ return HAL_ERROR_IMPOSSIBLE;
+
+ hal_ks_block_t *block = NULL;
+ hal_error_t err = HAL_OK;
+
+ if (alloc && (err = hal_ks_alloc_common(ks, NUM_FLASH_BLOCKS, KS_TOKEN_CACHE_SIZE, NULL, 0)) != HAL_OK)
+ return err;
+
+ if ((err = hal_ks_init_common(ks)) != HAL_OK)
+ return err;
+
+ /*
+ * Fetch or create the PIN block.
+ */
+
+ memset(&db->wheel_pin, 0, sizeof(db->wheel_pin));
+ memset(&db->so_pin, 0, sizeof(db->so_pin));
+ memset(&db->user_pin, 0, sizeof(db->user_pin));
+
+ err = fetch_pin_block(NULL, &block);
+
+ if (err == HAL_OK) {
+ db->wheel_pin = block->pin.wheel_pin;
+ db->so_pin = block->pin.so_pin;
+ db->user_pin = block->pin.user_pin;
+ }
+
+ else if (err == HAL_ERROR_KEY_NOT_FOUND) {
+ /*
+ * We found no PIN block, so create one, with the user and so PINs
+ * cleared and the wheel PIN set to the last-gasp value. The
+ * last-gasp WHEEL PIN is a terrible answer, but we need some kind
+ * of bootstrapping mechanism when all else fails. If you have a
+ * better suggestion, we'd love to hear it.
+ */
+
+ unsigned b;
+
+ if ((block = hal_ks_cache_pick_lru(ks)) == NULL)
+ return HAL_ERROR_IMPOSSIBLE;
+
+ memset(block, 0xFF, sizeof(*block));
+
+ block->header.block_type = HAL_KS_BLOCK_TYPE_PIN;
+ block->header.block_status = HAL_KS_BLOCK_STATUS_LIVE;
+
+ block->pin.wheel_pin = db->wheel_pin = hal_last_gasp_pin;
+ block->pin.so_pin = db->so_pin;
+ block->pin.user_pin = db->user_pin;
+
+ if ((err = hal_ks_index_add(ks, &hal_ks_pin_uuid, &b, NULL)) != HAL_OK)
+ return err;
+
+ hal_ks_cache_mark_used(ks, block, b);
+
+ err = ks_token_write(ks, b, block);
+
+ hal_ks_cache_release(ks, block);
+ }
+
+ return err;
+}
+
+/*
+ * Dispatch vector and keystore definition, now that we've defined all
+ * the driver functions.
+ */
+
+static const hal_ks_driver_t ks_token_driver = {
+ .init = ks_token_init,
+ .read = ks_token_read,
+ .write = ks_token_write,
+ .deprecate = ks_token_deprecate,
+ .zero = ks_token_zero,
+ .erase = ks_token_erase,
+ .erase_maybe = ks_token_erase_maybe,
+ .set_owner = ks_token_set_owner,
+ .test_owner = ks_token_test_owner,
+ .copy_owner = ks_token_copy_owner,
+ .logout = ks_token_logout
+};
+
+static ks_token_db_t _db = { .ks.driver = &ks_token_driver };
+
+hal_ks_t * const hal_ks_token = &_db.ks;
+
+/*
+ * The remaining functions aren't really part of the keystore API per se,
+ * but they all involve non-key data which we keep in the keystore
+ * because it's the flash we've got.
+ */
+
+/*
+ * Special bonus init routine used only by the bootloader, so that it
+ * can read PINs set by the main firmware. Yes, this is a kludge. We
+ * could of course call the real ks_init() routine instead, but it's
+ * slow, and we don't want to allow anything that would modify the
+ * flash here, so having a special entry point for this kludge is
+ * simplest, overall. Sigh.
+ */
+
+void hal_ks_init_read_only_pins_only(void)
+{
+ unsigned b, best_seen = NUM_FLASH_BLOCKS;
+ hal_ks_block_t block[1];
+
+ hal_ks_lock();
+
+ for (b = 0; b < NUM_FLASH_BLOCKS; b++) {
+ if (hal_ks_block_read(hal_ks_token, b, block) != HAL_OK ||
+ hal_ks_block_get_type(block) != HAL_KS_BLOCK_TYPE_PIN)
+ continue;
+ best_seen = b;
+ if (hal_ks_block_get_status(block) == HAL_KS_BLOCK_STATUS_LIVE)
+ break;
+ }
+
+ if (b != best_seen && best_seen != NUM_FLASH_BLOCKS &&
+ hal_ks_block_read(hal_ks_token, best_seen, block) != HAL_OK)
+ best_seen = NUM_FLASH_BLOCKS;
+
+ if (best_seen == NUM_FLASH_BLOCKS) {
+ memset(block, 0xFF, sizeof(*block));
+ block->pin.wheel_pin = hal_last_gasp_pin;
+ }
+
+ db->wheel_pin = block->pin.wheel_pin;
+ db->so_pin = block->pin.so_pin;
+ db->user_pin = block->pin.user_pin;
+
+ hal_ks_unlock();
+}
+
+/*
+ * Fetch PIN. This is always cached, so just returned cached value.
+ */
+
+hal_error_t hal_get_pin(const hal_user_t user,
+ const hal_ks_pin_t **pin)
+{
+ if (pin == NULL)
+ return HAL_ERROR_BAD_ARGUMENTS;
+
+ hal_error_t err = HAL_OK;
+
+ hal_ks_lock();
+
+ switch (user) {
+ case HAL_USER_WHEEL: *pin = &db->wheel_pin; break;
+ case HAL_USER_SO: *pin = &db->so_pin; break;
+ case HAL_USER_NORMAL: *pin = &db->user_pin; break;
+ default: err = HAL_ERROR_BAD_ARGUMENTS;
+ }
+
+ hal_ks_unlock();
+
+ return err;
+}
+
+/*
+ * Fetch PIN block. hint = 0 because we know that the all-zeros UUID
+ * should always sort to first slot in the index.
+ */
+
+static hal_error_t fetch_pin_block(unsigned *b, hal_ks_block_t **block)
+{
+ if (block == NULL)
+ return HAL_ERROR_IMPOSSIBLE;
+
+ hal_error_t err;
+ int hint = 0;
+ unsigned b_;
+
+ if (b == NULL)
+ b = &b_;
+
+ if ((err = hal_ks_index_find(hal_ks_token, &hal_ks_pin_uuid, b, &hint)) != HAL_OK ||
+ (err = hal_ks_block_read_cached(hal_ks_token, *b, block)) != HAL_OK)
+ return err;
+
+ hal_ks_cache_mark_used(hal_ks_token, *block, *b);
+
+ if (hal_ks_block_get_type(*block) != HAL_KS_BLOCK_TYPE_PIN)
+ return HAL_ERROR_IMPOSSIBLE;
+
+ return HAL_OK;
+}
+
+/*
+ * Update the PIN block. This block should always be present, but we
+ * have to do the zombie jamboree to make sure we write the new PIN
+ * block before destroying the old one. hint = 0 because we know that
+ * the all-zeros UUID should always sort to first slot in the index.
+ */
+
+static hal_error_t update_pin_block(const unsigned b,
+ hal_ks_block_t *block,
+ const hal_ks_pin_block_t * const new_data)
+{
+ if (block == NULL || new_data == NULL || hal_ks_block_get_type(block) != HAL_KS_BLOCK_TYPE_PIN)
+ return HAL_ERROR_IMPOSSIBLE;
+
+ int hint = 0;
+
+ block->pin = *new_data;
+
+ return hal_ks_block_update(hal_ks_token, b, block, &hal_ks_pin_uuid, &hint);
+}
+
+/*
+ * Change a PIN.
+ */
+
+hal_error_t hal_set_pin(const hal_user_t user,
+ const hal_ks_pin_t * const pin)
+{
+ if (pin == NULL)
+ return HAL_ERROR_BAD_ARGUMENTS;
+
+ hal_ks_block_t *block;
+ hal_error_t err;
+ unsigned b;
+
+ hal_ks_lock();
+
+ if ((err = fetch_pin_block(&b, &block)) != HAL_OK)
+ goto done;
+
+ hal_ks_pin_block_t new_data = block->pin;
+ hal_ks_pin_t *dp, *bp;
+
+ switch (user) {
+ case HAL_USER_WHEEL: bp = &new_data.wheel_pin; dp = &db->wheel_pin; break;
+ case HAL_USER_SO: bp = &new_data.so_pin; dp = &db->so_pin; break;
+ case HAL_USER_NORMAL: bp = &new_data.user_pin; dp = &db->user_pin; break;
+ default: err = HAL_ERROR_BAD_ARGUMENTS; goto done;
+ }
+
+ const hal_ks_pin_t old_pin = *dp;
+ *dp = *bp = *pin;
+
+ if ((err = update_pin_block(b, block, &new_data)) != HAL_OK)
+ *dp = old_pin;
+
+ done:
+ hal_ks_unlock();
+ return err;
+}
+
+#if HAL_MKM_FLASH_BACKUP_KLUDGE
+
+/*
+ * Horrible insecure kludge in lieu of a battery for the MKM.
+ *
+ * API here is a little strange: all calls pass a length parameter,
+ * but any length other than the compiled in constant just returns an
+ * immediate error, there's no notion of buffer max length vs buffer
+ * used length, querying for the size of buffer really needed, or
+ * anything like that.
+ *
+ * We might want to rewrite this some day, if we don't replace it with
+ * a battery first. For now we just preserve the API as we found it
+ * while re-implementing it on top of the new keystore.
+ */
+
+hal_error_t hal_mkm_flash_read_no_lock(uint8_t *buf, const size_t len)
+{
+ if (buf != NULL && len != KEK_LENGTH)
+ return HAL_ERROR_MASTERKEY_BAD_LENGTH;
+
+ hal_ks_block_t *block;
+ hal_error_t err;
+ unsigned b;
+
+ if ((err = fetch_pin_block(&b, &block)) != HAL_OK)
+ return err;
+
+ if (block->pin.kek_set != FLASH_KEK_SET)
+ return HAL_ERROR_MASTERKEY_NOT_SET;
+
+ if (buf != NULL)
+ memcpy(buf, block->pin.kek, len);
+
+ return HAL_OK;
+}
+
+hal_error_t hal_mkm_flash_read(uint8_t *buf, const size_t len)
+{
+ hal_ks_lock();
+ const hal_error_t err = hal_mkm_flash_read_no_lock(buf, len);
+ hal_ks_unlock();
+ return err;
+}
+
+hal_error_t hal_mkm_flash_write(const uint8_t * const buf, const size_t len)
+{
+ if (buf == NULL)
+ return HAL_ERROR_BAD_ARGUMENTS;
+
+ if (len != KEK_LENGTH)
+ return HAL_ERROR_MASTERKEY_BAD_LENGTH;
+
+ hal_ks_block_t *block;
+ hal_error_t err;
+ unsigned b;
+
+ hal_ks_lock();
+
+ if ((err = fetch_pin_block(&b, &block)) != HAL_OK)
+ goto done;
+
+ hal_ks_pin_block_t new_data = block->pin;
+
+ new_data.kek_set = FLASH_KEK_SET;
+ memcpy(new_data.kek, buf, len);
+
+ err = update_pin_block(b, block, &new_data);
+
+ done:
+ hal_ks_unlock();
+ return err;
+}
+
+hal_error_t hal_mkm_flash_erase(const size_t len)
+{
+ if (len != KEK_LENGTH)
+ return HAL_ERROR_MASTERKEY_BAD_LENGTH;
+
+ hal_ks_block_t *block;
+ hal_error_t err;
+ unsigned b;
+
+ hal_ks_lock();
+
+ if ((err = fetch_pin_block(&b, &block)) != HAL_OK)
+ goto done;
+
+ hal_ks_pin_block_t new_data = block->pin;
+
+ new_data.kek_set = FLASH_KEK_NOT_SET;
+ memset(new_data.kek, 0, len);
+
+ err = update_pin_block(b, block, &new_data);
+
+ done:
+ hal_ks_unlock();
+ return err;
+}
+
+#endif /* HAL_MKM_FLASH_BACKUP_KLUDGE */
+
+/*
+ * Local variables:
+ * indent-tabs-mode: nil
+ * End:
+ */