Skip to content

Commit c424af0

Browse files
committed
Add support for LDAP CA certificate validation
1 parent 8922bb7 commit c424af0

File tree

6 files changed

+122
-27
lines changed

6 files changed

+122
-27
lines changed

config.example

+2-8
Original file line numberDiff line numberDiff line change
@@ -59,21 +59,15 @@ userDB: !localUsers
5959
#
6060
#userDB: !ldapUsers
6161

62-
# The type of authentication to use.
63-
#
64-
# authentication: DIGEST-MD5
65-
6662
# This is a URL whose format is defined by the JNDI provider.
6763
# It is usually an LDAP URL that specifies the domain name of the directory server to connect to,
6864
# and optionally the port number and distinguished name (DN) of the required root naming context.
6965
#
7066
# connectionUrl: ldap://localhost:389/ou=groups,dc=mycompany,dc=com
7167

72-
# The JNDI context factory used to acquire our InitialContext. By
73-
# default, assumes use of an LDAP server using the standard JNDI LDAP
74-
# provider.
68+
# This is a file of LDAP SSL CA certificate.
7569
#
76-
# contextFactory: com.sun.jndi.ldap.LdapCtxFactory
70+
# ldapCertPem: ldap.pem
7771

7872
# Pattern specifying the LDAP search filter to use after substitution of the username.
7973
#

src/main/java/svnserver/auth/LDAPUserDB.java

+95-14
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@
77
*/
88
package svnserver.auth;
99

10-
import com.unboundid.ldap.sdk.DIGESTMD5BindRequest;
11-
import com.unboundid.ldap.sdk.LDAPConnection;
12-
import com.unboundid.ldap.sdk.LDAPException;
13-
import com.unboundid.ldap.sdk.ResultCode;
10+
import com.unboundid.ldap.sdk.*;
1411
import com.unboundid.util.ssl.SSLUtil;
1512
import com.unboundid.util.ssl.TrustAllTrustManager;
1613
import org.jetbrains.annotations.NotNull;
@@ -24,8 +21,22 @@
2421

2522
import javax.naming.NamingException;
2623
import javax.net.SocketFactory;
24+
import javax.net.ssl.TrustManager;
25+
import javax.net.ssl.TrustManagerFactory;
26+
import javax.net.ssl.X509TrustManager;
27+
import javax.xml.bind.DatatypeConverter;
28+
import java.io.ByteArrayInputStream;
29+
import java.io.File;
30+
import java.io.IOException;
2731
import java.net.URI;
32+
import java.nio.charset.StandardCharsets;
33+
import java.nio.file.Files;
2834
import java.security.GeneralSecurityException;
35+
import java.security.KeyStore;
36+
import java.security.KeyStoreException;
37+
import java.security.cert.CertificateException;
38+
import java.security.cert.CertificateFactory;
39+
import java.security.cert.X509Certificate;
2940
import java.text.MessageFormat;
3041
import java.util.Collection;
3142
import java.util.Collections;
@@ -54,7 +65,7 @@ public final class LDAPUserDB implements UserDB, PasswordChecker {
5465
@Nullable
5566
private final SocketFactory socketFactory;
5667

57-
public LDAPUserDB(@NotNull LDAPUserDBConfig config) {
68+
public LDAPUserDB(@NotNull LDAPUserDBConfig config, @NotNull File basePath) {
5869
URI ldapUri = URI.create(config.getConnectionUrl());
5970
SocketFactory factory;
6071
int defaultPort;
@@ -64,7 +75,7 @@ public LDAPUserDB(@NotNull LDAPUserDBConfig config) {
6475
defaultPort = 389;
6576
break;
6677
case "ldaps":
67-
factory = createSslFactory(config);
78+
factory = createSslFactory(config, basePath);
6879
defaultPort = 636;
6980
break;
7081
default:
@@ -77,11 +88,25 @@ public LDAPUserDB(@NotNull LDAPUserDBConfig config) {
7788
this.ldapHost = ldapUri.getHost();
7889
}
7990

80-
private static SocketFactory createSslFactory(@NotNull LDAPUserDBConfig config) {
91+
private static SocketFactory createSslFactory(@NotNull LDAPUserDBConfig config, @NotNull File basePath) {
8192
try {
82-
return new SSLUtil(null, new TrustAllTrustManager()).createSSLSocketFactory();
93+
final TrustManager trustManager;
94+
final String certPem = config.getLdapCertPem();
95+
if (certPem != null) {
96+
final File certFile = new File(basePath, certPem);
97+
log.info("Loading CA certificate from: {}", certFile.getAbsolutePath());
98+
trustManager = createTrustManager(Files.readAllBytes(certFile.toPath()));
99+
} else {
100+
log.error("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
101+
log.error("CA certificate for LDAP server is not defined. LDAP server validation is disabled");
102+
log.error("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
103+
trustManager = new TrustAllTrustManager();
104+
}
105+
return new SSLUtil(null, trustManager).createSSLSocketFactory();
83106
} catch (GeneralSecurityException e) {
84-
throw new IllegalStateException("Can't create SSL Socket Factory", e);
107+
throw new IllegalStateException(e);
108+
} catch (IOException e) {
109+
throw new IllegalStateException("Can't load certificate file", e);
85110
}
86111
}
87112

@@ -92,9 +117,9 @@ public User check(@NotNull String username, @NotNull String password) throws SVN
92117
LDAPConnection ldap = new LDAPConnection(socketFactory, ldapHost, ldapPort);
93118
try {
94119
ldap.bind(new DIGESTMD5BindRequest(username, password));
95-
com.unboundid.ldap.sdk.SearchResult search = ldap.search(
120+
SearchResult search = ldap.search(
96121
baseDn,
97-
config.isUserSubtree() ? com.unboundid.ldap.sdk.SearchScope.SUB : com.unboundid.ldap.sdk.SearchScope.ONE,
122+
config.isUserSubtree() ? SearchScope.SUB : SearchScope.ONE,
98123
MessageFormat.format(config.getUserSearch(), username),
99124
config.getNameAttribute(), config.getEmailAttribute()
100125
);
@@ -105,7 +130,7 @@ public User check(@NotNull String username, @NotNull String password) throws SVN
105130
log.error("Multiple LDAP entries found for {}", username);
106131
return null;
107132
}
108-
final com.unboundid.ldap.sdk.SearchResultEntry entry = search.getSearchEntries().get(0);
133+
final SearchResultEntry entry = search.getSearchEntries().get(0);
109134
final String realName = getAttribute(entry, config.getNameAttribute());
110135
final String email = getAttribute(entry, config.getEmailAttribute());
111136
return new User(username, realName != null ? realName : username, email);
@@ -123,8 +148,8 @@ public User check(@NotNull String username, @NotNull String password) throws SVN
123148
}
124149

125150
@Nullable
126-
private String getAttribute(@NotNull com.unboundid.ldap.sdk.SearchResultEntry entry, @NotNull String name) throws NamingException {
127-
com.unboundid.ldap.sdk.Attribute attribute = entry.getAttribute(name);
151+
private String getAttribute(@NotNull SearchResultEntry entry, @NotNull String name) throws NamingException {
152+
Attribute attribute = entry.getAttribute(name);
128153
return attribute == null ? null : attribute.getValue();
129154
}
130155

@@ -133,4 +158,60 @@ private String getAttribute(@NotNull com.unboundid.ldap.sdk.SearchResultEntry en
133158
public Collection<Authenticator> authenticators() {
134159
return authenticators;
135160
}
161+
162+
@NotNull
163+
public static byte[] parseDERFromPEM(@NotNull byte[] pem, @NotNull String beginDelimiter, @NotNull String endDelimiter) throws GeneralSecurityException {
164+
final String data = new String(pem, StandardCharsets.ISO_8859_1);
165+
String[] tokens = data.split(beginDelimiter);
166+
if (tokens.length != 2) {
167+
throw new GeneralSecurityException("Invalid PEM certificate data. Delimiter not found: " + beginDelimiter);
168+
}
169+
tokens = tokens[1].split(endDelimiter);
170+
if (tokens.length != 2) {
171+
throw new GeneralSecurityException("Invalid PEM certificate data. Delimiter not found: " + endDelimiter);
172+
}
173+
return DatatypeConverter.parseBase64Binary(tokens[0]);
174+
}
175+
176+
@NotNull
177+
public static KeyStore getKeyStoreFromDER(@NotNull byte[] certBytes) throws GeneralSecurityException {
178+
try {
179+
final CertificateFactory factory = CertificateFactory.getInstance("X.509");
180+
final KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
181+
keystore.load(null);
182+
keystore.setCertificateEntry("alias", factory.generateCertificate(new ByteArrayInputStream(certBytes)));
183+
return keystore;
184+
} catch (IOException e) {
185+
throw new KeyStoreException(e);
186+
}
187+
}
188+
189+
@NotNull
190+
public static TrustManager createTrustManager(@NotNull byte[] pem) throws GeneralSecurityException {
191+
final TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
192+
final KeyStore keystore = getKeyStoreFromDER(parseDERFromPEM(pem, "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----"));
193+
factory.init(keystore);
194+
195+
final TrustManager[] trustManagers = factory.getTrustManagers();
196+
return new X509TrustManager() {
197+
@Override
198+
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
199+
for (TrustManager trustManager : trustManagers) {
200+
((X509TrustManager) trustManager).checkClientTrusted(x509Certificates, s);
201+
}
202+
}
203+
204+
@Override
205+
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
206+
for (TrustManager trustManager : trustManagers) {
207+
((X509TrustManager) trustManager).checkServerTrusted(x509Certificates, s);
208+
}
209+
}
210+
211+
@Override
212+
public X509Certificate[] getAcceptedIssuers() {
213+
return new X509Certificate[0];
214+
}
215+
};
216+
}
136217
}

src/main/java/svnserver/config/LDAPUserDBConfig.java

+19-2
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@
88
package svnserver.config;
99

1010
import org.jetbrains.annotations.NotNull;
11+
import org.jetbrains.annotations.Nullable;
1112
import svnserver.auth.LDAPUserDB;
1213
import svnserver.auth.UserDB;
1314
import svnserver.config.serializer.ConfigType;
1415

16+
import java.io.File;
17+
1518
/**
1619
* @author Marat Radchenko <[email protected]>
1720
*/
@@ -47,6 +50,11 @@ public final class LDAPUserDBConfig implements UserDBConfig {
4750
*/
4851
@NotNull
4952
private String emailAttribute = "mail";
53+
/**
54+
* Certificate for validation LDAP server with SSL connection.
55+
*/
56+
@Nullable
57+
private String ldapCertPem;
5058

5159
@NotNull
5260
public String getConnectionUrl() {
@@ -88,9 +96,18 @@ public String getEmailAttribute() {
8896
return emailAttribute;
8997
}
9098

99+
@Nullable
100+
public String getLdapCertPem() {
101+
return ldapCertPem;
102+
}
103+
104+
public void setLdapCertPem(@Nullable String ldapCertPem) {
105+
this.ldapCertPem = ldapCertPem;
106+
}
107+
91108
@NotNull
92109
@Override
93-
public UserDB create() {
94-
return new LDAPUserDB(this);
110+
public UserDB create(@NotNull File basePath) {
111+
return new LDAPUserDB(this, basePath);
95112
}
96113
}

src/main/java/svnserver/config/LocalUserDBConfig.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import svnserver.auth.UserWithPassword;
1515
import svnserver.config.serializer.ConfigType;
1616

17+
import java.io.File;
18+
1719
/**
1820
* @author Marat Radchenko <[email protected]>
1921
*/
@@ -32,7 +34,7 @@ public LocalUserDBConfig(@NotNull UserEntry[] users) {
3234

3335
@NotNull
3436
@Override
35-
public UserDB create() {
37+
public UserDB create(@NotNull File basePath) {
3638
final LocalUserDB result = new LocalUserDB();
3739
for (UserEntry user : users)
3840
result.add(new UserWithPassword(new User(user.username, user.realName, user.email), user.password));

src/main/java/svnserver/config/UserDBConfig.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import svnserver.auth.UserDB;
1212

1313
import javax.xml.bind.annotation.XmlSeeAlso;
14+
import java.io.File;
1415

1516
/**
1617
* @author Marat Radchenko <[email protected]>
@@ -21,5 +22,5 @@
2122
})
2223
public interface UserDBConfig {
2324
@NotNull
24-
UserDB create();
25+
UserDB create(@NotNull File basePath);
2526
}

src/main/java/svnserver/server/SvnServer.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public SvnServer(@NotNull File basePath, @NotNull Config config) throws IOExcept
8989
this.config = config;
9090

9191
cacheDb = config.getCacheConfig().createCache(basePath);
92-
userDB = config.getUserDB().create();
92+
userDB = config.getUserDB().create(basePath);
9393

9494
commands.put("commit", new CommitCmd());
9595
commands.put("diff", new DeltaCmd(DiffParams.class));

0 commit comments

Comments
 (0)