Repository: rucio/rucio

Issue:
SSH public key padding
Motivation
----------

The base64 decoder requires width-padded strings, i.e., some strings need extra = padding elements in the string. Some keygens don't add them. Properly handle this corner case.


Discussion:
Adding a `None` ssh identity (in an attempt to avoid using default values in the bootstrap section) also results in padding error.

Resolving patch:
diff --git a/lib/rucio/core/authentication.py b/lib/rucio/core/authentication.py
--- a/lib/rucio/core/authentication.py
+++ b/lib/rucio/core/authentication.py
@@ -223,7 +223,9 @@ def get_auth_token_ssh(account, signature, appid, ip=None, session=None):
     # try all available SSH identities for the account with the provided signature
     match = False
     for identity in identities:
-        pub_k = paramiko.RSAKey(data=b64decode(identity['identity'].split()[1]))
+        data = identity['identity'].split()[1]
+        data += '=' * ((4 - len(data) % 4) % 4)  # adding required padding
+        pub_k = paramiko.RSAKey(data=b64decode(data))
         for challenge_token in active_challenge_tokens:
             if pub_k.verify_ssh_sig(str(challenge_token['token']).encode(),
                                     paramiko.Message(signature)):
diff --git a/lib/rucio/web/rest/flaskapi/v1/auth.py b/lib/rucio/web/rest/flaskapi/v1/auth.py
--- a/lib/rucio/web/rest/flaskapi/v1/auth.py
+++ b/lib/rucio/web/rest/flaskapi/v1/auth.py
@@ -735,6 +735,7 @@ def get(self):
 
         # decode the signature which must come in base64 encoded
         try:
+            signature += '=' * ((4 - len(signature) % 4) % 4)  # adding required padding
             signature = base64.b64decode(signature)
         except TypeError:
             return generate_http_error_flask(


Test patch:
diff --git a/lib/rucio/tests/test_authentication.py b/lib/rucio/tests/test_authentication.py
--- a/lib/rucio/tests/test_authentication.py
+++ b/lib/rucio/tests/test_authentication.py
@@ -50,6 +50,15 @@
              "lnvFLTlDI5Mgh4Z11NraQ8pv4YE1woolYpqOc/IMMBBXFniTT"\
              "4tC7cgikxWb9ZmFe+r4t6yCDpX4IL8L5GOQ== test_comment"
 
+INVALID_PADDED_PUBLIC_KEY = "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq5LySllrQFpPL"\
+    "614sulXQ7wnIr1aGhGtl8b+HCB/0FhMSMTHwSjX78UbfqEorZ"\
+    "V16rXrWPgUpvcbp2hqctw6eCbxwqcgu3uGWaeS5A0iWRw7oXU"\
+    "h6ydnVy89zGzX1FJFFDZ+AgiZ3ytp55tg1bjqqhK1OSC0pJxd"\
+    "Ne878TRVVo5MLI0S/rZY2UovCSGFaQG2iLj14wz/YqI7NFMUu"\
+    "JFR4e6xmNsOP7fCZ4bGMsmnhR0GmY0dWYTupNiP5WdYXAfKEx"\
+    "lnvFLTlDI5Mgh4Z11NraQ8pv4YE1woolYpqOc/IMMBBXFniTT"\
+    "4tC7cgikxWb9ZmFe+r4t6yCDpX4IL8L5GOQ test_comment"  # padding removed
+
 PRIVATE_KEY = """-----BEGIN RSA PRIVATE KEY-----
 MIIEoQIBAAKCAQEAq5LySllrQFpPL614sulXQ7wnIr1aGhGtl8b+HCB/0FhMSMTH
 wSjX78UbfqEorZV16rXrWPgUpvcbp2hqctw6eCbxwqcgu3uGWaeS5A0iWRw7oXUh
@@ -84,6 +93,7 @@ class TestAuthCoreApi(unittest.TestCase):
     '''
     TestAuthCoreApi
     '''
+
     def setUp(self):
         if config_get_bool('common', 'multi_vo', raise_exception=False, default=False):
             self.vo = {'vo': config_get('client', 'vo', raise_exception=False, default='tst')}
@@ -136,6 +146,24 @@ def test_get_auth_token_ssh_fail(self):
 
         del_account_identity(PUBLIC_KEY, IdentityType.SSH, root)
 
+    def test_invalid_padding(self):
+        """AUTHENTICATION (CORE): SSH RSA public key exchange (public key with invalid padding)."""
+
+        root = InternalAccount('root', **self.vo)
+        try:
+            add_account_identity(INVALID_PADDED_PUBLIC_KEY, IdentityType.SSH, root, email='ph-adp-ddm-lab@cern.ch')
+        except Duplicate:
+            pass  # might already exist, can skip
+
+        challenge_token = get_ssh_challenge_token(account='root', appid='test', ip='127.0.0.1', **self.vo).token
+
+        ssh_sign_string = ssh_sign(PRIVATE_KEY, challenge_token)
+        signature = base64.b64decode(ssh_sign_string)
+        result = get_auth_token_ssh(account='root', signature=signature, appid='test', ip='127.0.0.1', **self.vo)
+        assert result is not None
+
+        del_account_identity(INVALID_PADDED_PUBLIC_KEY, IdentityType.SSH, root)
+
     def test_get_auth_token_saml_success(self):
         """AUTHENTICATION (CORE): SAML NameID (correct credentials)."""
         root = InternalAccount('root', **self.vo)