diff options
author | Bjørn Christian Seime <bjorncs@oath.com> | 2018-07-09 15:13:33 +0200 |
---|---|---|
committer | Bjørn Christian Seime <bjorncs@oath.com> | 2018-07-25 16:26:59 +0200 |
commit | 3a9d916073fa1f90610fdc219d3214b0fb3b2223 (patch) | |
tree | eec66c2e98653224d029d13a11efab9c1de4ac9f /vespa-athenz | |
parent | 7ffbc291ea4623c7877628fa0bc3274b7232cadd (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')
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..64cdb13d66c --- /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.ofNullable(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()); + } + +} |