/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 net.jini.jeri.ssl;
import java.math.BigInteger;
import java.security.Key;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.PrivilegedAction;
import java.security.PublicKey;
import java.security.cert.CertPath;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import javax.security.auth.Subject;
import javax.security.auth.x500.X500Principal;
import javax.security.auth.x500.X500PrivateCredential;
import net.jini.security.Security;
/**
* Provides methods for retrieving credentials from a Subject.
*
* @author Sun Microsystems, Inc.
*/
class SubjectCredentials extends Utilities {
/** This class shouldn't be instantiated */
private SubjectCredentials() { }
/**
* Retrieves the X.509 CertPath for a credential name. Returns null if the
* chain associated with the credential name is not found. Does not check
* if either principal or private key associated with the chain are
* present.
*
* @param subject the Subject containing the credentials or null
* @param name the name of the credentials
* @return the certificate chain or null
*/
static CertPath getCertificateChain(Subject subject, String name) {
if (subject == null) {
return null;
}
CertificateMatcher matcher = CertificateMatcher.create(name);
if (matcher != null) {
Set publicCreds = subject.getPublicCredentials();
synchronized (publicCreds) {
for (Iterator it = publicCreds.iterator(); it.hasNext(); ) {
Object cred = it.next();
if (isX509CertificateChain(cred)) {
CertPath chain = (CertPath) cred;
if (matcher.matches(firstX509Cert(chain))) {
return chain;
}
}
}
}
}
return null;
}
/**
* Returns the credential name for an X.509 certificate.
*
* @param cert the certificate
* @return the credential name
*/
static String getCertificateName(X509Certificate cert) {
return CertificateMatcher.getName(cert);
}
/**
* Provides utilities for converting between X.509 certificates and unique
* certificate names.
*/
private static class CertificateMatcher {
private final BigInteger serialNumber;
private final String issuerName;
/**
* Creates an object that can be compared with an X.509 certificate.
* Returns null if the argument is not a valid certificate name.
*/
static CertificateMatcher create(String certificateName) {
if (certificateName == null) {
return null;
}
int atSignPosition = certificateName.indexOf('@');
if (atSignPosition < 0) {
return null;
}
BigInteger serialNumber;
try {
serialNumber = new BigInteger(
certificateName.substring(0, atSignPosition),
16);
} catch (NumberFormatException e) {
return null;
}
String issuerName = certificateName.substring(atSignPosition + 1);
return new CertificateMatcher(serialNumber, issuerName);
}
private CertificateMatcher(BigInteger serialNumber, String issuerName)
{
this.serialNumber = serialNumber;
this.issuerName = issuerName;
}
/** Returns the unique certificate name for an X.509 certificate */
static String getName(X509Certificate certificate) {
/*
* Use the certificate serial number, which is unique for all
* certificates from a given issuer, plus the issuer name, to get a
* unique name for the certificate.
*/
return certificate.getSerialNumber().toString(16) + "@" +
getIssuerName(certificate);
}
/**
* Returns true if an X.509 certificate matches the certificate name
* specified in the constructor.
*/
boolean matches(X509Certificate certificate) {
return certificate.getSerialNumber().equals(serialNumber)
&& getIssuerName(certificate).equals(issuerName);
}
/** Returns the canonical issuer name for an X.509 certificate. */
private static String getIssuerName(X509Certificate certificate) {
return certificate.getIssuerX500Principal().getName(
X500Principal.CANONICAL);
}
}
/**
* Returns the X.509 CertPaths stored in the public credentials of the
* subject. Does not check if the associated principals or private keys
* are present. Returns null if none are found.
*
* @param subject the subject containing the X.509 CertPaths or null
* @return List of the X.509 CertPaths in the subject
*/
static List getCertificateChains(Subject subject) {
List result = null;
if (subject != null) {
Set publicCreds = subject.getPublicCredentials();
synchronized (publicCreds) {
for (Iterator it = publicCreds.iterator(); it.hasNext(); ) {
Object cred = it.next();
if (isX509CertificateChain(cred)) {
if (result == null) {
result = new ArrayList(publicCreds.size());
}
result.add(cred);
}
}
}
}
return result;
}
/**
* Checks if the subject's public credentials contain a certificate chain
* that starts with a certificate with the same subject and public key, and
* returns the certificate chain if it does. Does not check the validity
* of the certificate chain, or for associated private credentials or
* principal.
*
* @param cert the certificate
* @return the certificate chain starting with an equivalent certificate,
* if present, otherwise null
*/
static CertPath getCertificateChain(Subject subject, X509Certificate cert)
{
if (subject != null) {
Principal subjectDN = null;
PublicKey key = null;
Set publicCreds = subject.getPublicCredentials();
synchronized (publicCreds) {
for (Iterator it = publicCreds.iterator(); it.hasNext(); ) {
Object cred = it.next();
if (!isX509CertificateChain(cred)) {
continue;
}
CertPath chain = (CertPath) cred;
X509Certificate start = firstX509Cert(chain);
if (cert.equals(start)) {
return chain;
}
if (subjectDN == null) {
subjectDN = cert.getSubjectDN();
key = cert.getPublicKey();
}
if (subjectDN.equals(start.getSubjectDN())
&& key.equals(start.getPublicKey()))
{
return chain;
}
}
}
}
return null;
}
/**
* Retrieves the principals in the subject with X.509 CertPaths which use
* the specified key algorithm and, optionally, have associated private
* credentials. Uses the specified private credentials rather than getting
* them from the Subject to permit callers to cache them and avoid the cost
* of repeated permission checks to access them.
*
* @param subject the Subject containing the principals
* @param keyAlgorithms the permitted key algorithms, an OR of any of
* DSA_KEY_ALGORITHM and RSA_KEY_ALGORITHM
* @param privateCredentials the available private credentials, or null if
* private credentials are not required
* @return set of matching principals
*/
static Set getPrincipals(Subject subject,
int keyAlgorithms,
X500PrivateCredential[] privateCredentials)
{
Set result = new HashSet(subject.getPrincipals().size());
List certPaths = getCertificateChains(subject);
if (certPaths != null) {
for (int i = certPaths.size(); --i >= 0; ) {
CertPath chain = (CertPath) certPaths.get(i);
X509Certificate cert = firstX509Cert(chain);
String alg = cert.getPublicKey().getAlgorithm();
if (!permittedKeyAlgorithm(alg, keyAlgorithms)) {
continue;
}
X500Principal principal = getPrincipal(subject, cert);
if (principal != null) {
boolean pcOK = privateCredentials == null;
if (!pcOK) {
for (int j = privateCredentials.length; --j >= 0; ) {
X500PrivateCredential xpc = privateCredentials[j];
if (cert.equals(xpc.getCertificate())) {
pcOK = true;
break;
}
}
}
if (pcOK) {
result.add(principal);
}
}
}
}
return result;
}
/**
* A privileged action that gets the private credentials for an X.509
* certificate.
*/
static class GetPrivateCredentialAction implements PrivilegedAction {
private final Subject subject;
private final X509Certificate cert;
GetPrivateCredentialAction(Subject subject, X509Certificate cert) {
this.subject = subject;
this.cert = cert;
}
public Object run() {
return SubjectCredentials.getPrivateCredential(subject, cert);
}
}
/**
* A privileged action that returns all the X.500 private credentials for a
* subject as an X500PrivateCredential array. Assumes that the subject is
* non-null.
*/
static class GetAllPrivateCredentialsAction implements PrivilegedAction {
private final Subject subject;
GetAllPrivateCredentialsAction(Subject subject) {
this.subject = subject;
}
public Object run() {
Set pcs = subject.getPrivateCredentials();
List xpcs = new ArrayList(pcs.size());
synchronized (pcs) {
/*
* XXX: Include this synchronization to work around BugID
* 4892913, Subject.getPrivateCredentials not thread-safe
* against changes to principals. -tjb[22.Jul.2003]
*
* synchronized (subject.getPrincipals()) {
*/
for (Iterator iter = pcs.iterator(); iter.hasNext(); ) {
Object pc = iter.next();
if (pc instanceof X500PrivateCredential) {
xpcs.add(pc);
}
}
}
return xpcs.toArray(new X500PrivateCredential[xpcs.size()]);
}
}
/**
* Returns the X500PrivateCredential for an X.509 certificate. Returns
* null if the associated private credential is missing from the subject.
* Does not check if the public credential or principal are present.
* Assumes that the subject is non-null. The caller should check for
* AuthenticationPermission and then call this method from within
* AccessController.doPrivileged to give it private credential permissions.
*
* @param subject the Subject containing the credentials
* @param cert the X.509 certificate
* @return the X500PrivateCredential or null
*/
static X500PrivateCredential getPrivateCredential(Subject subject,
X509Certificate cert)
{
X500PrivateCredential result = null;
Set privateCreds = subject.getPrivateCredentials();
synchronized (privateCreds) {
/*
* XXX: Include this synchronization to work around BugID 4892913,
* Subject.getPrivateCredentials not thread-safe against changes to
* principals. -tjb[18.Jul.2003]
*
* synchronized (subject.getPrincipals()) {
*/
for (Iterator it = privateCreds.iterator(); it.hasNext(); ) {
Object cred = it.next();
if (cred instanceof X500PrivateCredential) {
X500PrivateCredential xpc =
(X500PrivateCredential) cred;
if (cert.equals(xpc.getCertificate())) {
result = xpc;
break;
}
}
}
}
return result;
}
/**
* Returns the subject principal matching the X.509 certificate. Returns
* null if the principal is not found. Does not check if the associated
* private key is present. Assumes that the subject is non-null.
*
* @param subject the Subject containing the credentials
* @param cert the X.509 certificate
* @return the X.500 principal or null
*/
static X500Principal getPrincipal(Subject subject, X509Certificate cert) {
X500Principal x500 = cert.getSubjectX500Principal();
String name = x500.getName(X500Principal.CANONICAL);
Set principals = subject.getPrincipals();
synchronized (principals) {
for (Iterator i = principals.iterator(); i.hasNext(); ) {
Object next = i.next();
if (!(next instanceof X500Principal)) {
continue;
}
X500Principal principal = (X500Principal) next;
if (principal.getName(X500Principal.CANONICAL).equals(name)) {
return principal;
}
}
}
return null;
}
/**
* Returns a String that describes the credentials in the subject.
*
* @param subject the Subject containing the credentials
* @return a String describing the credentials
* @throws NullPointerException if the subject is null
*/
static String credentialsString(Subject subject) {
List certPaths = getCertificateChains(subject);
if (certPaths == null) {
return "";
}
StringBuffer buf = new StringBuffer();
for (int i = certPaths.size(); --i >= 0; ) {
CertPath chain = (CertPath) certPaths.get(i);
X509Certificate cert = firstX509Cert(chain);
X500Principal principal = getPrincipal(subject, cert);
if (principal != null) {
buf.append(" Principal: ").append(principal).append('\n');
buf.append(" Public key: ");
appendKeyString(cert.getPublicKey(), buf);
buf.append('\n');
buf.append(" Private key: ");
try {
X500PrivateCredential cred =
(X500PrivateCredential) Security.doPrivileged(
new GetPrivateCredentialAction(subject, cert));
PrivateKey privateKey =
cred != null ? cred.getPrivateKey() : null;
if (privateKey == null) {
buf.append("Not found");
} else {
appendKeyString(privateKey, buf);
}
} catch (SecurityException e) {
buf.append("No permission");
}
}
}
return buf.toString();
}
/** Appends information about a key to a StringBuffer. */
private static void appendKeyString(Key key, StringBuffer buf) {
String className = key.getClass().getName();
buf.append(className.substring(className.lastIndexOf('.') + 1));
buf.append('@');
buf.append(Integer.toHexString(System.identityHashCode(key)));
}
/**
* Determines if the argument is an X.509 certificate CertPath. Returns
* true if the argument is a non-null CertPath, has at least one
* certificate, and has type X.509.
*/
private static boolean isX509CertificateChain(Object credential) {
if (!(credential instanceof CertPath)) {
return false;
}
CertPath certPath = (CertPath) credential;
if (certPath.getCertificates().isEmpty()) {
return false;
} else if (!certPath.getType().equals("X.509")) {
return false;
}
return true;
}
}