diff options
-rw-r--r-- | freeipa.spec.in | 2 | ||||
-rw-r--r-- | install/tools/Makefile.am | 1 | ||||
-rwxr-xr-x | install/tools/ipa-otptoken-import | 25 | ||||
-rw-r--r-- | install/tools/man/Makefile.am | 1 | ||||
-rw-r--r-- | install/tools/man/ipa-otptoken-import.1 | 36 | ||||
-rw-r--r-- | ipaserver/install/ipa_otptoken_import.py | 530 | ||||
-rw-r--r-- | ipatests/test_ipaserver/data/full.xml | 48 | ||||
-rw-r--r-- | ipatests/test_ipaserver/data/pskc-figure3.xml | 32 | ||||
-rw-r--r-- | ipatests/test_ipaserver/data/pskc-figure4.xml | 31 | ||||
-rw-r--r-- | ipatests/test_ipaserver/data/pskc-figure5.xml | 57 | ||||
-rw-r--r-- | ipatests/test_ipaserver/data/pskc-figure6.xml | 47 | ||||
-rw-r--r-- | ipatests/test_ipaserver/data/pskc-figure7.xml | 68 | ||||
-rw-r--r-- | ipatests/test_ipaserver/data/pskc-figure8.xml | 53 | ||||
-rw-r--r-- | ipatests/test_ipaserver/data/pskc-invalid.xml | 3 | ||||
-rw-r--r-- | ipatests/test_ipaserver/data/pskc-mini.xml | 4 | ||||
-rw-r--r-- | ipatests/test_ipaserver/test_otptoken_import.py | 151 |
16 files changed, 1089 insertions, 0 deletions
diff --git a/freeipa.spec.in b/freeipa.spec.in index e19fd2a19..4631a5936 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 @@ -638,6 +639,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 2cf66c6df..485be91b7 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 000000000..090116dab --- /dev/null +++ b/install/tools/ipa-otptoken-import @@ -0,0 +1,25 @@ +#! /usr/bin/python2 -E +# Authors: Nathaniel McCallum <npmccallum@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/install/tools/man/Makefile.am b/install/tools/man/Makefile.am index 33e8a9e4b..b3f39b942 100644 --- a/install/tools/man/Makefile.am +++ b/install/tools/man/Makefile.am @@ -22,6 +22,7 @@ man1_MANS = \ ipa-backup.1 \ ipa-restore.1 \ ipa-advise.1 \ + ipa-otptoken-import.1 \ $(NULL) man8_MANS = \ diff --git a/install/tools/man/ipa-otptoken-import.1 b/install/tools/man/ipa-otptoken-import.1 new file mode 100644 index 000000000..920a08ca2 --- /dev/null +++ b/install/tools/man/ipa-otptoken-import.1 @@ -0,0 +1,36 @@ +.\" A man page for ipa-otptoken-import +.\" Copyright (C) 2014 Red Hat, Inc. +.\" +.\" 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/>. +.\" +.\" Author: Nathaniel McCallum <npmccallum@redhat.com> +.\" +.TH "ipa-otptoken-import" "1" "Jun 12 2014" "FreeIPA" "FreeIPA Manual Pages" +.SH "NAME" +ipa\-otptoken\-import \- Imports OTP tokens from RFC 6030 XML file +.SH "SYNOPSIS" +ipa\-otptoken\-import [options] <infile> <outfile> +.SH "DESCRIPTION" +Running the command will attempt to import all tokens specified in \fBinfile\fR. If the command is unable to import a token, the reason for the failure will be printed to standard error and all failed tokens will be written to the \fBoutfile\fR for further inspection. + +If the \fBinfile\fR contains encrypted token data, then the \fIkeyfile\fR (\fB-k\fR) option MUST be specified. + +.SH "OPTIONS" +.TP +\fB\-k\fR \fIkeyfile\fR +File containing the key used to decrypt the token data. +.SH "EXIT STATUS" +0 if the command was successful + +1 if an error occurred diff --git a/ipaserver/install/ipa_otptoken_import.py b/ipaserver/install/ipa_otptoken_import.py new file mode 100644 index 000000000..31a690201 --- /dev/null +++ b/ipaserver/install/ipa_otptoken_import.py @@ -0,0 +1,530 @@ +# Authors: Nathaniel McCallum <npmccallum@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 abc +import base64 +import datetime +import hashlib +import hmac +import os +import uuid +import struct + +from lxml import etree +import dateutil.parser +import dateutil.tz +import nss.nss as nss +import krbV + +from ipapython import admintool +from ipalib import api, errors +from ipaserver.plugins.ldap2 import ldap2 + + +class ValidationError(Exception): + pass + + +def fetchAll(element, xpath, conv=lambda x: x): + 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=lambda x: x, default=None): + result = fetchAll(element, xpath, conv) + return result[0] if result else default + + +def convertDate(value): + "Converts an ISO 8601 string into a UTC datetime object." + + 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()) + + +def convertTokenType(value): + "Converts token algorithm URI to token type string." + + return { + "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", + }.get(value.lower(), None) + + +def convertHashName(value): + "Converts hash names to their canonical names." + + return { + "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", + }.get(value.lower(), u"sha1") + + +def convertHMACType(value): + "Converts HMAC URI to hashlib object." + + return { + "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, + }.get(value.lower(), hashlib.sha1) + + +def convertAlgorithm(value): + "Converts encryption URI to (mech, ivlen)." + + return { + "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", + }.get(value.lower(), (None, None)) + + +def convertEncrypted(value, decryptor=None, pconv=base64.b64decode, econv=lambda x: x): + "Converts a value element, decrypting if necessary. See RFC 6030." + + v = fetch(value, "./pskc:PlainValue/text()", pconv) + 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 econv(decryptor(ev, mac)) + + return None + + +class XMLKeyDerivation(object): + "Interface for XML Encryption 1.1 key derivation." + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def __init__(self, enckey): + "Sets up key derivation parameters from the parent XML entity." + + @abc.abstractmethod + def derive(self, masterkey): + "Derives a key from the master key." + + +class PBKDF2KeyDerivation(XMLKeyDerivation): + def __init__(self, enckey): + params = fetch(enckey, "./xenc11:DerivedKey/xenc11:KeyDerivationMethod/xenc11:PBKDF2-params") + if params is None: + raise ValueError("XML file is missing PBKDF2 parameters!") + + self.salt = fetch(params, "./xenc11:Salt/xenc11:Specified/text()", base64.b64decode) + self.iter = fetch(params, "./xenc11:IterationCount/text()", int) + self.klen = fetch(params, "./xenc11:KeyLength/text()", int) + self.hmod = fetch(params, "./xenc11:PRF/@Algorithm", convertHMACType, hashlib.sha1) + + if self.salt is None: + raise ValueError("XML file is missing PBKDF2 salt!") + + if self.iter is None: + raise ValueError("XML file is missing PBKDF2 iteration count!") + + if self.klen is None: + raise ValueError("XML file is missing PBKDF2 key length!") + + def derive(self, masterkey): + mac = hmac.HMAC(masterkey, None, self.hmod) + + # Figure out how many blocks we will have to combine + # to expand the master key to the desired length. + blocks = self.klen // mac.digest_size + if self.klen % mac.digest_size != 0: + blocks += 1 + + # Loop through each block adding it to the derived key. + dk = [] + for i in xrange(1, blocks + 1): + # Set initial values. + last = self.salt + struct.pack('>I', i) + hash = [0] * mac.digest_size + + # Perform n iterations. + for j in xrange(self.iter): + tmp = mac.copy() + tmp.update(last) + last = tmp.digest() + + # XOR the previous hash with the new hash. + for k in xrange(mac.digest_size): + hash[k] ^= ord(last[k]) + + # Add block to derived key. + dk.extend(hash) + + return ''.join([chr(c) for c in dk])[:self.klen] + + +def convertKeyDerivation(value): + "Converts key derivation URI to a BaseKeyDerivation class." + + return { + "http://www.rsasecurity.com/rsalabs/pkcs/schemas/pkcs-5v2-0#pbkdf2": PBKDF2KeyDerivation, + }.get(value.lower(), 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", convertAlgorithm) + 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 PSKCKeyPackage(object): + _XML = { + 'pskc:DeviceInfo': { + 'pskc:IssueNo/text()': ('issueno', unicode), + 'pskc:ExpiryDate/text()': ('notafter.hw', convertDate), + 'pskc:Manufacturer/text()': ('vendor', unicode), + 'pskc:Model/text()': ('model', unicode), + 'pskc:SerialNo/text()': ('serial', unicode), + 'pskc:StartDate/text()': ('notbefore.hw', convertDate), + 'pskc:UserId/text()': ('owner', unicode), + }, + + 'pskc:Key': { + '@Algorithm': ('type', convertTokenType), + '@Id': ('id', unicode), + 'pskc:FriendlyName/text()': ('description', unicode), + 'pskc:Issuer/text()': ('issuer', unicode), + 'pskc:KeyReference/text()': ('keyref', unicode), + + 'pskc:AlgorithmParameters': { + 'pskc:Suite/text()': ('algorithm', convertHashName), + 'pskc:ResponseFormat/@CheckDigit': ('checkdigit', unicode), + 'pskc:ResponseFormat/@Encoding': ('encoding', unicode), + 'pskc:ResponseFormat/@Length': ('digits', int), + }, + + 'pskc:Data': { + 'pskc:Counter': ('counter', lambda v, d: convertEncrypted(v, d, long, long)), + 'pskc:Secret': ('key', convertEncrypted), + 'pskc:Time': ('time', lambda v, d: convertEncrypted(v, d, int, int)), + 'pskc:TimeDrift': ('offset', lambda v, d: convertEncrypted(v, d, int, int)), + 'pskc:TimeInterval': ('interval', lambda v, d: convertEncrypted(v, d, int, int)), + }, + + 'pskc:Policy': { + 'pskc:ExpiryDate/text()': ('notafter.sw', convertDate), + 'pskc:KeyUsage/text()': ('keyusage', unicode), + 'pskc:NumberOfTransactions': ('maxtransact', lambda v: v), + 'pskc:PINPolicy': ('pinpolicy', lambda v: v), + 'pskc:StartDate/text()': ('notbefore.sw', convertDate), + }, + }, + } + + _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()), + ('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, element, decryptor): + self.__element = element + self.__decryptor = decryptor + self.__id = None + self.__options = None + + @property + def id(self): + if self.__id is None: + self.__process() + + return self.__id + + @property + def options(self): + if self.__options is None: + self.__process() + + return self.__options + + def remove(self): + self.__element.getparent().remove(self.__element) + + def __process(self): + # Parse and validate. + data = self.__parse(self.__decryptor, self.__element, ".", self._XML) + self.__validate(data) + + # Copy values into output. + options = {} + for (dk, ok, f) in self._MAP: + if dk in data: + options[ok] = f(data[dk], options) + + # Copy validity dates. + self.__dates(options, data, 'notbefore', max) + self.__dates(options, data, 'notafter', min) + + # Save attributes. + self.__options = options + self.__id = data.get('id', uuid.uuid4()) + + def __parse(self, decryptor, 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(decryptor, element, path, v)) + continue + + result = fetch(element, path) + if result is not None: + if getattr(getattr(v[1], "func_code", None), "co_argcount", 0) > 1: + data[v[0]] = v[1](result, decryptor) + else: + data[v[0]] = v[1](result) + + 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")) + + +class PSKCDocument(object): + @property + def keyname(self): + return self.__keyname + + def __init__(self, filename): + self.__keyname = None + self.__decryptor = None + self.__doc = etree.parse(filename) + self.__mkey = fetch(self.__doc, "./pskc:MACMethod/pskc:MACKey") + self.__algo = fetch(self.__doc, "./pskc:MACMethod/@Algorithm", convertHMACType) + + self.__keypackages = fetchAll(self.__doc, "./pskc:KeyPackage") + if not self.__keypackages: + raise ValueError("PSKC file is invalid!") + + self.__enckey = fetch(self.__doc, "./pskc:EncryptionKey") + if self.__enckey is not None: + # Check for x509 key. + x509key = fetch(self.__enckey, "./ds:X509Data") + if x509key is not None: + raise NotImplementedError("X.509 keys are not currently supported!") + + # Get the keyname. + self.__keyname = fetch(self.__enckey, "./ds:KeyName/text()") + if self.__keyname is None: + self.__keyname = fetch(self.__enckey, + "./xenc11:DerivedKey/xenc11:MasterKeyName/text()") + + def setKey(self, key): + # Derive the enckey if required. + kd = fetch(self.__enckey, + "./xenc11:DerivedKey/xenc11:KeyDerivationMethod/@Algorithm", + convertKeyDerivation) + if kd is not None: + key = kd(self.__enckey).derive(key) + + # Load the decryptor. + self.__decryptor = XMLDecryptor(key) + if self.__mkey is not None and self.__algo is not None: + tmp = hmac.HMAC(self.__decryptor(self.__mkey), digestmod=self.__algo) + self.__decryptor = XMLDecryptor(key, tmp) + + def getKeyPackages(self): + for kp in self.__keypackages: + yield PSKCKeyPackage(kp, self.__decryptor) + + def save(self, dest): + self.__doc.write(dest) + + +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() + + # Parse the file. + if len(self.args) < 1: + raise admintool.ScriptError("Import file required!") + self.doc = PSKCDocument(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!") + + # Verify a key is provided if one is needed. + if self.doc.keyname is not None: + if self.safe_options.keyfile is None: + raise admintool.ScriptError("Encryption key required: %s!" % self.doc.keyname) + + # Load the keyfile. + with open(self.safe_options.keyfile) as f: + self.doc.setKey(f.read()) + + 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?") + + try: + # Parse tokens + for keypkg in self.doc.getKeyPackages(): + try: + api.Command.otptoken_add(keypkg.id, **keypkg.options) + except Exception as e: + self.log.warn("Error adding token: %s", e) + else: + self.log.info("Added token: %s", keypkg.id) + keypkg.remove() + finally: + conn.disconnect() + + # Write out the XML file without the tokens that succeeded. + self.doc.save(self.output) diff --git a/ipatests/test_ipaserver/data/full.xml b/ipatests/test_ipaserver/data/full.xml new file mode 100644 index 000000000..0281b2881 --- /dev/null +++ b/ipatests/test_ipaserver/data/full.xml @@ -0,0 +1,48 @@ +<?xml version="1.0"?> +<KeyContainer xmlns="urn:ietf:params:xml:ns:keyprov:pskc" Version="1.0" Id="KCID"> + <KeyPackage> + <DeviceInfo> + <Manufacturer>iana.dummy</Manufacturer> + <SerialNo>SerialNo</SerialNo> + <Model>Model</Model> + <IssueNo>IssueNo</IssueNo> + <DeviceBinding>DeviceBinding</DeviceBinding> + <StartDate>2006-05-01T00:00:00Z</StartDate> + <ExpiryDate>2012-05-01T00:00:00Z</ExpiryDate> + <UserId>DeviceUserId</UserId> + </DeviceInfo> + <CryptoModuleInfo> + <Id>CMID</Id> + </CryptoModuleInfo> + <Key Id="KID1" Algorithm="urn:ietf:params:xml:ns:keyprov:pskc:hotp"> + <Issuer>Issuer</Issuer> + <AlgorithmParameters> + <Suite>Suite</Suite> + <ChallengeFormat Encoding="DECIMAL" Min="42" Max="4711" CheckDigits="true"/> + <ResponseFormat Encoding="DECIMAL" Length="8" CheckDigits="true"/> + </AlgorithmParameters> + <KeyProfileId>KeyProfileId</KeyProfileId> + <KeyReference>KeyReference</KeyReference> + <FriendlyName>FriendlyName</FriendlyName> + <Data> + <Secret> + <PlainValue>MTIzNDU2Nzg5MDEyMzQ1Njc4OTA=</PlainValue> + </Secret> + <Counter> + <PlainValue>0</PlainValue> + </Counter> + <TimeInterval> + <PlainValue>200</PlainValue> + </TimeInterval> + <TimeDrift> + <PlainValue>300</PlainValue> + </TimeDrift> + </Data> + <UserId>KeyUserId</UserId> + <Policy> + <StartDate>2006-05-01T00:00:00Z</StartDate> + <ExpiryDate>2006-05-31T00:00:00Z</ExpiryDate> + </Policy> + </Key> + </KeyPackage> +</KeyContainer> diff --git a/ipatests/test_ipaserver/data/pskc-figure3.xml b/ipatests/test_ipaserver/data/pskc-figure3.xml new file mode 100644 index 000000000..b02ac7945 --- /dev/null +++ b/ipatests/test_ipaserver/data/pskc-figure3.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<KeyContainer Version="1.0" + Id="exampleID1" + xmlns="urn:ietf:params:xml:ns:keyprov:pskc"> + <KeyPackage> + <DeviceInfo> + <Manufacturer>Manufacturer</Manufacturer> + <SerialNo>987654321</SerialNo> + <UserId>DC=example-bank,DC=net</UserId> + </DeviceInfo> + <CryptoModuleInfo> + <Id>CM_ID_001</Id> + </CryptoModuleInfo> + <Key Id="12345678" + Algorithm="urn:ietf:params:xml:ns:keyprov:pskc:hotp"> + <Issuer>Issuer</Issuer> + <AlgorithmParameters> + <ResponseFormat Length="8" Encoding="DECIMAL"/> + </AlgorithmParameters> + <Data> + <Secret> + <PlainValue>MTIzNDU2Nzg5MDEyMzQ1Njc4OTA= + </PlainValue> + </Secret> + <Counter> + <PlainValue>0</PlainValue> + </Counter> + </Data> + <UserId>UID=jsmith,DC=example-bank,DC=net</UserId> + </Key> + </KeyPackage> +</KeyContainer> diff --git a/ipatests/test_ipaserver/data/pskc-figure4.xml b/ipatests/test_ipaserver/data/pskc-figure4.xml new file mode 100644 index 000000000..186e02901 --- /dev/null +++ b/ipatests/test_ipaserver/data/pskc-figure4.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<KeyContainer Version="1.0" Id="exampleID1" + xmlns="urn:ietf:params:xml:ns:keyprov:pskc"> + <KeyPackage> + <DeviceInfo> + <Manufacturer>Manufacturer</Manufacturer> + <SerialNo>987654321</SerialNo> + </DeviceInfo> + <CryptoModuleInfo> + <Id>CM_ID_001</Id> + </CryptoModuleInfo> + <Key Id="12345678" + Algorithm="urn:ietf:params:xml:ns:keyprov:pskc:hotp"> + <Issuer>Issuer</Issuer> + <AlgorithmParameters> + <ResponseFormat Length="8" Encoding="DECIMAL"/> + </AlgorithmParameters> + <KeyProfileId>keyProfile1</KeyProfileId> + <KeyReference>MasterKeyLabel + </KeyReference> + <Data> + <Counter> + <PlainValue>0</PlainValue> + </Counter> + </Data> + <Policy> + <KeyUsage>OTP</KeyUsage> + </Policy> + </Key> + </KeyPackage> +</KeyContainer> diff --git a/ipatests/test_ipaserver/data/pskc-figure5.xml b/ipatests/test_ipaserver/data/pskc-figure5.xml new file mode 100644 index 000000000..16ab9bb3c --- /dev/null +++ b/ipatests/test_ipaserver/data/pskc-figure5.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<KeyContainer + Version="1.0" Id="exampleID1" + xmlns="urn:ietf:params:xml:ns:keyprov:pskc"> + <KeyPackage> + <DeviceInfo> + <Manufacturer>Manufacturer</Manufacturer> + <SerialNo>987654321</SerialNo> + </DeviceInfo> + <CryptoModuleInfo> + <Id>CM_ID_001</Id> + </CryptoModuleInfo> + <Key Id="12345678" + Algorithm="urn:ietf:params:xml:ns:keyprov:pskc:hotp"> + <Issuer>Issuer</Issuer> + <AlgorithmParameters> + <ResponseFormat Length="8" Encoding="DECIMAL"/> + </AlgorithmParameters> + <Data> + <Secret> + <PlainValue>MTIzNDU2Nzg5MDEyMzQ1Njc4OTA= + </PlainValue> + </Secret> + <Counter> + <PlainValue>0</PlainValue> + </Counter> + </Data> + <Policy> + <PINPolicy MinLength="4" MaxLength="4" + PINKeyId="123456781" PINEncoding="DECIMAL" + PINUsageMode="Local"/> + <KeyUsage>OTP</KeyUsage> + </Policy> + </Key> + </KeyPackage> + <KeyPackage> + <DeviceInfo> + <Manufacturer>Manufacturer</Manufacturer> + <SerialNo>987654321</SerialNo> + </DeviceInfo> + <CryptoModuleInfo> + <Id>CM_ID_001</Id> + </CryptoModuleInfo> + <Key Id="123456781" + Algorithm="urn:ietf:params:xml:ns:keyprov:pskc:pin"> + <Issuer>Issuer</Issuer> + <AlgorithmParameters> + <ResponseFormat Length="4" Encoding="DECIMAL"/> + </AlgorithmParameters> + <Data> + <Secret> + <PlainValue>MTIzNA==</PlainValue> + </Secret> + </Data> + </Key> + </KeyPackage> +</KeyContainer> diff --git a/ipatests/test_ipaserver/data/pskc-figure6.xml b/ipatests/test_ipaserver/data/pskc-figure6.xml new file mode 100644 index 000000000..0f4cd334f --- /dev/null +++ b/ipatests/test_ipaserver/data/pskc-figure6.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<KeyContainer Version="1.0" + xmlns="urn:ietf:params:xml:ns:keyprov:pskc" + xmlns:ds="http://www.w3.org/2000/09/xmldsig#" + xmlns:xenc="http://www.w3.org/2001/04/xmlenc#"> + <EncryptionKey> + <ds:KeyName>Pre-shared-key</ds:KeyName> + </EncryptionKey> + <MACMethod Algorithm="http://www.w3.org/2000/09/xmldsig#hmac-sha1"> + <MACKey> + <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/> + <xenc:CipherData> + <xenc:CipherValue>ESIzRFVmd4iZABEiM0RVZgKn6WjLaTC1sbeBMSvIhRejN9vJa2BOlSaMrR7I5wSX</xenc:CipherValue> + </xenc:CipherData> + </MACKey> + </MACMethod> + <KeyPackage> + <DeviceInfo> + <Manufacturer>Manufacturer</Manufacturer> + <SerialNo>987654321</SerialNo> + </DeviceInfo> + <CryptoModuleInfo> + <Id>CM_ID_001</Id> + </CryptoModuleInfo> + <Key Id="12345678" + Algorithm="urn:ietf:params:xml:ns:keyprov:pskc:hotp"> + <Issuer>Issuer</Issuer> + <AlgorithmParameters> + <ResponseFormat Length="8" Encoding="DECIMAL"/> + </AlgorithmParameters> + <Data> + <Secret> + <EncryptedValue> + <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/> + <xenc:CipherData> + <xenc:CipherValue>AAECAwQFBgcICQoLDA0OD+cIHItlB3Wra1DUpxVvOx2lef1VmNPCMl8jwZqIUqGv</xenc:CipherValue> + </xenc:CipherData> + </EncryptedValue> + <ValueMAC>Su+NvtQfmvfJzF6bmQiJqoLRExc=</ValueMAC> + </Secret> + <Counter> + <PlainValue>0</PlainValue> + </Counter> + </Data> + </Key> + </KeyPackage> +</KeyContainer> diff --git a/ipatests/test_ipaserver/data/pskc-figure7.xml b/ipatests/test_ipaserver/data/pskc-figure7.xml new file mode 100644 index 000000000..1fb04fc31 --- /dev/null +++ b/ipatests/test_ipaserver/data/pskc-figure7.xml @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8"?> +<pskc:KeyContainer + xmlns:pskc="urn:ietf:params:xml:ns:keyprov:pskc" + xmlns:xenc11="http://www.w3.org/2009/xmlenc11#" + xmlns:pkcs5="http://www.rsasecurity.com/rsalabs/pkcs/schemas/pkcs-5v2-0#" + xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Version="1.0"> + <pskc:EncryptionKey> + <xenc11:DerivedKey> + <xenc11:KeyDerivationMethod + Algorithm="http://www.rsasecurity.com/rsalabs/pkcs/schemas/pkcs-5v2-0#pbkdf2"> + <xenc11:PBKDF2-params> + <xenc11:Salt> + <xenc11:Specified>Ej7/PEpyEpw=</xenc11:Specified> + </xenc11:Salt> + <xenc11:IterationCount>1000</xenc11:IterationCount> + <xenc11:KeyLength>16</xenc11:KeyLength> + <xenc11:PRF/> + </xenc11:PBKDF2-params> + </xenc11:KeyDerivationMethod> + <xenc:ReferenceList> + <xenc:DataReference URI="#ED"/> + </xenc:ReferenceList> + <xenc11:MasterKeyName>My Password 1</xenc11:MasterKeyName> + </xenc11:DerivedKey> + </pskc:EncryptionKey> + <pskc:MACMethod + Algorithm="http://www.w3.org/2000/09/xmldsig#hmac-sha1"> + <pskc:MACKey> + <xenc:EncryptionMethod + Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/> + <xenc:CipherData> + <xenc:CipherValue> + 2GTTnLwM3I4e5IO5FkufoOEiOhNj91fhKRQBtBJYluUDsPOLTfUvoU2dStyOwYZx + </xenc:CipherValue> + </xenc:CipherData> + </pskc:MACKey> + </pskc:MACMethod> + <pskc:KeyPackage> + <pskc:DeviceInfo> + <pskc:Manufacturer>TokenVendorAcme</pskc:Manufacturer> + <pskc:SerialNo>987654321</pskc:SerialNo> + </pskc:DeviceInfo> + <pskc:CryptoModuleInfo> + <pskc:Id>CM_ID_001</pskc:Id> + </pskc:CryptoModuleInfo> + <pskc:Key Algorithm="urn:ietf:params:xml:ns:keyprov:pskc:hotp" Id="123456"> + <pskc:Issuer>Example-Issuer</pskc:Issuer> + <pskc:AlgorithmParameters> + <pskc:ResponseFormat Length="8" Encoding="DECIMAL"/> + </pskc:AlgorithmParameters> + <pskc:Data> + <pskc:Secret> + <pskc:EncryptedValue Id="ED"> + <xenc:EncryptionMethod + Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/> + <xenc:CipherData> + <xenc:CipherValue> + oTvo+S22nsmS2Z/RtcoF8Hfh+jzMe0RkiafpoDpnoZTjPYZu6V+A4aEn032yCr4f + </xenc:CipherValue> + </xenc:CipherData> + </pskc:EncryptedValue> + <pskc:ValueMAC>LP6xMvjtypbfT9PdkJhBZ+D6O4w= + </pskc:ValueMAC> + </pskc:Secret> + </pskc:Data> + </pskc:Key> + </pskc:KeyPackage> +</pskc:KeyContainer> diff --git a/ipatests/test_ipaserver/data/pskc-figure8.xml b/ipatests/test_ipaserver/data/pskc-figure8.xml new file mode 100644 index 000000000..c9f63cf02 --- /dev/null +++ b/ipatests/test_ipaserver/data/pskc-figure8.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<KeyContainer + xmlns:ds="http://www.w3.org/2000/09/xmldsig#" + xmlns="urn:ietf:params:xml:ns:keyprov:pskc" + xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" + Id="KC0001" + Version="1.0"> + <EncryptionKey> + <ds:X509Data> + <ds:X509Certificate>MIIB5zCCAVCgAwIBAgIESZp/vDANBgkqhkiG9w0BAQUFADA4M + Q0wCwYDVQQKEwRJRVRGMRMwEQYDVQQLEwpLZXlQcm92IFdHMRIwEAYDVQQDEwlQU0tDIF + Rlc3QwHhcNMDkwMjE3MDkxMzMyWhcNMTEwMjE3MDkxMzMyWjA4MQ0wCwYDVQQKEwRJRVR + GMRMwEQYDVQQLEwpLZXlQcm92IFdHMRIwEAYDVQQDEwlQU0tDIFRlc3QwgZ8wDQYJKoZI + hvcNAQEBBQADgY0AMIGJAoGBALCWLDa2ItYJ6su80hd1gL4cggQYdyyKK17btt/aS6Q/e + DsKjsPyFIODsxeKVV/uA3wLT4jQJM5euKJXkDajzGGOy92+ypfzTX4zDJMkh61SZwlHNJ + xBKilAM5aW7C+BQ0RvCxvdYtzx2LTdB+X/KMEBA7uIYxLfXH2Mnub3WIh1AgMBAAEwDQY + JKoZIhvcNAQEFBQADgYEAe875m84sYUJ8qPeZ+NG7REgTvlHTmoCdoByU0LBBLotUKuqf + rnRuXJRMeZXaaEGmzY1kLonVjQGzjAkU4dJ+RPmiDlYuHLZS41Pg6VMwY+03lhk6I5A/w + 4rnqdkmwZX/NgXg06alnc2pBsXWhL4O7nk0S2ZrLMsQZ6HcsXgdmHo= + </ds:X509Certificate> + </ds:X509Data> + </EncryptionKey> + <KeyPackage> + <DeviceInfo> + <Manufacturer>TokenVendorAcme</Manufacturer> + <SerialNo>987654321</SerialNo> + </DeviceInfo> + <Key Id="MBK000000001" + Algorithm="urn:ietf:params:xml:ns:keyprov:pskc:hotp"> + <Issuer>Example-Issuer</Issuer> + <AlgorithmParameters> + <ResponseFormat Length="6" Encoding="DECIMAL"/> + </AlgorithmParameters> + <Data> + <Secret> + <EncryptedValue> + <xenc:EncryptionMethod + Algorithm="http://www.w3.org/2001/04/xmlenc#rsa_1_5"/> + <xenc:CipherData> + <xenc:CipherValue>hJ+fvpoMPMO9BYpK2rdyQYGIxiATYHTHC7e/sPLKYo5/r1v+4 + xTYG3gJolCWuVMydJ7Ta0GaiBPHcWa8ctCVYmHKfSz5fdeV5nqbZApe6dofTqhRwZK6 + Yx4ufevi91cjN2vBpSxYafvN3c3+xIgk0EnTV4iVPRCR0rBwyfFrPc4= + </xenc:CipherValue> + </xenc:CipherData> + </EncryptedValue> + </Secret> + <Counter> + <PlainValue>0</PlainValue> + </Counter> + </Data> + </Key> + </KeyPackage> +</KeyContainer> diff --git a/ipatests/test_ipaserver/data/pskc-invalid.xml b/ipatests/test_ipaserver/data/pskc-invalid.xml new file mode 100644 index 000000000..688e3479d --- /dev/null +++ b/ipatests/test_ipaserver/data/pskc-invalid.xml @@ -0,0 +1,3 @@ +<?xml version="1.0"?> +<SomethingElse> +</SomethingElse> diff --git a/ipatests/test_ipaserver/data/pskc-mini.xml b/ipatests/test_ipaserver/data/pskc-mini.xml new file mode 100644 index 000000000..e6ee7b55c --- /dev/null +++ b/ipatests/test_ipaserver/data/pskc-mini.xml @@ -0,0 +1,4 @@ +<?xml version="1.0"?> +<KeyContainer xmlns="urn:ietf:params:xml:ns:keyprov:pskc" Version="1.0"> + <KeyPackage/> +</KeyContainer> diff --git a/ipatests/test_ipaserver/test_otptoken_import.py b/ipatests/test_ipaserver/test_otptoken_import.py new file mode 100644 index 000000000..7ee0754da --- /dev/null +++ b/ipatests/test_ipaserver/test_otptoken_import.py @@ -0,0 +1,151 @@ +# Authors: +# Nathaniel McCallum <npmccallum@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 os +import sys +import nose +from nss import nss + +from ipaserver.install.ipa_otptoken_import import PSKCDocument, ValidationError + +basename = os.path.join(os.path.dirname(__file__), "data") + +class test_otptoken_import(object): + def test_figure3(self): + doc = PSKCDocument(os.path.join(basename, "pskc-figure3.xml")) + assert doc.keyname is None + assert [(t.id, t.options) for t in doc.getKeyPackages()] == \ + [(u'12345678', { + 'ipatokenotpkey': u'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', + 'ipatokenvendor': u'Manufacturer', + 'ipatokenserial': u'987654321', + 'ipatokenhotpcounter': 0L, + 'ipatokenotpdigits': 8, + 'type': u'hotp', + })] + + def test_figure4(self): + doc = PSKCDocument(os.path.join(basename, "pskc-figure4.xml")) + assert doc.keyname is None + try: + [(t.id, t.options) for t in doc.getKeyPackages()] + except ValidationError: # Referenced keys are not supported. + pass + else: + assert False + + def test_figure5(self): + doc = PSKCDocument(os.path.join(basename, "pskc-figure5.xml")) + assert doc.keyname is None + try: + [(t.id, t.options) for t in doc.getKeyPackages()] + except ValidationError: # PIN Policy is not supported. + pass + else: + assert False + + def test_figure6(self): + nss.nss_init_nodb() + try: + doc = PSKCDocument(os.path.join(basename, "pskc-figure6.xml")) + assert doc.keyname == 'Pre-shared-key' + doc.setKey('12345678901234567890123456789012'.decode('hex')) + assert [(t.id, t.options) for t in doc.getKeyPackages()] == \ + [(u'12345678', { + 'ipatokenotpkey': u'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', + 'ipatokenvendor': u'Manufacturer', + 'ipatokenserial': u'987654321', + 'ipatokenhotpcounter': 0L, + 'ipatokenotpdigits': 8, + 'type': u'hotp'})] + finally: + nss.nss_shutdown() + + def test_figure7(self): + nss.nss_init_nodb() + try: + doc = PSKCDocument(os.path.join(basename, "pskc-figure7.xml")) + assert doc.keyname == 'My Password 1' + doc.setKey('qwerty') + assert [(t.id, t.options) for t in doc.getKeyPackages()] == \ + [(u'123456', { + 'ipatokenotpkey': u'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', + 'ipatokenvendor': u'TokenVendorAcme', + 'ipatokenserial': u'987654321', + 'ipatokenotpdigits': 8, + 'type': u'hotp'})] + finally: + nss.nss_shutdown() + + def test_figure8(self): + nss.nss_init_nodb() + try: + doc = PSKCDocument(os.path.join(basename, "pskc-figure8.xml")) + except NotImplementedError: # X.509 is not supported. + pass + else: + assert False + finally: + nss.nss_shutdown() + + def test_invalid(self): + nss.nss_init_nodb() + try: + doc = PSKCDocument(os.path.join(basename, "pskc-invalid.xml")) + except ValueError: # File is invalid. + pass + else: + assert False + finally: + nss.nss_shutdown() + + def test_mini(self): + nss.nss_init_nodb() + try: + doc = PSKCDocument(os.path.join(basename, "pskc-mini.xml")) + [(t.id, t.options) for t in doc.getKeyPackages()] + except ValidationError: # Unsupported token type. + pass + else: + assert False + finally: + nss.nss_shutdown() + + def test_full(self): + nss.nss_init_nodb() + try: + doc = PSKCDocument(os.path.join(basename, "full.xml")) + assert [(t.id, t.options) for t in doc.getKeyPackages()] == \ + [(u'KID1', { + 'ipatokenotpkey': u'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ', + 'ipatokennotafter': u'20060531000000Z', + 'ipatokennotbefore': u'20060501000000Z', + 'ipatokenserial': u'SerialNo-IssueNo', + 'ipatokentotpclockoffset': 60000, + 'ipatokenotpalgorithm': u'sha1', + 'ipatokenvendor': u'iana.dummy', + 'description': u'FriendlyName', + 'ipatokentotptimestep': 200, + 'ipatokenhotpcounter': 0L, + 'ipatokenmodel': u'Model', + 'ipatokenotpdigits': 8, + 'type': u'hotp', + })] + finally: + nss.nss_shutdown() |