Saturday, October 24, 2009

simon willison on redis

Proving again and old-school c is still fast and relevant, he talks about a "data structure" storage server (that which is not a database, but still a means to store data). Any programmer can see why this would be a valuable hook for the debugging process (think HSQL, MockDB, etc).

The fascinating tidbit at the end of the article is what got me interested however:
hurl.it

"Hurl makes HTTP requests. Enter a URL, set some headers, view the response, then share it with others. Perfect for demoing and debugging APIs. "

Sounds like a great addition to a system test toolkit for anyone who works on web apps.

Sunday, October 11, 2009

WebSphere Trust Access Interceptor

Background - What is a TAI?

To correlate between external authentication (e.g. SSO with Oracle AM WebGate, IBM TAM, etc.) and the WebSphere Application Server (WAS) User Registry identity, use a Trust Association Interceptor within the WebSphere security layer.

Lightweight Third Party Authentication (LTPA) is the mechanism by which WAS supports SSO. With LTPA a token is created with the user data and expiry time, signed by the LTPA keys (which why multiple nodes in a cell must have their LTPA keys in sync). The LTPA token can be forwarded by authenticated resources. This token passes to other servers, in the same cell or in a different cell, either through cookies (for Web resources) or through the authentication layer Security Authentication Service (SAS) or CSIv2 for EJBs.

A TAI is an identity provider, it takes information from an HTTP request, and, assuming trust is validated, provides either a principal (to be filled in by the User Registry) or a fully populated subject. In either case the user must exist in the WAS User Registry. The option for the TAI to populate the Subject allows the implementer to override existing group membership in the WAS LDAP.

Trust Interceptors operate completely within the WebSphere security layer, usually loaded on server startup from AppServer lib ext directory. The interceptor is only executed when an authenticated resource is requested via HTTP(S), i.e. the web.xml has a security constraint for that URL. If the user has already authenticated to any participating cell in the WebSphere deployment, they will have a valid LTPA token for the resource; the interceptor will skip.

This interceptor implementation is an adjunct to the security system; fallback to the usual authentication mechanism is always possible. Make note that a TAI is available for web authentication only (as opposed to JAAS); there is no intercepting programmatic login or access to Enterprise Beans.

The negotiation stage is only executed only if the target is defined as protected by the implementing class. Targets are evaluated in the isTargetInterceptor method.

For more information, this article is dated now, but a great start to WAS security.

Implementation

I have implemented TAIs for WAS v6.0, v6.1 and v7.0. Slight differences, but I will cover my implementation for v6.1.

Basically, implement the interface com.ibm.wsspi.security.tai.TrustAssociationInterceptor from the websphere_apis.jar.

Order of execution is:

1. initialize(Properties props) - on server startup.
2. boolean isTargetInterceptor (HttpServletRequest req) - on each request - if true, continue to negociate.
3. TAIResult negotiateValidateandEstablishTrust (HttpServletRequest req, HttpServletResponse res)

TAI Result holds either a string user id to be retrieved from the Registry, or the fully qualified Subject.

Initialize

Load your TAI settings, a list of regex patterns to intercept, the name of the SSO Request Header, etc. Will be loaded from the referenced properties file or from the console custom properties. Console settings override.

public int initialize(Properties props) throws WebTrustAssociationFailedException {
// read the Resource Bundle and add to static properties
// *console values* will overwrite props file.
try {
ResourceBundle rb = Constants.getResourceBundle();
Enumeration keys = rb.getKeys();
while (keys.hasMoreElements()) {
String key = (String) keys.nextElement();
log.debug("Prop from " + key + "=" + rb.getObject(key));
taiProps.put(key, rb.getObject(key));
}
} catch (Exception e) {
// check the constants RESOURCE_PKG/RESOURCES values to see
// if the properties files exist!
log.warn("Did not find/load iaatai_en.properties " + e.getMessage());
// No rethrow, console might be used to set properties.
}
try {
// read properties from console config add to static list
if (props != null) {
props.list(System.out);
taiProps.putAll(props);
}
// setup url patterns and header names from props.
configureFromProperties();
} catch (Throwable t) {
log.error("Initialization error (props not loaded)", t);
}
// return zero for success - any other value is failure.
return 0;
}


isTargetInterceptor

Should the target URL be intercepted? For this, setup a Map (cache) of URLs, and from the properties compare to set of Regex Patterns to intercept.

public boolean isTargetInterceptor(HttpServletRequest request) {
log.info("isTargetInterceptor: Check URI " + request.getRequestURI());
try {
// if we can get away without regexing, do so
if (targetUris.contains(request.getRequestURI())) {
log.debug("Found uri in cache");
return true;
}
// iterate over the patterns (values)
Iterator urlPatterns = targetUrlPatterns.values().iterator();
while (urlPatterns.hasNext()) {
Pattern p = (Pattern) urlPatterns.next();
// log.debug("Test URI pattern: " + p.pattern());
Matcher m = p.matcher(request.getRequestURI());
if (m.matches()) {
log.debug("Matched Pattern " + p.pattern());
targetUris.add(request.getRequestURI());
return true;
}
}
} catch (Exception e) {
log.error("ERROR: isTargetInterceptor", e);
}
log.info("TAI will not intercept this target URI");
return false;
}


Negotiate Trust

Quick and easy - direct mapping to a WAS registry ID. The simplest way, if the SSO header exists map it to an existing user.

public TAIResult negotiateValidateandEstablishTrust(HttpServletRequest request,
HttpServletResponse response) throws WebTrustAssociationFailedException {
log.debug("negotiateValidateandEstablishTrust starting.");
String ssoId = null;
TAIResult result = TAIResult.create(401);
ssoId = request.getHeader(Constants.HEADER_USER_ID);
if (ssoId != null && !"".equals(ssoId)) {
return TAIResult.create(HttpServletResponse.SC_OK, wasId);
}
// no header, return the default result, unauthorized 401
return result;
}


Another option for TAI's using Basic Authentication:

public TAIResult negotiateValidateandEstablishTrust(HttpServletRequest req,
HttpServletResponse resp) throws WebTrustAssociationFailedException {
log.debug("negotiateValidateandEstablishTrust");

String basicAuth = req.getHeader("Authorization");
TAIResult tairesult;
if (basicAuth == null || basicAuth.equals("") || !basicAuth.startsWith("Basic ")) {
tairesult = challengeClient(resp);
}
else {
String userId = null;
String passwd = "";
try {
String basicDecoded = new String(Base64.decode(basicAuth.substring(6), '='),
"UTF-8");
int i = basicDecoded.indexOf(':');
if (i == -1) {
userId = basicDecoded;
}
else {
userId = basicDecoded.substring(0, i);
passwd = basicDecoded.substring(i + 1);
}
log.debug("negotiateValidateandEstablishTrust userID:" + userId);
if (userId != null && (passwd == null || passwd.length() == 0)) {
log.warn("negotiateValidateandEstablishTrust FAILED, no password for "
+ userId);
tairesult = challengeClient(resp);
}
else {
Subject subject = doBasicWASLogin(userId, passwd);
tairesult = TAIResult.create(200, userId, subject);
}
} catch (LoginException e) {
log.error("negotiateValidateandEstablishTrust " + userId, e);
tairesult = challengeClient(resp);
} catch (UnsupportedEncodingException e) {
tairesult = null;
}
}
log.info("negotiateValidateandEstablishTrust " + tairesult.getStatus());
return tairesult;
}

private TAIResult challengeClient(HttpServletResponse resp)
throws WebTrustAssociationFailedException {
resp.setHeader("WWW-Authenticate", wwwAuthenticateHeader);
resp.setStatus(401);
return TAIResult.create(401);
}

private Subject doBasicWASLogin(String user, String pass) throws LoginException {
Subject subject = null;
CallbackHandler callbackhandler = WSCallbackHandlerFactory.getInstance()
.getCallbackHandler(user, pass);

LoginContext logincontext = new LoginContext(basicLoginTarget, callbackhandler);
log.debug("doBasicWASLogin built LoginContext");
logincontext.login();
log.debug("doBasicWASLogin login done");
return logincontext.getSubject();
}


For our actual implementation, we break user types into two paths by the application URL. getRealm is a custom function not shown here, it translates URLs to just a context root and maps to a list of Anonymous/MustExistInRegistry type apps.

Anonymous user mapping. Each user with a valid header is allowed in, and if they don't exist the user is created.

Must-exist users are higher security and must exist in the WAS registry before access.

public TAIResult negotiateValidateandEstablishTrust(HttpServletRequest request,
HttpServletResponse response) throws WebTrustAssociationFailedException {
log.debug("negotiateValidateandEstablishTrust starting.");
String ssoId = null;
String wasId = null;
String realm = "";
String lang = "en";
// http authenticate response
TAIResult result = TAIResult.create(401);
try {

// headers will be set by GA runtime, actual header
// name is configured via properties.
ssoId = request.getHeader(Constants.HEADER_USER_ID);
lang = request.getHeader(Constants.HEADER_LANG_ID);
if (ssoId == null
&& "true".equals(taiProps.getProperty("DEBUG_HEADER_VALUE", "false"))) {
log.warn("SSO_ID null, but DEBUG_HEADER_VALUE found to be true.");
ssoId = "debuguser " + new Random().nextInt();
}

if (ssoId == null) {
// SSO *not* protecting this resource, no SSO_ID header &
// DEBUG_HEADER_VALUE not set.
log.warn("TAI cannot trust, no header. Should we be intercepting this URL?");
throw new WebTrustAssociationUserException("Null SSO user");
}

// retrieve the name of the application, used to decide if anon/mustexist
realm = getRealm(request);
wasId = mapToAnonOrMustExistId(ssoId, realm);

// Anonymous users will be auto-created
log.debug(realm + ":requires that users exist? " + mustexistRealms);
boolean mustExist = (mustexistRealms != null && mustexistRealms.contains(realm));
// Setup the sharedState user credentials
Map sharedState = null;

// Using the sharedState map is an example of creating the subject
// to change the user credentials or state. It may not be necessary
// in your implementation. If not, simply return an ID WAS will
// recognize:
// TAIResult.create(HttpServletResponse.SC_OK, wasId);

UserRegistryLookup userRegLookup = new UserRegistryLookup();
try {
// lookup the user, group, etc 
sharedState = userRegLookup.getSharedState(wasId, lang, realm);
} catch (EntryNotFoundException enf) {
log.warn("No match for wasId " + wasId);
// Check for another chance - create the user
// then attempt to read sharedState for newly created.
if (!ismustexist) {
createNewUser(ssoId, wasId, lang, realm);
log.debug("WAS user created, return TAIResult OK.");
return TAIResult.create(HttpServletResponse.SC_OK, wasId);
}
}
if (sharedState != null) {
// Create the subject and add credentials
Subject authenticatedPrincipal = new Subject();
authenticatedPrincipal.getPublicCredentials().add(sharedState);
// N.B. the principal field (arg1) is unnecesary when
// providing the Subject object.
log.debug("Return the authenticated principal. " + wasId);
return TAIResult.create(HttpServletResponse.SC_OK, wasId,
authenticatedPrincipal);
}

} catch (Throwable e) {
// most actions do not throw, this must be a catastrophic failure
log.error("Trust ERROR for:" + ssoId + "::" + wasId, e);
throw new WebTrustAssociationFailedException(e.getMessage());
}
// return the default result, unauthorized 401
return result;
}


Reading the User Registry - creating a Shared State from scratch. This give the TAI the option to alter user groups/roles.

public Map getSharedState(final String user, final String lang, final String realm)
throws NamingException, RemoteException, EntryNotFoundException,
CustomRegistryException {

Hashtable secTable = new Hashtable();
InitialContext _ctx = VmmServiceManager.getInitialContext();

// Retrieve the local UserRegistry implementation.
// For v6.0 this is WMM-UR (WMM custom registry)
// Impl: com.ibm.websphere.security._UserRegistry_Stub
// v6.1 Use com.ibm.ws.wim.registry.WIMUserRegistry
UserRegistry reg = (UserRegistry) _ctx.lookup("UserRegistry");
log.info("Got User Registry "
+ (reg != null ? reg.getRealm() : "NULL"));
if (reg == null) {
log.error("Failed connnection to WMM User Registry ");
return null;
}

// Retrieves the user registry uniqueID based on the uid specified
// in the NameCallback. Unique ID is the full DN.
String uniqueID = reg.getUniqueUserId(user);
log.debug("From user registry: uniqueID " + uniqueID);

// read the WAS user ID from registry userID@realm
String wsUserId = WSSecurityPropagationHelper
.getUserFromUniqueID(uniqueID);
log.debug("wsUserId from WSSecurityPropagationHelper "
+ wsUserId);

// Retrieves display name from the user registry based on the
// WAS UserId.
String securityName = reg.getUserSecurityName(wsUserId);
// display name from user registry
log.debug("securityName from user registry " + securityName);

// Retrieves the groups associated with the uniqueID.
List groupList = null;
try {
groupList = reg.getUniqueGroupIds(wsUserId);
log.debug("found groupList " + groupList);
} catch (Exception e) {
log.warn("ERROR retrieving groupList " + e.getMessage());
groupList = new ArrayList();
}


// N.B. This is the ideal place to override group membership.
// Example, set all users in the group name "Realmusers".
try {
String usersGroupUniqueId = reg.getUniqueGroupId(realm + "users");
groupList.add(usersGroupUniqueId);
} catch (Exception e) {
log.warn("ERROR finding users group " + realm + " " + e);
}
// Creates the java.util.Hashtable with the information that you
// gathered from the UserRegistry implementation.
log.debug("Now creating the subject shared state. " + realm
+ " " + uniqueID);
secTable.put(AttributeNameConstants.WSCREDENTIAL_USERID, securityName);
secTable.put(AttributeNameConstants.WSCREDENTIAL_UNIQUEID, uniqueID);
secTable.put(AttributeNameConstants.WSCREDENTIAL_SECURITYNAME,
securityName);
// secTable.put(AttributeNameConstants.WSCREDENTIAL_REALM,
// reg.getRealm());
secTable.put(AttributeNameConstants.WSCREDENTIAL_GROUPS, groupList);

// Adds a cache key that is used as part of the lookup mechanism for
// the created Subject. The cache key can be an object, but should have
// an implemented toString() method. Make sure that the cacheKey
// contains enough information to scope it to the user and any
// additional attributes that you are using. If you do not specify this
// property the Subject is scoped to the returned WSCREDENTIAL_UNIQUEID,
// by default.
secTable.put(AttributeNameConstants.WSCREDENTIAL_CACHE_KEY, realm
+ uniqueID);
return secTable;
}


Updating and creating users in the TAI requires a privileged request. The entire class to setup users and update properties is included here:

public class VmmServiceManager implements VmmService {
private static final Logger log = Logger.getLogger(VmmServiceManager.class);

/**
* Provider URL for JNDI lookup - default provider for WAS - first Node only.
*/
public static String providerURL = "corbaloc:iiop:localhost:10031";

/**
* Constructor for Virtual Member Manager Service Manager.
*/
public VmmServiceManager() {
super();
}

/**
* @return InitialContext for the current environment.
* @throws NamingException
*/
public static InitialContext getInitialContext() throws NamingException {

Hashtable env = getEnv();
if (env.size() == 2) {
return new InitialContext(env);
}
return new InitialContext();
}

/**
* @return Hashtable containing INITIAL_CONTEXT_FACTORY and
*         PROVIDER_URL
* @throws NamingException
*/
private static Hashtable getEnv() throws NamingException {
ResourceBundle rb = Constants.getResourceBundle();
// look for a server specific provider url
String serverid = System.getProperty(Constants.SERVER_NAME);
log.info("getInitialContext SERVER_NAME=" + serverid);
String providerURL = null;
try {
providerURL = rb.getString("provider.url." + serverid);
} catch (Exception e) {
// no providerURL for this serverid (serverid may have been null).
try {
// look in properties for the default provider URL
providerURL = rb.getString("provider.url");
} catch (Exception e2) {
// no providerURL, WAS will use the local provider.
}
}
log.info("Init Ctx lookup using providerURL=" + providerURL);
Hashtable env = new Hashtable(2);
if (providerURL != null) {
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.ibm.websphere.naming.WsnInitialContextFactory");
env.put(Context.PROVIDER_URL, providerURL);
}
return env;
}

/**
* @return LocalServiceProvider Wim Client com.ibm.websphere.wim.Service
*/
public LocalServiceProvider getLocalServiceProvider() {
//http://publib.boulder.ibm.com/infocenter/wasinfo/v6r1/index.jsp?topic=
// /com.ibm.websphere.javadoc.doc/vmm/com/ibm/websphere/wim/client/
// LocalServiceProvider.html
try {
// holds reference to "ejb/com/ibm/websphere/wim/ejb/WIMServiceHome"
LocalServiceProvider service = new LocalServiceProvider(getEnv());

return service;
} catch (Exception e) {
log.error("LocalServiceProvider setup fail.", e);
}
return null;
}

/**
* @param userId
* @param lang
* @param containerDn
* @return boolean success flag
*/
public boolean createUser(final String userId, final String lang,
String containerDn) {

LocalServiceProvider service = null;
try {

service = getLocalServiceProvider();
if (service == null)
return false;

// containerDn has to be null - empty string is bad.
if (containerDn != null && containerDn.trim().length() == 0) {
containerDn = null;
}
DataObject root = SDOHelper.createRootDataObject();
// API entity type might be "PersonAccount" or "Person"
// this is unclear to me. using the SchemaConstants field.
// leave the 2nd param blank to use the default container.
DataObject entity = SDOHelper.createEntityDataObject(root, containerDn,
SchemaConstants.DO_PERSON_ACCOUNT);
entity.set(Constants.UID, userId);
entity.set(Constants.CN, userId);
entity.set(Constants.SN, userId);
List displayName = new ArrayList();
displayName.add(userId);
entity.set(Constants.DISPLAY_NAME, displayName);
entity.set(Constants.PREFERRED_LANGUAGE, lang);

log.info("PersonAccount object assembled " + entity);

// Assemble a logged-in subject with admin (create-user) rights.
Subject adminSubject = retrieveAdminSubject();
log.info("VMM AdminSubject logged in " + adminSubject);

root = doSecureCreate(adminSubject, root);

log.info("PersonAccount create completed: " + root);
return true;
} catch (Throwable t) {
log.error("PersonAccount create failed", t);
}
return false;
}

/**
* @param uniqueId
* @param lang
* @return boolean success flag
*/
public boolean updateUser(final String uniqueId, final String lang) {

LocalServiceProvider service = null;
try {

service = getLocalServiceProvider();
if (service == null)
return false;
DataObject root = SDOHelper.createRootDataObject();
DataObject entity = SDOHelper.createEntityDataObject(root, null,
SchemaConstants.DO_PERSON_ACCOUNT);
entity.createDataObject(SchemaConstants.DO_IDENTIFIER).set(
SchemaConstants.PROP_UNIQUE_NAME, uniqueId);

List displayName = new ArrayList();
displayName.add(lang);
entity.set(Constants.DISPLAY_NAME, displayName);
entity.set(Constants.PREFERRED_LANGUAGE, lang);

log.info("PersonAccount object for update " + entity);

// Assemble a logged-in subject with admin (update-user) rights.
Subject adminSubject = retrieveAdminSubject();
log.info("VMM AdminSubject logged in " + adminSubject);

root = doSecureUpdate(adminSubject, root);

log.info("PersonAccount update completed: " + root);
return true;
} catch (Throwable t) {
log.error("PersonAccount update failed", t);
}
return false;
}

/**
* @param adminSubject
* @param userRoot
* @return DataObject
* @throws PrivilegedActionException
*/
@SuppressWarnings("unchecked")
protected DataObject doSecureCreate(final Subject adminSubject, final DataObject userRoot)
throws PrivilegedActionException {

return (DataObject) WSSubject.doAs(adminSubject,
new java.security.PrivilegedExceptionAction() {
public Object run() throws PrivilegedActionException {
// Subject is associated with the current thread context
return AccessController.doPrivileged(new CreateUserPrivileged(
userRoot));
}
// Subject is re-associated with the current thread context
}); // end doAs admin.
}

@SuppressWarnings("unchecked")
protected DataObject doSecureUpdate(final Subject adminSubject, final DataObject userRoot)
throws PrivilegedActionException {

return (DataObject) WSSubject.doAs(adminSubject,
new java.security.PrivilegedExceptionAction() {
public Object run() throws PrivilegedActionException {
// Subject is associated with the current thread context
return AccessController.doPrivileged(new UpdateUserPrivileged(
userRoot));
}
// Subject is re-associated with the current thread context
}); // end doAs
}

/**
* Create new user in the WebSphere Repository. This private class runs
* inside PrivilegedAction; must implement PrivilegedAction interface.
*/
class CreateUserPrivileged implements PrivilegedExceptionAction {
private DataObject rootWithNewUser;

/**
* @param containsUserObj
*/
public CreateUserPrivileged(DataObject containsUserObj) {
this.rootWithNewUser = containsUserObj;
}

/**
* Cannot throw specific exceptions on this method signature, so return
* the DataObject, or general Exception with cause.
* 
* @return Object DataObject if success, null otherwise.
* @throws Exception
*             RemoteException, WIMExcpetion
* @see java.security.PrivilegedExceptionAction#run()
*/
public Object run() throws Exception {
// Subject cut off from the current thread context.
LocalServiceProvider service = getLocalServiceProvider();
if (service == null)
return null;
log.debug("Connected to WIM service " + service.getClass().getName());
return service.create(rootWithNewUser);

}
}

/**
* Update user in the WebSphere Repository. This private class runs inside
* PrivilegedAction; must implement PrivilegedAction interface.
*/
class UpdateUserPrivileged implements PrivilegedExceptionAction {
private DataObject rootWithUser;

/**
* @param containsUserObj
*/
public UpdateUserPrivileged(DataObject containsUserObj) {
this.rootWithUser = containsUserObj;
}

/**
* Cannot throw specific exceptions on this method signature, so return
* the DataObject, or general Exception with cause.
* 
* @return Object DataObject if success, null otherwise.
* @throws Exception
*             RemoteException, WIMExcpetion
* @see java.security.PrivilegedExceptionAction#run()
*/
public Object run() throws Exception {
// Subject cut off from the current thread context.
LocalServiceProvider service = getLocalServiceProvider();
if (service == null)
return null;
log.debug("Connected to WIM service " + service.getClass().getName());
return service.update(rootWithUser);

}
}

/**
* @return Subject logged-in administrator subject.
* @throws NamingException
*/
private Subject retrieveAdminSubject() throws NamingException {
// hard-set the locale to ensure the expected file is always found.
// see also, IaaTrustAssociationInterceptor notes.
ResourceBundle rb = Constants.getResourceBundle();
final String wasAdminUser = rb.getString("admin.user");
final String wasAdminPasswd = rb.getString("admin.pass");
// log.debug("REMOVE ME LATER " + wasAdminUser + " " +
// wasAdminPasswd);

// This will only work with admin credentials, see props
Subject adminSubject = null;
LoginContext lc = null;
try {
lc = new LoginContext("WSLogin", new WSCallbackHandlerImpl(
wasAdminUser, wasAdminPasswd));
lc.login();
adminSubject = lc.getSubject();

} catch (Throwable t) {
log.error("LoginContext error " + t.getMessage());
// everything falls apart
}
return adminSubject;
}

Thursday, October 8, 2009

Axis SSL for a non-trusted Certificate - Dev, Testing, only.

The problem is the Entrust 2048 SSL certificates which were recently issued, every platform I use normally has no trouble accepting them (no imported certificates required!), but deploying on the IBM WebSphere 6.1 JDK/Server is nothing but pain.

My errors without an import - "No trusted certificate found":

00000026 WSX509TrustMa E   CWPKI0022E: SSL HANDSHAKE FAILURE:  
A signer with SubjectDN "blah" was sent from target host:port "blah:443".  
The signer may need to be added to local trust store
"../base_v61/profiles/was61profile1/config/cells/CellName/nodes/NodeName/trust.p12" located in 
SSL configuration alias "NodeDefaultSSLSettings". 
The extended error message from the SSL handshake exception is: 
"No trusted certificate found".


My errors after attempting to import to trust.p12 - "End user tried to act as a CA"

WSX509TrustMa E   CWPKI0022E: SSL HANDSHAKE FAILURE:  
A signer with SubjectDN "blah" was sent from target host:port "blah:443".  
The signer may need to be added to local trust store 
"../base_v61/profiles/was61profile1/config/cells/CellName/nodes/NodeName/trust.p12"
located in SSL configuration alias "NodeDefaultSSLSettings". 
The extended error message from the SSL handshake exception is: 
"End user tried to act as a CA".


No matter what I import or try, I keep seeing "End user tried to act as a CA". Well if that's the way it's going to be IBM-TrustManager, that's the way I'm going to play.

My solution, though designed and tested on IBM runtimes is JDK independent.

My first play was to switch to xfire, which is just about the coolest/simplest SOAP implementor out there - and I know it uses the commons httpclient package.

Use Easy SSL TrustManager implementation, so easy to override.

EasySSLTrustManager is designed to accept self-signed certs, not those that are completely borked. So for me, it was required that I copy the httpclient contrib source and add a quick try/catch around the checkClientTrust and checkServerTrusted. This way I still see errors, but the ssl communications proceeds unhindered.

Then I tried to do the same for an Axis client. There are a few misleading pages on the Axis wiki; which I completely disregarded in favour of control by AxisProperties.

I'm running on Axis 1.4, but my stubs were generated years ago and I don't want to bother regenerating.

Here's the great site that set me on the "Axis client even with self-signed SSL cert" path.

My implementation, however, is much simpler, and reuses the EasySSLTrustManager :)

We create a TrustAllSSLSocketFactory that implements Axis' SecureSocketFactory
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Hashtable;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;

import org.apache.axis.components.net.JSSESocketFactory;
import org.apache.axis.components.net.SecureSocketFactory;
import org.apache.commons.httpclient.contrib.ssl.EasyX509TrustManager;

/**
* Custom SSL socket factory to ignore certificate errors.
* 
* Based loosely on org.apache.axis.components.net.SunJSSESocketFactory
* Praise to open source libraries.
*/
public class TrustAllSSLSocketFactory extends JSSESocketFactory implements
SecureSocketFactory {
/**
* Constructor TrustAllSSLSocketFactory
* @param attributes
*/
@SuppressWarnings("unchecked")
public TrustAllSSLSocketFactory(Hashtable attributes) {
super(attributes);
}

/**
* Init the SSL socket factory with our own Trust Manager.
* 
* This overrides the parent class to provide our SocketFactory
* implementation.
* 
* @throws IOException
*/
protected void initFactory() throws IOException {

try {
SSLContext context = getContext();
sslFactory = context.getSocketFactory();
} catch (Exception e) {
if (e instanceof IOException) {
throw (IOException) e;
}
throw new IOException(e.getMessage());
}
}

/**
* Gets a custom SSL Context with no keystore and a Trust All Manager.
* 
* @return SSLContext 
* @throws Exception
*/
protected SSLContext getContext() throws Exception {

try {
// congifure a local SSLContext to use created keystores
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, new TrustManager[] { new EasyX509TrustManager(null) },
new SecureRandom());

return sslContext;
} catch (Exception e) {
throw new Exception("Error creating context for SSLSocket!", e);
}
}

}


Now we tell Axis about it's new SSL Socket Factory.

I chose to do so in the WSProxy class initializing method, but it can exist any where before the call.

public class SampleWSProxy implements SampleWS {
private String _endpoint = null;
private com.example.application.webservice.ISampleWS iSampleWS = null;

public ISampleWSProxy() {
_initISampleWSProxy();
}

private void _initISampleWSProxy() {
try {
// this Axis default class only works for self-signed certs,
//  or certs loaded by custom keystore. NB. Has version for Sun JDK also.
// AxisProperties.setProperty("axis.socketSecureFactory",
// "org.apache.axis.components.net.IBMJSSESocketFactory");

// this works for all certs - even when an exception is thrown
// connection ignores the invalid cert and moves on :)
AxisProperties.setProperty("axis.socketSecureFactory",
"full.package.to.TrustAllSSLSocketFactory");

String socketSecureFactory = AxisProperties
.getProperty("axis.socketSecureFactory");
System.out.println("axis.socketSecureFactory : " + socketSecureFactory);

iSampleWS = (new ISampleWSServiceLocator())
.getSampleWS();
if (iSampleWS != null) {
if (_endpoint != null) {
((javax.xml.rpc.Stub) iSampleWS)._setProperty(
"javax.xml.rpc.service.endpoint.address", _endpoint);
}
else {
_endpoint = (String) ((javax.xml.rpc.Stub) iSampleWS)
._getProperty("javax.xml.rpc.service.endpoint.address");
}
// Staging has a test/generic provisioning web service
// with default username password as: admin
// callers may/must override! :)
((javax.xml.rpc.Stub) iSampleWS)._setProperty(
javax.xml.rpc.Stub.USERNAME_PROPERTY, "admin");
((javax.xml.rpc.Stub) iSampleWS)._setProperty(
javax.xml.rpc.Stub.PASSWORD_PROPERTY, "admin");

}

} catch (javax.xml.rpc.ServiceException serviceException) {
}
}
... end of _initISampleWSProxy .. followed by available web service methods ...
}

Wednesday, October 7, 2009

building on solaris - make can't find ar

Happens all the time while making apache, always have to google the solution!

" ar: command not found "

Solution:

export PATH=$PATH:/usr/ccs/bin/
make

.. back on the path to win.