diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000000..715dedf3c2a --- /dev/null +++ b/pom.xml @@ -0,0 +1,165 @@ + + + 4.0.0 + + org.elasticsearch + elasticsearch-license + 1.0-SNAPSHOT + + + 1.4.0-SNAPSHOT + + + + + + + org.elasticsearch + elasticsearch + ${elasticsearch.version} + test + test-jar + + + + junit + junit + 4.11 + test + + + + + net.nicholaswilliams.java.licensing + licensing-core + 1.1.0 + + + + net.nicholaswilliams.java.licensing + licensing-licensor-base + 1.1.0 + + + + org.codehaus.jackson + jackson-mapper-asl + 1.8.5 + + + + org.elasticsearch + elasticsearch + ${elasticsearch.version} + + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 1.3.1 + + + enforce-versions + + enforce + + + + + [1.7,) + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.7 + 1.7 + true + 512m + + false + + -XDignore.symbol.file + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.16 + + + **/*Tests.java + + + + + + + + maven-assembly-plugin + 2.3 + + false + ${project.build.directory}/releases/ + + ${basedir}/src/main/assemblies/plugin.xml + + + jar-with-dependencies + + + + + package + + attached + + + + assemble-all + package + + single + + + + + + + + + \ No newline at end of file diff --git a/sample/gen_license.json b/sample/gen_license.json new file mode 100644 index 00000000000..0e3058826bc --- /dev/null +++ b/sample/gen_license.json @@ -0,0 +1,13 @@ +{ + "licenses" : [ { + "uid" : "d8bcf9e8-bcb0-4f72-81ca-8a7537a436c5", + "type" : "internal", + "subscription_type" : "none", + "issued_to" : "issuedTo", + "issue_date" : "2014-09-29", + "expiry_date" : "2015-08-29", + "feature" : "shield", + "max_nodes" : 1, + "signature" : "naPgicfKM2+IJ0AoYgAAAG0AAAAAVGdIQ01qZUtCeEZNbS8wcTF4RU5mYUpiY01hdFlQNEVkdFJhYitoZndrSTI5eVZrY3ZRZ3lYU0s1QWdYb0Y5d1dBQmRUK01leE1aR0RUOHhoRVVhVUE9PaztAAVzcgAxbmV0Lm5pY2hvbGFzd2lsbGlhbXMuamF2YS5saWNlbnNpbmcuU2lnbmVkTGljZW5zZYqE/59+smqEAgACWwAObGljZW5zZUNvbnRlbnR0AAJbQlsAEHNpZ25hdHVyZUNvbnRlbnRxAH4AAXhwdXIAAltCrPMX+AYIVOACAAB4cAAAAMCsH5r77/8FtWY+JxKd9MiBTYQLcXgmXMm+Y83VaNwmlr1lASJ2yf7rWojiuHTWemtUNtOZcXeSrLfs/oKwBzXIfvEZV8X/vPCWnpi7VtU4Hp+OZUFO4c0NQ1PnVdDk1uns16Dqe99/ota3FSvdFrmlzkz2E+2bbx0fwWbKnGDXFXy6eE7OISRJdCqa8gljMo9PA1+RI7MFQ8bSzs9up0cEkSuPzgtafFW5zfyn2vpoPZTxDpJslTBk7S3mdchE0eJ1cQB+AAMAAAEAdikZHpJVMxWMxNsksYnNOD7F+15SK3MCtUWJnQdhYCuVHdKQUE3YxWv59QQuDmKuLbnvi0DsuPGlq3hEx0AXmbpaBOhkwTv3DKZH7V6C0YmXj7RLZobaDTtGY2pwV6Qf5+teq5dV493a1k6YGFiwUoERuWQxqmA36naLdVo2diCSh8QmZ4ihKnhqxwswh2TlnCVuaNN3E7HuGeE0wYgFEfgISJOFlEOnLOItRlrQOTzCq+mhASKbANxx/Z42eMGrgs+GJsxYQZfnBh8K3NQFQk2SjWR1sEgqUPXC+0Z7ungzkkwoSBbrdJfRPKbqXFDthWI1DY9SSZnTbwpUC2XA6Q==" + } ] +} \ No newline at end of file diff --git a/sample/license_spec.json b/sample/license_spec.json new file mode 100644 index 00000000000..92997a9f242 --- /dev/null +++ b/sample/license_spec.json @@ -0,0 +1,14 @@ +{ + "licenses": [ + { + "type": "internal", + "subscription_type": "none", + "issued_to": "issuedTo", + "issuer": "issuer", + "issue_date": "2014-09-29", + "expiry_date": "2015-08-29", + "feature": "shield", + "max_nodes": 1 + } + ] +} diff --git a/src/main/assemblies/plugin.xml b/src/main/assemblies/plugin.xml new file mode 100644 index 00000000000..8e8e42e7c05 --- /dev/null +++ b/src/main/assemblies/plugin.xml @@ -0,0 +1,18 @@ + + + plugin + + zip + + false + + + / + true + true + + org.elasticsearch:elasticsearch + + + + \ No newline at end of file diff --git a/src/main/java/org/elasticsearch/license/core/DateUtils.java b/src/main/java/org/elasticsearch/license/core/DateUtils.java new file mode 100644 index 00000000000..5ad19febae9 --- /dev/null +++ b/src/main/java/org/elasticsearch/license/core/DateUtils.java @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.license.core; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; + +public class DateUtils { + public static final DateFormat DATE_FORMAT; + public static final TimeZone TIME_ZONE = TimeZone.getTimeZone("UTC"); + + static { + DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); + DATE_FORMAT.setTimeZone(DateUtils.TIME_ZONE); + DATE_FORMAT.setLenient(false); + } + + public static long longExpiryDateFromDate(long date) { + Date dateObj = new Date(date); + + Calendar calendar = Calendar.getInstance(); + calendar.clear(); + calendar.setTimeZone(TIME_ZONE); + calendar.setTimeInMillis(dateObj.getTime()); + + calendar.set(Calendar.HOUR, 23); + calendar.set(Calendar.MINUTE, 59); + calendar.set(Calendar.SECOND, 59); + + return calendar.getTimeInMillis(); + } + + public static long longFromDateString(String dateStr) throws ParseException { + Date dateObj = DATE_FORMAT.parse(dateStr); + Calendar calendar = Calendar.getInstance(); + calendar.clear(); + calendar.setTimeZone(TIME_ZONE); + calendar.setTimeInMillis(dateObj.getTime()); + return calendar.getTimeInMillis(); + } + + public static long longExpiryDateFromString(String dateStr) throws ParseException { + return longExpiryDateFromDate(longFromDateString(dateStr)); + } + + public static String dateStringFromLongDate(long date) { + Date dateObj = new Date(date); + Calendar calendar = Calendar.getInstance(); + calendar.clear(); + calendar.setTimeZone(TIME_ZONE); + calendar.setTimeInMillis(dateObj.getTime()); + return DATE_FORMAT.format(calendar.getTime()); + } +} diff --git a/src/main/java/org/elasticsearch/license/core/ESLicenses.java b/src/main/java/org/elasticsearch/license/core/ESLicenses.java new file mode 100644 index 00000000000..015a5bc28f1 --- /dev/null +++ b/src/main/java/org/elasticsearch/license/core/ESLicenses.java @@ -0,0 +1,251 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.license.core; + +import java.util.Collection; +import java.util.Set; + + +/** + * Interface for ESLicenses, ESLicense + * and enums for Type, SubscriptionType and FeatureType. + *

+ * This is the main contract between the licensor and the license manager + */ +public interface ESLicenses extends Iterable { + + /** + * @return list of licenses contained under this instance + */ + public Collection licenses(); + + /** + * @return Set of features for which there exists an underlying license + */ + public Set features(); + + /** + * @return a license for a code>featureType< + */ + public ESLicense get(FeatureType featureType); + + /** + * Enum for License Type + */ + public enum Type { + TRIAL((byte) 0, "trial"), + SUBSCRIPTION((byte) 1, "subscription"), + INTERNAL((byte) 2, "internal"); + + private final byte id; + private final String name; + + private Type(byte id, String name) { + this.id = id; + this.name = name; + } + + public String string() { + return name; + } + + public byte id() { + return id; + } + + public static Type fromId(byte id) { + switch (id) { + case 0: + return TRIAL; + case 1: + return SUBSCRIPTION; + case 2: + return INTERNAL; + default: + throw new IllegalArgumentException("Invalid Type id=" + id); + } + } + + public static Type fromString(String type) { + if (type.equalsIgnoreCase(TRIAL.string())) { + return TRIAL; + } else if (type.equalsIgnoreCase(SUBSCRIPTION.string())) { + return SUBSCRIPTION; + } else if (type.equalsIgnoreCase(INTERNAL.string())) { + return INTERNAL; + } else { + throw new IllegalArgumentException("Invalid Type=" + type); + } + + } + } + + /** + * Enum for License Subscription Type + */ + public enum SubscriptionType { + NONE((byte) 0, "none"), + DEVELOPMENT((byte) 1, "development"), + SILVER((byte) 2, "silver"), + GOLD((byte) 3, "gold"), + PLATINUM((byte) 4, "platinum"); + + public static SubscriptionType DEFAULT = NONE; + + private final byte id; + private final String name; + + private SubscriptionType(byte id, String name) { + this.id = id; + this.name = name; + } + + public String string() { + return name; + } + + public byte id() { + return id; + } + + public static SubscriptionType fromId(byte id) { + switch (id) { + case 0: + return NONE; + case 1: + return DEVELOPMENT; + case 2: + return SILVER; + case 3: + return GOLD; + case 4: + return PLATINUM; + default: + throw new IllegalArgumentException("Invalid SubscriptionType id=" + id); + } + } + + + public static SubscriptionType fromString(String subscriptionType) { + if (subscriptionType.equalsIgnoreCase(NONE.string())) { + return NONE; + } else if (subscriptionType.equalsIgnoreCase(DEVELOPMENT.string())) { + return DEVELOPMENT; + } else if (subscriptionType.equalsIgnoreCase(SILVER.string())) { + return SILVER; + } else if (subscriptionType.equalsIgnoreCase(GOLD.string())) { + return GOLD; + } else if (subscriptionType.equalsIgnoreCase(PLATINUM.string())) { + return PLATINUM; + } else { + throw new IllegalArgumentException("Invalid SubscriptionType=" + subscriptionType); + } + } + } + + /** + * Enum for License FeatureType + */ + public enum FeatureType { + SHIELD((byte) 0, "shield"), + MARVEL((byte) 1, "marvel"); + + private final byte id; + + private final String name; + + private FeatureType(byte id, String name) { + this.id = id; + this.name = name; + } + + public String string() { + return name; + } + + public byte id() { + return id; + } + + public static FeatureType fromId(byte id) { + switch (id) { + case 0: + return SHIELD; + case 1: + return MARVEL; + default: + throw new IllegalArgumentException("Invalid FeatureType id=" + id); + } + } + + public static FeatureType fromString(String featureType) { + if (featureType.equalsIgnoreCase(SHIELD.string())) { + return SHIELD; + } else if (featureType.equalsIgnoreCase(MARVEL.string())) { + return MARVEL; + } else { + throw new IllegalArgumentException("Invalid FeatureType=" + featureType); + } + } + } + + /** + * Interface representing all the license fields + */ + public interface ESLicense { + + /** + * @return a unique identifier for a license (currently just a UUID) + */ + public String uid(); + + /** + * @return type of the license [trial, subscription, internal] + */ + public Type type(); + + /** + * @return subscription type of the license [none, silver, gold, platinum] + */ + public SubscriptionType subscriptionType(); + + /** + * @return the issueDate in milliseconds + */ + public long issueDate(); + + /** + * @return the featureType for the license [shield, marvel] + */ + public FeatureType feature(); + + /** + * @return the expiry date in milliseconds + */ + public long expiryDate(); + + /** + * @return the maximum number of nodes this license has been issued for + */ + public int maxNodes(); + + /** + * @return a string representing the entity this licenses has been issued to + */ + public String issuedTo(); + + /** + * @return a string representing the entity responsible for issuing this license (internal) + */ + public String issuer(); + + /** + * @return a string representing the signature of the license used for license verification + */ + public String signature(); + } + +} diff --git a/src/main/java/org/elasticsearch/license/core/LicenseBuilders.java b/src/main/java/org/elasticsearch/license/core/LicenseBuilders.java new file mode 100644 index 00000000000..1799e8e883e --- /dev/null +++ b/src/main/java/org/elasticsearch/license/core/LicenseBuilders.java @@ -0,0 +1,308 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.license.core; + +import java.util.*; + +import static org.elasticsearch.license.core.ESLicenses.*; + +public class LicenseBuilders { + + /** + * @return a licenses builder instance to build a {@link org.elasticsearch.license.core.ESLicenses} + */ + public static LicensesBuilder licensesBuilder() { + return new LicensesBuilder(); + } + + /** + * @return a license builder instance to build a {@link org.elasticsearch.license.core.ESLicenses.ESLicense} + * if internal is set to true, then license fields (which are internal) are required to be set + */ + public static LicenseBuilder licenseBuilder(boolean internal) { + return new LicenseBuilder(internal); + } + + /** + * Merges all the sub-licenses of the provided licenses parameters by + * longest expiry date for each license feature and merges out any + * sub-licenses that have already expired + * + * @return a merged ESLicenses instance from licenses + * and mergedLicenses + */ + public static ESLicenses merge(ESLicenses licenses, ESLicenses mergeLicenses) { + if (licenses == null && mergeLicenses == null) { + throw new IllegalArgumentException("both licenses can not be null"); + } else if (licenses == null) { + return mergeLicenses; + } else if (mergeLicenses == null) { + return licenses; + } else { + return licensesBuilder() + .licenses(licenses) + .licenses(mergeLicenses) + .build(); + } + } + + public static class LicensesBuilder { + private Map licenseMap; + + public LicensesBuilder() { + } + + public LicensesBuilder license(LicenseBuilder builder) { + return license(builder.build()); + } + + public LicensesBuilder license(ESLicense license) { + initLicenses(); + putIfAppropriate(license); + return this; + } + + public LicensesBuilder licenses(Collection licenses) { + for (ESLicense esLicense : licenses) { + license(esLicense); + } + return this; + } + + public LicensesBuilder licenses(ESLicenses licenses) { + return licenses(licenses.licenses()); + } + + public ESLicenses build() { + return new ESLicenses() { + @Override + public Collection licenses() { + return licenseMap.values(); + } + + @Override + public Set features() { + return licenseMap.keySet(); + } + + @Override + public ESLicense get(FeatureType featureType) { + return licenseMap.get(featureType); + } + + @Override + public Iterator iterator() { + return licenseMap.values().iterator(); + } + }; + } + + private void initLicenses() { + if (licenseMap == null) { + licenseMap = new HashMap<>(); + } + } + + /** + * Add a {@link org.elasticsearch.license.core.ESLicenses.ESLicense} to + * {@link org.elasticsearch.license.core.ESLicenses} only if + * there exists no License for the feature that has a longer expiry date + * and if the license in question has an expiryDate that has + * not expired yet + * + * @param license license in question + */ + private void putIfAppropriate(ESLicense license) { + final FeatureType featureType = license.feature(); + if (licenseMap.containsKey(featureType)) { + final ESLicense previousLicense = licenseMap.get(featureType); + if (license.expiryDate() > previousLicense.expiryDate()) { + licenseMap.put(featureType, license); + } + } else if (license.expiryDate() > System.currentTimeMillis()) { + licenseMap.put(featureType, license); + } + } + } + + public static class LicenseBuilder { + private String uid; + private String issuer; + private String issuedTo; + private long issueDate = -1; + private Type type; + private SubscriptionType subscriptionType = SubscriptionType.DEFAULT; + private FeatureType feature; + private String signature; + private long expiryDate = -1; + private int maxNodes; + + + private final boolean internal; + + public LicenseBuilder(boolean internal) { + this.internal = internal; + } + + public LicenseBuilder uid(String uid) { + this.uid = uid; + return this; + } + + public LicenseBuilder issuer(String issuer) { + this.issuer = issuer; + return this; + } + + public LicenseBuilder issuedTo(String issuedTo) { + this.issuedTo = issuedTo; + return this; + } + + public LicenseBuilder issueDate(long issueDate) { + this.issueDate = issueDate; + return this; + } + + public LicenseBuilder type(Type type) { + this.type = type; + return this; + } + + public LicenseBuilder subscriptionType(SubscriptionType subscriptionType) { + this.subscriptionType = subscriptionType; + return this; + } + + public LicenseBuilder feature(FeatureType feature) { + this.feature = feature; + return this; + } + + public LicenseBuilder expiryDate(long expiryDate) { + this.expiryDate = expiryDate; + return this; + } + + public LicenseBuilder maxNodes(int maxNodes) { + this.maxNodes = maxNodes; + return this; + } + + public LicenseBuilder signature(String signature) { + if (signature != null) { + this.signature = signature; + } + return this; + } + + public LicenseBuilder fromLicense(ESLicense license) { + LicenseBuilder builder = this.uid(license.uid()) + .issuedTo(license.issuedTo()) + .issueDate(license.issueDate()) + .type(license.type()) + .subscriptionType(license.subscriptionType()) + .feature(license.feature()) + .maxNodes(license.maxNodes()) + .expiryDate(license.expiryDate()); + + return (internal) + ? builder.issuer(license.issuer()).signature(license.signature()) + : builder; + + + } + + public ESLicense build() { + if (uid == null) { + uid = UUID.randomUUID().toString(); + } + verify(); + return new ESLicense() { + @Override + public String uid() { + return uid; + } + + @Override + public Type type() { + return type; + } + + @Override + public SubscriptionType subscriptionType() { + return subscriptionType; + } + + @Override + public long issueDate() { + return issueDate; + } + + @Override + public FeatureType feature() { + return feature; + } + + @Override + public long expiryDate() { + return expiryDate; + } + + @Override + public int maxNodes() { + return maxNodes; + } + + @Override + public String issuer() { + return issuer; + } + + @Override + public String issuedTo() { + return issuedTo; + } + + @Override + public String signature() { + return signature; + } + }; + } + + private void verify() { + String msg = null; + if (internal && issuer == null) { + msg = "issuer can not be null"; + } else if (issuedTo == null) { + msg = "issuedTo can not be null"; + } else if (issueDate == -1) { + msg = "issueDate has to be set"; + } else if (type == null) { + msg = "type can not be null"; + } else if (subscriptionType == null) { + msg = "subscriptionType can not be null"; + } else if (uid == null) { + msg = "uid can not be null"; + } else if (feature == null) { + msg = "at least one feature has to be enabled"; + } else if (internal && signature == null) { + msg = "signature can not be null"; + } else if (maxNodes == -1) { + msg = "maxNodes has to be set"; + } else if (expiryDate == -1) { + msg = "expiryDate has to be set"; + } + + if (msg != null) { + throw new IllegalStateException(msg); + } + } + } + + +} diff --git a/src/main/java/org/elasticsearch/license/core/LicenseUtils.java b/src/main/java/org/elasticsearch/license/core/LicenseUtils.java new file mode 100644 index 00000000000..c535a8943c7 --- /dev/null +++ b/src/main/java/org/elasticsearch/license/core/LicenseUtils.java @@ -0,0 +1,182 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.license.core; +import org.apache.commons.io.FileUtils; +import org.codehaus.jackson.JsonFactory; +import org.codehaus.jackson.JsonGenerator; +import org.codehaus.jackson.JsonNode; +import org.codehaus.jackson.map.ObjectMapper; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.text.ParseException; +import java.util.HashSet; +import java.util.Set; + +public class LicenseUtils { + + public static void dumpLicenseAsJson(ESLicenses esLicenses, OutputStream out) throws IOException { + JsonGenerator generator = new JsonFactory().createJsonGenerator(out); + generator.useDefaultPrettyPrinter(); + + generator.writeStartObject(); + { + generator.writeArrayFieldStart("licenses"); + { + for (ESLicenses.ESLicense esLicense : esLicenses) { + generator.writeStartObject(); + { + generator.writeStringField("uid", esLicense.uid()); + generator.writeStringField("type", esLicense.type().string()); + generator.writeStringField("subscription_type", esLicense.subscriptionType().string()); + generator.writeStringField("issued_to", esLicense.issuedTo()); + generator.writeStringField("issue_date", DateUtils.dateStringFromLongDate(esLicense.issueDate())); + generator.writeStringField("expiry_date", DateUtils.dateStringFromLongDate(esLicense.expiryDate())); + generator.writeStringField("feature", esLicense.feature().string()); + generator.writeNumberField("max_nodes", esLicense.maxNodes()); + generator.writeStringField("signature", esLicense.signature()); + } + generator.writeEndObject(); + } + } + generator.writeEndArray(); + } + generator.writeEndObject(); + generator.flush(); + } + + public static Set readLicensesFromFiles(Set licenseFiles) throws IOException { + Set esLicensesSet = new HashSet<>(); + for (File licenseFile : licenseFiles) { + esLicensesSet.add(LicenseUtils.readLicenseFile(licenseFile)); + } + return esLicensesSet; + } + + + public static Set readLicensesFromDirectory(File licenseDirectory) throws IOException { + Set esLicensesSet = new HashSet<>(); + if (!licenseDirectory.exists()) { + throw new IllegalArgumentException(licenseDirectory.getAbsolutePath() + " does not exist!"); + } + if (licenseDirectory.isDirectory()) { + for (File licenseFile : FileUtils.listFiles(licenseDirectory, new String[]{"json"}, false)) { + esLicensesSet.add(readLicenseFile(licenseFile)); + } + } else if (licenseDirectory.isFile()) { + esLicensesSet.add(readLicenseFile(licenseDirectory)); + } else { + throw new IllegalArgumentException(licenseDirectory.getAbsolutePath() + "is not a file or a directory"); + } + return esLicensesSet; + } + + public static ESLicenses readLicenseFile(File licenseFile) throws IOException { + try (FileInputStream fileInputStream = new FileInputStream(licenseFile)) { + JsonNode jsonNode = new ObjectMapper().readTree(fileInputStream); + return extractLicenseFromJson(jsonNode); + } + } + + public static ESLicenses readLicensesFromString(String licensesString) throws IOException { + JsonNode jsonNode = new ObjectMapper().readTree(licensesString); + return extractLicenseFromJson(jsonNode); + } + + private static ESLicenses extractLicenseFromJson(final JsonNode jsonNode) { + final LicenseBuilders.LicensesBuilder licensesBuilder = LicenseBuilders.licensesBuilder(); + JsonNode licensesNode = jsonNode.get("licenses"); + if (licensesNode.isArray()) { + for (JsonNode licenseNode : licensesNode) { + licensesBuilder.license(LicenseBuilders.licenseBuilder(false) + .uid(getValueAsString(licenseNode, "uid", true)) + .issuedTo(getValueAsString(licenseNode, "issued_to")) + .issuer(getValueAsString(licenseNode, "issuer", true)) + .issueDate(getValueAsDate(licenseNode, "issue_date")) + .type(ESLicenses.Type.fromString(getValueAsString(licenseNode, "type"))) + .subscriptionType(ESLicenses.SubscriptionType.fromString(getValueAsString(licenseNode, "subscription_type"))) + .feature(ESLicenses.FeatureType.fromString(getValueAsString(licenseNode, "feature"))) + .expiryDate(getValueAsExpiryDate(licenseNode, "expiry_date")) + .maxNodes(getValueAsInt(licenseNode, "max_nodes")) + .signature(getValueAsString(licenseNode, "signature", true)) + .build()); + } + } else { + throw new IllegalStateException("'licenses' field is not an array"); + } + return licensesBuilder.build(); + + } + + private static int getValueAsInt(final JsonNode jsonNode, String field) { + JsonNode node = getFieldNode(jsonNode, field, false); + assert node.isNumber(); + return node.getValueAsInt(); + } + + private static String getValueAsString(final JsonNode jsonNode, String field) { + return getValueAsString(jsonNode, field, false); + } + + private static String getValueAsString(final JsonNode jsonNode, String field, boolean optional) { + JsonNode node = getFieldNode(jsonNode, field, optional); + assert node != null || optional; + if (node == null) { + return null; + } + assert !node.isObject(); + return node.getTextValue(); + } + + private static long getValueAsDate(final JsonNode jsonNode, String field) { + JsonNode node = getFieldNode(jsonNode, field, false); + assert !node.isObject(); + final String value = node.getTextValue(); + try { + return DateUtils.longFromDateString(value); + } catch (ParseException e) { + throw new IllegalArgumentException(e); + } + } + + private static long getValueAsExpiryDate(final JsonNode jsonNode, String field) { + long actualDate = getValueAsDate(jsonNode, field); + return DateUtils.longExpiryDateFromDate(actualDate); + } + + + private static JsonNode getFieldNode(final JsonNode jsonNode, String field, boolean optional) { + JsonNode node = jsonNode.get(field); + if (node == null && !optional) { + throw new IllegalArgumentException("field ['" + field + "'] is missing"); + } + return node; + } + + + public static void printLicense(ESLicenses licenses) { + for (ESLicenses.ESLicense license : licenses) { + System.out.println("==="); + printValue(" uid", license.uid()); + printValue(" type", license.type().string()); + printValue(" subscription_type", license.subscriptionType().string()); + printValue(" issueDate", DateUtils.dateStringFromLongDate(license.issueDate())); + printValue(" issuedTo", license.issuedTo()); + printValue(" feature", license.feature().string()); + printValue(" maxNodes", license.maxNodes()); + printValue(" expiryDate", DateUtils.dateStringFromLongDate(license.expiryDate())); + printValue(" signature", license.signature()); + System.out.println("==="); + } + + } + + private static void printValue(String name, Object value) { + System.out.println(name + " : " + value); + } +} diff --git a/src/main/java/org/elasticsearch/license/licensor/ESLicenseSigner.java b/src/main/java/org/elasticsearch/license/licensor/ESLicenseSigner.java new file mode 100644 index 00000000000..fa7681723b7 --- /dev/null +++ b/src/main/java/org/elasticsearch/license/licensor/ESLicenseSigner.java @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.license.licensor; + +import net.nicholaswilliams.java.licensing.License; +import net.nicholaswilliams.java.licensing.encryption.Hasher; +import net.nicholaswilliams.java.licensing.encryption.PasswordProvider; +import net.nicholaswilliams.java.licensing.encryption.PrivateKeyDataProvider; +import net.nicholaswilliams.java.licensing.exception.KeyNotFoundException; +import net.nicholaswilliams.java.licensing.licensor.LicenseCreator; +import net.nicholaswilliams.java.licensing.licensor.LicenseCreatorProperties; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.FileUtils; +import org.elasticsearch.license.core.ESLicenses; +import org.elasticsearch.license.core.LicenseBuilders; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Random; + +import static org.elasticsearch.license.core.ESLicenses.ESLicense; + +public class ESLicenseSigner { + + private final static int VERSION_START = 0; + private final static int VERSION = VERSION_START; + + private final static int MAGIC_LENGTH = 13; + + private final LicenseCreator licenseCreator; + private final SignerOptions options; + + public static class SignerOptions { + final String privateKeyPath; + final String publicKeyPath; + final String password; + + public SignerOptions(String privateKeyPath, String publicKeyPath, String password) { + this.privateKeyPath = privateKeyPath; + this.publicKeyPath = publicKeyPath; + this.password = password; + } + } + + public ESLicenseSigner(final SignerOptions options) { + LicenseCreatorProperties.setPrivateKeyDataProvider(new PrivateKeyDataProvider() { + @Override + public byte[] getEncryptedPrivateKeyData() throws KeyNotFoundException { + File privateKeyFile = new File(options.privateKeyPath); + assert privateKeyFile.exists(); + try { + return FileUtils.readFileToByteArray(privateKeyFile); + } catch (IOException e) { + e.printStackTrace(); + throw new IllegalStateException(e); + } + + } + }); + LicenseCreatorProperties.setPrivateKeyPasswordProvider(new PasswordProvider() { + @Override + public char[] getPassword() { + return options.password.toCharArray(); + } + }); + this.licenseCreator = LicenseCreator.getInstance(); + this.options = options; + } + + public ESLicenses sign(ESLicenses esLicenses) throws IOException { + final LicenseBuilders.LicensesBuilder licensesBuilder = LicenseBuilders.licensesBuilder(); + for (ESLicense license : esLicenses) { + licensesBuilder.license(sign(license)); + } + return licensesBuilder.build(); + } + + /** + * Generates a signature for the esLicense. + * Signature structure: + * | MAGIC | HEADER_LENGTH | VERSION | PUB_KEY_DIGEST | SIGNED_LICENSE_CONTENT | + * + * @return a signed ESLicense (with signature) + * @throws IOException + */ + public ESLicense sign(ESLicense esLicense) throws IOException { + License.Builder licenseBuilder = new License.Builder() + .withGoodBeforeDate(esLicense.expiryDate()) + .withIssueDate(esLicense.issueDate()) + .withProductKey(esLicense.uid()) + .withHolder(esLicense.issuedTo()) + .withIssuer(esLicense.issuer()) + .addFeature(esLicense.feature().string(), esLicense.expiryDate()) + .addFeature("maxNodes:" + String.valueOf(esLicense.maxNodes())) + .addFeature("type:" + esLicense.type().string()) + .addFeature("subscription_type:" + esLicense.subscriptionType().string()); + + final License license = licenseBuilder.build(); + + final byte[] magic = new byte[MAGIC_LENGTH]; + Random random = new Random(); + random.nextBytes(magic); + final byte[] licenseSignature = licenseCreator.signAndSerializeLicense(license); + final byte[] hash = Hasher.hash(Base64.encodeBase64String( + FileUtils.readFileToByteArray(new File(options.publicKeyPath))) + ).getBytes(Charset.forName("UTF-8")); + int headerLength = MAGIC_LENGTH + hash.length + 4 + 4; + byte[] bytes = new byte[headerLength + licenseSignature.length]; + + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + byteBuffer.put(magic) + .putInt(headerLength) + .putInt(VERSION) + .put(hash) + .put(licenseSignature); + String signature = Base64.encodeBase64String(bytes); + + return LicenseBuilders.licenseBuilder(true).fromLicense(esLicense).signature(signature).build(); + } +} diff --git a/src/main/java/org/elasticsearch/license/licensor/tools/KeyPairGeneratorTool.java b/src/main/java/org/elasticsearch/license/licensor/tools/KeyPairGeneratorTool.java new file mode 100644 index 00000000000..a5568a75d1f --- /dev/null +++ b/src/main/java/org/elasticsearch/license/licensor/tools/KeyPairGeneratorTool.java @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.license.licensor.tools; + +import net.nicholaswilliams.java.licensing.encryption.RSAKeyPairGenerator; +import net.nicholaswilliams.java.licensing.exception.AlgorithmNotSupportedException; +import net.nicholaswilliams.java.licensing.exception.InappropriateKeyException; +import net.nicholaswilliams.java.licensing.exception.InappropriateKeySpecificationException; +import net.nicholaswilliams.java.licensing.exception.RSA2048NotSupportedException; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.security.KeyPair; + +public class KeyPairGeneratorTool { + + static class Options { + private final String publicKeyFilePath; + private final String privateKeyFilePath; + private final String keyPass; + + Options(String publicKeyFilePath, String privateKeyFilePath, String keyPass) { + this.publicKeyFilePath = publicKeyFilePath; + this.privateKeyFilePath = privateKeyFilePath; + this.keyPass = keyPass; + } + } + + private static Options parse(String[] args) { + String privateKeyPath = null; + String publicKeyPath = null; + String keyPass = null; + + for (int i = 0; i < args.length; i++) { + String command = args[i]; + switch (command) { + case "--publicKeyPath": + publicKeyPath = args[++i]; + break; + case "--privateKeyPath": + privateKeyPath = args[++i]; + break; + case "--keyPass": + keyPass = args[++i]; + break; + } + } + + if (publicKeyPath == null) { + throw new IllegalArgumentException("mandatory option '--publicKeyPath' is missing"); + } + if (privateKeyPath == null) { + throw new IllegalArgumentException("mandatory option '--privateKeyPath' is missing"); + } + if (keyPass == null) { + throw new IllegalArgumentException("mandatory option '--keyPass' is missing"); + } + + return new Options(publicKeyPath, privateKeyPath, keyPass); + } + + public static void main(String[] args) throws IOException { + run(args, System.out); + } + + public static void run(String[] args, OutputStream out) throws IOException { + PrintWriter printWriter = new PrintWriter(out); + + Options options = parse(args); + + if (exists(options.privateKeyFilePath)) { + throw new IllegalArgumentException("private key already exists in " + options.privateKeyFilePath); + } else if (exists(options.publicKeyFilePath)) { + throw new IllegalArgumentException("public key already exists in " + options.publicKeyFilePath); + } + + KeyPair keyPair = generateKeyPair(options.privateKeyFilePath, options.publicKeyFilePath, options.keyPass); + if (keyPair != null) { + printWriter.println("Successfully generated new keyPair [publicKey: " + options.publicKeyFilePath + ", privateKey: " + options.privateKeyFilePath + "]"); + } + } + + private static boolean exists(String filePath) { + return new File(filePath).exists(); + } + + + private static KeyPair generateKeyPair(String privateKeyFileName, String publicKeyFileName, String password) { + RSAKeyPairGenerator generator = new RSAKeyPairGenerator(); + + KeyPair keyPair; + try { + keyPair = generator.generateKeyPair(); + } catch (RSA2048NotSupportedException e) { + return null; + } + + try { + generator.saveKeyPairToFiles(keyPair, privateKeyFileName, publicKeyFileName, password.toCharArray()); + } catch (IOException | AlgorithmNotSupportedException | InappropriateKeyException | InappropriateKeySpecificationException e) { + throw new IllegalStateException(e); + } + return keyPair; + } +} diff --git a/src/main/java/org/elasticsearch/license/licensor/tools/LicenseGeneratorTool.java b/src/main/java/org/elasticsearch/license/licensor/tools/LicenseGeneratorTool.java new file mode 100644 index 00000000000..57d72587e6a --- /dev/null +++ b/src/main/java/org/elasticsearch/license/licensor/tools/LicenseGeneratorTool.java @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.license.licensor.tools; + +import org.apache.commons.io.FileUtils; +import org.elasticsearch.license.core.ESLicenses; +import org.elasticsearch.license.core.LicenseUtils; +import org.elasticsearch.license.licensor.ESLicenseSigner; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; + +public class LicenseGeneratorTool { + + static class Options { + private final String licensesInput; + private final String publicKeyFilePath; + private final String privateKeyFilePath; + private final String keyPass; + + Options(String licensesInput, String publicKeyFilePath, String privateKeyFilePath, String keyPass) { + this.licensesInput = licensesInput; + this.publicKeyFilePath = publicKeyFilePath; + this.privateKeyFilePath = privateKeyFilePath; + this.keyPass = keyPass; + } + } + + private static Options parse(String[] args) throws IOException { + String licenseInput = null; + String licenseFilePath = null; + String privateKeyPath = null; + String publicKeyPath = null; + String keyPass = null; + + for (int i = 0; i < args.length; i++) { + String command = args[i].trim(); + switch (command) { + case "--license": + licenseInput = args[++i]; + break; + case "--licenseFile": + licenseFilePath = args[++i]; + break; + case "--publicKeyPath": + publicKeyPath = args[++i]; + break; + case "--privateKeyPath": + privateKeyPath = args[++i]; + break; + case "--keyPass": + keyPass = args[++i]; + break; + } + } + + if ((licenseInput == null && licenseFilePath == null) || (licenseInput != null && licenseFilePath != null)) { + throw new IllegalArgumentException("only one of '--license' or '--licenseFile' option should be set"); + } else if (licenseFilePath != null) { + File licenseFile = new File(licenseFilePath); + if (licenseFile.exists()) { + licenseInput = FileUtils.readFileToString(licenseFile, Charset.forName("UTF-8")); + } else { + throw new IllegalArgumentException("provided --licenseFile " + licenseFile.getAbsolutePath() + " does not exist!"); + } + } + if (publicKeyPath == null) { + throw new IllegalArgumentException("mandatory option '--publicKeyPath' is missing"); + } + if (privateKeyPath == null) { + throw new IllegalArgumentException("mandatory option '--privateKeyPath' is missing"); + } + if (keyPass == null) { + throw new IllegalArgumentException("mandatory option '--keyPass' is missing"); + } + + return new Options(licenseInput, publicKeyPath, privateKeyPath, keyPass); + } + + public static void main(String[] args) throws IOException { + run(args, System.out); + } + + public static void run(String[] args, OutputStream out) throws IOException { + Options options = parse(args); + + ESLicenses esLicenses = LicenseUtils.readLicensesFromString(options.licensesInput); + + ESLicenseSigner signer = new ESLicenseSigner(new ESLicenseSigner.SignerOptions(options.privateKeyFilePath, options.publicKeyFilePath, options.keyPass)); + ESLicenses signedLicences = signer.sign(esLicenses); + + LicenseUtils.dumpLicenseAsJson(signedLicences, out); + } + +} diff --git a/src/main/java/org/elasticsearch/license/licensor/tools/LicenseVerificationTool.java b/src/main/java/org/elasticsearch/license/licensor/tools/LicenseVerificationTool.java new file mode 100644 index 00000000000..0e982ed5d93 --- /dev/null +++ b/src/main/java/org/elasticsearch/license/licensor/tools/LicenseVerificationTool.java @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.license.licensor.tools; + +import org.elasticsearch.license.core.ESLicenses; +import org.elasticsearch.license.core.LicenseUtils; +import org.elasticsearch.license.manager.ESLicenseManager; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class LicenseVerificationTool { + + static class Options { + private final Set licensesFiles; + private final String publicKeyFilePath; + private final String keyPass; + + Options(Set licensesFiles, String publicKeyFilePath, String keyPass) { + this.licensesFiles = licensesFiles; + this.publicKeyFilePath = publicKeyFilePath; + this.keyPass = keyPass; + } + + static Set asFiles(Set filePaths) { + Set files = new HashSet<>(filePaths.size()); + for (String filePath : filePaths) { + final File file = new File(filePath); + if (file.exists()) { + files.add(file); + } else { + throw new IllegalArgumentException(file.getAbsolutePath() + " does not exist!"); + } + } + return files; + } + } + + private static Options parse(String[] args) { + Set licenseFilePaths = null; + Set licenseFiles = null; + String publicKeyPath = null; + String keyPass = null; + + for (int i = 0; i < args.length; i++) { + String command = args[i]; + switch (command) { + case "--licensesFiles": + licenseFilePaths = new HashSet<>(); + licenseFilePaths.addAll(Arrays.asList(args[++i].split(":"))); + break; + case "--publicKeyPath": + publicKeyPath = args[++i]; + break; + case "--keyPass": + keyPass = args[++i]; + break; + } + } + if (licenseFilePaths == null) { + throw new IllegalArgumentException("mandatory option '--licensesFiles' is missing"); + } else { + licenseFiles = Options.asFiles(licenseFilePaths); + if (licenseFiles.size() == 0) { + throw new IllegalArgumentException("no license file found for provided license files"); + } + } + if (publicKeyPath == null) { + throw new IllegalArgumentException("mandatory option '--publicKeyPath' is missing"); + } + if (keyPass == null) { + throw new IllegalArgumentException("mandatory option '--keyPass' is missing"); + } + return new Options(licenseFiles, publicKeyPath, keyPass); + } + + public static void main(String[] args) throws IOException { + run(args, System.out); + } + + public static void run(String[] args, OutputStream out) throws IOException { + Options options = parse(args); + + // read licenses + Set esLicensesSet = LicenseUtils.readLicensesFromFiles(options.licensesFiles); + + // verify licenses + ESLicenseManager licenseManager = new ESLicenseManager(esLicensesSet, options.publicKeyFilePath, options.keyPass); + licenseManager.verifyLicenses(); + + // dump effective licences + LicenseUtils.dumpLicenseAsJson(licenseManager.getEffectiveLicenses(), out); + } + +} diff --git a/src/main/java/org/elasticsearch/license/manager/ESLicenseManager.java b/src/main/java/org/elasticsearch/license/manager/ESLicenseManager.java new file mode 100644 index 00000000000..0e654610662 --- /dev/null +++ b/src/main/java/org/elasticsearch/license/manager/ESLicenseManager.java @@ -0,0 +1,269 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.license.manager; + +import net.nicholaswilliams.java.licensing.*; +import net.nicholaswilliams.java.licensing.encryption.FilePublicKeyDataProvider; +import net.nicholaswilliams.java.licensing.encryption.Hasher; +import net.nicholaswilliams.java.licensing.encryption.PasswordProvider; +import net.nicholaswilliams.java.licensing.exception.ExpiredLicenseException; +import net.nicholaswilliams.java.licensing.exception.InvalidLicenseException; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.FileUtils; +import org.elasticsearch.license.core.ESLicenses; +import org.elasticsearch.license.core.LicenseBuilders; +import sun.reflect.generics.reflectiveObjects.NotImplementedException; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; + +import static org.elasticsearch.license.core.ESLicenses.*; + +/** + * Class responsible for reading signed licenses, maintaining an effective esLicenses instance, verification of licenses + * and querying against licenses on a feature basis + * + * TODO: + * - integration with cluster state + * - use ESLicenseProvider to query license from cluster state + */ +public class ESLicenseManager { + + private final LicenseManager licenseManager; + private final ESLicenses esLicenses; + private final FilePublicKeyDataProvider publicKeyDataProvider; + + public ESLicenseManager(Set esLicensesSet, String publicKeyFile, String password) throws IOException { + this.publicKeyDataProvider = new FilePublicKeyDataProvider(publicKeyFile); + this.esLicenses = merge(esLicensesSet); + LicenseManagerProperties.setLicenseProvider(new ESLicenseProvider()); + LicenseManagerProperties.setPublicKeyDataProvider(publicKeyDataProvider); + LicenseManagerProperties.setLicenseValidator(new DefaultLicenseValidator()); + LicenseManagerProperties.setPublicKeyPasswordProvider(new ESPublicKeyPasswordProvider(password)); + this.licenseManager = LicenseManager.getInstance(); + } + + + public ESLicenseManager(ESLicenses esLicenses, String publicKeyFile, String password) throws IOException { + this(Collections.singleton(esLicenses), publicKeyFile, password); + } + + private static ESLicenses merge(Set esLicensesSet) { + ESLicenses mergedLicenses = null; + for (ESLicenses licenses : esLicensesSet) { + mergedLicenses = LicenseBuilders.merge(mergedLicenses, licenses); + } + return mergedLicenses; + } + + public ESLicenses getEffectiveLicenses() { + return esLicenses; + } + + private License getLicense(FeatureType featureType) { + ESLicense esLicense = esLicenses.get(featureType); + if (esLicense != null) { + String signature = esLicense.signature(); + try { + License license = this.licenseManager.decryptAndVerifyLicense(extractSignedLicence(signature)); + this.licenseManager.validateLicense(license); + return license; + } catch (IOException e) { + throw new IllegalStateException("bogus"); + } + } + return null; + } + + /** + * Extract a signedLicense (SIGNED_LICENSE_CONTENT) from the signature. + * Validates the public key used to decrypt the license by comparing their hashes + *

+ * Signature structure: + * | MAGIC | HEADER_LENGTH | VERSION | PUB_KEY_DIGEST | SIGNED_LICENSE_CONTENT | + * + * @param signature of a single license + * @return signed license content for the license + * @throws IOException + */ + private SignedLicense extractSignedLicence(String signature) throws IOException { + byte[] signatureBytes = Base64.decodeBase64(signature); + ByteBuffer byteBuffer = ByteBuffer.wrap(signatureBytes); + byteBuffer = (ByteBuffer) byteBuffer.position(13); + int start = byteBuffer.getInt(); + int version = byteBuffer.getInt(); + byte[] hash = new byte[start - 13 - 4 - 4]; + byteBuffer.get(hash); + + final byte[] computedHash = Hasher.hash(Base64.encodeBase64String( + FileUtils.readFileToByteArray(publicKeyDataProvider.getPublicKeyFile())) + ).getBytes(Charset.forName("UTF-8")); + + if (!Arrays.equals(hash, computedHash)) { + throw new InvalidLicenseException("Invalid License"); + } + + return new ObjectSerializer().readObject(SignedLicense.class, Arrays.copyOfRange(signatureBytes, start, signatureBytes.length)); + } + + + public void verifyLicenses() { + for (FeatureType featureType : esLicenses.features()) { + final License license = getLicense(featureType); + assert license != null : "license should not be null for feature: " + featureType.string(); + verifyLicenseFields(license, esLicenses.get(featureType)); + } + } + + + private static void verifyLicenseFields(License license, ESLicense eslicense) { + boolean licenseValid = license.getProductKey().equals(eslicense.uid()) + && license.getHolder().equals(eslicense.issuedTo()) + && license.getIssueDate() == eslicense.issueDate() + && license.getGoodBeforeDate() == eslicense.expiryDate(); + assert license.getFeatures().size() == 4 : "one license should have only four feature"; + String maxNodesPrefix = "maxNodes:"; + String typePrefix = "type:"; + String subscriptionTypePrefix = "subscription_type:"; + boolean maxNodesValid = false; + boolean featureValid = false; + boolean typeValid = false; + boolean subscriptionTypeValid = false; + for (License.Feature feature : license.getFeatures()) { + String featureName = feature.getName(); + if (featureName.startsWith(maxNodesPrefix)) { + maxNodesValid = eslicense.maxNodes() == Integer.parseInt(featureName.substring(maxNodesPrefix.length())); + } else if (featureName.startsWith(typePrefix)) { + typeValid = eslicense.type() == Type.fromString(featureName.substring(typePrefix.length())); + } else if (featureName.startsWith(subscriptionTypePrefix)) { + subscriptionTypeValid = eslicense.subscriptionType() == SubscriptionType.fromString(featureName.substring(subscriptionTypePrefix.length())); + } else { + featureValid = feature.getName().equals(eslicense.feature().string()) + && feature.getGoodBeforeDate() == eslicense.expiryDate(); + } + } + if (!licenseValid || !featureValid || !maxNodesValid || !typeValid || !subscriptionTypeValid) { + String msg = "licenseValid: " + licenseValid + "\n" + + "featureValid: " + featureValid + "\n" + + "maxNodeValide: " + maxNodesValid + "\n" + + "typeValid: " + typeValid + "\n" + + "subscriptionTypeValid: " + subscriptionTypeValid + "\n"; + throw new InvalidLicenseException("Invalid License"); + } + } + + + public boolean hasLicenseForFeature(FeatureType featureType) { + try { + final License license = getLicense(featureType); + if (license == null) { + return false; + } + return license.hasLicenseForFeature(featureType.string()); + } catch (ExpiredLicenseException e) { + return false; + } catch (InvalidLicenseException e) { + return false; + } + } + + public boolean hasLicenseForNodes(FeatureType featureType, int nodes) { + ESLicense esLicense = generateESLicense(featureType); + return esLicense.maxNodes() >= nodes; + } + + public String getIssuerForLicense(FeatureType featureType) { + final License license = getLicense(featureType); + return license.getIssuer(); + } + + public long getIssueDateForLicense(FeatureType featureType) { + final License license = getLicense(featureType); + return license.getIssueDate(); + } + + public long getExpiryDateForLicense(FeatureType featureType) { + final License license = getLicense(featureType); + return license.getGoodBeforeDate(); + } + + public String getIssuedToForLicense(FeatureType featureType) { + final License license = getLicense(featureType); + return license.getHolder(); + } + + public Type getTypeForLicense(FeatureType featureType) { + ESLicense esLicense = generateESLicense(featureType); + return esLicense.type(); + } + + public SubscriptionType getSubscriptionTypeForLicense(FeatureType featureType) { + ESLicense esLicense = generateESLicense(featureType); + return esLicense.subscriptionType(); + } + + private ESLicense generateESLicense(FeatureType featureType) { + final License license = getLicense(featureType); + return convertToESLicense(license); + } + + static ESLicense convertToESLicense(License license) { + final LicenseBuilders.LicenseBuilder licenseBuilder = LicenseBuilders.licenseBuilder(false); + licenseBuilder + .expiryDate(license.getGoodBeforeDate()) + .issueDate(license.getIssueDate()) + .uid(license.getProductKey()) + .issuedTo(license.getHolder()) + .issuer(license.getIssuer()); + + assert license.getFeatures().size() == 4 : "one license should have only four feature"; + String maxNodesPrefix = "maxNodes:"; + String typePrefix = "type:"; + String subscriptionTypePrefix = "subscription_type:"; + for (License.Feature feature : license.getFeatures()) { + String featureName = feature.getName(); + if (featureName.startsWith(maxNodesPrefix)) { + licenseBuilder.maxNodes(Integer.parseInt(featureName.substring(maxNodesPrefix.length()))); + } else if (featureName.startsWith(typePrefix)) { + licenseBuilder.type(Type.fromString(featureName.substring(typePrefix.length()))); + } else if (featureName.startsWith(subscriptionTypePrefix)) { + licenseBuilder.subscriptionType(SubscriptionType.fromString(featureName.substring(subscriptionTypePrefix.length()))); + } else { + licenseBuilder.feature(FeatureType.fromString(featureName)); + } + } + return licenseBuilder.build(); + } + + /** + * Used by the underlying license manager (make sure it is never called for now) + * This should be retrieving licenses from the custom metadata in the cluster state + */ + public class ESLicenseProvider implements LicenseProvider { + @Override + public SignedLicense getLicense(Object context) { + throw new NotImplementedException(); + } + } + + private class ESPublicKeyPasswordProvider implements PasswordProvider { + + private final String pass; + + private ESPublicKeyPasswordProvider(String pass) { + this.pass = pass; + } + + @Override + public char[] getPassword() { + return pass.toCharArray(); + } + } +} diff --git a/src/main/java/org/elasticsearch/license/manager/Utils.java b/src/main/java/org/elasticsearch/license/manager/Utils.java new file mode 100644 index 00000000000..d62f734e29e --- /dev/null +++ b/src/main/java/org/elasticsearch/license/manager/Utils.java @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.license.manager; + +import net.nicholaswilliams.java.licensing.LicenseManager; +import net.nicholaswilliams.java.licensing.ObjectSerializer; +import net.nicholaswilliams.java.licensing.SignedLicense; +import org.apache.commons.codec.binary.Base64; +import org.elasticsearch.license.core.ESLicenses; +import org.elasticsearch.license.core.LicenseBuilders; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Set; + +import static org.elasticsearch.license.core.ESLicenses.ESLicense; + +public class Utils { + + private Utils() { + } + + static ESLicenses getESLicensesFromSignatures(final LicenseManager licenseManager, Set signatures) { + final LicenseBuilders.LicensesBuilder licensesBuilder = LicenseBuilders.licensesBuilder(); + for (String signature : signatures) { + licensesBuilder.license(getESLicenseFromSignature(licenseManager, signature)); + } + return licensesBuilder.build(); + } + + private static ESLicense getESLicenseFromSignature(LicenseManager licenseManager, String signature) { + byte[] signatureBytes = Base64.decodeBase64(signature); + ByteBuffer byteBuffer = ByteBuffer.wrap(signatureBytes); + byteBuffer = (ByteBuffer) byteBuffer.position(13); + int start = byteBuffer.getInt(); + SignedLicense signedLicense = new ObjectSerializer() + .readObject(SignedLicense.class, Arrays.copyOfRange(signatureBytes, start, signatureBytes.length)); + return ESLicenseManager.convertToESLicense(licenseManager.decryptAndVerifyLicense(signedLicense)); + } + + +} diff --git a/src/main/java/org/elasticsearch/license/plugin/LicensePlugin.java b/src/main/java/org/elasticsearch/license/plugin/LicensePlugin.java new file mode 100644 index 00000000000..fc5834648d6 --- /dev/null +++ b/src/main/java/org/elasticsearch/license/plugin/LicensePlugin.java @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.license.plugin; + +import org.elasticsearch.plugins.AbstractPlugin; + +//TODO: plugin hooks +public class LicensePlugin extends AbstractPlugin { + + @Override + public String name() { + return "license"; + } + + @Override + public String description() { + return "Internal Elasticsearch Licensing Plugin"; + } +} diff --git a/src/main/resources/es-plugin.properties b/src/main/resources/es-plugin.properties new file mode 100644 index 00000000000..4c64a9302c6 --- /dev/null +++ b/src/main/resources/es-plugin.properties @@ -0,0 +1 @@ +plugin=org.elasticsearch.license.plugin.LicensePlugin \ No newline at end of file diff --git a/src/test/java/org/elasticsearch/license/TestUtils.java b/src/test/java/org/elasticsearch/license/TestUtils.java new file mode 100644 index 00000000000..5fdbfe3dc67 --- /dev/null +++ b/src/test/java/org/elasticsearch/license/TestUtils.java @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.license; + +import org.apache.commons.io.FileUtils; +import org.elasticsearch.license.core.DateUtils; +import org.elasticsearch.license.core.ESLicenses; +import org.elasticsearch.license.licensor.tools.LicenseGeneratorTool; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.text.ParseException; +import java.util.Map; + +import static org.junit.Assert.assertTrue; + +public class TestUtils { + + + public static String generateESLicenses(Map featureAttributes) { + StringBuilder licenseBuilder = new StringBuilder(); + int size = featureAttributes.values().size(); + int i = 0; + for (FeatureAttributes attributes : featureAttributes.values()) { + licenseBuilder.append("{\n" + + " \"type\" : \"" + attributes.type + "\",\n" + + " \"subscription_type\" : \"" + attributes.subscriptionType + "\",\n" + + " \"issued_to\" : \"" + attributes.issuedTo + "\",\n" + + " \"issuer\" : \"" + attributes.issuer + "\",\n" + + " \"issue_date\" : \"" + attributes.issueDate + "\",\n" + + " \"expiry_date\" : \"" + attributes.expiryDate + "\",\n" + + " \"feature\" : \"" + attributes.featureType + "\",\n" + + " \"max_nodes\" : " + attributes.maxNodes + + "}"); + if (++i < size) { + licenseBuilder.append(",\n"); + } + } + return "{\n" + + " \"licenses\" : [" + + licenseBuilder.toString() + + "]\n" + + "}"; + + } + + public static String runLicenseGenerationTool(String[] args) throws IOException { + File temp = File.createTempFile("temp", ".out"); + temp.deleteOnExit(); + try (FileOutputStream outputStream = new FileOutputStream(temp)) { + LicenseGeneratorTool.run(args, outputStream); + } + return FileUtils.readFileToString(temp); + } + + public static void verifyESLicenses(ESLicenses esLicenses, Map featureAttributes) throws ParseException { + assertTrue("Number of feature licenses should be " + featureAttributes.size(), esLicenses.features().size() == featureAttributes.size()); + for (Map.Entry featureAttrTuple : featureAttributes.entrySet()) { + ESLicenses.FeatureType featureType = featureAttrTuple.getKey(); + FeatureAttributes attributes = featureAttrTuple.getValue(); + final ESLicenses.ESLicense esLicense = esLicenses.get(featureType); + assertTrue("license for " + featureType.string() + " should be present", esLicense != null); + assertTrue("expected value for issuedTo was: " + attributes.issuedTo + " but got: " + esLicense.issuedTo(), esLicense.issuedTo().equals(attributes.issuedTo)); + assertTrue("expected value for type was: " + attributes.type + " but got: " + esLicense.type().string(), esLicense.type().string().equals(attributes.type)); + assertTrue("expected value for subscriptionType was: " + attributes.subscriptionType + " but got: " + esLicense.subscriptionType().string(), esLicense.subscriptionType().string().equals(attributes.subscriptionType)); + assertTrue("expected value for feature was: " + attributes.featureType + " but got: " + esLicense.feature().string(), esLicense.feature().string().equals(attributes.featureType)); + assertTrue("expected value for issueDate was: " + DateUtils.longFromDateString(attributes.issueDate) + " but got: " + esLicense.issueDate(), esLicense.issueDate() == DateUtils.longFromDateString(attributes.issueDate)); + assertTrue("expected value for expiryDate: " + DateUtils.longExpiryDateFromString(attributes.expiryDate) + " but got: " + esLicense.expiryDate(), esLicense.expiryDate() == DateUtils.longExpiryDateFromString(attributes.expiryDate)); + assertTrue("expected value for maxNodes: " + attributes.maxNodes + " but got: " + esLicense.maxNodes(), esLicense.maxNodes() == attributes.maxNodes); + + assertTrue("generated licenses should have non-null uid field", esLicense.uid() != null); + assertTrue("generated licenses should have non-null signature field", esLicense.signature() != null); + } + } + + public static class FeatureAttributes { + + public final String featureType; + public final String type; + public final String subscriptionType; + public final String issuedTo; + public final int maxNodes; + public final String issueDate; + public final String expiryDate; + public final String issuer; + + public FeatureAttributes(String featureType, String type, String subscriptionType, String issuedTo, String issuer, int maxNodes, String issueDateStr, String expiryDateStr) throws ParseException { + this.featureType = featureType; + this.type = type; + this.subscriptionType = subscriptionType; + this.issuedTo = issuedTo; + this.issuer = issuer; + this.maxNodes = maxNodes; + this.issueDate = issueDateStr; + this.expiryDate = expiryDateStr; + } + } +} diff --git a/src/test/java/org/elasticsearch/license/licensor/LicenseGenerationTests.java b/src/test/java/org/elasticsearch/license/licensor/LicenseGenerationTests.java new file mode 100644 index 00000000000..cf02a5f5644 --- /dev/null +++ b/src/test/java/org/elasticsearch/license/licensor/LicenseGenerationTests.java @@ -0,0 +1,221 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.license.licensor; + +import org.elasticsearch.license.TestUtils; +import org.elasticsearch.license.core.ESLicenses; +import org.elasticsearch.license.core.LicenseUtils; +import org.elasticsearch.license.licensor.tools.KeyPairGeneratorTool; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.text.ParseException; +import java.util.HashMap; +import java.util.Map; + +import static org.elasticsearch.license.core.ESLicenses.FeatureType; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class LicenseGenerationTests { + + private static String pubKeyPath = null; + private static String priKeyPath = null; + private static String keyPass = null; + + @BeforeClass + public static void setup() throws IOException { + + // Generate temp KeyPair spec + File privateKeyFile = File.createTempFile("privateKey", ".key"); + File publicKeyFile = File.createTempFile("publicKey", ".key"); + LicenseGenerationTests.pubKeyPath = publicKeyFile.getAbsolutePath(); + LicenseGenerationTests.priKeyPath = privateKeyFile.getAbsolutePath(); + assert privateKeyFile.delete(); + assert publicKeyFile.delete(); + String keyPass = "password"; + LicenseGenerationTests.keyPass = keyPass; + + // Generate keyPair + String[] args = new String[6]; + args[0] = "--publicKeyPath"; + args[1] = LicenseGenerationTests.pubKeyPath; + args[2] = "--privateKeyPath"; + args[3] = LicenseGenerationTests.priKeyPath; + args[4] = "--keyPass"; + args[5] = LicenseGenerationTests.keyPass; + KeyPairGeneratorTool.main(args); + } + + @Test + public void testSimpleLicenseGeneration() throws ParseException, IOException { + Map map = new HashMap<>(); + TestUtils.FeatureAttributes featureAttributes = + new TestUtils.FeatureAttributes("shield", "subscription", "platinum", "foo bar Inc.", "elasticsearch", 2, "2014-12-13", "2015-12-13"); + map.put(FeatureType.SHIELD, featureAttributes); + + String licenseString = TestUtils.generateESLicenses(map); + + String[] args = new String[8]; + args[0] = "--license"; + args[1] = licenseString; + args[2] = "--publicKeyPath"; + args[3] = pubKeyPath; + args[4] = "--privateKeyPath"; + args[5] = priKeyPath; + args[6] = "--keyPass"; + args[7] = keyPass; + + String licenseOutput = TestUtils.runLicenseGenerationTool(args); + + ESLicenses esLicensesOutput = LicenseUtils.readLicensesFromString(licenseOutput); + + TestUtils.verifyESLicenses(esLicensesOutput, map); + } + + @Test + public void testMultipleFeatureTypes() throws ParseException, IOException { + + Map map = new HashMap<>(); + TestUtils.FeatureAttributes shildFeatureAttributes = + new TestUtils.FeatureAttributes("shield", "trial", "none", "foo bar Inc.", "elasticsearch", 2, "2014-12-13", "2015-12-13"); + TestUtils.FeatureAttributes marvelFeatureAttributes = + new TestUtils.FeatureAttributes("marvel", "subscription", "silver", "foo1 bar Inc.", "elasticsearc3h", 10, "2014-01-13", "2014-12-13"); + map.put(FeatureType.SHIELD, shildFeatureAttributes); + map.put(FeatureType.MARVEL, marvelFeatureAttributes); + + String licenseString = TestUtils.generateESLicenses(map); + String[] args = new String[8]; + args[0] = "--license"; + args[1] = licenseString; + args[2] = "--publicKeyPath"; + args[3] = pubKeyPath; + args[4] = "--privateKeyPath"; + args[5] = priKeyPath; + args[6] = "--keyPass"; + args[7] = keyPass; + + String licenseOutput = TestUtils.runLicenseGenerationTool(args); + + ESLicenses esLicensesOutput = LicenseUtils.readLicensesFromString(licenseOutput); + + TestUtils.verifyESLicenses(esLicensesOutput, map); + } + + @Test + public void testMissingCLTArgs() throws ParseException, IOException { + + Map map = new HashMap<>(); + TestUtils.FeatureAttributes featureAttributes = + new TestUtils.FeatureAttributes("shiedgdsld", "internal", "none", "foo bar Inc.", "elasticsearch", 23, "2014-12-13", "2015-12-13"); + map.put(FeatureType.SHIELD, featureAttributes); + + String licenseString = TestUtils.generateESLicenses(map); + + String[] args = new String[8]; + args[0] = "--linse"; + args[1] = licenseString; + args[2] = "--publicKeyPath"; + args[3] = pubKeyPath; + args[4] = "--privateKeyPath"; + args[5] = priKeyPath; + args[6] = "--keyPass"; + args[7] = keyPass; + + try { + String licenseOutput = TestUtils.runLicenseGenerationTool(args); + fail(); + } catch (IllegalArgumentException e) { + assertTrue("Exception should indicate mandatory param --license, got: " + e.getMessage(), e.getMessage().contains("license")); + } + } + + @Test + public void testInvalidFeatureType() throws ParseException, IOException { + + Map map = new HashMap<>(); + TestUtils.FeatureAttributes featureAttributes = + new TestUtils.FeatureAttributes("shiedgdsld", "internal", "none", "foo bar Inc.", "elasticsearch", 23, "2014-12-13", "2015-12-13"); + map.put(FeatureType.SHIELD, featureAttributes); + + String licenseString = TestUtils.generateESLicenses(map); + + String[] args = new String[8]; + args[0] = "--license"; + args[1] = licenseString; + args[2] = "--publicKeyPath"; + args[3] = pubKeyPath; + args[4] = "--privateKeyPath"; + args[5] = priKeyPath; + args[6] = "--keyPass"; + args[7] = keyPass; + + try { + String licenseOutput = TestUtils.runLicenseGenerationTool(args); + fail(); + } catch (IllegalArgumentException e) { + assertTrue("Exception should indicate invalid FeatureType, got: " + e.getMessage(), e.getMessage().contains("Invalid FeatureType")); + } + } + + @Test + public void testInvalidSubscriptionType() throws ParseException, IOException { + Map map = new HashMap<>(); + TestUtils.FeatureAttributes featureAttributes = + new TestUtils.FeatureAttributes("shield", "trial", "nodavne", "foo bar Inc.", "elasticsearch", 25, "2014-12-13", "2015-12-13"); + map.put(FeatureType.SHIELD, featureAttributes); + + String licenseString = TestUtils.generateESLicenses(map); + + String[] args = new String[8]; + args[0] = "--license"; + args[1] = licenseString; + args[2] = "--publicKeyPath"; + args[3] = pubKeyPath; + args[4] = "--privateKeyPath"; + args[5] = priKeyPath; + args[6] = "--keyPass"; + args[7] = keyPass; + + try { + String licenseOutput = TestUtils.runLicenseGenerationTool(args); + fail(); + } catch (IllegalArgumentException e) { + assertTrue("Exception should indicate invalid SubscriptionType, got: " + e.getMessage(), e.getMessage().contains("Invalid SubscriptionType")); + } + } + + @Test + public void testInvalidType() throws ParseException, IOException { + + Map map = new HashMap<>(); + TestUtils.FeatureAttributes featureAttributes = + new TestUtils.FeatureAttributes("shield", "inininternal", "gold", "foo bar Inc.", "elasticsearch", 12, "2014-12-13", "2015-12-13"); + map.put(FeatureType.SHIELD, featureAttributes); + + String licenseString = TestUtils.generateESLicenses(map); + + String[] args = new String[8]; + args[0] = "--license"; + args[1] = licenseString; + args[2] = "--publicKeyPath"; + args[3] = pubKeyPath; + args[4] = "--privateKeyPath"; + args[5] = priKeyPath; + args[6] = "--keyPass"; + args[7] = keyPass; + + try { + String licenseOutput = TestUtils.runLicenseGenerationTool(args); + fail(); + } catch (IllegalArgumentException e) { + assertTrue("Exception should indicate invalid Type, got: " + e.getMessage(), e.getMessage().contains("Invalid Type")); + } + } + +} diff --git a/src/test/java/org/elasticsearch/license/licensor/LicenseVerificationToolTests.java b/src/test/java/org/elasticsearch/license/licensor/LicenseVerificationToolTests.java new file mode 100644 index 00000000000..4ae012be99f --- /dev/null +++ b/src/test/java/org/elasticsearch/license/licensor/LicenseVerificationToolTests.java @@ -0,0 +1,200 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.license.licensor; + +import org.apache.commons.io.FileUtils; +import org.elasticsearch.license.TestUtils; +import org.elasticsearch.license.core.ESLicenses; +import org.elasticsearch.license.core.LicenseUtils; +import org.elasticsearch.license.licensor.tools.KeyPairGeneratorTool; +import org.elasticsearch.license.licensor.tools.LicenseVerificationTool; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +@Ignore("Enable once maven is setup properly; now it throws invalid signature error for all the tests when the tests always pass in intellij") +public class LicenseVerificationToolTests { + + private static String pubKeyPath = null; + private static String priKeyPath = null; + private static String keyPass = null; + + @BeforeClass + public static void setup() throws IOException { + + // Generate temp KeyPair spec + File privateKeyFile = File.createTempFile("privateKey", ".key"); + File publicKeyFile = File.createTempFile("publicKey", ".key"); + LicenseVerificationToolTests.pubKeyPath = publicKeyFile.getAbsolutePath(); + LicenseVerificationToolTests.priKeyPath = privateKeyFile.getAbsolutePath(); + assert privateKeyFile.delete(); + assert publicKeyFile.delete(); + LicenseVerificationToolTests.keyPass = "password"; + + // Generate keyPair + String[] args = new String[6]; + args[0] = "--publicKeyPath"; + args[1] = LicenseVerificationToolTests.pubKeyPath; + args[2] = "--privateKeyPath"; + args[3] = LicenseVerificationToolTests.priKeyPath; + args[4] = "--keyPass"; + args[5] = LicenseVerificationToolTests.keyPass; + KeyPairGeneratorTool.main(args); + } + + @Test + public void testEffectiveLicenseGeneration() throws Exception { + Map map = new HashMap<>(); + TestUtils.FeatureAttributes featureWithLongerExpiryDate = + new TestUtils.FeatureAttributes("shield", "subscription", "platinum", "foo bar Inc.", "elasticsearch", 10, "2014-12-13", "2015-12-13"); + map.put(ESLicenses.FeatureType.SHIELD, featureWithLongerExpiryDate); + + String signedLicense = runLicenseGenerationTool(TestUtils.generateESLicenses(map)); + String firstLicenseFile = getAsFilePath(signedLicense); + + TestUtils.FeatureAttributes featureWithShorterExpiryDate = + new TestUtils.FeatureAttributes("shield", "trial", "none", "foo bar Inc.", "elasticsearch", 2, "2014-12-13", "2015-01-13"); + map.put(ESLicenses.FeatureType.SHIELD, featureWithShorterExpiryDate); + + signedLicense = runLicenseGenerationTool(TestUtils.generateESLicenses(map)); + String secondLicenseFile = getAsFilePath(signedLicense); + + String[] args = new String[6]; + args[0] = "--licensesFiles"; + args[1] = firstLicenseFile + ":" + secondLicenseFile; + args[2] = "--publicKeyPath"; + args[3] = pubKeyPath; + args[4] = "--keyPass"; + args[5] = keyPass; + + String effectiveLicenseStr = runLicenseVerificationTool(args); + ESLicenses effectiveLicense = LicenseUtils.readLicensesFromString(effectiveLicenseStr); + + map.put(ESLicenses.FeatureType.SHIELD, featureWithLongerExpiryDate); + + // verify that the effective license strips out license for the same feature with earlier expiry dates + TestUtils.verifyESLicenses(effectiveLicense, map); + } + + @Test + public void testEffectiveLicenseForMultiFeatures() throws Exception { + Map map = new HashMap<>(); + TestUtils.FeatureAttributes shieldFeatureWithLongerExpiryDate = + new TestUtils.FeatureAttributes("shield", "subscription", "platinum", "foo bar Inc.", "elasticsearch", 10, "2014-12-13", "2015-12-13"); + map.put(ESLicenses.FeatureType.SHIELD, shieldFeatureWithLongerExpiryDate); + + String signedLicense = runLicenseGenerationTool(TestUtils.generateESLicenses(map)); + String firstLicenseFile = getAsFilePath(signedLicense); + + TestUtils.FeatureAttributes marvelFeatureWithShorterExpiryDate = + new TestUtils.FeatureAttributes("marvel", "trial", "none", "foo bar Inc.", "elasticsearch", 2, "2014-12-13", "2015-01-13"); + map.put(ESLicenses.FeatureType.MARVEL, marvelFeatureWithShorterExpiryDate); + + signedLicense = runLicenseGenerationTool(TestUtils.generateESLicenses(map)); + String secondLicenseFile = getAsFilePath(signedLicense); + + String[] args = new String[6]; + args[0] = "--licensesFiles"; + args[1] = firstLicenseFile + ":" + secondLicenseFile; + args[2] = "--publicKeyPath"; + args[3] = pubKeyPath; + args[4] = "--keyPass"; + args[5] = keyPass; + + String effectiveLicenseStr = runLicenseVerificationTool(args); + ESLicenses effectiveLicense = LicenseUtils.readLicensesFromString(effectiveLicenseStr); + + // verify that the effective license contains both feature licenses + TestUtils.verifyESLicenses(effectiveLicense, map); + } + + @Test + public void testEffectiveLicenseForMultiFeatures2() throws Exception { + Map map = new HashMap<>(); + + TestUtils.FeatureAttributes shieldFeatureWithLongerExpiryDate = + new TestUtils.FeatureAttributes("shield", "subscription", "platinum", "foo bar Inc.", "elasticsearch", 10, "2014-12-13", "2015-12-13"); + TestUtils.FeatureAttributes marvelFeatureWithShorterExpiryDate = + new TestUtils.FeatureAttributes("marvel", "trial", "none", "foo bar Inc.", "elasticsearch", 2, "2014-12-13", "2015-01-13"); + + map.put(ESLicenses.FeatureType.SHIELD, shieldFeatureWithLongerExpiryDate); + map.put(ESLicenses.FeatureType.MARVEL, marvelFeatureWithShorterExpiryDate); + + String signedLicense = runLicenseGenerationTool(TestUtils.generateESLicenses(map)); + String firstLicenseFile = getAsFilePath(signedLicense); + + TestUtils.FeatureAttributes shieldFeatureWithShorterExpiryDate = + new TestUtils.FeatureAttributes("shield", "subscription", "platinum", "foo bar Inc.", "elasticsearch", 10, "2014-12-13", "2015-11-13"); + TestUtils.FeatureAttributes marvelFeatureWithLongerExpiryDate = + new TestUtils.FeatureAttributes("marvel", "trial", "none", "foo bar Inc.", "elasticsearch", 2, "2014-12-13", "2015-11-13"); + + map.put(ESLicenses.FeatureType.SHIELD, shieldFeatureWithShorterExpiryDate); + map.put(ESLicenses.FeatureType.MARVEL, marvelFeatureWithLongerExpiryDate); + + signedLicense = runLicenseGenerationTool(TestUtils.generateESLicenses(map)); + String secondLicenseFile = getAsFilePath(signedLicense); + + String[] args = new String[6]; + args[0] = "--licensesFiles"; + args[1] = firstLicenseFile + ":" + secondLicenseFile; + args[2] = "--publicKeyPath"; + args[3] = pubKeyPath; + args[4] = "--keyPass"; + args[5] = keyPass; + + String effectiveLicenseStr = runLicenseVerificationTool(args); + ESLicenses effectiveLicense = LicenseUtils.readLicensesFromString(effectiveLicenseStr); + + map.put(ESLicenses.FeatureType.SHIELD, shieldFeatureWithLongerExpiryDate); + map.put(ESLicenses.FeatureType.MARVEL, marvelFeatureWithLongerExpiryDate); + + // verify that the generated effective license is generated from choosing individual licences from multiple files + TestUtils.verifyESLicenses(effectiveLicense, map); + } + + public static String runLicenseVerificationTool(String[] args) throws IOException { + File temp = File.createTempFile("temp", ".out"); + temp.deleteOnExit(); + try (FileOutputStream outputStream = new FileOutputStream(temp)) { + LicenseVerificationTool.run(args, outputStream); + } + return FileUtils.readFileToString(temp); + } + + public static String runLicenseGenerationTool(String licenseInput) throws IOException { + String args[] = new String[8]; + + args[0] = "--license"; + args[1] = licenseInput; + args[2] = "--publicKeyPath"; + args[3] = pubKeyPath; + args[4] = "--privateKeyPath"; + args[5] = priKeyPath; + args[6] = "--keyPass"; + args[7] = keyPass; + + return TestUtils.runLicenseGenerationTool(args); + } + + private static String getAsFilePath(String content) throws IOException { + File temp = File.createTempFile("license", ".out"); + temp.deleteOnExit(); + FileUtils.write(temp, content); + String tempFilePath = temp.getAbsolutePath(); + while (tempFilePath.contains(":")) { + assert temp.delete(); + tempFilePath = getAsFilePath(content); + } + return tempFilePath; + } + +} diff --git a/src/test/java/org/elasticsearch/license/manager/LicenseVerificationTests.java b/src/test/java/org/elasticsearch/license/manager/LicenseVerificationTests.java new file mode 100644 index 00000000000..0807076d663 --- /dev/null +++ b/src/test/java/org/elasticsearch/license/manager/LicenseVerificationTests.java @@ -0,0 +1,240 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.license.manager; + +import net.nicholaswilliams.java.licensing.exception.InvalidLicenseException; +import org.elasticsearch.license.TestUtils; +import org.elasticsearch.license.core.DateUtils; +import org.elasticsearch.license.core.ESLicenses; +import org.elasticsearch.license.core.LicenseBuilders; +import org.elasticsearch.license.licensor.tools.KeyPairGeneratorTool; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.text.ParseException; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import static org.elasticsearch.license.core.ESLicenses.FeatureType; +import static org.elasticsearch.license.core.LicenseUtils.readLicensesFromString; +import static org.junit.Assert.*; + +@Ignore("Enable once maven is setup properly; now it throws invalid signature error for all the tests when the tests always pass in intellij") +public class LicenseVerificationTests { + + private static String pubKeyPath = null; + private static String priKeyPath = null; + private static String keyPass = null; + + @BeforeClass + public static void setup() throws IOException { + + // Generate temp KeyPair spec + File privateKeyFile = File.createTempFile("privateKey", ".key"); + File publicKeyFile = File.createTempFile("publicKey", ".key"); + LicenseVerificationTests.pubKeyPath = publicKeyFile.getAbsolutePath(); + LicenseVerificationTests.priKeyPath = privateKeyFile.getAbsolutePath(); + assert privateKeyFile.delete(); + assert publicKeyFile.delete(); + LicenseVerificationTests.keyPass = "password"; + + // Generate keyPair + String[] args = new String[6]; + args[0] = "--publicKeyPath"; + args[1] = LicenseVerificationTests.pubKeyPath; + args[2] = "--privateKeyPath"; + args[3] = LicenseVerificationTests.priKeyPath; + args[4] = "--keyPass"; + args[5] = LicenseVerificationTests.keyPass; + KeyPairGeneratorTool.main(args); + } + + @Test + public void testGeneratedLicenses() throws Exception { + Date issueDate = new Date(); + String issueDateStr = DateUtils.dateStringFromLongDate(issueDate.getTime()); + String expiryDateStr = DateUtils.dateStringFromLongDate(DateUtils.longExpiryDateFromDate(issueDate.getTime() + 24 * 60 * 60l)); + Map map = new HashMap<>(); + TestUtils.FeatureAttributes featureAttributes = + new TestUtils.FeatureAttributes("shield", "subscription", "platinum", "foo bar Inc.", "elasticsearch", 2, issueDateStr, expiryDateStr); + map.put(FeatureType.SHIELD, featureAttributes); + + String licenseString = TestUtils.generateESLicenses(map); + + String[] args = new String[8]; + args[0] = "--license"; + args[1] = licenseString; + args[2] = "--publicKeyPath"; + args[3] = pubKeyPath; + args[4] = "--privateKeyPath"; + args[5] = priKeyPath; + args[6] = "--keyPass"; + args[7] = keyPass; + + String licenseOutput = TestUtils.runLicenseGenerationTool(args); + + ESLicenses esLicensesOutput = readLicensesFromString(licenseOutput); + + ESLicenseManager esLicenseManager = new ESLicenseManager(esLicensesOutput, pubKeyPath, keyPass); + + esLicenseManager.verifyLicenses(); + + verifyLicenseManager(esLicenseManager, map); + } + + @Test + public void testMultipleFeatureLicenses() throws Exception { + Date issueDate = new Date(); + String issueDateStr = DateUtils.dateStringFromLongDate(issueDate.getTime()); + String expiryDateStr = DateUtils.dateStringFromLongDate(DateUtils.longExpiryDateFromDate(issueDate.getTime() + 24 * 60 * 60 * 1000l)); + + Map map = new HashMap<>(); + TestUtils.FeatureAttributes shildFeatureAttributes = + new TestUtils.FeatureAttributes("shield", "trial", "none", "foo bar Inc.", "elasticsearch", 2, issueDateStr, expiryDateStr); + TestUtils.FeatureAttributes marvelFeatureAttributes = + new TestUtils.FeatureAttributes("marvel", "subscription", "silver", "foo1 bar Inc.", "elasticsearc3h", 10, issueDateStr, expiryDateStr); + map.put(FeatureType.SHIELD, shildFeatureAttributes); + map.put(FeatureType.MARVEL, marvelFeatureAttributes); + + String licenseString = TestUtils.generateESLicenses(map); + + String[] args = new String[8]; + args[0] = "--license"; + args[1] = licenseString; + args[2] = "--publicKeyPath"; + args[3] = pubKeyPath; + args[4] = "--privateKeyPath"; + args[5] = priKeyPath; + args[6] = "--keyPass"; + args[7] = keyPass; + + String licenseOutput = TestUtils.runLicenseGenerationTool(args); + + ESLicenses esLicensesOutput = readLicensesFromString(licenseOutput); + + ESLicenseManager esLicenseManager = new ESLicenseManager(esLicensesOutput, pubKeyPath, keyPass); + + esLicenseManager.verifyLicenses(); + + verifyLicenseManager(esLicenseManager, map); + } + + @Test + public void testLicenseExpiry() throws Exception { + + Date issueDate = new Date(); + String issueDateStr = DateUtils.dateStringFromLongDate(issueDate.getTime()); + String expiryDateStr = DateUtils.dateStringFromLongDate(DateUtils.longExpiryDateFromDate(issueDate.getTime() + 24 * 60 * 60l)); + + String expiredExpiryDateStr = DateUtils.dateStringFromLongDate(DateUtils.longExpiryDateFromDate(issueDate.getTime() - 5 * 24 * 60 * 60 * 1000l)); + + Map map = new HashMap<>(); + TestUtils.FeatureAttributes shildFeatureAttributes = + new TestUtils.FeatureAttributes("shield", "trial", "none", "foo bar Inc.", "elasticsearch", 2, issueDateStr, expiryDateStr); + TestUtils.FeatureAttributes marvelFeatureAttributes = + new TestUtils.FeatureAttributes("marvel", "subscription", "silver", "foo1 bar Inc.", "elasticsearc3h", 10, issueDateStr, expiredExpiryDateStr); + map.put(FeatureType.SHIELD, shildFeatureAttributes); + map.put(FeatureType.MARVEL, marvelFeatureAttributes); + + String licenseString = TestUtils.generateESLicenses(map); + + String[] args = new String[8]; + args[0] = "--license"; + args[1] = licenseString; + args[2] = "--publicKeyPath"; + args[3] = pubKeyPath; + args[4] = "--privateKeyPath"; + args[5] = priKeyPath; + args[6] = "--keyPass"; + args[7] = keyPass; + + String licenseOutput = TestUtils.runLicenseGenerationTool(args); + + ESLicenses esLicensesOutput = readLicensesFromString(licenseOutput); + + ESLicenseManager esLicenseManager = new ESLicenseManager(esLicensesOutput, pubKeyPath, keyPass); + + // All validation for shield license should be normal as expected + verifyLicenseManager(esLicenseManager, Collections.singletonMap(FeatureType.SHIELD, shildFeatureAttributes)); + + assertFalse("license for marvel should not be valid due to expired expiry date", esLicenseManager.hasLicenseForFeature(FeatureType.MARVEL)); + } + + @Test + public void testLicenseTampering() throws Exception { + + Date issueDate = new Date(); + String issueDateStr = DateUtils.dateStringFromLongDate(issueDate.getTime()); + String expiryDateStr = DateUtils.dateStringFromLongDate(DateUtils.longExpiryDateFromDate(issueDate.getTime() + 24 * 60 * 60l)); + Map map = new HashMap<>(); + TestUtils.FeatureAttributes featureAttributes = + new TestUtils.FeatureAttributes("shield", "subscription", "platinum", "foo bar Inc.", "elasticsearch", 2, issueDateStr, expiryDateStr); + map.put(FeatureType.SHIELD, featureAttributes); + + String licenseString = TestUtils.generateESLicenses(map); + + String[] args = new String[8]; + args[0] = "--license"; + args[1] = licenseString; + args[2] = "--publicKeyPath"; + args[3] = pubKeyPath; + args[4] = "--privateKeyPath"; + args[5] = priKeyPath; + args[6] = "--keyPass"; + args[7] = keyPass; + + String licenseOutput = TestUtils.runLicenseGenerationTool(args); + + ESLicenses esLicensesOutput = readLicensesFromString(licenseOutput); + + ESLicenses.ESLicense esLicense = esLicensesOutput.get(FeatureType.SHIELD); + + long originalExpiryDate = esLicense.expiryDate(); + final ESLicenses.ESLicense tamperedLicense = LicenseBuilders.licenseBuilder(true) + .fromLicense(esLicense) + .expiryDate(esLicense.expiryDate() + 10 * 24 * 60 * 60 * 1000l) + .feature(FeatureType.SHIELD) + .issuer("elasticsqearch") + .build(); + + ESLicenses tamperedLicenses = LicenseBuilders.licensesBuilder().license(tamperedLicense).build(); + + ESLicenseManager esLicenseManager = null; + try { + esLicenseManager = new ESLicenseManager(tamperedLicenses, pubKeyPath, keyPass); + assertTrue("License manager should always report the original (signed) expiry date", esLicenseManager.getExpiryDateForLicense(FeatureType.SHIELD) == originalExpiryDate); + esLicenseManager.verifyLicenses(); + fail(); + } catch (InvalidLicenseException e) { + assertTrue("Exception should contain 'Invalid License' ", e.getMessage().contains("Invalid License")); + } + } + + public static void verifyLicenseManager(ESLicenseManager esLicenseManager, Map featureAttributeMap) throws ParseException { + + for (Map.Entry entry : featureAttributeMap.entrySet()) { + TestUtils.FeatureAttributes featureAttributes = entry.getValue(); + FeatureType featureType = entry.getKey(); + assertTrue("License should have issuedTo of " + featureAttributes.issuedTo, esLicenseManager.getIssuedToForLicense(featureType).equals(featureAttributes.issuedTo)); + assertTrue("License should have issuer of " + featureAttributes.issuer, esLicenseManager.getIssuerForLicense(featureType).equals(featureAttributes.issuer)); + assertTrue("License should have issue date of " + DateUtils.longFromDateString(featureAttributes.issueDate), esLicenseManager.getIssueDateForLicense(featureType) == DateUtils.longFromDateString(featureAttributes.issueDate)); + assertTrue("License should have expiry date of " + DateUtils.longExpiryDateFromString(featureAttributes.expiryDate), esLicenseManager.getExpiryDateForLicense(featureType) == DateUtils.longExpiryDateFromString(featureAttributes.expiryDate)); + assertTrue("License should have type of " + featureAttributes.featureType, esLicenseManager.getTypeForLicense(featureType) == ESLicenses.Type.fromString(featureAttributes.type)); + assertTrue("License should have subscription type of " + featureAttributes.subscriptionType, esLicenseManager.getSubscriptionTypeForLicense(featureType) == ESLicenses.SubscriptionType.fromString(featureAttributes.subscriptionType)); + + + assertTrue("License should be valid for shield", esLicenseManager.hasLicenseForFeature(featureType)); + assertTrue("License should be valid for maxNodes = " + (featureAttributes.maxNodes - 1), esLicenseManager.hasLicenseForNodes(featureType, featureAttributes.maxNodes - 1)); + assertTrue("License should be valid for maxNodes = " + (featureAttributes.maxNodes), esLicenseManager.hasLicenseForNodes(featureType, featureAttributes.maxNodes)); + assertFalse("License should not be valid for maxNodes = " + (featureAttributes.maxNodes + 1), esLicenseManager.hasLicenseForNodes(featureType, featureAttributes.maxNodes + 1)); + } + } +}