crypto modules: fix sanity errors (#60046)
* Remove sanity warnings. * Linting. * More linting. * Forgot one place. * no_bytes -> num_bytes
This commit is contained in:
parent
ceff0029cb
commit
26b9c81a8e
17 changed files with 63 additions and 64 deletions
|
@ -231,13 +231,13 @@ def _parse_key_openssl(openssl_binary, module, key_file=None, key_content=None):
|
|||
if asn1_oid_curve == 'prime256v1' or nist_curve == 'p-256':
|
||||
bits = 256
|
||||
alg = 'ES256'
|
||||
hash = 'sha256'
|
||||
hashalg = 'sha256'
|
||||
point_size = 32
|
||||
curve = 'P-256'
|
||||
elif asn1_oid_curve == 'secp384r1' or nist_curve == 'p-384':
|
||||
bits = 384
|
||||
alg = 'ES384'
|
||||
hash = 'sha384'
|
||||
hashalg = 'sha384'
|
||||
point_size = 48
|
||||
curve = 'P-384'
|
||||
elif asn1_oid_curve == 'secp521r1' or nist_curve == 'p-521':
|
||||
|
@ -245,13 +245,13 @@ def _parse_key_openssl(openssl_binary, module, key_file=None, key_content=None):
|
|||
# https://github.com/letsencrypt/boulder/issues/2217
|
||||
bits = 521
|
||||
alg = 'ES512'
|
||||
hash = 'sha512'
|
||||
hashalg = 'sha512'
|
||||
point_size = 66
|
||||
curve = 'P-521'
|
||||
else:
|
||||
return 'unknown elliptic curve: %s / %s' % (asn1_oid_curve, nist_curve), {}
|
||||
bytes = (bits + 7) // 8
|
||||
if len(pub_hex) != 2 * bytes:
|
||||
num_bytes = (bits + 7) // 8
|
||||
if len(pub_hex) != 2 * num_bytes:
|
||||
return 'bad elliptic curve point (%s / %s)' % (asn1_oid_curve, nist_curve), {}
|
||||
return None, {
|
||||
'key_file': key_file,
|
||||
|
@ -260,10 +260,10 @@ def _parse_key_openssl(openssl_binary, module, key_file=None, key_content=None):
|
|||
'jwk': {
|
||||
"kty": "EC",
|
||||
"crv": curve,
|
||||
"x": nopad_b64(pub_hex[:bytes]),
|
||||
"y": nopad_b64(pub_hex[bytes:]),
|
||||
"x": nopad_b64(pub_hex[:num_bytes]),
|
||||
"y": nopad_b64(pub_hex[num_bytes:]),
|
||||
},
|
||||
'hash': hash,
|
||||
'hash': hashalg,
|
||||
'point_size': point_size,
|
||||
}
|
||||
|
||||
|
@ -363,13 +363,13 @@ def _parse_key_cryptography(module, key_file=None, key_content=None):
|
|||
if pk.curve.name == 'secp256r1':
|
||||
bits = 256
|
||||
alg = 'ES256'
|
||||
hash = 'sha256'
|
||||
hashalg = 'sha256'
|
||||
point_size = 32
|
||||
curve = 'P-256'
|
||||
elif pk.curve.name == 'secp384r1':
|
||||
bits = 384
|
||||
alg = 'ES384'
|
||||
hash = 'sha384'
|
||||
hashalg = 'sha384'
|
||||
point_size = 48
|
||||
curve = 'P-384'
|
||||
elif pk.curve.name == 'secp521r1':
|
||||
|
@ -377,12 +377,12 @@ def _parse_key_cryptography(module, key_file=None, key_content=None):
|
|||
# https://github.com/letsencrypt/boulder/issues/2217
|
||||
bits = 521
|
||||
alg = 'ES512'
|
||||
hash = 'sha512'
|
||||
hashalg = 'sha512'
|
||||
point_size = 66
|
||||
curve = 'P-521'
|
||||
else:
|
||||
return 'unknown elliptic curve: {0}'.format(pk.curve.name), {}
|
||||
bytes = (bits + 7) // 8
|
||||
num_bytes = (bits + 7) // 8
|
||||
return None, {
|
||||
'key_obj': key,
|
||||
'type': 'ec',
|
||||
|
@ -390,10 +390,10 @@ def _parse_key_cryptography(module, key_file=None, key_content=None):
|
|||
'jwk': {
|
||||
"kty": "EC",
|
||||
"crv": curve,
|
||||
"x": nopad_b64(_convert_int_to_bytes(bytes, pk.x)),
|
||||
"y": nopad_b64(_convert_int_to_bytes(bytes, pk.y)),
|
||||
"x": nopad_b64(_convert_int_to_bytes(num_bytes, pk.x)),
|
||||
"y": nopad_b64(_convert_int_to_bytes(num_bytes, pk.y)),
|
||||
},
|
||||
'hash': hash,
|
||||
'hash': hashalg,
|
||||
'point_size': point_size,
|
||||
}
|
||||
else:
|
||||
|
@ -404,16 +404,16 @@ def _sign_request_cryptography(module, payload64, protected64, key_data):
|
|||
sign_payload = "{0}.{1}".format(protected64, payload64).encode('utf8')
|
||||
if isinstance(key_data['key_obj'], cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
|
||||
padding = cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15()
|
||||
hash = cryptography.hazmat.primitives.hashes.SHA256()
|
||||
signature = key_data['key_obj'].sign(sign_payload, padding, hash)
|
||||
hashalg = cryptography.hazmat.primitives.hashes.SHA256
|
||||
signature = key_data['key_obj'].sign(sign_payload, padding, hashalg())
|
||||
elif isinstance(key_data['key_obj'], cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
|
||||
if key_data['hash'] == 'sha256':
|
||||
hash = cryptography.hazmat.primitives.hashes.SHA256
|
||||
hashalg = cryptography.hazmat.primitives.hashes.SHA256
|
||||
elif key_data['hash'] == 'sha384':
|
||||
hash = cryptography.hazmat.primitives.hashes.SHA384
|
||||
hashalg = cryptography.hazmat.primitives.hashes.SHA384
|
||||
elif key_data['hash'] == 'sha512':
|
||||
hash = cryptography.hazmat.primitives.hashes.SHA512
|
||||
ecdsa = cryptography.hazmat.primitives.asymmetric.ec.ECDSA(hash())
|
||||
hashalg = cryptography.hazmat.primitives.hashes.SHA512
|
||||
ecdsa = cryptography.hazmat.primitives.asymmetric.ec.ECDSA(hashalg())
|
||||
r, s = cryptography.hazmat.primitives.asymmetric.utils.decode_dss_signature(key_data['key_obj'].sign(sign_payload, ecdsa))
|
||||
rr = _pad_hex(r, 2 * key_data['point_size'])
|
||||
ss = _pad_hex(s, 2 * key_data['point_size'])
|
||||
|
|
|
@ -26,6 +26,9 @@
|
|||
# Copyright (c) the OpenSSL contributors
|
||||
# For more details, search for the function _OID_MAP.
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
|
@ -1732,10 +1735,10 @@ def cryptography_get_name(name):
|
|||
raise OpenSSLObjectError('Cannot parse Subject Alternative Name "{0}" (potentially unsupported by cryptography backend)'.format(name))
|
||||
|
||||
|
||||
def _get_hex(bytes):
|
||||
if bytes is None:
|
||||
return bytes
|
||||
data = binascii.hexlify(bytes)
|
||||
def _get_hex(bytesstr):
|
||||
if bytesstr is None:
|
||||
return bytesstr
|
||||
data = binascii.hexlify(bytesstr)
|
||||
data = to_text(b':'.join(data[i:i + 2] for i in range(0, len(data), 2)))
|
||||
return data
|
||||
|
||||
|
|
|
@ -970,7 +970,7 @@ class ACMEClient(object):
|
|||
result, info = self.account.send_signed_request(auth['uri'], authz_deactivate)
|
||||
if 200 <= info['status'] < 300 and result.get('status') == 'deactivated':
|
||||
auth['status'] = 'deactivated'
|
||||
except Exception as e:
|
||||
except Exception as dummy:
|
||||
# Ignore errors on deactivating authzs
|
||||
pass
|
||||
if auth.get('status') != 'deactivated':
|
||||
|
|
|
@ -168,7 +168,7 @@ try:
|
|||
from distutils.version import LooseVersion
|
||||
HAS_CRYPTOGRAPHY = (LooseVersion(cryptography.__version__) >= LooseVersion('1.3'))
|
||||
_cryptography_backend = cryptography.hazmat.backends.default_backend()
|
||||
except ImportError as e:
|
||||
except ImportError as dummy:
|
||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
||||
HAS_CRYPTOGRAPHY = False
|
||||
|
||||
|
|
|
@ -141,7 +141,7 @@ try:
|
|||
from distutils.version import LooseVersion
|
||||
HAS_CRYPTOGRAPHY = (LooseVersion(cryptography.__version__) >= LooseVersion('1.5'))
|
||||
_cryptography_backend = cryptography.hazmat.backends.default_backend()
|
||||
except ImportError as e:
|
||||
except ImportError as dummy:
|
||||
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
|
||||
HAS_CRYPTOGRAPHY = False
|
||||
|
||||
|
@ -185,7 +185,7 @@ def is_parent(module, cert, potential_parent):
|
|||
module.warn('Unknown public key type "{0}"'.format(public_key))
|
||||
return False
|
||||
return True
|
||||
except cryptography.exceptions.InvalidSignature as e:
|
||||
except cryptography.exceptions.InvalidSignature as dummy:
|
||||
return False
|
||||
except Exception as e:
|
||||
module.fail_json(msg='Unknown error on signature validation: {0}'.format(e))
|
||||
|
@ -258,9 +258,9 @@ class CertificateSet(object):
|
|||
'''
|
||||
b_path = to_bytes(path, errors='surrogate_or_strict')
|
||||
if os.path.isdir(b_path):
|
||||
for dir, dummy, files in os.walk(b_path, followlinks=True):
|
||||
for directory, dummy, files in os.walk(b_path, followlinks=True):
|
||||
for file in files:
|
||||
self._load_file(os.path.join(dir, file))
|
||||
self._load_file(os.path.join(directory, file))
|
||||
else:
|
||||
self._load_file(b_path)
|
||||
|
||||
|
|
|
@ -362,11 +362,11 @@ class ConditionsHandler(Handler):
|
|||
# container is not open
|
||||
return None
|
||||
|
||||
if (self._module.params['name'] is None):
|
||||
if self._module.params['name'] is None:
|
||||
# container is already opened
|
||||
return name
|
||||
|
||||
if (name != self._module.params['name']):
|
||||
if name != self._module.params['name']:
|
||||
# the container is already open but with different name:
|
||||
# suspicious. back off
|
||||
self._module.fail_json(msg="LUKS container is already opened "
|
||||
|
|
|
@ -196,13 +196,11 @@ info:
|
|||
|
||||
import os
|
||||
import errno
|
||||
import random
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import MINYEAR, MAXYEAR
|
||||
from datetime import timedelta
|
||||
from shutil import copy2
|
||||
from shutil import rmtree
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
@ -366,8 +364,8 @@ class Certificate(object):
|
|||
def is_same_datetime(self, datetime_one, datetime_two):
|
||||
|
||||
# This function is for backwards compatability only because .total_seconds() is new in python2.7
|
||||
def timedelta_total_seconds(timedelta):
|
||||
return ((timedelta.microseconds + 0.0 + (timedelta.seconds + timedelta.days * 24 * 3600) * 10 ** 6) / 10 ** 6)
|
||||
def timedelta_total_seconds(time_delta):
|
||||
return (time_delta.microseconds + 0.0 + (time_delta.seconds + time_delta.days * 24 * 3600) * 10 ** 6) / 10 ** 6
|
||||
# try to use .total_ seconds() from python2.7
|
||||
try:
|
||||
return (datetime_one - datetime_two).total_seconds() == 0.0
|
||||
|
@ -519,7 +517,6 @@ class Certificate(object):
|
|||
raise CertificateError(exc)
|
||||
else:
|
||||
pass
|
||||
return
|
||||
|
||||
|
||||
def main():
|
||||
|
|
|
@ -726,9 +726,9 @@ class CertificateSigningRequestCryptography(CertificateSigningRequestBase):
|
|||
current_subject = [(sub.oid, sub.value) for sub in csr.subject]
|
||||
return set(subject) == set(current_subject)
|
||||
|
||||
def _find_extension(extensions, type):
|
||||
def _find_extension(extensions, exttype):
|
||||
return next(
|
||||
(ext for ext in extensions if isinstance(ext.value, type)),
|
||||
(ext for ext in extensions if isinstance(ext.value, exttype)),
|
||||
None
|
||||
)
|
||||
|
||||
|
|
|
@ -164,14 +164,12 @@ public_key_fingerprints:
|
|||
|
||||
|
||||
import abc
|
||||
import datetime
|
||||
import os
|
||||
import traceback
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.module_utils import crypto as crypto_utils
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils.six import string_types
|
||||
from ansible.module_utils._text import to_native, to_text, to_bytes
|
||||
from ansible.module_utils.compat import ipaddress as compat_ipaddress
|
||||
|
||||
|
|
|
@ -270,7 +270,6 @@ else:
|
|||
from ansible.module_utils import crypto as crypto_utils
|
||||
from ansible.module_utils._text import to_native, to_bytes
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils.six import string_types
|
||||
|
||||
|
||||
class PrivateKeyError(crypto_utils.OpenSSLObjectError):
|
||||
|
@ -488,7 +487,7 @@ class PrivateKeyCryptography(PrivateKeyBase):
|
|||
self.module.fail_json(msg='Your cryptography version does not support Ed448')
|
||||
|
||||
def _generate_private_key_data(self):
|
||||
format = cryptography.hazmat.primitives.serialization.PrivateFormat.TraditionalOpenSSL
|
||||
export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.TraditionalOpenSSL
|
||||
try:
|
||||
if self.type == 'RSA':
|
||||
self.privatekey = cryptography.hazmat.primitives.asymmetric.rsa.generate_private_key(
|
||||
|
@ -503,16 +502,16 @@ class PrivateKeyCryptography(PrivateKeyBase):
|
|||
)
|
||||
if CRYPTOGRAPHY_HAS_X25519_FULL and self.type == 'X25519':
|
||||
self.privatekey = cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.generate()
|
||||
format = cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8
|
||||
export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8
|
||||
if CRYPTOGRAPHY_HAS_X448 and self.type == 'X448':
|
||||
self.privatekey = cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.generate()
|
||||
format = cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8
|
||||
export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8
|
||||
if CRYPTOGRAPHY_HAS_ED25519 and self.type == 'Ed25519':
|
||||
self.privatekey = cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.generate()
|
||||
format = cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8
|
||||
export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8
|
||||
if CRYPTOGRAPHY_HAS_ED448 and self.type == 'Ed448':
|
||||
self.privatekey = cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.generate()
|
||||
format = cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8
|
||||
export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8
|
||||
if self.type == 'ECC' and self.curve in self.curves:
|
||||
if self.curves[self.curve]['deprecated']:
|
||||
self.module.warn('Elliptic curves of type {0} should not be used for new keys!'.format(self.curve))
|
||||
|
@ -520,7 +519,7 @@ class PrivateKeyCryptography(PrivateKeyBase):
|
|||
curve=self.curves[self.curve]['create'](self.size),
|
||||
backend=self.cryptography_backend
|
||||
)
|
||||
except cryptography.exceptions.UnsupportedAlgorithm as e:
|
||||
except cryptography.exceptions.UnsupportedAlgorithm as dummy:
|
||||
self.module.fail_json(msg='Cryptography backend does not support the algorithm required for {0}'.format(self.type))
|
||||
|
||||
# Select key encryption
|
||||
|
@ -534,7 +533,7 @@ class PrivateKeyCryptography(PrivateKeyBase):
|
|||
# Serialize key
|
||||
return self.privatekey.private_bytes(
|
||||
encoding=cryptography.hazmat.primitives.serialization.Encoding.PEM,
|
||||
format=format,
|
||||
format=export_format,
|
||||
encryption_algorithm=encryption_algorithm
|
||||
)
|
||||
|
||||
|
|
|
@ -440,11 +440,11 @@ class PrivateKeyInfoPyOpenSSL(PrivateKeyInfo):
|
|||
'''Convert OpenSSL BIGINT to Python integer'''
|
||||
if bn == OpenSSL._util.ffi.NULL:
|
||||
return None
|
||||
hexstr = OpenSSL._util.lib.BN_bn2hex(bn)
|
||||
try:
|
||||
hex = OpenSSL._util.lib.BN_bn2hex(bn)
|
||||
return int(OpenSSL._util.ffi.string(hex), 16)
|
||||
return int(OpenSSL._util.ffi.string(hexstr), 16)
|
||||
finally:
|
||||
OpenSSL._util.lib.OPENSSL_free(hex)
|
||||
OpenSSL._util.lib.OPENSSL_free(hexstr)
|
||||
|
||||
def _get_key_info(self):
|
||||
key_public_data = dict()
|
||||
|
|
|
@ -237,7 +237,7 @@ class PublicKey(crypto_utils.OpenSSLObject):
|
|||
else:
|
||||
try:
|
||||
return crypto.dump_publickey(crypto.FILETYPE_PEM, self.privatekey)
|
||||
except AttributeError as exc:
|
||||
except AttributeError as dummy:
|
||||
raise PublicKeyError('You need to have PyOpenSSL>=16.0.0 to generate public keys')
|
||||
|
||||
def generate(self, module):
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
# Copyright: (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class ModuleDocFragment(object):
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
from sys import argv
|
||||
from subprocess import Popen, PIPE, STDOUT
|
||||
|
||||
|
|
|
@ -211,8 +211,6 @@ lib/ansible/module_utils/compat/ipaddress.py no-assert
|
|||
lib/ansible/module_utils/compat/ipaddress.py no-unicode-literals
|
||||
lib/ansible/module_utils/connection.py future-import-boilerplate
|
||||
lib/ansible/module_utils/connection.py metaclass-boilerplate
|
||||
lib/ansible/module_utils/crypto.py future-import-boilerplate
|
||||
lib/ansible/module_utils/crypto.py metaclass-boilerplate
|
||||
lib/ansible/module_utils/database.py future-import-boilerplate
|
||||
lib/ansible/module_utils/database.py metaclass-boilerplate
|
||||
lib/ansible/module_utils/digital_ocean.py future-import-boilerplate
|
||||
|
@ -6084,8 +6082,6 @@ lib/ansible/plugins/doc_fragments/a10.py future-import-boilerplate
|
|||
lib/ansible/plugins/doc_fragments/a10.py metaclass-boilerplate
|
||||
lib/ansible/plugins/doc_fragments/aci.py future-import-boilerplate
|
||||
lib/ansible/plugins/doc_fragments/aci.py metaclass-boilerplate
|
||||
lib/ansible/plugins/doc_fragments/acme.py future-import-boilerplate
|
||||
lib/ansible/plugins/doc_fragments/acme.py metaclass-boilerplate
|
||||
lib/ansible/plugins/doc_fragments/aireos.py future-import-boilerplate
|
||||
lib/ansible/plugins/doc_fragments/aireos.py metaclass-boilerplate
|
||||
lib/ansible/plugins/doc_fragments/alicloud.py future-import-boilerplate
|
||||
|
@ -6325,8 +6321,6 @@ test/integration/targets/aws_lambda/files/mini_lambda.py metaclass-boilerplate
|
|||
test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/MyPSMU.psm1 pslint:PSUseApprovedVerbs
|
||||
test/integration/targets/expect/files/test_command.py future-import-boilerplate
|
||||
test/integration/targets/expect/files/test_command.py metaclass-boilerplate
|
||||
test/integration/targets/get_certificate/files/process_certs.py future-import-boilerplate
|
||||
test/integration/targets/get_certificate/files/process_certs.py metaclass-boilerplate
|
||||
test/integration/targets/get_url/files/testserver.py future-import-boilerplate
|
||||
test/integration/targets/get_url/files/testserver.py metaclass-boilerplate
|
||||
test/integration/targets/group/files/gidget.py future-import-boilerplate
|
||||
|
@ -6484,8 +6478,6 @@ test/units/mock/path.py future-import-boilerplate
|
|||
test/units/mock/path.py metaclass-boilerplate
|
||||
test/units/mock/yaml_helper.py future-import-boilerplate
|
||||
test/units/mock/yaml_helper.py metaclass-boilerplate
|
||||
test/units/module_utils/acme/test_acme.py future-import-boilerplate
|
||||
test/units/module_utils/acme/test_acme.py metaclass-boilerplate
|
||||
test/units/module_utils/aws/test_aws_module.py metaclass-boilerplate
|
||||
test/units/module_utils/basic/test__symbolic_mode_to_octal.py future-import-boilerplate
|
||||
test/units/module_utils/basic/test_deprecate_warn.py future-import-boilerplate
|
||||
|
@ -6640,8 +6632,6 @@ test/units/modules/cloud/xenserver/FakeXenAPI.py future-import-boilerplate
|
|||
test/units/modules/cloud/xenserver/FakeXenAPI.py metaclass-boilerplate
|
||||
test/units/modules/conftest.py future-import-boilerplate
|
||||
test/units/modules/conftest.py metaclass-boilerplate
|
||||
test/units/modules/crypto/test_luks_device.py future-import-boilerplate
|
||||
test/units/modules/crypto/test_luks_device.py metaclass-boilerplate
|
||||
test/units/modules/files/test_copy.py future-import-boilerplate
|
||||
test/units/modules/messaging/rabbitmq/test_rabbimq_user.py future-import-boilerplate
|
||||
test/units/modules/messaging/rabbitmq/test_rabbimq_user.py metaclass-boilerplate
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
import os.path
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import pytest
|
||||
from ansible.modules.crypto import luks_device
|
||||
|
||||
|
|
Loading…
Reference in a new issue