Milton Licensing: Openness vs survival

19/05/2014

In the beginning Milton was completely open source, on the Apache license. I had a real job back then, and I'd work on Milton on the ferry on the way to work, during my lunch hour and in my evenings and weekends. And it was great having conversations with developers around the world and helping them, and i got a real buzz out of it.

I ended up spending about a day a week on milton, peaking at the equivalent of 2 full time days a week during peak development times. So i did what most open source devs do at some point which is try to generate some income from the project. I pitched a book to a tech publisher. I tried putting google ads no the site. I put a donate button on the site. And of course i appealed to milton users for implementation and consulting work. But nothing really came of it, I received a total of under 3k during the first 5 years of milton.

That might have differently for other people in other parts of the world. Evert Pot, founder of Sabre.io (PHP equivalent of milton) says he's had no problem getting sponsorship for his work. But maybe its because I'm here in NZ, on the edge of the world with a tiny IT industry, that sponsorship has been impossible to come by.

Then at some point, partly out of choice, partly not so much, i ended up not having a real job. I picked up scraps of consulting here and there to try to get by on while i worked on my startup project (FuseLMS.com). I have to say i was really struggling for a while there, actually faced not being able to make rent. And just about then Google started using Milton for iPhone integration with Google Contacts. And of course i got no license fees from it, although i did get a small amount of consulting work.

So there's me struggling to get by while one of the world's biggest and most profitable companies ever makes greater profits thanks to my software. Things werent working.

The result was that I introduced a licensing model to Milton, where I'm trying to keep the project as open as possible, while also having a fairly robust license checking routine.

But having a runtime license check when all of your source open is effectively impossible. Whatever part of your code checks the license can be commented out, and then the jar built. To be really sure no one does that you'd obfuscate the jars and keep the source code under lock and key, but thats just not going to work for Milton. It is an API, and it needs to be accessible.

My solution was to take a little bit of code and move it somewhere secret, and to have that little bit of code include a runtime license check. So if some dishonest programmer wants to avoid paying a fee they need to re-implement that class without the license check. And thats not even particularly difficult, but at a standard hourly rate for a programmer I'm pretty sure it would cost more to do that then to pay the license fee. And, most importantly, getting around the license requires a deliberately dishonest act. There's no way anyone could claim ignorance, as they could when Milton was licensed on AGPL.

There's not much to the actual license check. I just start with a plain text file which contains the terms of the license (eg who the licensee is, the number of servers, etc) and then i generate an X509 certificate with my secret key for the license file, and then I use my public key in the license check to verify the signature. If its not valid it just disables the enterprise features. Source code is below. Its not pretty, but it doesnt need to be!

 

Generate a signature for a license file

public class GenSig {

    public static void main(String[] args) {

        /* Generate a DSA signature */

        try {
            KeyFactory keyFactory = KeyFactory.getInstance("DSA", "SUN");

            /* Load key pair */
            File privateKeyFile = new File("miltonPrivateKey");
            FileInputStream keyfis = new FileInputStream(privateKeyFile);
            byte[] privateKey = new byte[keyfis.available()];
            keyfis.read(privateKey);
            String k = new String(privateKey);
            System.out.println("private key: " + k);
            privateKey = Base64.decodeBase64(privateKey);
            System.out.println("read private key: " + privateKeyFile.getAbsolutePath() + " - " + privateKey.length);
            keyfis.close();
            PKCS8EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(privateKey);
            PrivateKey priv = keyFactory.generatePrivate(privKeySpec);

            File publicKeyFile = new File("miltonPublicKey");
            keyfis = new FileInputStream(publicKeyFile);
            byte[] publicKey = new byte[keyfis.available()];
            keyfis.read(publicKey);
            System.out.println("pub key: " + new String(publicKey));
            publicKey = Base64.decodeBase64(publicKey);
            System.out.println("read public key: " + publicKeyFile.getAbsolutePath() + " - " + publicKey.length);
            keyfis.close();

            X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(publicKey);
            PublicKey pub = keyFactory.generatePublic(pubKeySpec);

            /* Create a Signature object and initialize it with the private key */

            Signature dsa = Signature.getInstance("SHA1withDSA", "SUN");

            dsa.initSign(priv);

            /* Update and sign the data */

            FileInputStream fis = new FileInputStream("milton.license.properties");
            byte[] licenseBytes = LockUtils.readNormalisedLineEndings(fis);
            dsa.update(licenseBytes, 0, licenseBytes.length);

            /* Now that all the data to be signed has been read in,
             generate a signature for it */

            byte[] realSig = dsa.sign();

            /* Save the signature in a file */
            FileOutputStream sigfos = new FileOutputStream("milton.license.sig");
            byte[] arr = Base64.encodeBase64String(realSig).getBytes("UTF-8");
            sigfos.write(arr);
            sigfos.close();
            System.out.println("wrote signature: sig: " + arr.length + " bytes");

        } catch (Exception e) {
            System.err.println("Caught exception " + e.toString());
            e.printStackTrace();
        }
    }
}

 

And here is the code to check the signature:

public static Properties getValidatedLicenseProperties() {
        try {
            byte[] licenseBytes;
            {
                InputStream in = getResource("milton.license.properties");
                if (in == null) {
                    return null;
                }
                licenseBytes = LockUtils.readNormalisedLineEndings(in);
            }

            KeyFactory keyFactory = KeyFactory.getInstance("DSA");

            Signature calculatedSig;
            {
                InputStream in = getResource("miltonPublicKey");
                if (in == null) {
                    logLicense.warn("No Milton2 public key file found on the classpath. Expected to find /miltonPublicKey - please contact the licensor at http://milton.io");
                    return null;
                }
                byte[] publicKey = new byte[in.available()];
                in.read(publicKey);
                in.close();
                publicKey = Base64.decodeBase64(publicKey);
                X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(publicKey);
                PublicKey pub = keyFactory.generatePublic(pubKeySpec);

                calculatedSig = Signature.getInstance("SHA1withDSA");
                calculatedSig.initVerify(pub);
            }

            {
                ByteArrayInputStream bin = new ByteArrayInputStream(licenseBytes);
                byte[] buffer = new byte[1024];
                int len;
                while (bin.available() != 0) {
                    len = bin.read(buffer);
                    calculatedSig.update(buffer, 0, len);
                }
                bin.close();
            }

            InputStream in = getResource("milton.license.sig");
            if (in == null) {
                logLicense.warn("No Milton2 license signature found. Please create a classpath resource /milton.license.sig containg the signature provided, or contact the licensor at http://milton.io");
                return null;
            }
            byte[] sigToVerify = new byte[in.available()];
            in.read(sigToVerify);
            in.close();
            sigToVerify = Base64.decodeBase64(sigToVerify);

            boolean verifies = calculatedSig.verify(sigToVerify);

            if (verifies) {
                Properties props = new Properties();
                ByteArrayInputStream bin = new ByteArrayInputStream(licenseBytes);
                props.load(bin);

                if (props.containsKey("Expires")) {
                    String sExpires = props.getProperty("Expires");
                    if (sExpires != null && sExpires.trim().length() > 0) {
                        DateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
                        Date expiryDate = sdf.parse(sExpires);
                        Date now = new Date();
                        if (now.after(expiryDate)) {
                            logLicense.warn("WARNING: Your Milton2 license has expired. Please contact the licensor at http://milton.io");
                            return null;
                        }
                    }
                }
                return props;
            } else {
                logLicense.warn("The Milton2 license signature is not valid for the supplied license file and will be ignored. Please check the files are exactly as provided, with no additional whitespace or any other changes. Please contact us if you still experience problems at http://milton.io/contactus");
                return null;
            }

        } catch (Exception e) {
            logLicense.warn("Exception checking for milton commercial license: " + e.toString() + " If you have a commercial license please check with the licensor at http://milton.io");
            e.printStackTrace();
            return null;
        }
    }

 

One of the bits that i struggled with the most is the loading of the resources. There is such a bewildering number of different class loader strategies around, and some people want these resources in their WAR, others want it in the container classpath, others want it as an external file, that its actually quite difficult finding a way to load resources that works for everyone. Anyway, this is what i've come up with:

    private static InputStream getResource(String resName) {
        String sysProp = System.getProperty("milton.license.dir");
        if (sysProp != null && sysProp.length() > 0) {
            File f = new File(sysProp);
            if (f.exists()) {
                if (f.isDirectory()) {
                    f = new File(f, resName);
                    if (f.exists()) {
                        if (f.isFile()) {
                            try {
                                return new FileInputStream(f);
                            } catch (Exception e) {
                                log.error("Exception looking for resource: " + resName + " in file: " + f.getAbsolutePath() + " Will try classpath", e);
                            }
                        } else {
                            log.info("Found milton.license.dir property, but required resource " + resName + " is not a file so will be ignored: " + f.getAbsolutePath());
                        }
                    } else {
                        log.info("Found milton.license.dir property, but could not find required resource: " + resName + " Will try classpath..");
                    }
                } else {
                    log.warn("Found milton.license.dir system property, but it refers to a file instead of a directory: " + f.getAbsolutePath());
                }
            } else {
                log.warn("Found milton.license.dir system property, but it does not exist, so will be ignored. Absolute path=" + f.getAbsolutePath());
            }
        }

        // Try with local class loader
        InputStream in = LockUtils.class.getResourceAsStream("/" + resName);
        if (in == null) {
            // Not found so try with parent class loader
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            in = cl.getResourceAsStream("/" + resName);
            if (in == null) {
                log.warn("Could not find required resource: " + resName + " in classpath from local classloader:" + LockUtils.class.getClassLoader() + " or parent classloader: " + cl);
            }

        }
        return in;
    }

 

So I hope that gives some reason to what might look like a fairly complex licensing structure. And if anyone can suggest something better, please do!

 

/Brad

 


Questions and answers