You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pulsar.apache.org by mm...@apache.org on 2019/06/14 15:46:01 UTC

[pulsar] branch master updated: [pulsar-broker] Add support for other algorithms in token auth (#4528)

This is an automated email from the ASF dual-hosted git repository.

mmerli pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pulsar.git


The following commit(s) were added to refs/heads/master by this push:
     new 04e5fee  [pulsar-broker] Add support for other algorithms in token auth (#4528)
04e5fee is described below

commit 04e5fee6e5490ba40c102edfa0377f76993f3ca3
Author: Addison Higham <ad...@gmail.com>
AuthorDate: Fri Jun 14 09:45:55 2019 -0600

    [pulsar-broker] Add support for other algorithms in token auth (#4528)
    
    Before this patch, all keys are read as RSA, which meant that only RSA
    compatible JWT signing algorithms could be used, specifically, this
    limited the use of ECDSA family of JWT keys.
    
    This changes this by changing the signature we use to parse keys to also
    take a SignatureAlgorithm and also adds a new config option
    `tokenPublicAlg` which can be used to signify what algorithm the
    broker/proxy should use when reading public keys. However, these all
    default to RS256, which, should indicate to decode as RSA (even if
    another RS/PS algoritm is used).
    
    This also adds some new options to the Token CLI tool for those commands
    that weren't respecting the algorithm, but these are defaulted to RS256
    as well.
---
 .../AuthenticationProviderToken.java               | 26 +++++++++-
 .../authentication/utils/AuthTokenUtils.java       | 20 ++++++--
 .../AuthenticationProviderTokenTest.java           | 60 ++++++++++++++++++++--
 .../pulsar/utils/auth/tokens/TokensCliUtils.java   | 11 +++-
 site2/docs/reference-configuration.md              |  2 +
 site2/docs/security-token-admin.md                 |  6 +--
 6 files changed, 110 insertions(+), 15 deletions(-)

diff --git a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProviderToken.java b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProviderToken.java
index f5ee57c..7ad39fc 100644
--- a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProviderToken.java
+++ b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProviderToken.java
@@ -22,12 +22,14 @@ import io.jsonwebtoken.Claims;
 import io.jsonwebtoken.Jwt;
 import io.jsonwebtoken.JwtException;
 import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
 
 import java.io.IOException;
 import java.security.Key;
 
 import javax.naming.AuthenticationException;
 
+import io.jsonwebtoken.security.SignatureException;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.pulsar.broker.ServiceConfiguration;
 import org.apache.pulsar.broker.authentication.utils.AuthTokenUtils;
@@ -46,10 +48,14 @@ public class AuthenticationProviderToken implements AuthenticationProvider {
     // The token's claim that corresponds to the "role" string
     final static String CONF_TOKEN_AUTH_CLAIM = "tokenAuthClaim";
 
+    // When using public key's, the algorithm of the key
+    final static String CONF_TOKEN_PUBLIC_ALG = "tokenPublicAlg";
+
     final static String TOKEN = "token";
 
     private Key validationKey;
     private String roleClaim;
+    private SignatureAlgorithm publicKeyAlg;
 
     @Override
     public void close() throws IOException {
@@ -57,7 +63,9 @@ public class AuthenticationProviderToken implements AuthenticationProvider {
     }
 
     @Override
-    public void initialize(ServiceConfiguration config) throws IOException {
+    public void initialize(ServiceConfiguration config) throws IOException, IllegalArgumentException {
+        // we need to fetch the algorithm before we fetch the key
+        this.publicKeyAlg = getPublicKeyAlgType(config);
         this.validationKey = getValidationKey(config);
         this.roleClaim = getTokenRoleClaim(config);
     }
@@ -130,7 +138,7 @@ public class AuthenticationProviderToken implements AuthenticationProvider {
                 && StringUtils.isNotBlank((String) conf.getProperty(CONF_TOKEN_PUBLIC_KEY))) {
             final String validationKeyConfig = (String) conf.getProperty(CONF_TOKEN_PUBLIC_KEY);
             final byte[] validationKey = AuthTokenUtils.readKeyFromUrl(validationKeyConfig);
-            return AuthTokenUtils.decodePublicKey(validationKey);
+            return AuthTokenUtils.decodePublicKey(validationKey, publicKeyAlg);
         } else {
             throw new IOException("No secret key was provided for token authentication");
         }
@@ -144,4 +152,18 @@ public class AuthenticationProviderToken implements AuthenticationProvider {
             return Claims.SUBJECT;
         }
     }
+
+    private SignatureAlgorithm getPublicKeyAlgType(ServiceConfiguration conf) throws IllegalArgumentException {
+        if (conf.getProperty(CONF_TOKEN_PUBLIC_ALG) != null
+                && StringUtils.isNotBlank((String) conf.getProperty(CONF_TOKEN_PUBLIC_ALG))) {
+            String alg = (String) conf.getProperty(CONF_TOKEN_PUBLIC_ALG);
+            try {
+                return SignatureAlgorithm.forName(alg);
+            } catch (SignatureException ex) {
+                throw new IllegalArgumentException("invalid algorithm provided " + alg, ex);
+            }
+        } else {
+            return SignatureAlgorithm.RS256;
+        }
+    }
 }
diff --git a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/utils/AuthTokenUtils.java b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/utils/AuthTokenUtils.java
index 08ff1c7..de30773 100644
--- a/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/utils/AuthTokenUtils.java
+++ b/pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/utils/AuthTokenUtils.java
@@ -55,26 +55,38 @@ public class AuthTokenUtils {
         return Keys.hmacShaKeyFor(secretKey);
     }
 
-    public static PrivateKey decodePrivateKey(byte[] key) throws IOException {
+    public static PrivateKey decodePrivateKey(byte[] key, SignatureAlgorithm algType) throws IOException {
         try {
             PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(key);
-            KeyFactory kf = KeyFactory.getInstance("RSA");
+            KeyFactory kf = KeyFactory.getInstance(keyTypeForSignatureAlgorithm(algType));
             return kf.generatePrivate(spec);
         } catch (Exception e) {
             throw new IOException("Failed to decode private key", e);
         }
     }
 
-    public static PublicKey decodePublicKey(byte[] key) throws IOException {
+
+    public static PublicKey decodePublicKey(byte[] key, SignatureAlgorithm algType) throws IOException {
         try {
             X509EncodedKeySpec spec = new X509EncodedKeySpec(key);
-            KeyFactory kf = KeyFactory.getInstance("RSA");
+            KeyFactory kf = KeyFactory.getInstance(keyTypeForSignatureAlgorithm(algType));
             return kf.generatePublic(spec);
         } catch (Exception e) {
             throw new IOException("Failed to decode public key", e);
         }
     }
 
+    private static String keyTypeForSignatureAlgorithm(SignatureAlgorithm alg) {
+        if (alg.getFamilyName().equals("RSA")) {
+            return "RSA";
+        } else if (alg.getFamilyName().equals("ECDSA")) {
+            return "EC";
+        } else {
+            String msg = "The " + alg.name() + " algorithm does not support Key Pairs.";
+            throw new IllegalArgumentException(msg);
+        }
+    }
+
     public static String encodeKeyBase64(Key key) {
         return Encoders.BASE64.encode(key.getEncoded());
     }
diff --git a/pulsar-broker-common/src/test/java/org/apache/pulsar/broker/authentication/AuthenticationProviderTokenTest.java b/pulsar-broker-common/src/test/java/org/apache/pulsar/broker/authentication/AuthenticationProviderTokenTest.java
index 22385bd..b5183e3 100644
--- a/pulsar-broker-common/src/test/java/org/apache/pulsar/broker/authentication/AuthenticationProviderTokenTest.java
+++ b/pulsar-broker-common/src/test/java/org/apache/pulsar/broker/authentication/AuthenticationProviderTokenTest.java
@@ -93,13 +93,13 @@ public class AuthenticationProviderTokenTest {
         String privateKey = AuthTokenUtils.encodeKeyBase64(keyPair.getPrivate());
         String publicKey = AuthTokenUtils.encodeKeyBase64(keyPair.getPublic());
 
-        String token = AuthTokenUtils.createToken(AuthTokenUtils.decodePrivateKey(Decoders.BASE64.decode(privateKey)),
+        String token = AuthTokenUtils.createToken(AuthTokenUtils.decodePrivateKey(Decoders.BASE64.decode(privateKey), SignatureAlgorithm.RS256),
                 SUBJECT,
                 Optional.empty());
 
         @SuppressWarnings("unchecked")
         Jwt<?, Claims> jwt = Jwts.parser()
-                .setSigningKey(AuthTokenUtils.decodePublicKey(Decoders.BASE64.decode(publicKey)))
+                .setSigningKey(AuthTokenUtils.decodePublicKey(Decoders.BASE64.decode(publicKey), SignatureAlgorithm.RS256))
                 .parse(token);
 
         assertNotNull(jwt);
@@ -274,7 +274,7 @@ public class AuthenticationProviderTokenTest {
         provider.initialize(conf);
 
         // Use private key to generate token
-        PrivateKey privateKey = AuthTokenUtils.decodePrivateKey(Decoders.BASE64.decode(privateKeyStr));
+        PrivateKey privateKey = AuthTokenUtils.decodePrivateKey(Decoders.BASE64.decode(privateKeyStr), SignatureAlgorithm.RS256);
         String token = AuthTokenUtils.createToken(privateKey, SUBJECT, Optional.empty());
 
         // Pulsar protocol auth
@@ -318,7 +318,7 @@ public class AuthenticationProviderTokenTest {
 
 
         // Use private key to generate token
-        PrivateKey privateKey = AuthTokenUtils.decodePrivateKey(Decoders.BASE64.decode(privateKeyStr));
+        PrivateKey privateKey = AuthTokenUtils.decodePrivateKey(Decoders.BASE64.decode(privateKeyStr), SignatureAlgorithm.RS256);
         String token = Jwts.builder()
                 .setClaims(new HashMap<String, Object>() {{
                     put(authRoleClaim, authRole);
@@ -344,6 +344,46 @@ public class AuthenticationProviderTokenTest {
         provider.close();
     }
 
+    @Test
+    public void testAuthSecretKeyPairWithECDSA() throws Exception {
+        KeyPair keyPair = Keys.keyPairFor(SignatureAlgorithm.ES256);
+
+        String privateKeyStr = AuthTokenUtils.encodeKeyBase64(keyPair.getPrivate());
+        String publicKeyStr = AuthTokenUtils.encodeKeyBase64(keyPair.getPublic());
+
+        AuthenticationProviderToken provider = new AuthenticationProviderToken();
+
+        Properties properties = new Properties();
+        // Use public key for validation
+        properties.setProperty(AuthenticationProviderToken.CONF_TOKEN_PUBLIC_KEY, publicKeyStr);
+        // Set that we are using EC keys
+        properties.setProperty(AuthenticationProviderToken.CONF_TOKEN_PUBLIC_ALG, SignatureAlgorithm.ES256.getValue());
+
+        ServiceConfiguration conf = new ServiceConfiguration();
+        conf.setProperties(properties);
+        provider.initialize(conf);
+
+        // Use private key to generate token
+        PrivateKey privateKey = AuthTokenUtils.decodePrivateKey(Decoders.BASE64.decode(privateKeyStr), SignatureAlgorithm.ES256);
+        String token = AuthTokenUtils.createToken(privateKey, SUBJECT, Optional.empty());
+
+        // Pulsar protocol auth
+        String subject = provider.authenticate(new AuthenticationDataSource() {
+            @Override
+            public boolean hasDataFromCommand() {
+                return true;
+            }
+
+            @Override
+            public String getCommandData() {
+                return token;
+            }
+        });
+        assertEquals(subject, SUBJECT);
+
+        provider.close();
+    }
+
     @Test(expectedExceptions = AuthenticationException.class)
     public void testAuthenticateWhenNoJwtPassed() throws AuthenticationException {
         AuthenticationProviderToken provider = new AuthenticationProviderToken();
@@ -481,4 +521,16 @@ public class AuthenticationProviderTokenTest {
 
         new AuthenticationProviderToken().initialize(conf);
     }
+
+    @Test(expectedExceptions = IllegalArgumentException.class)
+    public void testValidationWhenPublicKeyAlgIsInvalid() throws IOException {
+        Properties properties = new Properties();
+        properties.setProperty(AuthenticationProviderToken.CONF_TOKEN_PUBLIC_ALG,
+                "invalid");
+
+        ServiceConfiguration conf = new ServiceConfiguration();
+        conf.setProperties(properties);
+
+        new AuthenticationProviderToken().initialize(conf);
+    }
 }
diff --git a/pulsar-broker/src/main/java/org/apache/pulsar/utils/auth/tokens/TokensCliUtils.java b/pulsar-broker/src/main/java/org/apache/pulsar/utils/auth/tokens/TokensCliUtils.java
index 2ebead5..03cc0bf 100644
--- a/pulsar-broker/src/main/java/org/apache/pulsar/utils/auth/tokens/TokensCliUtils.java
+++ b/pulsar-broker/src/main/java/org/apache/pulsar/utils/auth/tokens/TokensCliUtils.java
@@ -109,6 +109,9 @@ public class TokensCliUtils {
 
     @Parameters(commandDescription = "Create a new token")
     public static class CommandCreateToken {
+        @Parameter(names = { "-a",
+                "--signature-algorithm" }, description = "The signature algorithm for the new key pair.")
+        SignatureAlgorithm algorithm = SignatureAlgorithm.RS256;
 
         @Parameter(names = { "-s",
                 "--subject" }, description = "Specify the 'subject' or 'principal' associate with this token", required = true)
@@ -141,7 +144,7 @@ public class TokensCliUtils {
 
             if (privateKey != null) {
                 byte[] encodedKey = AuthTokenUtils.readKeyFromUrl(privateKey);
-                signingKey = AuthTokenUtils.decodePrivateKey(encodedKey);
+                signingKey = AuthTokenUtils.decodePrivateKey(encodedKey, algorithm);
             } else {
                 byte[] encodedKey = AuthTokenUtils.readKeyFromUrl(secretKey);
                 signingKey = AuthTokenUtils.decodeSecretKey(encodedKey);
@@ -202,6 +205,10 @@ public class TokensCliUtils {
     @Parameters(commandDescription = "Validate a token against a key")
     public static class CommandValidateToken {
 
+        @Parameter(names = { "-a",
+                "--signature-algorithm" }, description = "The signature algorithm for the key pair if using public key.")
+        SignatureAlgorithm algorithm = SignatureAlgorithm.RS256;
+
         @Parameter(description = "The token string", arity = 1)
         private java.util.List<String> args;
 
@@ -254,7 +261,7 @@ public class TokensCliUtils {
 
             if (publicKey != null) {
                 byte[] encodedKey = AuthTokenUtils.readKeyFromUrl(publicKey);
-                validationKey = AuthTokenUtils.decodePublicKey(encodedKey);
+                validationKey = AuthTokenUtils.decodePublicKey(encodedKey, algorithm);
             } else {
                 byte[] encodedKey = AuthTokenUtils.readKeyFromUrl(secretKey);
                 validationKey = AuthTokenUtils.decodeSecretKey(encodedKey);
diff --git a/site2/docs/reference-configuration.md b/site2/docs/reference-configuration.md
index 4a85995..b526386 100644
--- a/site2/docs/reference-configuration.md
+++ b/site2/docs/reference-configuration.md
@@ -144,6 +144,7 @@ Pulsar brokers are responsible for handling incoming messages from producers, di
 |tlsCiphers|Specify the tls cipher the broker will use to negotiate during TLS Handshake. Multiple values can be specified, separated by commas. Example:- ```TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256```||
 |tokenSecretKey| Configure the secret key to be used to validate auth tokens. The key can be specified like: `tokenSecretKey=data:base64,xxxxxxxxx` or `tokenSecretKey=file:///my/secret.key`||
 |tokenPublicKey| Configure the public key to be used to validate auth tokens. The key can be specified like: `tokenPublicKey=data:base64,xxxxxxxxx` or `tokenPublicKey=file:///my/secret.key`||
+|tokenPublicAlg| Configure the algorithm to be used to validate auth tokens. This can be any of the asymettric algorithms supported by Java JWT (https://github.com/jwtk/jjwt#signature-algorithms-keys) |RS256|
 |tokenAuthClaim| Specify which of the token's claims will be used as the authentication "principal" or "role". The default "sub" claim will be used if this is left blank ||
 |maxUnackedMessagesPerConsumer| Max number of unacknowledged messages allowed to receive messages by a consumer on a shared subscription. Broker will stop sending messages to consumer once, this limit reaches until consumer starts acknowledging messages back. Using a value of 0, is disabling unackeMessage limit check and consumer can receive messages without any restriction  |50000|
 |maxUnackedMessagesPerSubscription| Max number of unacknowledged messages allowed per shared subscription. Broker will stop dispatching messages to all consumers of the subscription once this limit reaches until consumer starts acknowledging messages back and unack count reaches to limit/2. Using a value of 0, is disabling unackedMessage-limit check and dispatcher can dispatch messages without any restriction  |200000|
@@ -458,6 +459,7 @@ The [Pulsar proxy](concepts-architecture-overview.md#pulsar-proxy) can be config
 |tlsCiphers|Specify the tls cipher the broker will use to negotiate during TLS Handshake. Multiple values can be specified, separated by commas. Example:- ```TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256```||
 |tokenSecretKey| Configure the secret key to be used to validate auth tokens. The key can be specified like: `tokenSecretKey=data:base64,xxxxxxxxx` or `tokenSecretKey=file:///my/secret.key`||
 |tokenPublicKey| Configure the public key to be used to validate auth tokens. The key can be specified like: `tokenPublicKey=data:base64,xxxxxxxxx` or `tokenPublicKey=file:///my/secret.key`||
+|tokenPublicAlg| Configure the algorithm to be used to validate auth tokens. This can be any of the asymettric algorithms supported by Java JWT (https://github.com/jwtk/jjwt#signature-algorithms-keys) |RS256|
 |tokenAuthClaim| Specify the token claim that will be used as the authentication "principal" or "role". The "subject" field will be used if this is left blank ||
 
 ## ZooKeeper
diff --git a/site2/docs/security-token-admin.md b/site2/docs/security-token-admin.md
index 54043da..72799ce 100644
--- a/site2/docs/security-token-admin.md
+++ b/site2/docs/security-token-admin.md
@@ -47,18 +47,18 @@ the brokers to allow them to validate the clients.
 
 #### Creating a secret key
 
-> Output file will be generated in the root of your pulsar installation directory. You can also provide absolute path for the output file. 
+> Output file will be generated in the root of your pulsar installation directory. You can also provide absolute path for the output file.
 ```shell
 $ bin/pulsar tokens create-secret-key --output my-secret.key
 ```
-To generate base64 encoded private key 
+To generate base64 encoded private key
 ```shell
 $ bin/pulsar tokens create-secret-key --output  /opt/my-secret.key --base64
 ```
 
 ### Public/Private keys
 
-With public/private, we need to create a pair of keys.
+With public/private, we need to create a pair of keys. Pulsar supports all algorithms supported by the Java JWT library shown [here](https://github.com/jwtk/jjwt#signature-algorithms-keys)
 
 #### Creating a key pair