diff options
author | Legrandin <gooksankoo@hoiptorrow.mailexpire.com> | 2011-10-10 08:11:31 +0200 |
---|---|---|
committer | Legrandin <gooksankoo@hoiptorrow.mailexpire.com> | 2011-10-11 23:53:26 +0200 |
commit | 621d44a7308eea42bc4a97548b01891e6d67cbbe (patch) | |
tree | e79faadf9282da8e30e1a6b6c248e290b2f8e69a | |
parent | 674b80db746ab4898cfb6a5fe44a642c7cb4c946 (diff) | |
download | pycrypto-621d44a7308eea42bc4a97548b01891e6d67cbbe.tar.gz |
Added support for export and import of unencrypted PKCS#8 keys (with tests).
FIX: Certain public exponents were not correctly exported in OpenSSH keys.
-rw-r--r-- | lib/Crypto/PublicKey/RSA.py | 122 | ||||
-rw-r--r-- | lib/Crypto/SelfTest/PublicKey/test_importKey.py | 60 |
2 files changed, 143 insertions, 39 deletions
diff --git a/lib/Crypto/PublicKey/RSA.py b/lib/Crypto/PublicKey/RSA.py index e8b4c0a..fadfa22 100644 --- a/lib/Crypto/PublicKey/RSA.py +++ b/lib/Crypto/PublicKey/RSA.py @@ -38,7 +38,7 @@ from Crypto.Util.number import getRandomRange, bytes_to_long, long_to_bytes from Crypto.PublicKey import _RSA, _slowmath, pubkey from Crypto import Random -from Crypto.Util.asn1 import DerObject, DerSequence +from Crypto.Util.asn1 import DerObject, DerSequence, DerNull import binascii import struct @@ -166,19 +166,28 @@ class _RSAobj(pubkey.pubkey): attrs.append("private") return "<%s @0x%x %s>" % (self.__class__.__name__, id(self), ",".join(attrs)) - def exportKey(self, format='PEM', passphrase=None): + def exportKey(self, format='PEM', passphrase=None, pkcs=1): """Export this RSA key. - :Parameter format: The encoding to use to wrap the key. + :Parameter format: The format to use for wrapping the key. - - *'DER'* for PKCS#1. Always unencrypted. - - *'OpenSSH'* for OpenSSH public keys (no private key) - - *'PEM'* for RFC1421/3. Unencrypted (default) or encrypted. + - *'DER'*. Binary encoding, always unencrypted. + - *'PEM'*. Textual encoding, done according to RFC1421/3. + Unencrypted (default) or encrypted. + - *'OpenSSH'*. Textual encoding, done according to OpenSSH specification. + Only suitable for public keys (not private keys). :Type format: string :Parameter passphrase: In case of PEM, the pass phrase to derive the encryption key from. :Type passphrase: string + :Parameter pkcs: The PKCS standard to follow for encoding the key. + You have two choices: **1** (PKCS#1, RFC3447) or **8** (PKCS#8, RFC5208). + PKCS#8 is only available for private keys. + PKCS#1 is the default. + PKCS standards are not relevant for the *OpenSSH* format. + :Type pkcs: integer + :Return: A string with the encoded public or private half. :Raise ValueError: When the format is unknown. @@ -186,34 +195,38 @@ class _RSAobj(pubkey.pubkey): if format=='OpenSSH': eb = long_to_bytes(self.e) nb = long_to_bytes(self.n) - if ord(eb[0]) & 0x80: eb='\x00'+nb + if ord(eb[0]) & 0x80: eb='\x00'+eb if ord(nb[0]) & 0x80: nb='\x00'+nb keyparts = [ 'ssh-rsa', eb, nb ] keystring = ''.join([ struct.pack(">I",len(kp))+kp for kp in keyparts]) return 'ssh-rsa '+binascii.b2a_base64(keystring)[:-1] - # PKCS#1 is a direct DER encoding. PEM uses it as well, but - # wraps in an ASCII envelope. + # DER format is always used, even in case of PEM, which simply + # encodes it into BASE64. der = DerSequence() if self.has_private(): - keyType = "RSA PRIVATE" + keyType= { 1: 'RSA PRIVATE', 8: 'PRIVATE' }[pkcs] der[:] = [ 0, self.n, self.e, self.d, self.p, self.q, self.d % (self.p-1), self.d % (self.q-1), inverse(self.q, self.p) ] + if pkcs==8: + derkey = der.encode() + der = DerSequence([0]) + der.append(algorithmIdentifier) + der.append(DerObject('OCTET STRING', derkey).encode()) else: keyType = "PUBLIC" - der.append('\x30\x0D\x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x01\x01\x05\x00') + der.append(algorithmIdentifier) bitmap = DerObject('BIT STRING') - derPK = DerSequence() - derPK[:] = [ self.n, self.e ] + derPK = DerSequence( [ self.n, self.e ] ) bitmap.payload = '\x00' + derPK.encode() der.append(bitmap.encode()) if format=='DER': return der.encode() if format=='PEM': pem = "-----BEGIN %s KEY-----\n" % keyType - keyobj = None - if passphrase and keyType=='RSA PRIVATE': + objenc = None + if passphrase and keyType.endswith('PRIVATE'): # We only support 3DES for encryption import Crypto.Hash.MD5 from Crypto.Cipher import DES3 @@ -221,15 +234,15 @@ class _RSAobj(pubkey.pubkey): salt = self._randfunc(8) key = PBKDF1(passphrase, salt, 16, 1, Crypto.Hash.MD5) key += PBKDF1(key+passphrase, salt, 8, 1, Crypto.Hash.MD5) - keyobj = DES3.new(key, Crypto.Cipher.DES3.MODE_CBC, salt) + objenc = DES3.new(key, Crypto.Cipher.DES3.MODE_CBC, salt) pem += 'Proc-Type: 4,ENCRYPTED\n' pem += 'DEK-Info: DES-EDE3-CBC,' + binascii.b2a_hex(salt).upper() + '\n\n' binaryKey = der.encode() - if keyobj: + if objenc: # Add PKCS#7-like padding - padding = keyobj.block_size-len(binaryKey)%keyobj.block_size - binaryKey = keyobj.encrypt(binaryKey+chr(padding)*padding) + padding = objenc.block_size-len(binaryKey)%objenc.block_size + binaryKey = objenc.encrypt(binaryKey+chr(padding)*padding) # Each BASE64 line can take up to 64 characters (=48 bytes of data) chunks = [ binascii.b2a_base64(binaryKey[i:i+48]) for i in range(0, len(binaryKey), 48) ] @@ -371,34 +384,47 @@ class RSAImplementation(object): def _importKeyDER(self, externKey): """Import an RSA key (public or private half), encoded in DER form.""" - der = DerSequence() - der.decode(externKey, True) - if len(der)==9 and der.hasOnlyInts() and der[0]==0: + + try: + + der = DerSequence() + der.decode(externKey, True) + + # Try PKCS#1 first, for a private key + if len(der)==9 and der.hasOnlyInts() and der[0]==0: # ASN.1 RSAPrivateKey element del der[6:] # Remove d mod (p-1), d mod (q-1), and q^{-1} mod p der.append(inverse(der[4],der[5])) # Add p^{-1} mod q del der[0] # Remove version return self.construct(der[:]) - if len(der)==2: + + # Keep on trying PKCS#1, but now for a public key + if len(der)==2: # The DER object is a SubjectPublicKeyInfo SEQUENCE with two elements: - # an algorithm SEQUENCE (or algorithmIdentifier) and a subjectPublicKey BIT STRING. - # - # The first element is always the same. It contains the oid of - # the RSA algorithm and its parameters (none). - # 0x30 0x0D SEQUENCE, 12 bytes of payload - # 0x06 0x09 OBJECT IDENTIFIER, 9 bytes of payload - # 0x2A 0x86 0x48 0x86 0xF7 0x0D 0x01 0x01 0x01 - # rsaEncryption (1 2 840 113549 1 1 1) (PKCS #1) - # 0x05 0x00 NULL - # - # subjectPublicKey encapsulates the actual ASN.1 RSAPublicKey element. - if der[0]=='\x30\x0D\x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x01\x01\x05\x00': + # an 'algorithm' (or 'algorithmIdentifier') SEQUENCE and a 'subjectPublicKey' BIT STRING. + # 'algorithm' takes the value given a few lines above. + # 'subjectPublicKey' encapsulates the actual ASN.1 RSAPublicKey element. + if der[0]==algorithmIdentifier: bitmap = DerObject() bitmap.decode(der[1], True) - if bitmap.typeTag=='\x03' and bitmap.payload[0]=='\x00': + if bitmap.isType('BIT STRING') and bitmap.payload[0]=='\x00': der.decode(bitmap.payload[1:], True) if len(der)==2 and der.hasOnlyInts(): return self.construct(der[:]) + + # Try unencrypted PKCS#8 + if der[0]==0: + # The second element in the SEQUENCE is algorithmIdentifier. + # It must say RSA (see above for description). + if der[1]==algorithmIdentifier: + privateKey = DerObject() + privateKey.decode(der[2], True) + if privateKey.isType('OCTET STRING'): + return self._importKeyDER(privateKey.payload) + + except ValueError, IndexError: + pass + raise ValueError("RSA key format is not supported") def importKey(self, externKey, passphrase=None): @@ -407,8 +433,15 @@ class RSAImplementation(object): :Parameter externKey: The RSA key to import, encoded as a string. - The key can be in DER (PKCS#1), OpenSSH or in PEM format (RFC1421/3). - In case of PEM, the key can be encrypted with DES or 3TDES according to a certain pass phrase. + The key can be in any of the following formats: + + - DER + PKCS#1 (binary) + - PEM + PKCS#1 (textual, according to RFC1421/3) + - DER + PKCS#8 (binary, private key only) + - PEM + PKCS#8 (textual, according to RFC5208, private key only) + - OpenSSH (textual public key only) + + In case of PEM + PKCS#1, the key can be encrypted with DES or 3TDES according to a certain ``pass phrase``. Only OpenSSL-compatible pass phrases are supported. :Type externKey: string @@ -470,6 +503,19 @@ class RSAImplementation(object): return self._importKeyDER(externKey) raise ValueError("RSA key format is not supported") +#: This is the ASN.1 DER object that qualifies an algorithm as +#: compliant to PKCS#1 (that is, the standard RSA). +# It is found in all 'algorithm' fields (also called 'algorithmIdentifier'). +# It is a SEQUENCE with the oid assigned to RSA and with its parameters (none). +# 0x06 0x09 OBJECT IDENTIFIER, 9 bytes of payload +# 0x2A 0x86 0x48 0x86 0xF7 0x0D 0x01 0x01 0x01 +# rsaEncryption (1 2 840 113549 1 1 1) (PKCS #1) +# 0x05 0x00 NULL +algorithmIdentifier = DerSequence( + [ '\x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x01\x01', + DerNull().encode() ] + ).encode() + _impl = RSAImplementation() #: #: Randomly generate a fresh, new RSA key object. diff --git a/lib/Crypto/SelfTest/PublicKey/test_importKey.py b/lib/Crypto/SelfTest/PublicKey/test_importKey.py index fe4fee2..f48e3c0 100644 --- a/lib/Crypto/SelfTest/PublicKey/test_importKey.py +++ b/lib/Crypto/SelfTest/PublicKey/test_importKey.py @@ -31,7 +31,7 @@ from Crypto.Util.number import inverse class ImportKeyTests(unittest.TestCase): - # 512-bit RSA key generated with openssl + # 512-bit RSA key generated with openssl (pure PEM format) rsaKeyPEM = '''-----BEGIN RSA PRIVATE KEY----- MIIBOwIBAAJBAL8eJ5AKoIsjURpcEoGubZMxLD7+kT+TLr7UkvEtFrRhDDKMtuII q19FrL4pUIMymPMSLBn3hJLe30Dw48GQM4UCAwEAAQJACUSDEp8RTe32ftq8IwG8 @@ -42,6 +42,18 @@ JACAr3sJQJGxIQIgarRp+m1WSKV1MciwMaTOnbU7wxFs9DP1pva76lYBzgUCIQC9 n0CnZCJ6IZYqSt0H5N7+Q+2Ro64nuwV/OSQfM6sBwQ== -----END RSA PRIVATE KEY-----''' + # As above, but this is actually an unencrypted PKCS#8 key + rsaKeyPEM8 = '''-----BEGIN PRIVATE KEY----- +MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAvx4nkAqgiyNRGlwS +ga5tkzEsPv6RP5MuvtSS8S0WtGEMMoy24girX0WsvilQgzKY8xIsGfeEkt7fQPDj +wZAzhQIDAQABAkAJRIMSnxFN7fZ+2rwjAbxaiOXmYB3XAWIg6tn9S/xv3rdYk4mK +5BxU3b2/FTn4zL0Y9ntEDeGsMEQCgdQM+sg5AiEA8g8vPh2mGIP2KYCSK9jfVFzk +B8cmJBEDteLFNyMSSiMCIQDKH+kkeSz8yWv6t080Smi0GN9XgzgGSAYAD+KlyZoC +NwIhAIe+HDApUEvPNOxxPYd5R0R4EyiJdcokAICvewlAkbEhAiBqtGn6bVZIpXUx +yLAxpM6dtTvDEWz0M/Wm9rvqVgHOBQIhAL2fQKdkInohlipK3Qfk3v5D7ZGjrie7 +BX85JB8zqwHB +-----END PRIVATE KEY-----''' + # The same RSA private key as in rsaKeyPEM, but now encrypted rsaKeyEncryptedPEM=( @@ -84,6 +96,7 @@ Lr7UkvEtFrRhDDKMtuIIq19FrL4pUIMymPMSLBn3hJLe30Dw48GQM4UCAwEAAQ== # Obtained using 'ssh-keygen -i -m PKCS8 -f rsaPublicKeyPEM' rsaPublicKeyOpenSSH = '''ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQC/HieQCqCLI1EaXBKBrm2TMSw+/pE/ky6+1JLxLRa0YQwyjLbiCKtfRay+KVCDMpjzEiwZ94SS3t9A8OPBkDOF comment\n''' + # The private key, in PKCS#1 format encoded with DER rsaKeyDER = a2b_hex( '''3082013b020100024100bf1e27900aa08b23511a5c1281ae6d93312c3efe 913f932ebed492f12d16b4610c328cb6e208ab5f45acbe2950833298f312 @@ -98,6 +111,22 @@ Lr7UkvEtFrRhDDKMtuIIq19FrL4pUIMymPMSLBn3hJLe30Dw48GQM4UCAwEAAQ== e4defe43ed91a3ae27bb057f39241f33ab01c1 '''.replace(" ","")) + # The private key, in unencrypted PKCS#8 format encoded with DER + rsaKeyDER8 = a2b_hex( + '''30820155020100300d06092a864886f70d01010105000482013f3082013 + b020100024100bf1e27900aa08b23511a5c1281ae6d93312c3efe913f932 + ebed492f12d16b4610c328cb6e208ab5f45acbe2950833298f3122c19f78 + 492dedf40f0e3c190338502030100010240094483129f114dedf67edabc2 + 301bc5a88e5e6601dd7016220ead9fd4bfc6fdeb75893898ae41c54ddbdb + f1539f8ccbd18f67b440de1ac30440281d40cfac839022100f20f2f3e1da + 61883f62980922bd8df545ce407c726241103b5e2c53723124a23022100c + a1fe924792cfcc96bfab74f344a68b418df578338064806000fe2a5c99a0 + 23702210087be1c3029504bcf34ec713d877947447813288975ca240080a + f7b094091b12102206ab469fa6d5648a57531c8b031a4ce9db53bc3116cf + 433f5a6f6bbea5601ce05022100bd9f40a764227a21962a4add07e4defe4 + 3ed91a3ae27bb057f39241f33ab01c1 + '''.replace(" ","")) + rsaPublicKeyDER = a2b_hex( '''305c300d06092a864886f70d0101010500034b003048024100bf1e27900a a08b23511a5c1281ae6d93312c3efe913f932ebed492f12d16b4610c328c @@ -173,6 +202,25 @@ Lr7UkvEtFrRhDDKMtuIIq19FrL4pUIMymPMSLBn3hJLe30Dw48GQM4UCAwEAAQ== self.assertEqual(key.p, self.p) self.assertEqual(key.q, self.q) + def testImportKey9(self): + key = self.rsa.importKey(self.rsaKeyDER8) + self.failUnless(key.has_private()) + self.assertEqual(key.n, self.n) + self.assertEqual(key.e, self.e) + self.assertEqual(key.d, self.d) + self.assertEqual(key.p, self.p) + self.assertEqual(key.q, self.q) + + def testImportKey10(self): + key = self.rsa.importKey(self.rsaKeyPEM8) + self.failUnless(key.has_private()) + self.assertEqual(key.n, self.n) + self.assertEqual(key.e, self.e) + self.assertEqual(key.d, self.d) + self.assertEqual(key.p, self.p) + self.assertEqual(key.q, self.q) + + ### def testExportKey1(self): key = self.rsa.construct([self.n, self.e, self.d, self.p, self.q, self.pInv]) @@ -210,6 +258,16 @@ Lr7UkvEtFrRhDDKMtuIIq19FrL4pUIMymPMSLBn3hJLe30Dw48GQM4UCAwEAAQ== pemKey = key.exportKey("PEM", t[0]) self.assertEqual(pemKey, t[1]) + def testExportKey5(self): + key = self.rsa.construct([self.n, self.e, self.d, self.p, self.q, self.pInv]) + derKey = key.exportKey("DER", pkcs=8) + self.assertEqual(derKey, self.rsaKeyDER8) + + def testExportKey6(self): + key = self.rsa.construct([self.n, self.e, self.d, self.p, self.q, self.pInv]) + pemKey = key.exportKey("PEM", pkcs=8) + self.assertEqual(pemKey, self.rsaKeyPEM8) + class ImportKeyTestsSlow(ImportKeyTests): def setUp(self): self.rsa = RSA.RSAImplementation(use_fast_math=0) |