aboutsummaryrefslogtreecommitdiffstats
path: root/vespa-athenz
diff options
context:
space:
mode:
authorBjørn Christian Seime <bjorncs@oath.com>2018-07-09 15:13:33 +0200
committerBjørn Christian Seime <bjorncs@oath.com>2018-07-09 15:22:53 +0200
commit3588ab015c45e5e8682e9a9299cabec25937d9d8 (patch)
tree4911645bf82062d85402ba2329358914733e812e /vespa-athenz
parent257bfcde6220c40e7ceab46d1f5b5ab8c5e650a0 (diff)
Move NTokenValidator to vespa-athenz + load pub keys from file
- Move NTokenValidator from controller-server to vespa-athenz - Remodel ZmsKeystore as AthenzTruststore - Use file-backed truststore on controller (replaces download of public keys) - Remove ZmsClient.getPublicKey/getPublicKeys
Diffstat (limited to 'vespa-athenz')
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzPublicKey.java49
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/utils/ntoken/AthenzConfTruststore.java52
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/utils/ntoken/AthenzTruststore.java14
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/utils/ntoken/NTokenValidator.java71
-rw-r--r--vespa-athenz/src/main/java/com/yahoo/vespa/athenz/utils/ntoken/package-info.java8
-rw-r--r--vespa-athenz/src/test/java/com/yahoo/vespa/athenz/utils/ntoken/NTokenValidatorTest.java87
6 files changed, 232 insertions, 49 deletions
diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzPublicKey.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzPublicKey.java
deleted file mode 100644
index 1c810e3e9c9..00000000000
--- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/api/AthenzPublicKey.java
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.athenz.api;
-
-import java.security.PublicKey;
-import java.util.Objects;
-
-/**
- * @author bjorncs
- */
-public class AthenzPublicKey {
-
- private final PublicKey publicKey;
- private final String keyId;
-
- public AthenzPublicKey(PublicKey publicKey, String keyId) {
- this.publicKey = publicKey;
- this.keyId = keyId;
- }
-
- public PublicKey getPublicKey() {
- return publicKey;
- }
-
- public String getKeyId() {
- return keyId;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- AthenzPublicKey that = (AthenzPublicKey) o;
- return Objects.equals(publicKey, that.publicKey) &&
- Objects.equals(keyId, that.keyId);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(publicKey, keyId);
- }
-
- @Override
- public String toString() {
- return "AthenzPublicKey{" +
- "publicKey=" + publicKey +
- ", keyId='" + keyId + '\'' +
- '}';
- }
-}
diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/utils/ntoken/AthenzConfTruststore.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/utils/ntoken/AthenzConfTruststore.java
new file mode 100644
index 00000000000..9d9a19d55cb
--- /dev/null
+++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/utils/ntoken/AthenzConfTruststore.java
@@ -0,0 +1,52 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.athenz.utils.ntoken;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.yahoo.athenz.auth.util.Crypto;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Path;
+import java.security.PublicKey;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * A {@link AthenzTruststore} that is backed by athenz.conf
+ *
+ * @author bjorncs
+ */
+public class AthenzConfTruststore implements AthenzTruststore {
+
+ private final Map<String, PublicKey> publicKeys;
+
+ public AthenzConfTruststore(Path athenzConfFile) {
+ this.publicKeys = loadPublicKeys(athenzConfFile);
+ }
+
+ @Override
+ public Optional<PublicKey> getPublicKey(String keyId) {
+ return Optional.of(publicKeys.get(keyId));
+ }
+
+ private static Map<String, PublicKey> loadPublicKeys(Path athenzConfFile) {
+ try {
+ Map<String, PublicKey> publicKeys = new HashMap<>();
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode root = mapper.readTree(athenzConfFile.toFile());
+ ArrayNode keysArray = (ArrayNode) root.get("ztsPublicKeys");
+ for (JsonNode keyEntry : keysArray) {
+ String keyId = keyEntry.get("id").textValue();
+ String encodedPublicKey = keyEntry.get("key").textValue();
+ PublicKey publicKey = Crypto.loadPublicKey(Crypto.ybase64DecodeString(encodedPublicKey));
+ publicKeys.put(keyId, publicKey);
+ }
+ return publicKeys;
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+}
diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/utils/ntoken/AthenzTruststore.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/utils/ntoken/AthenzTruststore.java
new file mode 100644
index 00000000000..3139e6b847f
--- /dev/null
+++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/utils/ntoken/AthenzTruststore.java
@@ -0,0 +1,14 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.athenz.utils.ntoken;
+
+import java.security.PublicKey;
+import java.util.Optional;
+
+/**
+ * A truststore that contains all ZMS and ZTS public keys
+ *
+ * @author bjorncs
+ */
+public interface AthenzTruststore {
+ Optional<PublicKey> getPublicKey(String keyId);
+}
diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/utils/ntoken/NTokenValidator.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/utils/ntoken/NTokenValidator.java
new file mode 100644
index 00000000000..85c62bc07ff
--- /dev/null
+++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/utils/ntoken/NTokenValidator.java
@@ -0,0 +1,71 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.athenz.utils.ntoken;
+
+import com.yahoo.athenz.auth.token.PrincipalToken;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.athenz.api.AthenzDomain;
+import com.yahoo.vespa.athenz.api.AthenzPrincipal;
+import com.yahoo.vespa.athenz.api.NToken;
+import com.yahoo.vespa.athenz.utils.AthenzIdentities;
+
+import java.nio.file.Path;
+import java.security.PublicKey;
+import java.time.Duration;
+import java.util.logging.Logger;
+
+/**
+ * Validates the content of an NToken:
+ * 1) Verifies that the token is signed by Athenz
+ * 2) Verifies that the token is not expired
+ *
+ * @author bjorncs
+ */
+public class NTokenValidator {
+ // Max allowed skew in token timestamp (only for creation, not expiry timestamp)
+ private static final long ALLOWED_TIMESTAMP_OFFSET = Duration.ofMinutes(5).getSeconds();
+
+ private static final Logger log = Logger.getLogger(NTokenValidator.class.getName());
+ private final AthenzTruststore truststore;
+
+
+ public NTokenValidator(AthenzTruststore truststore) {
+ this.truststore = truststore;
+ }
+
+ public NTokenValidator(Path athenzConfFile) {
+ this(new AthenzConfTruststore(athenzConfFile));
+ }
+
+ public AthenzPrincipal validate(NToken token) throws InvalidTokenException {
+ PrincipalToken principalToken = new PrincipalToken(token.getRawToken());
+ String keyId = principalToken.getKeyId();
+ PublicKey zmsPublicKey = truststore.getPublicKey(keyId)
+ .orElseThrow(() -> {
+ String message = "NToken has an unknown keyId: " + keyId;
+ log.log(LogLevel.WARNING, message);
+ return new InvalidTokenException(message);
+ });
+ validateSignatureAndExpiration(principalToken, zmsPublicKey);
+ return new AthenzPrincipal(
+ AthenzIdentities.from(
+ new AthenzDomain(principalToken.getDomain()),
+ principalToken.getName()),
+ token);
+ }
+
+ private static void validateSignatureAndExpiration(PrincipalToken token, PublicKey zmsPublicKey) throws InvalidTokenException {
+ StringBuilder errorMessageBuilder = new StringBuilder();
+ if (!token.validate(zmsPublicKey, (int) ALLOWED_TIMESTAMP_OFFSET, true, errorMessageBuilder)) {
+ String message = "NToken is expired or has invalid signature: " + errorMessageBuilder.toString();
+ log.log(LogLevel.WARNING, message);
+ throw new InvalidTokenException(message);
+ }
+ }
+
+ public static class InvalidTokenException extends RuntimeException {
+ public InvalidTokenException(String message) {
+ super(message);
+ }
+ }
+
+}
diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/utils/ntoken/package-info.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/utils/ntoken/package-info.java
new file mode 100644
index 00000000000..8760c02d27d
--- /dev/null
+++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/utils/ntoken/package-info.java
@@ -0,0 +1,8 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * @author bjorncs
+ */
+@ExportPackage
+package com.yahoo.vespa.athenz.utils.ntoken;
+
+import com.yahoo.osgi.annotation.ExportPackage; \ No newline at end of file
diff --git a/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/utils/ntoken/NTokenValidatorTest.java b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/utils/ntoken/NTokenValidatorTest.java
new file mode 100644
index 00000000000..0e70993792f
--- /dev/null
+++ b/vespa-athenz/src/test/java/com/yahoo/vespa/athenz/utils/ntoken/NTokenValidatorTest.java
@@ -0,0 +1,87 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.athenz.utils.ntoken;
+
+import com.yahoo.athenz.auth.token.PrincipalToken;
+import com.yahoo.vespa.athenz.api.AthenzIdentity;
+import com.yahoo.vespa.athenz.api.AthenzPrincipal;
+import com.yahoo.vespa.athenz.api.AthenzUser;
+import com.yahoo.vespa.athenz.api.NToken;
+import com.yahoo.vespa.athenz.tls.KeyAlgorithm;
+import com.yahoo.vespa.athenz.tls.KeyUtils;
+import com.yahoo.vespa.athenz.utils.ntoken.NTokenValidator.InvalidTokenException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.time.Instant;
+import java.util.Optional;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author bjorncs
+ */
+public class NTokenValidatorTest {
+
+ private static final KeyPair TRUSTED_KEY = KeyUtils.generateKeypair(KeyAlgorithm.RSA);
+ private static final KeyPair UNKNOWN_KEY = KeyUtils.generateKeypair(KeyAlgorithm.RSA);
+ private static final AthenzIdentity IDENTITY = AthenzUser.fromUserId("myuser");
+
+ @Rule
+ public ExpectedException exceptionRule = ExpectedException.none();
+
+ @Test
+ public void valid_token_is_accepted() throws InvalidTokenException {
+ NTokenValidator validator = new NTokenValidator(createTruststore());
+ NToken token = createNToken(IDENTITY, Instant.now(), TRUSTED_KEY.getPrivate(), "0");
+ AthenzPrincipal principal = validator.validate(token);
+ assertEquals("user.myuser", principal.getIdentity().getFullName());
+ }
+
+ @Test
+ public void invalid_signature_is_not_accepted() throws InvalidTokenException {
+ NTokenValidator validator = new NTokenValidator(createTruststore());
+ NToken token = createNToken(IDENTITY, Instant.now(), UNKNOWN_KEY.getPrivate(), "0");
+ exceptionRule.expect(InvalidTokenException.class);
+ exceptionRule.expectMessage("NToken is expired or has invalid signature");
+ validator.validate(token);
+ }
+
+ @Test
+ public void expired_token_is_not_accepted() throws InvalidTokenException {
+ NTokenValidator validator = new NTokenValidator(createTruststore());
+ NToken token = createNToken(IDENTITY, Instant.ofEpochMilli(1234) /*long time ago*/, TRUSTED_KEY.getPrivate(), "0");
+ exceptionRule.expect(InvalidTokenException.class);
+ exceptionRule.expectMessage("NToken is expired or has invalid signature");
+ validator.validate(token);
+ }
+
+ @Test
+ public void unknown_keyId_is_not_accepted() throws InvalidTokenException {
+ NTokenValidator validator = new NTokenValidator(createTruststore());
+ NToken token = createNToken(IDENTITY, Instant.now(), TRUSTED_KEY.getPrivate(), "unknown-key-id");
+ exceptionRule.expect(InvalidTokenException.class);
+ exceptionRule.expectMessage("NToken has an unknown keyId");
+ validator.validate(token);
+ }
+
+ private static AthenzTruststore createTruststore() {
+ return keyId -> keyId.equals("0") ? Optional.of(TRUSTED_KEY.getPublic()) : Optional.empty();
+ }
+
+ private static NToken createNToken(AthenzIdentity identity, Instant issueTime, PrivateKey privateKey, String keyId) {
+ PrincipalToken token = new PrincipalToken.Builder("U1", identity.getDomain().getName(), identity.getName())
+ .keyId(keyId)
+ .salt("1234")
+ .host("host")
+ .ip("1.2.3.4")
+ .issueTime(issueTime.getEpochSecond())
+ .expirationWindow(1000)
+ .build();
+ token.sign(privateKey);
+ return new NToken(token.getSignedToken());
+ }
+
+}