JSSE With Object Capabilities

I gave a talk earlier this year about Security in Scala, mostly talking about object capabilities and how they're useful in general situations.

The big idea of capabilities is that your access to the object is mediated through a proxy which does some computation on every call out to your object. Scala's support for call-by-name and lazy evaluation makes it straightforward to add object capabilities into the mix, but it's also possible to do this in Java, using only a supplier with a proxy.

Assume you have a Foo interface.

public interface Foo {
  void doTheThing();
}

You can create a proxy implementation of Foo that will mediate access to the original:

class FooImpl implements Foo {
  void doTheThing() { 
    System.out.println("hello world!");
  }
}

class ProxyFoo implements Foo {
  private final Supplier<Foo> supplier;
  public ProxyFoo(Foo foo) {
    this(() -> foo);
  }
  public ProxyFoo(final Supplier<Foo> supplier) {
    this.supplier = supplier;
  }
  public void doTheThing() {
    supplier.get().doTheThing();
  }
}

Now that you have a supplier and a proxy, you can control access and audit calls to the original implementation:

Foo fooImpl = new FooImpl();
Instant expirationDate = Instant.now().plusDays(1);
Supplier<Foo> fooSupplier = () -> {
  if (Instant.now().isAfter(expirationDate)) {
    throw new IllegalStateException("You cannot use this after the expiration date!");
  }
  return fooImpl;
};
Foo timeLimitedFoo = new ProxyFoo(fooSupplier);

This is the basis of object capabilities – code that uses timeLimitedFoo has no way to execute methods on fooImpl after a certain date. The caller's access is revoked.

The really nice thing about this is that it's all straightforward. There's no reflection, there's no code generation, there's no tricks. Setting up a proxy class is something that IntelliJ IDEA will do automatically if you use the "Delegate" action, and you're done in five minutes.

I've written a small project called ocapjsse that provides proxies for X509Certificate, X509ExtendedTrustManager, X509ExtendedKeyManager, and SSLEngine. There are two sub-projects: one that handles logging, and another than handles revocation.

The logging subproject will take an individual key manager or trust manager, and print out method call access through SLF4J. It's a lightweight alternative to using the debugjsse provider because it's only on one instance, and can be tightly scoped.

public class LoggingX509ExtendedTrustManagerTest {
  @Test
  public void testLog() throws Exception {
    final TraceLogger tracer = ...
    try {
      final SSLContext sslContext = SSLContext.getInstance("TLS");
      final TrustManagerFactory tmf =
          TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
      tmf.init((KeyStore) null);
      final TrustManager[] tms =
          Arrays.stream(tmf.getTrustManagers())
              .map(LoggingX509ExtendedTrustManager.transform(tracer))
              .toArray(TrustManager[]::new);

      sslContext.init(null, tms, null);
      final HttpsURLConnection urlConnection =
          (HttpsURLConnection) new URL("https://www.google.com").openConnection();
      urlConnection.setSSLSocketFactory(sslContext.getSocketFactory());

      try (final BufferedReader in =
          new BufferedReader(new InputStreamReader(urlConnection.getInputStream()))) {
        final String result = in.lines().collect(Collectors.joining());
        System.out.println(result);
      }
    } catch (final Exception e) {
      e.printStackTrace();
    }
  }
}

The revocation sub-project adds a Caretaker class that returns a proxied capability, and a Revoker to shut off access through that proxy. You can use this to shut off credentials or a TLS connection whenever security guarantees are violated, or you may want to make double sure that an expired certificate can't be used incorrectly.

package com.tersesystems.ocapjsse.revocable;

public class RevocableTrustManagerTest {
  @Test
  public void testRevokedEngine() throws Exception {
    X509ExtendedTrustManager trustManager = createTrustManager();
    assertThat(trustManager).isNotNull();

    Caretaker<X509ExtendedTrustManager> caretaker = Caretaker
        .create(trustManager, ProxyX509ExtendedTrustManager::new);
    X509ExtendedTrustManager proxyTrustManager = caretaker.getCapability();

    X509Certificate[] issuers = proxyTrustManager.getAcceptedIssuers();
    assertThat(issuers).isNotNull();
    caretaker.getRevoker().revoke();

    Throwable throwable = catchThrowable(proxyTrustManager::getAcceptedIssuers);
    assertThat(throwable).isInstanceOf(RevokedException.class);
  }
}

This code isn't published on Maven or Bintray yet, but I'm working on it.

Beyond the logging and revocation, you can do more powerful things like responsibility tracking and simulate latency and errors in your low level stack for a chaos monkey type check of your recovery code.

I hope this gives an idea of how simple and powerful object capabilities can be. This only really scratches the surface, but it shows how you can add behavior around an object from the outside without changing the internals.

Comments