Skip to content

Commit

Permalink
Merge pull request #5563 from boundlessgeo/BD-2469-pem-key-encoding
Browse files Browse the repository at this point in the history
[auth][bugfix] Import pvt keys with unknown file extension
  • Loading branch information
elpaso committed Nov 13, 2017
2 parents 2c49631 + 822a123 commit fe4f150
Show file tree
Hide file tree
Showing 16 changed files with 155 additions and 76 deletions.
3 changes: 1 addition & 2 deletions python/core/auth/qgsauthcertutils.sip
Expand Up @@ -81,11 +81,10 @@ Map certificate sha1 to certificate as simple cache
%End


static QByteArray fileData( const QString &path, bool astext = false );
static QByteArray fileData( const QString &path );
%Docstring
Return data from a local file via a read-only operation
\param path Path to file to read
\param astext Whether to open the file as text, otherwise as binary
:return: All data contained in file or empty contents if file does not exist
:rtype: QByteArray
%End
Expand Down
70 changes: 42 additions & 28 deletions src/core/auth/qgsauthcertutils.cpp
Expand Up @@ -101,7 +101,7 @@ QMap<QString, QList<QgsAuthConfigSslServer> > QgsAuthCertUtils::sslConfigsGroupe
return orgconfigs;
}

QByteArray QgsAuthCertUtils::fileData( const QString &path, bool astext )
QByteArray QgsAuthCertUtils::fileData( const QString &path )
{
QByteArray data;
QFile file( path );
Expand All @@ -112,8 +112,6 @@ QByteArray QgsAuthCertUtils::fileData( const QString &path, bool astext )
}
// TODO: add checks for locked file, etc., to ensure it can be read
QFile::OpenMode openflags( QIODevice::ReadOnly );
if ( astext )
openflags |= QIODevice::Text;
bool ret = file.open( openflags );
if ( ret )
{
Expand All @@ -128,7 +126,7 @@ QList<QSslCertificate> QgsAuthCertUtils::certsFromFile( const QString &certspath
{
QList<QSslCertificate> certs;
bool pem = certspath.endsWith( QLatin1String( ".pem" ), Qt::CaseInsensitive );
certs = QSslCertificate::fromData( QgsAuthCertUtils::fileData( certspath, pem ), pem ? QSsl::Pem : QSsl::Der );
certs = QSslCertificate::fromData( QgsAuthCertUtils::fileData( certspath ), pem ? QSsl::Pem : QSsl::Der );
if ( certs.isEmpty() )
{
QgsDebugMsg( QString( "Parsed cert(s) EMPTY for path: %1" ).arg( certspath ) );
Expand Down Expand Up @@ -191,37 +189,53 @@ QSslKey QgsAuthCertUtils::keyFromFile( const QString &keypath,
const QString &keypass,
QString *algtype )
{
bool pem = keypath.endsWith( QLatin1String( ".pem" ), Qt::CaseInsensitive );
QByteArray keydata( QgsAuthCertUtils::fileData( keypath, pem ) );

// The approach here is to try all possible encodings and algorithms
QByteArray keydata( QgsAuthCertUtils::fileData( keypath ) );
QSslKey clientkey;
clientkey = QSslKey( keydata,
QSsl::Rsa,
pem ? QSsl::Pem : QSsl::Der,
QSsl::PrivateKey,
!keypass.isEmpty() ? keypass.toUtf8() : QByteArray() );
if ( clientkey.isNull() )
{
// try DSA algorithm, since Qt can't seem to determine it otherwise

QSsl::EncodingFormat keyEncoding( keydata.contains( QByteArrayLiteral( "-----BEGIN " ) ) ?
QSsl::Pem :
QSsl::Der );

const std::vector<QSsl::KeyAlgorithm> algs
{
QSsl::KeyAlgorithm::Rsa,
QSsl::KeyAlgorithm::Dsa,
QSsl::KeyAlgorithm::Ec,
QSsl::KeyAlgorithm::Opaque
};

for ( const auto &alg : algs )
{
clientkey = QSslKey( keydata,
QSsl::Dsa,
pem ? QSsl::Pem : QSsl::Der,
alg,
keyEncoding,
QSsl::PrivateKey,
!keypass.isEmpty() ? keypass.toUtf8() : QByteArray() );
if ( clientkey.isNull() )
if ( ! clientkey.isNull() )
{
return QSslKey();
if ( algtype )
{
switch ( alg )
{
case QSsl::KeyAlgorithm::Rsa:
*algtype = QStringLiteral( "rsa" );
break;
case QSsl::KeyAlgorithm::Dsa:
*algtype = QStringLiteral( "dsa" );
break;
case QSsl::KeyAlgorithm::Ec:
*algtype = QStringLiteral( "ec" );
break;
case QSsl::KeyAlgorithm::Opaque:
*algtype = QStringLiteral( "opaque" );
break;
}
}
return clientkey;
}
if ( algtype )
*algtype = QStringLiteral( "dsa" );
}
else
{
if ( algtype )
*algtype = QStringLiteral( "rsa" );
}

return clientkey;
return QSslKey();
}

QList<QSslCertificate> QgsAuthCertUtils::certsFromString( const QString &pemtext )
Expand Down
3 changes: 1 addition & 2 deletions src/core/auth/qgsauthcertutils.h
Expand Up @@ -108,10 +108,9 @@ class CORE_EXPORT QgsAuthCertUtils
/**
* Return data from a local file via a read-only operation
* \param path Path to file to read
* \param astext Whether to open the file as text, otherwise as binary
* \returns All data contained in file or empty contents if file does not exist
*/
static QByteArray fileData( const QString &path, bool astext = false );
static QByteArray fileData( const QString &path );

//! Return list of concatenated certs from a PEM or DER formatted file
static QList<QSslCertificate> certsFromFile( const QString &certspath );
Expand Down
23 changes: 2 additions & 21 deletions src/core/auth/qgsauthconfig.cpp
Expand Up @@ -183,35 +183,16 @@ const QgsPkiBundle QgsPkiBundle::fromPemPaths( const QString &certPath,
if ( !certPath.isEmpty() && !keyPath.isEmpty()
&& ( certPath.endsWith( QLatin1String( ".pem" ), Qt::CaseInsensitive )
|| certPath.endsWith( QLatin1String( ".der" ), Qt::CaseInsensitive ) )
&& ( keyPath.endsWith( QLatin1String( ".pem" ), Qt::CaseInsensitive )
|| keyPath.endsWith( QLatin1String( ".der" ), Qt::CaseInsensitive ) )
&& QFile::exists( certPath ) && QFile::exists( keyPath )
)
{
// client cert
bool pem = certPath.endsWith( QLatin1String( ".pem" ), Qt::CaseInsensitive );
QSslCertificate clientcert( QgsAuthCertUtils::fileData( certPath, pem ), pem ? QSsl::Pem : QSsl::Der );
QSslCertificate clientcert( QgsAuthCertUtils::fileData( certPath ), pem ? QSsl::Pem : QSsl::Der );
pkibundle.setClientCert( clientcert );

// client key
bool pem_key = keyPath.endsWith( QLatin1String( ".pem" ), Qt::CaseInsensitive );
QByteArray keydata( QgsAuthCertUtils::fileData( keyPath, pem_key ) );

QSslKey clientkey;
clientkey = QSslKey( keydata,
QSsl::Rsa,
pem_key ? QSsl::Pem : QSsl::Der,
QSsl::PrivateKey,
!keyPass.isNull() ? keyPass.toUtf8() : QByteArray() );
if ( clientkey.isNull() )
{
// try DSA algorithm, since Qt can't seem to determine it otherwise
clientkey = QSslKey( keydata,
QSsl::Dsa,
pem_key ? QSsl::Pem : QSsl::Der,
QSsl::PrivateKey,
!keyPass.isNull() ? keyPass.toUtf8() : QByteArray() );
}
clientkey = QgsAuthCertUtils::keyFromFile( keyPath, keyPass );
pkibundle.setClientKey( clientkey );
if ( !caChain.isEmpty() )
{
Expand Down
2 changes: 1 addition & 1 deletion src/gui/auth/qgsauthimportidentitydialog.cpp
Expand Up @@ -288,7 +288,7 @@ bool QgsAuthImportIdentityDialog::validatePkiPaths()

// check for valid private key and that any supplied password works
bool keypem = keypath.endsWith( QLatin1String( ".pem" ), Qt::CaseInsensitive );
QByteArray keydata( QgsAuthCertUtils::fileData( keypath, keypem ) );
QByteArray keydata( QgsAuthCertUtils::fileData( keypath ) );

QSslKey clientkey;
QString keypass = lePkiPathsKeyPass->text();
Expand Down
14 changes: 7 additions & 7 deletions tests/src/core/testqgsauthcertutils.cpp
Expand Up @@ -95,12 +95,12 @@ void TestQgsAuthCertUtils::testPkcsUtils()
{
QByteArray pkcs;

pkcs = QgsAuthCertUtils::fileData( sPkiData + "/gerardus_key.pem", false );
pkcs = QgsAuthCertUtils::fileData( sPkiData + "/gerardus_key.pem" );
QVERIFY( !pkcs.isEmpty() );
QVERIFY( !QgsAuthCertUtils::pemIsPkcs8( QString( pkcs ) ) );

pkcs.clear();
pkcs = QgsAuthCertUtils::fileData( sPkiData + "/gerardus_key-pkcs8-rsa.pem", false );
pkcs = QgsAuthCertUtils::fileData( sPkiData + "/gerardus_key-pkcs8-rsa.pem" );
QVERIFY( !pkcs.isEmpty() );
QVERIFY( QgsAuthCertUtils::pemIsPkcs8( QString( pkcs ) ) );

Expand All @@ -116,31 +116,31 @@ void TestQgsAuthCertUtils::testPkcsUtils()
pkcs.clear();
pkcs1.clear();
// Is actually a PKCS#1 key, not #8
pkcs = QgsAuthCertUtils::fileData( sPkiData + "/gerardus_key.der", false );
pkcs = QgsAuthCertUtils::fileData( sPkiData + "/gerardus_key.der" );
QVERIFY( !pkcs.isEmpty() );
pkcs1 = QgsAuthCertUtils::pkcs8PrivateKey( pkcs );
QVERIFY( pkcs1.isEmpty() );

pkcs.clear();
pkcs1.clear();
// Is PKCS#1 PEM text, not DER
pkcs = QgsAuthCertUtils::fileData( sPkiData + "/gerardus_key.pem", false );
pkcs = QgsAuthCertUtils::fileData( sPkiData + "/gerardus_key.pem" );
QVERIFY( !pkcs.isEmpty() );
pkcs1 = QgsAuthCertUtils::pkcs8PrivateKey( pkcs );
QVERIFY( pkcs1.isEmpty() );

pkcs.clear();
pkcs1.clear();
// Is PKCS#8 PEM text, not DER
pkcs = QgsAuthCertUtils::fileData( sPkiData + "/gerardus_key-pkcs8-rsa.pem", false );
pkcs = QgsAuthCertUtils::fileData( sPkiData + "/gerardus_key-pkcs8-rsa.pem" );
QVERIFY( !pkcs.isEmpty() );
pkcs1 = QgsAuthCertUtils::pkcs8PrivateKey( pkcs );
QVERIFY( pkcs1.isEmpty() );

pkcs.clear();
pkcs1.clear();
// Correct PKCS#8 DER input
pkcs = QgsAuthCertUtils::fileData( sPkiData + "/gerardus_key-pkcs8-rsa.der", false );
pkcs = QgsAuthCertUtils::fileData( sPkiData + "/gerardus_key-pkcs8-rsa.der" );
QVERIFY( !pkcs.isEmpty() );
pkcs1 = QgsAuthCertUtils::pkcs8PrivateKey( pkcs );
QVERIFY( !pkcs1.isEmpty() );
Expand All @@ -156,7 +156,7 @@ void TestQgsAuthCertUtils::testPkcsUtils()
QVERIFY( !pkcs1Key.isNull() );

// Converted PKCS#8 DER should match PKCS#1 PEM
QByteArray pkcs1PemRef = QgsAuthCertUtils::fileData( sPkiData + "/gerardus_key.pem", true );
QByteArray pkcs1PemRef = QgsAuthCertUtils::fileData( sPkiData + "/gerardus_key.pem" );
QVERIFY( !pkcs1PemRef.isEmpty() );
QCOMPARE( pkcs1Key.toPem(), pkcs1PemRef );
#endif
Expand Down
53 changes: 38 additions & 15 deletions tests/src/python/test_qgsauthsystem.py
Expand Up @@ -71,6 +71,14 @@ def widget_dialog(self, widget):
dlg.setLayout(layout)
return dlg

def mkPEMBundle(self, client_cert, client_key, password, chain):
return QgsPkiBundle.fromPemPaths(PKIDATA + '/' + client_cert,
PKIDATA + '/' + client_key,
password,
QgsAuthCertUtils.certsFromFile(
PKIDATA + '/' + chain
))

def show_editors_widget(self):
editors = QgsAuthEditorWidgets()
dlg = self.widget_dialog(editors)
Expand Down Expand Up @@ -648,16 +656,8 @@ def testChain(path):
def test_validate_pki_bundle(self):
"""Text the pki bundle validation"""

def mkPEMBundle(client_cert, client_key, password, chain):
return QgsPkiBundle.fromPemPaths(PKIDATA + '/' + client_cert,
PKIDATA + '/' + client_key,
password,
QgsAuthCertUtils.certsFromFile(
PKIDATA + '/' + chain
))

# Valid bundle:
bundle = mkPEMBundle('fra_cert.pem', 'fra_key.pem', 'password', 'chain_subissuer-issuer-root.pem')
bundle = self.mkPEMBundle('fra_cert.pem', 'fra_key.pem', 'password', 'chain_subissuer-issuer-root.pem')

# Test valid bundle with intermediates and without trusted root
self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle), ['The root certificate of the certificate chain is self-signed, and untrusted'])
Expand All @@ -667,7 +667,7 @@ def mkPEMBundle(client_cert, client_key, password, chain):
self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle, True, True), [])

# Wrong chain
bundle = mkPEMBundle('fra_cert.pem', 'fra_key.pem', 'password', 'chain_issuer2-root2.pem')
bundle = self.mkPEMBundle('fra_cert.pem', 'fra_key.pem', 'password', 'chain_issuer2-root2.pem')
# Test invalid bundle with intermediates and without trusted root
self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle), ['The issuer certificate of a locally looked up certificate could not be found', 'No certificates could be verified'])
# Test valid without intermediates
Expand All @@ -676,7 +676,7 @@ def mkPEMBundle(client_cert, client_key, password, chain):
self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle, True, True), ['The issuer certificate of a locally looked up certificate could not be found', 'No certificates could be verified'])

# Wrong key
bundle = mkPEMBundle('fra_cert.pem', 'ptolemy_key.pem', 'password', 'chain_subissuer-issuer-root.pem')
bundle = self.mkPEMBundle('fra_cert.pem', 'ptolemy_key.pem', 'password', 'chain_subissuer-issuer-root.pem')
# Test invalid bundle with intermediates and without trusted root
self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle), ['The root certificate of the certificate chain is self-signed, and untrusted', 'Private key does not match client certificate public key.'])
# Test invalid without intermediates
Expand All @@ -685,25 +685,25 @@ def mkPEMBundle(client_cert, client_key, password, chain):
self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle, True, True), ['Private key does not match client certificate public key.'])

# Expired root CA
bundle = mkPEMBundle('piri_cert.pem', 'piri_key.pem', 'password', 'chain_issuer3-root3-EXPIRED.pem')
bundle = self.mkPEMBundle('piri_cert.pem', 'piri_key.pem', 'password', 'chain_issuer3-root3-EXPIRED.pem')
self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle), ['The root certificate of the certificate chain is self-signed, and untrusted', 'The certificate has expired'])
self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle, False), ['The issuer certificate of a locally looked up certificate could not be found', 'No certificates could be verified'])
self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle, True, True), ['The root certificate of the certificate chain is self-signed, and untrusted', 'The certificate has expired'])

# Expired intermediate CA
bundle = mkPEMBundle('marinus_cert-EXPIRED.pem', 'marinus_key_w-pass.pem', 'password', 'chain_issuer2-root2.pem')
bundle = self.mkPEMBundle('marinus_cert-EXPIRED.pem', 'marinus_key_w-pass.pem', 'password', 'chain_issuer2-root2.pem')
self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle), ['The root certificate of the certificate chain is self-signed, and untrusted', 'The certificate has expired'])
self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle, False), ['The issuer certificate of a locally looked up certificate could not be found', 'No certificates could be verified'])
self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle, True, True), ['The certificate has expired'])

# Expired client cert
bundle = mkPEMBundle('henricus_cert.pem', 'henricus_key_w-pass.pem', 'password', 'chain_issuer4-EXPIRED-root2.pem')
bundle = self.mkPEMBundle('henricus_cert.pem', 'henricus_key_w-pass.pem', 'password', 'chain_issuer4-EXPIRED-root2.pem')
self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle), ['The root certificate of the certificate chain is self-signed, and untrusted', 'The certificate has expired'])
self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle, False), ['The issuer certificate of a locally looked up certificate could not be found', 'No certificates could be verified'])
self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle, True, True), ['The certificate has expired'])

# Untrusted root, positive test before untrust is applied
bundle = mkPEMBundle('nicholas_cert.pem', 'nicholas_key.pem', 'password', 'chain_issuer2-root2.pem')
bundle = self.mkPEMBundle('nicholas_cert.pem', 'nicholas_key.pem', 'password', 'chain_issuer2-root2.pem')
# Test valid with intermediates and trusted root
self.assertEqual(QgsAuthCertUtils.validatePKIBundle(bundle, True, True), [])
# Untrust this root
Expand Down Expand Up @@ -743,6 +743,29 @@ def test_160_cert_viable(self):
self.assertTrue(QSslError(QSslError.CertificateExpired, cert) in res)
self.assertFalse(QgsAuthCertUtils.certIsViable(cert))

def test_170_pki_key_encoding(self):
"""Test that a DER/PEM RSA/DSA/EC keys can be opened whatever the extension is"""

self.assertFalse(QgsAuthCertUtils.keyFromFile(PKIDATA + '/' + 'ptolemy_key.pem').isNull())
self.assertFalse(QgsAuthCertUtils.keyFromFile(PKIDATA + '/' + 'ptolemy_key.der').isNull())
self.assertFalse(QgsAuthCertUtils.keyFromFile(PKIDATA + '/' + 'ptolemy_key_pem.key').isNull())
self.assertFalse(QgsAuthCertUtils.keyFromFile(PKIDATA + '/' + 'ptolemy_key_der.key').isNull())
self.assertFalse(QgsAuthCertUtils.keyFromFile(PKIDATA + '/' + 'donald_key_EC.pem').isNull())
self.assertFalse(QgsAuthCertUtils.keyFromFile(PKIDATA + '/' + 'donald_key_EC.der').isNull())
self.assertFalse(QgsAuthCertUtils.keyFromFile(PKIDATA + '/' + 'donald_key_DSA.pem').isNull())
self.assertFalse(QgsAuthCertUtils.keyFromFile(PKIDATA + '/' + 'donald_key_DSA.der').isNull())
self.assertFalse(QgsAuthCertUtils.keyFromFile(PKIDATA + '/' + 'donald_key_DSA_crlf.pem').isNull())
self.assertFalse(QgsAuthCertUtils.keyFromFile(PKIDATA + '/' + 'donald_key_DSA_nonl.pem').isNull())
donald_dsa = QgsAuthCertUtils.keyFromFile(PKIDATA + '/' + 'donald_key_DSA.pem').toPem()
self.assertEqual(donald_dsa, QgsAuthCertUtils.keyFromFile(PKIDATA + '/' + 'donald_key_DSA.der').toPem())
self.assertEqual(donald_dsa, QgsAuthCertUtils.keyFromFile(PKIDATA + '/' + 'donald_key_DSA_crlf.pem').toPem())
self.assertEqual(donald_dsa, QgsAuthCertUtils.keyFromFile(PKIDATA + '/' + 'donald_key_DSA_nonl.pem').toPem())

self.assertEqual(QgsAuthCertUtils.validatePKIBundle(self.mkPEMBundle('ptolemy_cert.pem', 'ptolemy_key.pem', 'password', 'chain_subissuer-issuer-root.pem'), True, True), [])
self.assertEqual(QgsAuthCertUtils.validatePKIBundle(self.mkPEMBundle('ptolemy_cert.pem', 'ptolemy_key.der', 'password', 'chain_subissuer-issuer-root.pem'), True, True), [])
self.assertEqual(QgsAuthCertUtils.validatePKIBundle(self.mkPEMBundle('ptolemy_cert.pem', 'ptolemy_key_pem.key', 'password', 'chain_subissuer-issuer-root.pem'), True, True), [])
self.assertEqual(QgsAuthCertUtils.validatePKIBundle(self.mkPEMBundle('ptolemy_cert.pem', 'ptolemy_key_der.key', 'password', 'chain_subissuer-issuer-root.pem'), True, True), [])


if __name__ == '__main__':
unittest.main()
Binary file not shown.
20 changes: 20 additions & 0 deletions tests/testdata/auth_system/certs_keys/donald_key_DSA.pem
@@ -0,0 +1,20 @@
-----BEGIN DSA PRIVATE KEY-----
MIIDVgIBAAKCAQEAwVdYLRBhy3TS0vjvf7cHOwSdumAqX3klBOvgUiU8uG3+BmBA
01u9x2xZDigsc6fAS0SF4ZO9Hzd3AQwixGIX9hXNVYCpxm35g/IosoyKfNyXHC+Y
qNRhPOIBuRft4+2EADpElWYPIXG9AKRTqmZ8Hoicga06gRdx71qofi6iQcbDl9vW
2lo6LDk4KCiaD3fWUvgO2nQplMk8G84JTLJ0mlXyWMDmF/72zeK80KRRN9GSr90b
4PgSQBC97UWQlAkL/+7ryVVoyQID/1K0jB8Y5sA697l3Z0Az55b177j9TNJLnzBw
ffs22KWPQ/h1h6R0XQbK6Ln948884pqmTg5xJwIhANZMSTMzqxSu6EaAOUmueWbd
GAMcGdzThv1XLCZWecTxAoIBAQC9UQifo/aMtI/oHDNYiGGT7kG5dhZrUvZERL37
9UBZAOh8REf7WjQkmwpG/WjCE+Mhzx5Rnvx7rQQ//PJEkHOI5FJJVZ7Ud439uVMA
iJt2kLOUtGQefNCY50fPzik/dC/juFOTsatEvXB9u2JTaDZUYZs8OiKs7dhkPKV9
u6fmhNl6hNjF8E9C1Q9jqxMjjJ3QWdGtmeOAj0XqKc6oLO+jP1qzIP6LEwOrX3SK
YZBA3z/g/+I1Z4lboeTtAgsRKh0TxDEIj8UxfKHYWh3lu1isccljPTH5qIlGLihN
MksKJSfJP0+oKN0E12hzS4+Ey0oBQuwa3rcuf6Dc14ujOnmNAoIBACq2MPjY9LhL
9ky11ZF9a7dKJ08SSlaqkzKWdwV6ZyqNsWbDNnc0IWvgcIfVjFpvdmom1VkOvZ4w
Qp7G/pN6FYfATbZbCcm1+EZTAIyqDbfqbE6Sh8w/U29FdBdE0xBChSfnqFcRT7Gt
5JpEsPrwfgDKkaqQBdZzXU6xfELheQUGEwqS5e2JgaC68x5QwqrbGyXGmM0qQA7s
1b/6gzSnX3xWADXSBBC+/9mjXmWj63LoQovAL2qJk/C/62AYrtNypsKCZmWAYXUz
woArLlJjKVKcsKkf2qcwntQuzWcke/+GCAdxKVgbvBj6bIxFAL4GbuR+pQJ0qJk5
nSook39BtmICIEIBucuiezcjv66iBble6PE+XycVvY68cLH7OLDN4rkV
-----END DSA PRIVATE KEY-----

0 comments on commit fe4f150

Please sign in to comment.