On Wed, 2014-06-11 at 14:24 +0200, Jan Cholasta wrote:
> Hi,
> 
> On 13.5.2014 18:40, Nathaniel McCallum wrote:
> > On Tue, 2014-05-13 at 12:38 -0400, Nathaniel McCallum wrote:
> >> This patch adds support for importing tokens using RFC 6030 key
> >> container files. This includes decryption support. For sysadmin sanity,
> >> any tokens which fail to add will be written to the output file for
> >> examination. The main use case here is where a small subset of a large
> >> set of tokens fails to validate or add. Using the output file, the
> >> sysadmin can attempt to recover these specific tokens.
> >>
> >> This code is implemented as a server-side script. However, it doesn't
> >> actually need to run on the server. This was done because importing is
> >> an odd fit for the IPA command framework:
> >> 1. We need to write an output file.
> >> 2. The operation may be long-running (thousands of tokens).
> >> 3. Only admins need to perform this task and it only happens
> >> infrequently.
> >
> > I forgot to put the link to the ticket in the commit message. Fixed.
> 
> 1) I think you should initialize NSS in ipa_otptoken_import.py, not in 
> the ipa-otptoken-import script.

Fixed.

> 2) The pep8 tool reports a lot of errors in ipa_otptoken_import.py.

Fixed (mostly). The remaining output from pep8 is, I think, entirely
justifiable.

> 3) Other error messages are in the form "message: %s", I think this one 
> should use that form as well:
> 
> +        if encoding != 'DECIMAL':
> +            raise ValidationError('Unsupported encoding (%s)!' % encoding)

Fixed.

> 4) This is not right:
> 
> +                except:
> +                    self.log.warn("Error adding token: " + 
> str(sys.exc_info()[1]))
> 
> I think it should be something like this instead:
> 
>      except ValidationError, e:
>          self.log.warn("Error adding token: %s", e)

Fixed.

> 5) There is no man page for ipa-otptoken-import.

TODO (tomorrow).

> 6) Output file is created even when ipa-otptoken-import fails with 
> "Unable to connect to LDAP! Did you kinit?" and other initialization 
> errors, which makes subsequent ipa-otptoken-import fail with "Output 
> file already exists!".

Fixed.

> 7) When a key is specified by reference in Key/KeyReference instead of 
> directly in Key/Data/Secret like in 
> <http://git.savannah.gnu.org/cgit/oath-toolkit.git/tree/pskctool/tests/pskc-figure4.xml>,
>  
> import fails with "Key not found in token!". I would expect a different 
> error message.

This error is now: Referenced keys are not supported!

> 8) Importing 
> <http://git.savannah.gnu.org/cgit/oath-toolkit.git/tree/pskctool/tests/pskc-figure5.xml>
>  
> produces this output:
> 
> /usr/lib/python2.7/site-packages/ipaserver/install/ipa_otptoken_import.py:307:
>  
> FutureWarning: The behavior of this method will change in future 
> versions. Use specific 'len(elem)' or 'elem is not None' test instead.
>    if data.get('pinpolicy', None):
> Error adding token: 'NoneType' object has no attribute 'strip'

This now states:
Error adding token: PINPolicy policy not supported!
Error adding token: Unsupported token type!

> 9) Using an arbitrary file in -k produces this output:
> 
> (SEC_ERROR_INVALID_KEY) The key does not support the requested operation.
> Traceback (most recent call last):
>    File "/usr/sbin/ipa-otptoken-import", line 29, in <module>
>      nss.nss_shutdown()
> nss.error.NSPRError: (SEC_ERROR_BUSY) NSS could not shutdown. Objects 
> are still in use.

What do you mean by "arbitrary file"? A file that is not the key?
Like /dev/null? I'm not able to reproduce this.

> 10) Importing 
> <http://git.savannah.gnu.org/cgit/oath-toolkit.git/tree/pskctool/tests/pskc-figure7.xml>
>  
> and 
> <http://git.savannah.gnu.org/cgit/oath-toolkit.git/tree/pskctool/tests/pskc-figure8.xml>
>  
> produces this output:
> 
> Error adding token: object of type 'NoneType' has no len()

Import fails with:
Derived keys are not currently supported!
 or
X.509 keys are not currently supported!

It would be nice to support these in the future.

> 11) Importing 
> <http://git.savannah.gnu.org/cgit/oath-toolkit.git/tree/pskctool/tests/pskc-all.xml>
>  
> or 
> <http://git.savannah.gnu.org/cgit/oath-toolkit.git/tree/pskctool/tests/pskc-all-signed.xml>
>  
> produces this output:
> 
> /usr/lib/python2.7/site-packages/ipaserver/install/ipa_otptoken_import.py:304:
>  
> FutureWarning: The behavior of this method will change in future 
> versions. Use specific 'len(elem)' or 'elem is not None' test instead.
>    if data.get('maxtransact', None):
> /usr/lib/python2.7/site-packages/ipaserver/install/ipa_otptoken_import.py:307:
>  
> FutureWarning: The behavior of this method will change in future 
> versions. Use specific 'len(elem)' or 'elem is not None' test instead.
>    if data.get('pinpolicy', None):

Both of these now output:
Error adding token: NumberOfTransactions policy not supported!

> 12) Importing 
> <http://git.savannah.gnu.org/cgit/oath-toolkit.git/tree/pskctool/tests/pskc-invalid.xml>
>  
> succeeds, but it should fail.

This now errors with:
PSKC file is invalid!

> 13) Importing 
> <http://git.savannah.gnu.org/cgit/oath-toolkit.git/tree/libpskc/examples/pskc-mini.xml>
>  
> fails, but it should succeed, I think.

I think this should fail in our case since we can't possibly support
that configuration.

Nathaniel

From 7318b6cf9947f4ee6e8ec6f56ad45a4063574c54 Mon Sep 17 00:00:00 2001
From: Nathaniel McCallum <npmccal...@redhat.com>
Date: Thu, 8 May 2014 11:06:16 -0400
Subject: [PATCH] Implement OTP token importing

This patch adds support for importing tokens using RFC 6030 key container
files. This includes decryption support. For sysadmin sanity, any tokens
which fail to add will be written to the output file for examination. The
main use case here is where a small subset of a large set of tokens fails
to validate or add. Using the output file, the sysadmin can attempt to
recover these specific tokens.

This code is implemented as a server-side script. However, it doesn't
actually need to run on the server. This was done because importing is an
odd fit for the IPA command framework:
1. We need to write an output file.
2. The operation may be long-running (thousands of tokens).
3. Only admins need to perform this task and it only happens infrequently.

https://fedorahosted.org/freeipa/ticket/4261
---
 freeipa.spec.in                          |   2 +
 install/tools/Makefile.am                |   1 +
 install/tools/ipa-otptoken-import        |  25 ++
 ipaserver/install/ipa_otptoken_import.py | 466 +++++++++++++++++++++++++++++++
 4 files changed, 494 insertions(+)
 create mode 100755 install/tools/ipa-otptoken-import
 create mode 100644 ipaserver/install/ipa_otptoken_import.py

diff --git a/freeipa.spec.in b/freeipa.spec.in
index b71c99435d222c1267dffebff0b6a60a8ab771ac..8aa35ca76a1ae67e1323c86f138d225cb78e3b4f 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -305,6 +305,7 @@ Requires: python-netaddr
 Requires: libipa_hbac-python
 Requires: python-qrcode
 Requires: python-pyasn1
+Requires: python-dateutil
 
 Obsoletes: ipa-python >= 1.0
 
@@ -651,6 +652,7 @@ fi
 %{_sbindir}/ipa-csreplica-manage
 %{_sbindir}/ipa-server-certinstall
 %{_sbindir}/ipa-ldap-updater
+%{_sbindir}/ipa-otptoken-import
 %{_sbindir}/ipa-compat-manage
 %{_sbindir}/ipa-nis-manage
 %{_sbindir}/ipa-managed-entries
diff --git a/install/tools/Makefile.am b/install/tools/Makefile.am
index 2cf66c6dfc1c272bb423253902e7339e7d159567..485be91b7bca2b0f3822a70d0f027793208918c1 100644
--- a/install/tools/Makefile.am
+++ b/install/tools/Makefile.am
@@ -20,6 +20,7 @@ sbin_SCRIPTS =			\
 	ipa-nis-manage		\
 	ipa-managed-entries     \
 	ipa-ldap-updater	\
+	ipa-otptoken-import	\
 	ipa-upgradeconfig	\
 	ipa-backup		\
 	ipa-restore		\
diff --git a/install/tools/ipa-otptoken-import b/install/tools/ipa-otptoken-import
new file mode 100755
index 0000000000000000000000000000000000000000..090116dab568d6561673b54eb6ea31b9c5ae03e1
--- /dev/null
+++ b/install/tools/ipa-otptoken-import
@@ -0,0 +1,25 @@
+#! /usr/bin/python2 -E
+# Authors: Nathaniel McCallum <npmccal...@redhat.com>
+#
+# Copyright (C) 2014  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program 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 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 <http://www.gnu.org/licenses/>.
+#
+
+from ipaserver.install.ipa_otptoken_import import OTPTokenImport
+import nss.nss as nss
+
+OTPTokenImport.run_cli()
+
diff --git a/ipaserver/install/ipa_otptoken_import.py b/ipaserver/install/ipa_otptoken_import.py
new file mode 100644
index 0000000000000000000000000000000000000000..ac90dde899fc1a00034c6e0d1ab3399bc9715ac6
--- /dev/null
+++ b/ipaserver/install/ipa_otptoken_import.py
@@ -0,0 +1,466 @@
+# Authors: Nathaniel McCallum <npmccal...@redhat.com>
+#
+# Copyright (C) 2014  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program 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 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 <http://www.gnu.org/licenses/>.
+#
+
+import base64
+import datetime
+import hashlib
+import hmac
+import os
+import uuid
+import sys
+
+from lxml import etree
+import dateutil.parser
+import dateutil.tz
+import nss.nss as nss
+import krbV
+
+from ipapython import admintool
+from ipapython.dn import DN
+from ipapython.ipautil import user_input, write_tmp_file
+from ipalib import api, errors
+from ipalib.constants import CACERT
+from ipaserver.install import certs, dsinstance, httpinstance, installutils
+from ipaserver.plugins.ldap2 import ldap2
+
+
+class ValidationError(Exception):
+    pass
+
+
+class NoOpConverter(object):
+    "Base class for other conversion classes. Performs no conversion."
+
+    def __call__(self, value, decryptor=None):
+        return value
+
+
+class UnicodeConverter(NoOpConverter):
+    "Converts strings to unicode."
+
+    def __call__(self, value, decryptor=None):
+        return unicode(value)
+
+
+class IntegerConverter(NoOpConverter):
+    "Converts strings to integers."
+
+    def __call__(self, value, decryptor=None):
+        return int(value)
+
+
+class DateConverter(NoOpConverter):
+    "Converts an ISO 8601 string into a UTC datetime object."
+
+    def __call__(self, value, decryptor=None):
+        dt = dateutil.parser.parse(value)
+
+        if dt.tzinfo is None:
+            dt = datetime.datetime(*dt.timetuple()[0:6],
+                                   tzinfo=dateutil.tz.tzlocal())
+
+        return dt.astimezone(dateutil.tz.tzutc())
+
+
+class BaseEnumConverter(NoOpConverter):
+    "Base class for enumerated conversions. Does nothing."
+
+    _DEFAULT = None
+    _ENUM = {}
+
+    def __call__(self, value, decryptor=None):
+        return self._ENUM.get(value.lower(), self._DEFAULT)
+
+
+class TokenTypeConverter(BaseEnumConverter):
+    "Converts token algorithm URI to token type string."
+
+    _ENUM = {
+        "urn:ietf:params:xml:ns:keyprov:pskc:hotp": u"hotp",
+        "urn:ietf:params:xml:ns:keyprov:pskc#hotp": u"hotp",
+        "urn:ietf:params:xml:ns:keyprov:pskc:totp": u"totp",
+        "urn:ietf:params:xml:ns:keyprov:pskc#totp": u"totp",
+    }
+
+
+class HashConverter(BaseEnumConverter):
+    "Converts hash names to their canonical names."
+
+    _ENUM = {
+        "sha1":    u"sha1",
+        "sha224":  u"sha224",
+        "sha256":  u"sha256",
+        "sha384":  u"sha384",
+        "sha512":  u"sha512",
+        "sha-1":   u"sha1",
+        "sha-224": u"sha224",
+        "sha-256": u"sha256",
+        "sha-384": u"sha384",
+        "sha-512": u"sha512",
+    }
+
+
+class HMACConverter(BaseEnumConverter):
+    "Converts HMAC URI to hashlib object."
+
+    _ENUM = {
+        "http://www.w3.org/2000/09/xmldsig#hmac-sha1":        hashlib.sha1,
+        "http://www.w3.org/2001/04/xmldsig-more#hmac-sha224": hashlib.sha224,
+        "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256": hashlib.sha256,
+        "http://www.w3.org/2001/04/xmldsig-more#hmac-sha384": hashlib.sha384,
+        "http://www.w3.org/2001/04/xmldsig-more#hmac-sha512": hashlib.sha512,
+    }
+
+
+class AlgorithmConverter(BaseEnumConverter):
+    "Converts encryption URI to (mech, ivlen)."
+
+    _DEFAULT = (None, None)
+    _ENUM = {
+        "http://www.w3.org/2001/04/xmlenc#aes128-cbc":        (nss.CKM_AES_CBC_PAD, 128),
+        "http://www.w3.org/2001/04/xmlenc#aes192-cbc":        (nss.CKM_AES_CBC_PAD, 192),
+        "http://www.w3.org/2001/04/xmlenc#aes256-cbc":        (nss.CKM_AES_CBC_PAD, 256),
+        "http://www.w3.org/2001/04/xmlenc#tripledes-cbc":     (nss.CKM_DES3_CBC_PAD, 64),
+        "http://www.w3.org/2001/04/xmldsig-more#camellia128": (nss.CKM_CAMELLIA_CBC_PAD, 128),
+        "http://www.w3.org/2001/04/xmldsig-more#camellia192": (nss.CKM_CAMELLIA_CBC_PAD, 192),
+        "http://www.w3.org/2001/04/xmldsig-more#camellia256": (nss.CKM_CAMELLIA_CBC_PAD, 256),
+
+        # TODO: add support for these formats.
+        # "http://www.w3.org/2001/04/xmlenc#kw-aes128": "kw-aes128",
+        # "http://www.w3.org/2001/04/xmlenc#kw-aes192": "kw-aes192",
+        # "http://www.w3.org/2001/04/xmlenc#kw-aes256": "kw-aes256",
+        # "http://www.w3.org/2001/04/xmlenc#kw-tripledes": "kw-tripledes",
+        # "http://www.w3.org/2001/04/xmldsig-more#kw-camellia128": "kw-camellia128",
+        # "http://www.w3.org/2001/04/xmldsig-more#kw-camellia192": "kw-camellia192",
+        # "http://www.w3.org/2001/04/xmldsig-more#kw-camellia256": "kw-camellia256",
+    }
+
+
+def fetchAll(element, xpath, conv=NoOpConverter()):
+    return map(conv, element.xpath(xpath, namespaces={
+        "pskc": "urn:ietf:params:xml:ns:keyprov:pskc",
+        "xenc11": "http://www.w3.org/2009/xmlenc11#";,
+        "xenc": "http://www.w3.org/2001/04/xmlenc#";,
+        "ds": "http://www.w3.org/2000/09/xmldsig#";,
+    }))
+
+
+def fetch(element, xpath, conv=NoOpConverter()):
+    result = fetchAll(element, xpath, conv)
+    return result[0] if result else None
+
+
+class XMLDecryptor(object):
+    """This decrypts values from XML as specified in:
+        * http://www.w3.org/TR/xmlenc-core/
+        * RFC 6931"""
+
+    def __init__(self, key, hmac=None):
+        self.__key = nss.SecItem(key)
+        self.__hmac = hmac
+
+    def __call__(self, element, mac=None):
+        (mech, ivlen) = fetch(element, "./xenc:EncryptionMethod/@Algorithm",
+                              AlgorithmConverter())
+        data = fetch(element, "./xenc:CipherData/xenc:CipherValue/text()",
+                     base64.b64decode)
+
+        # If a MAC is present, perform validation.
+        if mac:
+            tmp = self.__hmac.copy()
+            tmp.update(data)
+            if tmp.digest() != mac:
+                raise ValidationError("MAC validation failed!")
+
+        # Decrypt the data.
+        slot = nss.get_best_slot(mech)
+        key = nss.import_sym_key(slot, mech, nss.PK11_OriginUnwrap,
+                                 nss.CKA_ENCRYPT, self.__key)
+        iv = nss.param_from_iv(mech, nss.SecItem(data[0:ivlen/8]))
+        ctx = nss.create_context_by_sym_key(mech, nss.CKA_DECRYPT, key, iv)
+        out = ctx.cipher_op(data[ivlen / 8:])
+        out += ctx.digest_final()
+        return out
+
+
+class BaseDataTypeConverter(NoOpConverter):
+    "Converts a value element, decrypting if necessary. See RFC 6030."
+
+    _PV_CONV = NoOpConverter()
+    _EV_CONV = NoOpConverter()
+
+    def __call__(self, value, decryptor=None):
+        v = fetch(value, "./pskc:PlainValue/text()", self.__class__._PV_CONV)
+        if v is not None:
+            return v
+
+        mac = fetch(value, "./pskc:ValueMAC/text()", base64.b64decode)
+        ev = fetch(value, "./pskc:EncryptedValue")
+        if ev is not None and decryptor is not None:
+            return self.__class__._EV_CONV(decryptor(ev, mac))
+
+        return None
+
+
+class BinaryDataTypeConverter(BaseDataTypeConverter):
+    _PV_CONV = staticmethod(base64.b64decode)
+
+
+class IntegerDataTypeConverter(BaseDataTypeConverter):
+    _PV_CONV = int
+    _EV_CONV = int
+
+
+class LongDataTypeConverter(BaseDataTypeConverter):
+    _PV_CONV = long
+    _EV_CONV = long
+
+
+class TokenParser(object):
+    _XML = {
+        "pskc:DeviceInfo": {
+            'pskc:IssueNo/text()':      ('issueno',      UnicodeConverter()),
+            'pskc:ExpiryDate/text()':   ('notafter.hw',  DateConverter()),
+            'pskc:Manufacturer/text()': ('vendor',       UnicodeConverter()),
+            'pskc:Model/text()':        ('model',        UnicodeConverter()),
+            'pskc:SerialNo/text()':     ('serial',       UnicodeConverter()),
+            'pskc:StartDate/text()':    ('notbefore.hw', DateConverter()),
+            'pskc:UserId/text()':       ('owner',        UnicodeConverter()),
+        },
+
+        "pskc:Key": {
+            '@Algorithm':               ('type',        TokenTypeConverter()),
+            '@Id':                      ('id',          UnicodeConverter()),
+            'pskc:FriendlyName/text()': ('description', UnicodeConverter()),
+            'pskc:Issuer/text()':       ('issuer',      UnicodeConverter()),
+            'pskc:KeyReference/text()': ('keyref',      UnicodeConverter()),
+
+            'pskc:AlgorithmParameters': {
+                'pskc:Suite/text()':               ('algorithm',  HashConverter()),
+                'pskc:ResponseFormat/@CheckDigit': ('checkdigit', UnicodeConverter()),
+                'pskc:ResponseFormat/@Encoding':   ('encoding',   UnicodeConverter()),
+                'pskc:ResponseFormat/@Length':     ('digits',     IntegerConverter()),
+            },
+
+            'pskc:Data': {
+                'pskc:Counter':      ('counter',  LongDataTypeConverter()),
+                'pskc:Secret':       ('key',      BinaryDataTypeConverter()),
+                'pskc:Time':         ('time',     IntegerDataTypeConverter()),
+                'pskc:TimeDrift':    ('offset',   IntegerDataTypeConverter()),
+                'pskc:TimeInterval': ('interval', IntegerDataTypeConverter()),
+            },
+
+            'pskc:Policy': {
+                'pskc:ExpiryDate/text()':    ('notafter.sw',  DateConverter()),
+                'pskc:KeyUsage/text()':      ('keyusage',     UnicodeConverter()),
+                'pskc:NumberOfTransactions': ('maxtransact',  NoOpConverter()),
+                'pskc:PINPolicy':            ('pinpolicy',    NoOpConverter()),
+                'pskc:StartDate/text()':     ('notbefore.sw', DateConverter()),
+            },
+        },
+    }
+
+    _MAP = (
+        ('type',        'type',                    lambda v, o: v.strip()),
+        ('description', 'description',             lambda v, o: v.strip()),
+        ('vendor',      'ipatokenvendor',          lambda v, o: v.strip()),
+        ('model',       'ipatokenmodel',           lambda v, o: v.strip()),
+        ('serial',      'ipatokenserial',          lambda v, o: v.strip()),
+        ('issueno',     'ipatokenserial',          lambda v, o: o.get('ipatokenserial', '') + '-' + v.strip()),
+        ('owner',       'ipatokenowner',           lambda v, o: v.split(',')[0].split('=')[1].strip()),
+        ('key',         'ipatokenotpkey',          lambda v, o: unicode(base64.b32encode(v))),
+        ('digits',      'ipatokenotpdigits',       lambda v, o: v),
+        ('algorithm',   'ipatokenotpalgorithm',    lambda v, o: v),
+        ('counter',     'ipatokenhotpcounter',     lambda v, o: v),
+        ('interval',    'ipatokentotptimestep',    lambda v, o: v),
+        ('offset',      'ipatokentotpclockoffset', lambda v, o: o.get('ipatokentotptimestep', 30) * v),
+    )
+
+    def __init__(self, decryptor=None):
+        self.__decryptor = decryptor
+
+    def __parse(self, element, prefix, table):
+        "Recursively parses the xml from a table."
+
+        data = {}
+        for k, v in table.items():
+            path = prefix + "/" + k
+
+            if isinstance(v, dict):
+                data.update(self.__parse(element, path, v))
+                continue
+
+            result = fetch(element, path)
+            if result is not None:
+                data[v[0]] = v[1](result, self.__decryptor)
+
+        return data
+
+    def __validate(self, data):
+        "Validates the parsed data."
+
+        if 'type' not in data or data['type'] not in ('totp', 'hotp'):
+            raise ValidationError("Unsupported token type!")
+
+        if 'key' not in data:
+            if 'keyref' in data:
+                raise ValidationError("Referenced keys are not supported!")
+            raise ValidationError("Key not found in token!")
+
+        if data.get('checkdigit', 'FALSE').upper() != 'FALSE':
+            raise ValidationError("CheckDigit not supported!")
+
+        if data.get('maxtransact', None) is not None:
+            raise ValidationError('NumberOfTransactions policy not supported!')
+
+        if data.get('pinpolicy', None) is not None:
+            raise ValidationError('PINPolicy policy not supported!')
+
+        if data.get('time', 0) != 0:
+            raise ValidationError('Specified time is not supported!')
+
+        encoding = data.get('encoding', 'DECIMAL').upper()
+        if encoding != 'DECIMAL':
+            raise ValidationError('Unsupported encoding: %s!' % encoding)
+
+        usage = data.get('keyusage', 'OTP')
+        if usage != 'OTP':
+            raise ValidationError('Unsupported key usage: %s' % usage)
+
+    def __dates(self, out, data, key, reducer):
+        dates = (data.get(key + '.sw', None), data.get(key + '.hw', None))
+        dates = filter(lambda x: x is not None, dates)
+        if dates:
+            out['ipatoken' + key] = unicode(reducer(dates).strftime("%Y%m%d%H%M%SZ"))
+
+    def __call__(self, element):
+        data = self.__parse(element, ".", self._XML)
+        self.__validate(data)
+
+        # Copy values into output.
+        out = {}
+        for (dk, ok, f) in self._MAP:
+            if dk in data:
+                out[ok] = f(data[dk], out)
+
+        # Copy validity dates.
+        self.__dates(out, data, 'notbefore', max)
+        self.__dates(out, data, 'notafter', min)
+
+        return (data.get('id', uuid.uuid4()), out)
+
+
+class OTPTokenImport(admintool.AdminTool):
+    command_name = 'ipa-otptoken-import'
+    description = "Import OTP tokens."
+    usage = "%prog [options] <PSKC file> <output file>"
+
+    @classmethod
+    def main(cls, argv):
+        nss.nss_init_nodb()
+        try:
+            super(OTPTokenImport, cls).main(argv)
+        finally:
+            nss.nss_shutdown()
+
+    @classmethod
+    def add_options(cls, parser):
+        super(OTPTokenImport, cls).add_options(parser)
+
+        parser.add_option("-k", "--keyfile", dest="keyfile",
+                          help="File containing the key used to decrypt token secrets")
+
+    def validate_options(self):
+        super(OTPTokenImport, self).validate_options()
+
+        # Load the keyfile.
+        self.key = None
+        if self.safe_options.keyfile is not None:
+            with open(self.safe_options.keyfile) as f:
+                self.key = f.read()
+
+        # Parse the file.
+        if len(self.args) < 1:
+            raise admintool.ScriptError("Import file required!")
+        self.doc = etree.parse(self.args[0])
+
+        # Get the output file.
+        if len(self.args) < 2:
+            raise admintool.ScriptError("Output file required!")
+        self.output = self.args[1]
+        if os.path.exists(self.output):
+            raise admintool.ScriptError("Output file already exists!")
+
+        # Check for derived key.
+        derivedkey = fetch(self.doc, "./pskc:EncryptionKey/xenc11:DerivedKey")
+        if derivedkey is not None:
+            raise admintool.ScriptError("Derived keys are not currently supported!")
+
+        # Check for x509 key.
+        x509key = fetch(self.doc, "./pskc:EncryptionKey/ds:X509Data")
+        if x509key is not None:
+            raise admintool.ScriptError("X.509 keys are not currently supported!")
+
+        # Load the decryptor.
+        self.decryptor = None
+        keyname = fetch(self.doc, "./pskc:EncryptionKey/ds:KeyName/text()")
+        if keyname is not None:
+            if self.key is None:
+                raise admintool.ScriptError("Encryption key required: %s!" % keyname)
+
+            self.decryptor = XMLDecryptor(self.key)
+            mkey = fetch(self.doc, "./pskc:MACMethod/pskc:MACKey")
+            algo = fetch(self.doc, "./pskc:MACMethod/@Algorithm", HMACConverter())
+            if mkey is not None and algo is not None:
+                tmp = hmac.HMAC(self.decryptor(mkey), digestmod=algo)
+                self.decryptor = XMLDecryptor(self.key, tmp)
+
+    def run(self):
+        api.bootstrap(in_server=True)
+        api.finalize()
+
+        conn = ldap2()
+
+        try:
+            ccache = krbV.default_context().default_ccache()
+            conn.connect(ccache=ccache)
+        except (krbV.Krb5Error, errors.ACIError):
+            raise admintool.ScriptError("Unable to connect to LDAP! Did you kinit?")
+
+        keypackages = fetchAll(self.doc, "./pskc:KeyPackage")
+        if not keypackages:
+            raise admintool.ScriptError("PSKC file is invalid!")
+
+        try:
+            # Parse tokens
+            parser = TokenParser(self.decryptor)
+            for keypkg in keypackages:
+                try:
+                    id, options = parser(keypkg)
+                    api.Command.otptoken_add(id, **options)
+                except Exception as e:
+                    self.log.warn("Error adding token: %s", e)
+                else:
+                    self.log.info("Added token: %s", id)
+                    keypkg.getparent().remove(keypkg)
+        finally:
+            conn.disconnect()
+
+        # Write out the XML file without the tokens that succeeded.
+        self.doc.write(self.output)
-- 
2.0.0

_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel

Reply via email to