From e07a5db938951964c514dc7a7aa4b41dcbcd184c Mon Sep 17 00:00:00 2001 From: Henry Lee Date: Tue, 11 Jul 2023 17:33:14 -0700 Subject: [PATCH 1/2] Test --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e06383635d..e89df2996e 100644 --- a/README.md +++ b/README.md @@ -277,7 +277,7 @@ try { } ``` -### Use a Client With PKCV Authentication +### Use a Client With PKCV Authentication Additional documentation here: https://twilio.com/docs/iam/pkcv/quickstart From 6d974a56513fc68a1565d1a8117d14aa5c15770a Mon Sep 17 00:00:00 2001 From: Henry Lee Date: Tue, 1 Aug 2023 09:20:40 -0700 Subject: [PATCH 2/2] POC --- .../com/twilio/example/ValidationExample.java | 22 ++++-- .../com/twilio/http/ValidationClient.java | 72 ++++++++++++++++++- .../twilio/http/ValidationInterceptor.java | 21 +++++- src/main/java/com/twilio/jwt/Jwt.java | 1 + .../jwt/validation/ValidationToken.java | 48 ++++++++++++- .../com/twilio/http/ValidationClientTest.java | 1 + .../jwt/validation/ValidationTokenTest.java | 58 +++++++++++++++ 7 files changed, 211 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/twilio/example/ValidationExample.java b/src/main/java/com/twilio/example/ValidationExample.java index aae09834cd..798ccce36e 100644 --- a/src/main/java/com/twilio/example/ValidationExample.java +++ b/src/main/java/com/twilio/example/ValidationExample.java @@ -13,8 +13,11 @@ import java.security.KeyPairGenerator; import java.util.Base64; -public class ValidationExample { +import io.jsonwebtoken.SignatureAlgorithm; + +import static com.twilio.http.HttpClient.DEFAULT_REQUEST_CONFIG; +public class ValidationExample { public static final String ACCOUNT_SID = System.getenv("TWILIO_ACCOUNT_SID"); public static final String AUTH_TOKEN = System.getenv("TWILIO_AUTH_TOKEN"); @@ -26,6 +29,7 @@ public class ValidationExample { */ public static void main(String[] args) throws Exception { + //Twilio.setRegion("dev"); // Generate public/private key pair KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); keyGen.initialize(2048); @@ -34,21 +38,25 @@ public static void main(String[] args) throws Exception { // Use the default rest client TwilioRestClient client = - new TwilioRestClient.Builder(ACCOUNT_SID, AUTH_TOKEN) - .build(); + new TwilioRestClient.Builder(ACCOUNT_SID, AUTH_TOKEN) + .region("dev") + .build(); // Create a public key and signing key using the default client PublicKey key = PublicKey.creator( - Base64.getEncoder().encodeToString(pk.getEncoded()) + Base64.getEncoder().encodeToString(pk.getEncoded()) ).setFriendlyName("Public Key").create(client); NewSigningKey signingKey = NewSigningKey.creator().create(client); // Switch to validation client as the default client TwilioRestClient validationClient = new TwilioRestClient.Builder(signingKey.getSid(), signingKey.getSecret()) - .accountSid(ACCOUNT_SID) - .httpClient(new ValidationClient(ACCOUNT_SID, key.getSid(), signingKey.getSid(), pair.getPrivate())) - .build(); + .accountSid(ACCOUNT_SID) + .region("dev") + // Validation client supports RS256 or PS256 algorithm. Default is RS256. + .httpClient(new ValidationClient(ACCOUNT_SID, key.getSid(), signingKey.getSid(), pair.getPrivate(), DEFAULT_REQUEST_CONFIG, + SignatureAlgorithm.PS256)) + .build(); // Make REST API requests Iterable messages = Message.reader().read(validationClient); diff --git a/src/main/java/com/twilio/http/ValidationClient.java b/src/main/java/com/twilio/http/ValidationClient.java index 521983787e..0a7f986cec 100644 --- a/src/main/java/com/twilio/http/ValidationClient.java +++ b/src/main/java/com/twilio/http/ValidationClient.java @@ -17,8 +17,16 @@ import java.security.PrivateKey; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; + +import io.jsonwebtoken.SignatureAlgorithm; + +import static io.jsonwebtoken.SignatureAlgorithm.PS256; +import static io.jsonwebtoken.SignatureAlgorithm.RS256; public class ValidationClient extends HttpClient { @@ -39,6 +47,23 @@ public ValidationClient(final String accountSid, this(accountSid, credentialSid, signingKey, privateKey, DEFAULT_REQUEST_CONFIG); } + /** + * Create a new ValidationClient. + * + * @param accountSid Twilio Account SID + * @param credentialSid Twilio Credential SID + * @param signingKey Twilio Signing key + * @param privateKey Private Key + * @param algorithm Client validation algorithm + */ + public ValidationClient(final String accountSid, + final String credentialSid, + final String signingKey, + final PrivateKey privateKey, + final SignatureAlgorithm algorithm) { + this(accountSid, credentialSid, signingKey, privateKey, DEFAULT_REQUEST_CONFIG, algorithm); + } + /** * Create a new ValidationClient. * @@ -53,7 +78,26 @@ public ValidationClient(final String accountSid, final String signingKey, final PrivateKey privateKey, final RequestConfig requestConfig) { - this(accountSid, credentialSid, signingKey, privateKey, requestConfig, DEFAULT_SOCKET_CONFIG); + this(accountSid, credentialSid, signingKey, privateKey, requestConfig, DEFAULT_SOCKET_CONFIG, RS256); + } + + /** + * Create a new ValidationClient. + * + * @param accountSid Twilio Account SID + * @param credentialSid Twilio Credential SID + * @param signingKey Twilio Signing key + * @param privateKey Private Key + * @param requestConfig HTTP Request Config + * @param algorithm Client validation algorithm + */ + public ValidationClient(final String accountSid, + final String credentialSid, + final String signingKey, + final PrivateKey privateKey, + final RequestConfig requestConfig, + final SignatureAlgorithm algorithm) { + this(accountSid, credentialSid, signingKey, privateKey, requestConfig, DEFAULT_SOCKET_CONFIG, algorithm); } /** @@ -72,6 +116,28 @@ public ValidationClient(final String accountSid, final PrivateKey privateKey, final RequestConfig requestConfig, final SocketConfig socketConfig) { + + this(accountSid, credentialSid, signingKey, privateKey, requestConfig, socketConfig, RS256); + } + + /** + * Create a new ValidationClient. + * + * @param accountSid Twilio Account SID + * @param credentialSid Twilio Credential SID + * @param signingKey Twilio Signing key + * @param privateKey Private Key + * @param requestConfig HTTP Request Config + * @param socketConfig HTTP Socket Config + * @param algorithm Client validation algorithm + */ + public ValidationClient(final String accountSid, + final String credentialSid, + final String signingKey, + final PrivateKey privateKey, + final RequestConfig requestConfig, + final SocketConfig socketConfig, + final SignatureAlgorithm algorithm) { Collection headers = Arrays.asList( new BasicHeader("X-Twilio-Client", "java-" + Twilio.VERSION), new BasicHeader(HttpHeaders.ACCEPT, "application/json"), @@ -81,12 +147,14 @@ public ValidationClient(final String accountSid, final PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); connectionManager.setDefaultSocketConfig(socketConfig); + // should I validate algorithms here? Nah lets do it in validation token + client = HttpClientBuilder.create() .setConnectionManager(connectionManager) .setDefaultRequestConfig(requestConfig) .setDefaultHeaders(headers) .setMaxConnPerRoute(10) - .addInterceptorLast(new ValidationInterceptor(accountSid, credentialSid, signingKey, privateKey)) + .addInterceptorLast(new ValidationInterceptor(accountSid, credentialSid, signingKey, privateKey, algorithm)) .setRedirectStrategy(this.getRedirectStrategy()) .build(); } diff --git a/src/main/java/com/twilio/http/ValidationInterceptor.java b/src/main/java/com/twilio/http/ValidationInterceptor.java index 4f8b5e2f59..acd29c8703 100644 --- a/src/main/java/com/twilio/http/ValidationInterceptor.java +++ b/src/main/java/com/twilio/http/ValidationInterceptor.java @@ -12,6 +12,8 @@ import java.util.Arrays; import java.util.List; +import io.jsonwebtoken.SignatureAlgorithm; + public class ValidationInterceptor implements HttpRequestInterceptor { private static final List HEADERS = Arrays.asList("authorization", "host"); @@ -20,6 +22,7 @@ public class ValidationInterceptor implements HttpRequestInterceptor { private final String credentialSid; private final String signingKeySid; private final PrivateKey privateKey; + private final SignatureAlgorithm algorithm; /** * Create a new ValidationInterceptor. @@ -30,10 +33,25 @@ public class ValidationInterceptor implements HttpRequestInterceptor { * @param privateKey Private Key */ public ValidationInterceptor(String accountSid, String credentialSid, String signingKeySid, PrivateKey privateKey) { + this(accountSid, credentialSid, signingKeySid, privateKey, SignatureAlgorithm.RS256); + } + + /** + * Create a new ValidationInterceptor. + * + * @param accountSid Twilio Acocunt SID + * @param credentialSid Twilio Credential SID + * @param signingKeySid Twilio Signing Key + * @param privateKey Private Key + * @param algorithm Client validaiton algorithm + */ + public ValidationInterceptor(String accountSid, String credentialSid, String signingKeySid, PrivateKey privateKey, + SignatureAlgorithm algorithm) { this.accountSid = accountSid; this.credentialSid = credentialSid; this.signingKeySid = signingKeySid; this.privateKey = privateKey; + this.algorithm = algorithm; } @Override @@ -44,7 +62,8 @@ public void process(HttpRequest request, HttpContext context) throws HttpExcepti signingKeySid, privateKey, request, - HEADERS + HEADERS, + algorithm ); request.addHeader("Twilio-Client-Validation", jwt.toJwt()); } diff --git a/src/main/java/com/twilio/jwt/Jwt.java b/src/main/java/com/twilio/jwt/Jwt.java index da8349312e..a8c61086c2 100644 --- a/src/main/java/com/twilio/jwt/Jwt.java +++ b/src/main/java/com/twilio/jwt/Jwt.java @@ -6,6 +6,7 @@ import javax.crypto.spec.SecretKeySpec; import java.security.Key; +import java.time.Instant; import java.util.Date; import java.util.HashMap; import java.util.Map; diff --git a/src/main/java/com/twilio/jwt/validation/ValidationToken.java b/src/main/java/com/twilio/jwt/validation/ValidationToken.java index dc8832afff..5e6193f62e 100644 --- a/src/main/java/com/twilio/jwt/validation/ValidationToken.java +++ b/src/main/java/com/twilio/jwt/validation/ValidationToken.java @@ -8,13 +8,17 @@ import org.apache.http.HttpEntity; import org.apache.http.HttpEntityEnclosingRequest; import org.apache.http.HttpRequest; +import org.apache.http.impl.auth.UnsupportedDigestAlgorithmException; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import java.security.PrivateKey; import java.util.*; import java.util.function.Function; +import static io.jsonwebtoken.SignatureAlgorithm.PS256; +import static io.jsonwebtoken.SignatureAlgorithm.RS256; public class ValidationToken extends Jwt { @@ -30,9 +34,12 @@ public class ValidationToken extends Jwt { private final List signedHeaders; private final String requestBody; + private static final Set supportedAlgorithms + = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(PS256, RS256))); + private ValidationToken(Builder b) { super( - SignatureAlgorithm.RS256, + b.algorithm, b.privateKey, b.credentialSid, new Date(new Date().getTime() + b.ttl * 1000) @@ -91,19 +98,47 @@ public Map getClaims() { * @return The ValidationToken generated from the HttpRequest * @throws IOException when unable to generate */ + public static ValidationToken fromHttpRequest( + String accountSid, + String credentialSid, + String signingKeySid, + PrivateKey privateKey, + HttpRequest request, + List signedHeaders + ) throws IOException { + return fromHttpRequest(accountSid, credentialSid, signingKeySid, privateKey, request, signedHeaders, SignatureAlgorithm.RS256); + } + + /** + * Create a ValidationToken from an HTTP Request. + * + * @param accountSid Twilio Account SID + * @param credentialSid Twilio Credential SID + * @param signingKeySid Twilio Signing Key SID + * @param privateKey Private Key + * @param request HTTP Request + * @param signedHeaders Headers to sign + * @param algorithm Client validation algorithm + * @return The ValidationToken generated from the HttpRequest + * @throws IOException when unable to generate + */ public static ValidationToken fromHttpRequest( String accountSid, String credentialSid, String signingKeySid, PrivateKey privateKey, HttpRequest request, - List signedHeaders + List signedHeaders, + SignatureAlgorithm algorithm ) throws IOException { Builder builder = new Builder(accountSid, credentialSid, signingKeySid, privateKey); String method = request.getRequestLine().getMethod(); builder.method(method); + // I think it can go here... + builder.algorithm(algorithm); + String uri = request.getRequestLine().getUri(); if (uri.contains("?")) { String[] uriParts = uri.split("\\?"); @@ -150,6 +185,7 @@ public static class Builder { private List signedHeaders = Collections.emptyList(); private String requestBody = ""; private int ttl = 300; + private SignatureAlgorithm algorithm = SignatureAlgorithm.RS256; /** * Create a new ValidationToken Builder. @@ -206,6 +242,14 @@ public Builder ttl(int ttl) { return this; } + public Builder algorithm(SignatureAlgorithm algorithm) { + if (!supportedAlgorithms.contains(algorithm)) { + throw new IllegalArgumentException("Not supported!"); + } + this.algorithm = algorithm; + return this; + } + public ValidationToken build() { return new ValidationToken(this); } diff --git a/src/test/java/com/twilio/http/ValidationClientTest.java b/src/test/java/com/twilio/http/ValidationClientTest.java index 1df4497b26..27ca5f393f 100644 --- a/src/test/java/com/twilio/http/ValidationClientTest.java +++ b/src/test/java/com/twilio/http/ValidationClientTest.java @@ -5,6 +5,7 @@ import java.security.KeyPair; import java.security.KeyPairGenerator; +import io.jsonwebtoken.SignatureAlgorithm; import okhttp3.HttpUrl; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; diff --git a/src/test/java/com/twilio/jwt/validation/ValidationTokenTest.java b/src/test/java/com/twilio/jwt/validation/ValidationTokenTest.java index 9d8622e51b..a72b2775c7 100644 --- a/src/test/java/com/twilio/jwt/validation/ValidationTokenTest.java +++ b/src/test/java/com/twilio/jwt/validation/ValidationTokenTest.java @@ -4,6 +4,9 @@ import com.twilio.jwt.Jwt; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.InvalidKeyException; + import org.apache.http.*; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.message.BasicHeader; @@ -20,12 +23,19 @@ import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.PrivateKey; +import java.security.PublicKey; import java.util.*; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import static io.jsonwebtoken.SignatureAlgorithm.PS256; +import static io.jsonwebtoken.SignatureAlgorithm.PS384; +import static io.jsonwebtoken.SignatureAlgorithm.PS512; +import static io.jsonwebtoken.SignatureAlgorithm.RS256; +import static io.jsonwebtoken.SignatureAlgorithm.RS384; +import static io.jsonwebtoken.SignatureAlgorithm.RS512; import static org.mockito.Mockito.when; public class ValidationTokenTest { @@ -38,6 +48,8 @@ public class ValidationTokenTest { private Header[] headers; private PrivateKey privateKey; + private PublicKey publicKey; + @Mock private HttpRequest request; @@ -61,6 +73,7 @@ public void setup() throws Exception { keyGen.initialize(2048); KeyPair pair = keyGen.generateKeyPair(); privateKey = pair.getPrivate(); + publicKey = pair.getPublic(); } @Test @@ -86,6 +99,49 @@ public void testTokenBuilder() { Assert.assertEquals("4dc9b67bed579647914587b0e22a1c65c1641d8674797cd82de65e766cce5f80", claims.get("rqh")); } + @Test + public void testTokenValidAlgorithms() { + List validAlgorithms = Arrays.asList(RS256, PS256); + for (SignatureAlgorithm alg : validAlgorithms) { + Jwt jwt = new ValidationToken.Builder(ACCOUNT_SID, CREDENTIAL_SID, SIGNING_KEY_SID, privateKey) + .algorithm(alg) + .method("GET") + .uri("/Messages") + .queryString("PageSize=5&Limit=10") + .headers(headers) + .signedHeaders(SIGNED_HEADERS) + .requestBody("foobar") + .build(); + + Claims claims = + Jwts.parserBuilder().setSigningKey(publicKey).build() + .parseClaimsJws(jwt.toJwt()) + .getBody(); + validateToken(claims); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testTokenInvalidAlgorithms() { + List validAlgorithms = Arrays.asList(SignatureAlgorithm.HS256, SignatureAlgorithm.ES256, RS384, RS512, PS384, PS512); + for (SignatureAlgorithm alg : validAlgorithms) { + Jwt jwt = new ValidationToken.Builder(ACCOUNT_SID, CREDENTIAL_SID, SIGNING_KEY_SID, privateKey) + .algorithm(alg) + .method("GET") + .uri("/Messages") + .queryString("PageSize=5&Limit=10") + .headers(headers) + .signedHeaders(SIGNED_HEADERS) + .requestBody("foobar") + .build(); + + + Jwts.parserBuilder().setSigningKey(publicKey).build() + .parseClaimsJws(jwt.toJwt()) + .getBody(); + } + } + @Test public void testTokenFromHttpRequest() throws IOException { when(request.getRequestLine()).thenReturn(requestLine); @@ -116,6 +172,8 @@ public void testTokenFromEntityRequest() throws IOException { when(requestLine.getUri()).thenReturn("/Messages"); Jwt jwt = ValidationToken.fromHttpRequest(ACCOUNT_SID, CREDENTIAL_SID, SIGNING_KEY_SID, privateKey, requestWithEntity, SIGNED_HEADERS); + + // TODO: if jws was signed using a private key, then public key should be used for decrypt Claims claims = Jwts.parserBuilder() .setSigningKey(privateKey).build()