Skip to content

Commit f60d2c5

Browse files
committed
Add SSL factory SingleCertValidatingFactory rework from pull 88
1 parent 5ff886e commit f60d2c5

5 files changed

+597
-6
lines changed

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ jars
77
nbproject
88
.idea
99
*.iml
10-
10+
build.local.properties

build.xml

+8-5
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
<exclude name="${package}/jdcb4/Jdbc4*.java" unless="jdbc4any"/>
8484

8585
<!-- ssl -->
86+
<include name="${package}/ssl/SingleCertValidatingFactory.java" if="jdbc4any"/>
8687
<include name="${package}/ssl/jdbc4/*.java" if="jdbc4any"/>
8788
<include name="${package}/ssl/jdbc3/*.java" if="jdbc3any"/>
8889
<include name="${package}/ssl/*.java" if="jdbc3any"/>
@@ -492,6 +493,7 @@
492493
<test name="org.postgresql.test.extensions.ExtensionsSuite" outfile="${testResultsDir}/extensions"/>
493494
<test name="org.postgresql.test.jdbc4.Jdbc4TestSuite" if="jdbc4tests" outfile="${testResultsDir}/jdbc4"/>
494495
<test name="org.postgresql.test.ssl.SslTestSuite" if="jdbc4tests" outfile="${testResultsDir}/ssl"/>
496+
<test name="org.postgresql.test.ssl.SingleCertValidatingFactoryTest" if="jdbc4tests" outfile="${testResultsDir}/scsf-ssl"/>
495497
</junit>
496498
</target>
497499

@@ -516,11 +518,12 @@
516518
<include name="util/PGmoney.java" />
517519
<include name="util/PGInterval.java" />
518520
<include name="util/ServerErrorMessage.java" />
519-
<include name="ssl/WrappedFactory.java" />
520-
<include name="ssl/NonValidatingFactory.java" />
521-
<include name="ds/PG*.java" />
522-
<include name="ds/common/BaseDataSource.java" />
523-
<include name="xa/PGXADataSource.java" />
521+
<include name="ssl/WrappedFactory.java" />
522+
<include name="ssl/NonValidatingFactory.java" />
523+
<include name="ssl/SingleCertValidatingFactory.java" />
524+
<include name="ds/PG*.java" />
525+
<include name="ds/common/BaseDataSource.java" />
526+
<include name="xa/PGXADataSource.java" />
524527
</fileset>
525528
</javadoc>
526529
</target>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/*-------------------------------------------------------------------------
2+
*
3+
* Copyright (c) 2004-2011, PostgreSQL Global Development Group
4+
*
5+
*
6+
*-------------------------------------------------------------------------
7+
*/
8+
package org.postgresql.ssl;
9+
10+
import java.io.ByteArrayInputStream;
11+
import java.io.BufferedInputStream;
12+
import java.io.FileInputStream;
13+
import java.io.InputStream;
14+
import java.io.IOException;
15+
import java.util.UUID;
16+
17+
import java.security.KeyStore;
18+
import java.security.cert.CertificateException;
19+
import java.security.cert.CertificateFactory;
20+
import java.security.cert.X509Certificate;
21+
import java.security.GeneralSecurityException;
22+
23+
import javax.net.ssl.SSLContext;
24+
import javax.net.ssl.TrustManager;
25+
import javax.net.ssl.TrustManagerFactory;
26+
import javax.net.ssl.X509TrustManager;
27+
28+
import org.postgresql.util.GT;
29+
import org.postgresql.ssl.WrappedFactory;
30+
31+
/**
32+
* Provides a SSLSocketFactory that authenticates the remote server against
33+
* an explicit pre-shared SSL certificate. This is more secure than using the
34+
* NonValidatingFactory as it prevents "man in the middle" attacks. It is also
35+
* more secure than relying on a central CA signing your server's certificate
36+
* as it pins the server's certificate.
37+
*
38+
* <p />
39+
*
40+
* This class requires a single String parameter specified by setting
41+
* the connection property <code>sslfactoryarg</code>. The value of this property
42+
* is the PEM-encoded remote server's SSL certificate.
43+
*
44+
* <p />
45+
* Where the certificate is loaded from is based upon the prefix of the
46+
* <code>sslfactoryarg</code> property. The following table lists the valid
47+
* set of prefixes.
48+
* <table border="1">
49+
* <tr>
50+
* <th>Prefix</th>
51+
* <th>Example</th>
52+
* <th>Explanation</th>
53+
* </tr>
54+
* <tr>
55+
* <td><code>classpath:</code></td>
56+
* <td><code>classpath:ssl/server.crt</code></td>
57+
* <td>Loaded from the classpath.</td>
58+
* </tr>
59+
* <tr>
60+
* <td><code>file:</code></td>
61+
* <td><code>file:/foo/bar/server.crt</code></td>
62+
* <td>Loaded from the filesystem.</td>
63+
* </tr>
64+
* <tr>
65+
* <td><code>env:</code></td>
66+
* <td><code>env:mydb_cert<pre>
67+
* <td>Loaded from string value of the <code>mydb_cert</code>
68+
* environment variable.</td>
69+
* </tr>
70+
* <tr>
71+
* <td><code>sys:</code></td>
72+
* <td><code>sys:mydb_cert<pre>
73+
* <td>Loaded from string value of the <code>mydb_cert</code>
74+
* system property.</td>
75+
* </tr>
76+
* <tr>
77+
* <td><code>-----BEGIN CERTIFICATE------</code></td>
78+
* <td><pre>
79+
-----BEGIN CERTIFICATE-----
80+
MIIDQzCCAqygAwIBAgIJAOd1tlfiGoEoMA0GCSqGSIb3DQEBBQUAMHUxCzAJBgNV
81+
[... truncated ...]
82+
UCmmYqgiVkAGWRETVo+byOSDZ4swb10=
83+
-----END CERTIFICATE-----
84+
</pre></td>
85+
* <td>Loaded from string value of the argument.</td>
86+
* </tr>
87+
* </table>
88+
*/
89+
90+
public class SingleCertValidatingFactory extends WrappedFactory {
91+
private static final String FILE_PREFIX = "file:";
92+
private static final String CLASSPATH_PREFIX = "classpath:";
93+
private static final String ENV_PREFIX = "env:";
94+
private static final String SYS_PROP_PREFIX = "sys:";
95+
96+
public SingleCertValidatingFactory(String sslFactoryArg) throws GeneralSecurityException {
97+
if( sslFactoryArg == null || sslFactoryArg.equals("")) {
98+
throw new GeneralSecurityException(GT.tr("The sslfactoryarg property may not be empty."));
99+
}
100+
InputStream in = null;
101+
try {
102+
if( sslFactoryArg.startsWith(FILE_PREFIX) ) {
103+
String path = sslFactoryArg.substring(FILE_PREFIX.length());
104+
in = new BufferedInputStream(new FileInputStream(path));
105+
} else if( sslFactoryArg.startsWith(CLASSPATH_PREFIX) ) {
106+
String path = sslFactoryArg.substring(CLASSPATH_PREFIX.length());
107+
in = new BufferedInputStream(Thread.currentThread().getContextClassLoader().getResourceAsStream(path));
108+
} else if( sslFactoryArg.startsWith(ENV_PREFIX) ) {
109+
String name = sslFactoryArg.substring(ENV_PREFIX.length());
110+
String cert = System.getenv(name);
111+
if( cert == null || "".equals(cert) ) {
112+
throw new GeneralSecurityException(
113+
GT.tr("The environment variable containing the server's SSL certificate must not be empty."));
114+
}
115+
in = new ByteArrayInputStream(cert.getBytes("UTF-8"));
116+
} else if( sslFactoryArg.startsWith(SYS_PROP_PREFIX) ) {
117+
String name = sslFactoryArg.substring(SYS_PROP_PREFIX.length());
118+
String cert = System.getProperty(name);
119+
if( cert == null || "".equals(cert) ) {
120+
throw new GeneralSecurityException(
121+
GT.tr("The system property containing the server's SSL certificate must not be empty."));
122+
}
123+
in = new ByteArrayInputStream(cert.getBytes("UTF-8"));
124+
} else if( sslFactoryArg.startsWith("-----BEGIN CERTIFICATE-----") ) {
125+
in = new ByteArrayInputStream(sslFactoryArg.getBytes("UTF-8"));
126+
} else {
127+
throw new GeneralSecurityException(
128+
GT.tr("The sslfactoryarg property must start with the prefix file:, classpath:, env:, sys:, or -----BEGIN CERTIFICATE-----."));
129+
}
130+
131+
SSLContext ctx = SSLContext.getInstance("TLS");
132+
ctx.init(null, new TrustManager[] { new SingleCertTrustManager(in) }, null);
133+
_factory = ctx.getSocketFactory();
134+
} catch( RuntimeException e ) {
135+
throw (RuntimeException)e;
136+
} catch( Exception e ) {
137+
if( e instanceof GeneralSecurityException ) {
138+
throw (GeneralSecurityException) e;
139+
}
140+
throw new GeneralSecurityException(GT.tr("An error occurred reading the certificate"), e);
141+
} finally {
142+
if( in != null ) {
143+
try {
144+
in.close();
145+
} catch( Exception e2) {
146+
// ignore
147+
}
148+
}
149+
}
150+
}
151+
152+
public class SingleCertTrustManager implements X509TrustManager {
153+
X509Certificate cert;
154+
X509TrustManager trustManager;
155+
156+
public SingleCertTrustManager(InputStream in) throws IOException, GeneralSecurityException {
157+
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
158+
try {
159+
// Note: KeyStore requires it be loaded even if you don't load anything into it:
160+
ks.load(null);
161+
} catch (Exception e) {
162+
}
163+
CertificateFactory cf = CertificateFactory.getInstance("X509");
164+
cert = (X509Certificate) cf.generateCertificate(in);
165+
ks.setCertificateEntry(UUID.randomUUID().toString(), cert);
166+
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
167+
tmf.init(ks);
168+
for (TrustManager tm : tmf.getTrustManagers()) {
169+
if (tm instanceof X509TrustManager) {
170+
trustManager = (X509TrustManager) tm;
171+
break;
172+
}
173+
}
174+
if (trustManager == null) {
175+
throw new GeneralSecurityException(GT.tr("No X509TrustManager found"));
176+
}
177+
}
178+
179+
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
180+
}
181+
182+
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
183+
trustManager.checkServerTrusted(chain, authType);
184+
}
185+
186+
public X509Certificate[] getAcceptedIssuers() {
187+
return new X509Certificate[] { cert };
188+
}
189+
}
190+
}

0 commit comments

Comments
 (0)