From 586950cb87da0eea6658688c76972bca49289276 Mon Sep 17 00:00:00 2001 From: Yann Tavernier Date: Mon, 13 Sep 2021 08:37:24 +0200 Subject: [PATCH 1/2] fix: fix pattern and toString method --- .../tomitribe/auth/signatures/Signature.java | 25 +++++++++---------- .../auth/signatures/SignatureTest.java | 19 +++++++++++++- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/tomitribe/auth/signatures/Signature.java b/src/main/java/org/tomitribe/auth/signatures/Signature.java index 264dc5f..e8a49bc 100644 --- a/src/main/java/org/tomitribe/auth/signatures/Signature.java +++ b/src/main/java/org/tomitribe/auth/signatures/Signature.java @@ -132,9 +132,10 @@ public class Signature { * Regular expression pattern for fields present in the Authorization field. * Fields value may be double-quoted strings, e.g. algorithm="hs2019" * Some fields may be numerical values without double-quotes, e.g. created=123456 + * Fields that are numerical values with digits are formatted regarding the current Locale. The char used can be a "." (dot) or a "," coma. */ private static final Pattern RFC_2617_PARAM = Pattern - .compile("(?\\w+)=((\"(?[^\"]*)\")|(?\\d+\\.?\\d*))"); + .compile("(?\\w+)=((\"(?[^\"]*)\")|(?\\d+[.,]?\\d*))"); /** * The maximum time skew between the client and the server. @@ -561,24 +562,22 @@ public String toParamString() { } public String toString(final String prefix) { + final Object alg; if (SigningAlgorithm.HS2019.equals(signingAlgorithm)) { // When the signing algorithm is set to 'hs2019', the value of the algorithm // field must be set to 'hs2019'. The specific crypto algorithm is not // serialized in the 'Authorization' header, the server must derive the value // from the keyId. - return (prefix != null ? prefix + " " : "") + - "keyId=\"" + keyId + '\"' + - (signatureCreatedTime != null ? String.format(",created=%d", signatureCreatedTime / 1000L) : "") + - (signatureExpiresTime != null ? String.format(",expires=%.3f", signatureExpiresTime / 1000.0) : "") + - ",algorithm=\"" + signingAlgorithm + '\"' + - ",headers=\"" + Join.join(" ", headers) + '\"' + - ",signature=\"" + signature + '\"'; + alg = signingAlgorithm; } else { - return (prefix != null ? prefix + " " : "") + - "keyId=\"" + keyId + '\"' + - ",algorithm=\"" + algorithm + '\"' + - ",headers=\"" + Join.join(" ", headers) + '\"' + - ",signature=\"" + signature + '\"'; + alg = algorithm; } + return (prefix != null ? prefix + " " : "") + + "keyId=\"" + keyId + '\"' + + (signatureCreatedTime != null && headers.contains("(created)") ? String.format(",created=%d", signatureCreatedTime / 1000L) : "") + + (signatureExpiresTime != null && headers.contains("(expires)") ? String.format(",expires=%.3f", signatureExpiresTime / 1000.0) : "") + + ",algorithm=\"" + alg + '\"' + + ",headers=\"" + Join.join(" ", headers) + '\"' + + ",signature=\"" + signature + '\"'; } } diff --git a/src/test/java/org/tomitribe/auth/signatures/SignatureTest.java b/src/test/java/org/tomitribe/auth/signatures/SignatureTest.java index e42614c..f0f8253 100644 --- a/src/test/java/org/tomitribe/auth/signatures/SignatureTest.java +++ b/src/test/java/org/tomitribe/auth/signatures/SignatureTest.java @@ -21,6 +21,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Random; import static org.junit.Assert.assertEquals; @@ -121,9 +122,11 @@ public void signatureCreatedFieldIntegerTooLarge() throws Exception { /** * Invalid (created) field, the value must be an integer, decimal values are not supported. + * Locale EN */ @Test(expected = InvalidCreatedFieldException.class) - public void signatureCreatedFieldDecimalValue() throws Exception { + public void signatureCreatedFieldDecimalValueEN() throws Exception { + Locale.setDefault(Locale.ENGLISH); final String authorization = "Signature keyId=\"hmac-key-1\",algorithm=\"hmac-sha256\"," + "created=1591763110.123," + "headers=\"(created)\"" + @@ -131,6 +134,20 @@ public void signatureCreatedFieldDecimalValue() throws Exception { Signature.fromString(authorization, null); } + /** + * Invalid (created) field, the value must be an integer, decimal values are not supported. + * Locale FR + */ + @Test(expected = InvalidCreatedFieldException.class) + public void signatureCreatedFieldDecimalValueFR() throws Exception { + Locale.setDefault(Locale.FRENCH); + final String authorization = "Signature keyId=\"hmac-key-1\",algorithm=\"hmac-sha256\"," + + "created=1591763110,123," + + "headers=\"(created)\"" + + ",signature=\"Base64(HMAC-SHA256(signing string))\""; + Signature.fromString(authorization, null); + } + /** * Invalid (expires) field, the value must be a number, not a string. */ From 60ff99e03c46eb5d6c407a1f13d491d2ec6da882 Mon Sep 17 00:00:00 2001 From: Yann Tavernier Date: Mon, 13 Sep 2021 08:38:10 +0200 Subject: [PATCH 2/2] feat: ability to sign for a given crated and expires date --- .../org/tomitribe/auth/signatures/Signer.java | 33 ++++-- .../tomitribe/auth/signatures/SignerTest.java | 8 +- .../signatures/SignerWithAlgorithmTest.java | 107 ++++++++++++++++++ 3 files changed, 137 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/tomitribe/auth/signatures/Signer.java b/src/main/java/org/tomitribe/auth/signatures/Signer.java index e87875b..fa54902 100644 --- a/src/main/java/org/tomitribe/auth/signatures/Signer.java +++ b/src/main/java/org/tomitribe/auth/signatures/Signer.java @@ -85,22 +85,21 @@ public Signer(final Key key, final Signature signature, final Provider provider) } /** - * Create and return a HTTP signature object. + * Create and return a HTTP signature object configured with 'created' and 'expires' values. + * Useful if you want to recreate a Signature from configuration to validate another one. * * @param method The HTTP method. * @param uri The path and query of the request target of the message. * The value must already be encoded exactly as it will be sent in the * request line of the HTTP message. No URL encoding is performed by this method. * @param headers The HTTP headers. + * @param created the created timestamp + * @param expires the expires timestamp * * @return a Signature object containing the signed message. */ - public Signature sign(final String method, final String uri, final Map headers) throws IOException { - final Long created = System.currentTimeMillis(); - Long expires = signature.getSignatureMaxValidityMilliseconds(); - if (expires != null) { - expires += created; - } + public Signature sign(final String method, final String uri, final Map headers, Long created, Long expires) throws IOException { + final String signingString = createSigningString(method, uri, headers, created, expires); final byte[] binarySignature = sign.sign(signingString.getBytes("UTF-8")); @@ -114,6 +113,26 @@ public Signature sign(final String method, final String uri, final Map headers) throws IOException { + final long created = System.currentTimeMillis(); + Long expires = signature.getSignatureMaxValidityMilliseconds(); + if (expires != null) { + expires += created; + } + return sign(method, uri, headers, created, expires); + } + /** * Create and return the string which is used as input for the cryptographic signature. * diff --git a/src/test/java/org/tomitribe/auth/signatures/SignerTest.java b/src/test/java/org/tomitribe/auth/signatures/SignerTest.java index ae499d0..4afcab5 100644 --- a/src/test/java/org/tomitribe/auth/signatures/SignerTest.java +++ b/src/test/java/org/tomitribe/auth/signatures/SignerTest.java @@ -109,7 +109,7 @@ public void testSign() throws Exception { headers.put("Content-Length", "18"); final Signature signed = signer.sign(method, uri, headers); assertEquals("yT/NrPI9mKB5R7FTLRyFWvB+QLQOEAvbGmauC0tI+Jg=", signed.getSignature()); - assertToString("Signature keyId=\"hmac-key-1\",created=9999,algorithm=\"hs2019\"," + + assertToString("Signature keyId=\"hmac-key-1\",algorithm=\"hs2019\"," + "headers=\"content-length host date (request-target)\",signature=\"yT/NrPI9mKB5R7FTLRyFWvB+QLQOEAvbGmauC0tI+Jg=\"", signed); } @@ -125,7 +125,7 @@ public void testSign() throws Exception { headers.put("Content-Length", "18"); final Signature signed = signer.sign(method, uri, headers); assertEquals("DPIsA/PWeYjySmfjw2P2SLJXZj1szDOei/Hh8nTcaPo=", signed.getSignature()); - assertToString("Signature keyId=\"hmac-key-1\",created=9999,algorithm=\"hs2019\"," + + assertToString("Signature keyId=\"hmac-key-1\",algorithm=\"hs2019\"," + "headers=\"content-length host date (request-target)\",signature=\"DPIsA/PWeYjySmfjw2P2SLJXZj1szDOei/Hh8nTcaPo=\"", signed); } @@ -141,7 +141,7 @@ public void testSign() throws Exception { headers.put("Content-Length", "18"); final Signature signed = signer.sign(method, uri, headers); assertEquals("DPIsA/PWeYjySmfjw2P2SLJXZj1szDOei/Hh8nTcaPo=", signed.getSignature()); - assertToString("Signature keyId=\"hmac-key-1\",created=1628283435,algorithm=\"hs2019\"," + + assertToString("Signature keyId=\"hmac-key-1\",algorithm=\"hs2019\"," + "headers=\"content-length host date (request-target)\",signature=\"DPIsA/PWeYjySmfjw2P2SLJXZj1szDOei/Hh8nTcaPo=\"", signed); } @@ -157,7 +157,7 @@ public void testSign() throws Exception { headers.put("Content-Length", "18"); final Signature signed = signer.sign(method, uri, headers); assertEquals("IWTDxmOoEJI67YxY3eDIRzxrsAtlYYCuGZxKlkUSYdA=", signed.getSignature()); - assertToString("Signature keyId=\"hmac-key-1\",created=9999,algorithm=\"hs2019\"," + + assertToString("Signature keyId=\"hmac-key-1\",algorithm=\"hs2019\"," + "headers=\"content-length host date (request-target)\",signature=\"IWTDxmOoEJI67YxY3eDIRzxrsAtlYYCuGZxKlkUSYdA=\"", signed); } } diff --git a/src/test/java/org/tomitribe/auth/signatures/SignerWithAlgorithmTest.java b/src/test/java/org/tomitribe/auth/signatures/SignerWithAlgorithmTest.java index 979e047..7e4d07b 100644 --- a/src/test/java/org/tomitribe/auth/signatures/SignerWithAlgorithmTest.java +++ b/src/test/java/org/tomitribe/auth/signatures/SignerWithAlgorithmTest.java @@ -144,6 +144,113 @@ public void testSign() throws Exception { } } + /** + * It is an intentional part of the design that the same Signer instance + * can be reused on several HTTP Messages in a multi-threaded fashion + *

+ * Reuse is tested here + *

+ */ + @Test + public void testSignAtCreatedAndExpires() throws Exception { + + final Signature signature = new Signature("hmac-key-1", "hmac-sha256", null, "content-length", "host", "date", "(request-target)"); + + final Key key = new SecretKeySpec("don't tell".getBytes(), "HmacSHA256"); + final Signer signer = new Signer(key, signature); + + final long created = 1631187000; + final long expires = 1631191300786L; + + final String method = "GET"; + final String uri = "/foo/Bar"; + final Map headers = new HashMap(); + headers.put("Host", "example.org"); + headers.put("Date", "Tue, 07 Jun 2014 20:51:35 GMT"); + headers.put("Content-Type", "application/json"); + headers.put("Digest", "SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE="); + headers.put("Accept", "*/*"); + headers.put("Content-Length", "18"); + final Signature signed = signer.sign(method, uri, headers, created, expires); + assertEquals("yT/NrPI9mKB5R7FTLRyFWvB+QLQOEAvbGmauC0tI+Jg=", signed.getSignature()); + assertEquals("Signature keyId=\"hmac-key-1\",algorithm=\"hmac-sha256\",headers=\"content-length host date (request-target)\",signature=\"yT/NrPI9mKB5R7FTLRyFWvB+QLQOEAvbGmauC0tI+Jg=\"", signed.toString()); + } + + @Test + public void testSignAtCreatedAndExpiresWithCreatedHeader() throws Exception { + + final Signature signature = new Signature("hmac-key-1", "hmac-sha256", null, "content-length", "host", "date", "(request-target)", "(created)"); + + final Key key = new SecretKeySpec("don't tell".getBytes(), "HmacSHA256"); + final Signer signer = new Signer(key, signature); + + final long created = 1631187000; + final long expires = 1631191300786L; + + final String method = "GET"; + final String uri = "/foo/Bar"; + final Map headers = new HashMap(); + headers.put("Host", "example.org"); + headers.put("Date", "Tue, 07 Jun 2014 20:51:35 GMT"); + headers.put("Content-Type", "application/json"); + headers.put("Digest", "SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE="); + headers.put("Accept", "*/*"); + headers.put("Content-Length", "18"); + final Signature signed = signer.sign(method, uri, headers, created, expires); + assertEquals("im9YJlYjVPwq0TIndq3mEUC6kH6LaMAZWG4hZ7hVYi4=", signed.getSignature()); + assertEquals("Signature keyId=\"hmac-key-1\",created=1631187,algorithm=\"hmac-sha256\",headers=\"content-length host date (request-target) (created)\",signature=\"im9YJlYjVPwq0TIndq3mEUC6kH6LaMAZWG4hZ7hVYi4=\"", signed.toString()); + } + + @Test + public void testSignAtCreatedAndExpiresWithExpiresHeader() throws Exception { + + final Signature signature = new Signature("hmac-key-1", "hmac-sha256", null, "content-length", "host", "date", "(request-target)", "(expires)"); + + final Key key = new SecretKeySpec("don't tell".getBytes(), "HmacSHA256"); + final Signer signer = new Signer(key, signature); + + final long created = 1631187000; + final long expires = 1631191300786L; + + final String method = "GET"; + final String uri = "/foo/Bar"; + final Map headers = new HashMap(); + headers.put("Host", "example.org"); + headers.put("Date", "Tue, 07 Jun 2014 20:51:35 GMT"); + headers.put("Content-Type", "application/json"); + headers.put("Digest", "SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE="); + headers.put("Accept", "*/*"); + headers.put("Content-Length", "18"); + final Signature signed = signer.sign(method, uri, headers, created, expires); + assertEquals("wqu4SMw/Iqv8KMxqX18mWs6Zs94XFNXOU0Uh5tq23Zw=", signed.getSignature()); + assertEquals("Signature keyId=\"hmac-key-1\",expires=1631191300,786,algorithm=\"hmac-sha256\",headers=\"content-length host date (request-target) (expires)\",signature=\"wqu4SMw/Iqv8KMxqX18mWs6Zs94XFNXOU0Uh5tq23Zw=\"", signed.toString()); + } + + @Test + public void testSignAtCreatedAndExpiresWithCreatedAndExpiresHeader() throws Exception { + + final Signature signature = new Signature("hmac-key-1", "hmac-sha256", null, "content-length", "host", "date", "(request-target)", "(created)", "(expires)"); + + final Key key = new SecretKeySpec("don't tell".getBytes(), "HmacSHA256"); + final Signer signer = new Signer(key, signature); + + final long created = 1631187000; + final long expires = 1631191300786L; + + final String method = "GET"; + final String uri = "/foo/Bar"; + final Map headers = new HashMap(); + headers.put("Host", "example.org"); + headers.put("Date", "Tue, 07 Jun 2014 20:51:35 GMT"); + headers.put("Content-Type", "application/json"); + headers.put("Digest", "SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE="); + headers.put("Accept", "*/*"); + headers.put("Content-Length", "18"); + final Signature signed = signer.sign(method, uri, headers, created, expires); + assertEquals("OVtTtYTcmKgjl7X5kSLT00pbbaOKluD6Gk1pluNBnsU=", signed.getSignature()); + assertEquals("Signature keyId=\"hmac-key-1\",created=1631187,expires=1631191300,786,algorithm=\"hmac-sha256\",headers=\"content-length host date (request-target) (created) (expires)\",signature=\"OVtTtYTcmKgjl7X5kSLT00pbbaOKluD6Gk1pluNBnsU=\"", signed.toString()); + } + @Test public void defaultHeaderList() throws Exception { final Signature signature = new Signature("hmac-key-1", "hmac-sha256", null);