blob: 63da3e7b94c184b9f8adf89aef005d4664b9025d [file] [log] [blame]
/**
* $RCSfile$
* $Revision$
* $Date$
*
* Copyright 2003-2005 Jive Software.
*
* All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack;
import javax.net.ssl.X509TrustManager;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Trust manager that checks all certificates presented by the server. This class
* is used during TLS negotiation. It is possible to disable/enable some or all checkings
* by configuring the {@link ConnectionConfiguration}. The truststore file that contains
* knows and trusted CA root certificates can also be configure in {@link ConnectionConfiguration}.
*
* @author Gaston Dombiak
*/
class ServerTrustManager implements X509TrustManager {
private static Pattern cnPattern = Pattern.compile("(?i)(cn=)([^,]*)");
private ConnectionConfiguration configuration;
/**
* Holds the domain of the remote server we are trying to connect
*/
private String server;
private KeyStore trustStore;
private static Map<KeyStoreOptions, KeyStore> stores = new HashMap<KeyStoreOptions, KeyStore>();
public ServerTrustManager(String server, ConnectionConfiguration configuration) {
this.configuration = configuration;
this.server = server;
InputStream in = null;
synchronized (stores) {
KeyStoreOptions options = new KeyStoreOptions(configuration.getTruststoreType(),
configuration.getTruststorePath(), configuration.getTruststorePassword());
if (stores.containsKey(options)) {
trustStore = stores.get(options);
} else {
try {
trustStore = KeyStore.getInstance(options.getType());
in = new FileInputStream(options.getPath());
trustStore.load(in, options.getPassword().toCharArray());
} catch (Exception e) {
trustStore = null;
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException ioe) {
// Ignore.
}
}
}
stores.put(options, trustStore);
}
if (trustStore == null)
// Disable root CA checking
configuration.setVerifyRootCAEnabled(false);
}
}
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(X509Certificate[] arg0, String arg1)
throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] x509Certificates, String arg1)
throws CertificateException {
int nSize = x509Certificates.length;
List<String> peerIdentities = getPeerIdentity(x509Certificates[0]);
if (configuration.isVerifyChainEnabled()) {
// Working down the chain, for every certificate in the chain,
// verify that the subject of the certificate is the issuer of the
// next certificate in the chain.
Principal principalLast = null;
for (int i = nSize -1; i >= 0 ; i--) {
X509Certificate x509certificate = x509Certificates[i];
Principal principalIssuer = x509certificate.getIssuerDN();
Principal principalSubject = x509certificate.getSubjectDN();
if (principalLast != null) {
if (principalIssuer.equals(principalLast)) {
try {
PublicKey publickey =
x509Certificates[i + 1].getPublicKey();
x509Certificates[i].verify(publickey);
}
catch (GeneralSecurityException generalsecurityexception) {
throw new CertificateException(
"signature verification failed of " + peerIdentities);
}
}
else {
throw new CertificateException(
"subject/issuer verification failed of " + peerIdentities);
}
}
principalLast = principalSubject;
}
}
if (configuration.isVerifyRootCAEnabled()) {
// Verify that the the last certificate in the chain was issued
// by a third-party that the client trusts.
boolean trusted = false;
try {
trusted = trustStore.getCertificateAlias(x509Certificates[nSize - 1]) != null;
if (!trusted && nSize == 1 && configuration.isSelfSignedCertificateEnabled())
{
System.out.println("Accepting self-signed certificate of remote server: " +
peerIdentities);
trusted = true;
}
}
catch (KeyStoreException e) {
e.printStackTrace();
}
if (!trusted) {
throw new CertificateException("root certificate not trusted of " + peerIdentities);
}
}
if (configuration.isNotMatchingDomainCheckEnabled()) {
// Verify that the first certificate in the chain corresponds to
// the server we desire to authenticate.
// Check if the certificate uses a wildcard indicating that subdomains are valid
if (peerIdentities.size() == 1 && peerIdentities.get(0).startsWith("*.")) {
// Remove the wildcard
String peerIdentity = peerIdentities.get(0).replace("*.", "");
// Check if the requested subdomain matches the certified domain
if (!server.endsWith(peerIdentity)) {
throw new CertificateException("target verification failed of " + peerIdentities);
}
}
else if (!peerIdentities.contains(server)) {
throw new CertificateException("target verification failed of " + peerIdentities);
}
}
if (configuration.isExpiredCertificatesCheckEnabled()) {
// For every certificate in the chain, verify that the certificate
// is valid at the current time.
Date date = new Date();
for (int i = 0; i < nSize; i++) {
try {
x509Certificates[i].checkValidity(date);
}
catch (GeneralSecurityException generalsecurityexception) {
throw new CertificateException("invalid date of " + server);
}
}
}
}
/**
* Returns the identity of the remote server as defined in the specified certificate. The
* identity is defined in the subjectDN of the certificate and it can also be defined in
* the subjectAltName extension of type "xmpp". When the extension is being used then the
* identity defined in the extension in going to be returned. Otherwise, the value stored in
* the subjectDN is returned.
*
* @param x509Certificate the certificate the holds the identity of the remote server.
* @return the identity of the remote server as defined in the specified certificate.
*/
public static List<String> getPeerIdentity(X509Certificate x509Certificate) {
// Look the identity in the subjectAltName extension if available
List<String> names = getSubjectAlternativeNames(x509Certificate);
if (names.isEmpty()) {
String name = x509Certificate.getSubjectDN().getName();
Matcher matcher = cnPattern.matcher(name);
if (matcher.find()) {
name = matcher.group(2);
}
// Create an array with the unique identity
names = new ArrayList<String>();
names.add(name);
}
return names;
}
/**
* Returns the JID representation of an XMPP entity contained as a SubjectAltName extension
* in the certificate. If none was found then return <tt>null</tt>.
*
* @param certificate the certificate presented by the remote entity.
* @return the JID representation of an XMPP entity contained as a SubjectAltName extension
* in the certificate. If none was found then return <tt>null</tt>.
*/
private static List<String> getSubjectAlternativeNames(X509Certificate certificate) {
List<String> identities = new ArrayList<String>();
try {
Collection<List<?>> altNames = certificate.getSubjectAlternativeNames();
// Check that the certificate includes the SubjectAltName extension
if (altNames == null) {
return Collections.emptyList();
}
// Use the type OtherName to search for the certified server name
/*for (List item : altNames) {
Integer type = (Integer) item.get(0);
if (type == 0) {
// Type OtherName found so return the associated value
try {
// Value is encoded using ASN.1 so decode it to get the server's identity
ASN1InputStream decoder = new ASN1InputStream((byte[]) item.toArray()[1]);
DEREncodable encoded = decoder.readObject();
encoded = ((DERSequence) encoded).getObjectAt(1);
encoded = ((DERTaggedObject) encoded).getObject();
encoded = ((DERTaggedObject) encoded).getObject();
String identity = ((DERUTF8String) encoded).getString();
// Add the decoded server name to the list of identities
identities.add(identity);
}
catch (UnsupportedEncodingException e) {
// Ignore
}
catch (IOException e) {
// Ignore
}
catch (Exception e) {
e.printStackTrace();
}
}
// Other types are not good for XMPP so ignore them
System.out.println("SubjectAltName of invalid type found: " + certificate);
}*/
}
catch (CertificateParsingException e) {
e.printStackTrace();
}
return identities;
}
private static class KeyStoreOptions {
private final String type;
private final String path;
private final String password;
public KeyStoreOptions(String type, String path, String password) {
super();
this.type = type;
this.path = path;
this.password = password;
}
public String getType() {
return type;
}
public String getPath() {
return path;
}
public String getPassword() {
return password;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((password == null) ? 0 : password.hashCode());
result = prime * result + ((path == null) ? 0 : path.hashCode());
result = prime * result + ((type == null) ? 0 : type.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
KeyStoreOptions other = (KeyStoreOptions) obj;
if (password == null) {
if (other.password != null)
return false;
} else if (!password.equals(other.password))
return false;
if (path == null) {
if (other.path != null)
return false;
} else if (!path.equals(other.path))
return false;
if (type == null) {
if (other.type != null)
return false;
} else if (!type.equals(other.type))
return false;
return true;
}
}
}