Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@ public final class HTTPSignatureConstants extends RSSecurityConstants {
*/
public static final String RSSEC_HTTP_SIGNATURE_DIGEST_ALGORITHM = "rs.security.http.signature.digest.algorithm";

/**
* The maximum age in seconds of a signed "Date" header on an inbound request or response. A message whose
* signed "Date" is older than this is rejected, so that a captured signed message can not be replayed
* after its time to live has passed. The default is 300 seconds, a value of 0 or less disables the check.
*/
public static final String RSSEC_HTTP_SIGNATURE_TIME_TO_LIVE = "rs.security.http.signature.time.to.live";

/**
* The number of seconds a signed "Date" header on an inbound request or response may be in the future,
* to allow for clock skew. The default is 60 seconds, a negative value disables the check.
*/
public static final String RSSEC_HTTP_SIGNATURE_FUTURE_TIME_TO_LIVE =
"rs.security.http.signature.future.time.to.live";


private HTTPSignatureConstants() {
// complete
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,40 @@
*/
package org.apache.cxf.rs.security.httpsignature;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;

import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.helpers.CastUtils;
import org.apache.cxf.jaxrs.utils.HttpUtils;
import org.apache.cxf.message.Message;
import org.apache.cxf.message.MessageUtils;
import org.apache.cxf.rs.security.httpsignature.exception.InvalidSignatureException;
import org.apache.cxf.rs.security.httpsignature.exception.MissingSignatureHeaderException;
import org.apache.cxf.rs.security.httpsignature.exception.MultipleSignatureHeaderException;
import org.apache.cxf.rs.security.httpsignature.provider.AlgorithmProvider;
import org.apache.cxf.rs.security.httpsignature.provider.KeyProvider;
import org.apache.cxf.rs.security.httpsignature.provider.SecurityProvider;
import org.apache.cxf.rs.security.httpsignature.utils.DefaultSignatureConstants;
import org.apache.cxf.rs.security.httpsignature.utils.SignatureHeaderUtils;
import org.tomitribe.auth.signatures.Signature;

public class MessageVerifier {
/**
* The default maximum age in seconds of a signed "Date" header
*/
public static final int DEFAULT_TIME_TO_LIVE = 300;
/**
* The default number of seconds a signed "Date" header may be in the future
*/
public static final int DEFAULT_FUTURE_TIME_TO_LIVE = 60;

private static final Logger LOG = LogUtils.getL7dLogger(MessageVerifier.class);

private AlgorithmProvider algorithmProvider;
Expand All @@ -46,6 +60,8 @@ public class MessageVerifier {
private final SignatureValidator signatureValidator;
private final List<String> requiredHeaders;
private boolean addDefaultRequiredHeaders = true;
private int timeToLive = DEFAULT_TIME_TO_LIVE;
private int futureTimeToLive = DEFAULT_FUTURE_TIME_TO_LIVE;

public MessageVerifier(KeyProvider keyProvider) {
this(keyProvider, (List<String>)null);
Expand Down Expand Up @@ -151,9 +167,48 @@ public void verifyMessage(Map<String, List<String>> messageHeaders, String metho
method,
uri,
signedHeaders);
verifyDateHeader(messageHeaders);
LOG.fine("Finished signature verification");
}

/**
* Reject a message whose signed "Date" header is outside the configured time window, so that a captured
* signed message can not be replayed once its time to live has passed. A "Date" header which is not
* covered by the signature is ignored, as it can be changed along with the rest of the unsigned message.
*/
private void verifyDateHeader(Map<String, List<String>> messageHeaders) {
if (timeToLive <= 0 && futureTimeToLive < 0) {
return;
}
List<String> signedHeaders = Signature.fromString(messageHeaders.get("Signature").get(0)).getHeaders();
if (!signedHeaders.contains("date")) {
return;
}
String date = null;
for (Map.Entry<String, List<String>> entry : messageHeaders.entrySet()) {
if ("date".equalsIgnoreCase(entry.getKey()) && !entry.getValue().isEmpty()) {
date = entry.getValue().get(0);
break;
}
}
if (date == null) {
return;
}
final long dateTime;
try {
dateTime = HttpUtils.getHttpDateFormat().parse(date).getTime();
} catch (ParseException ex) {
throw new InvalidSignatureException("the signed Date header can not be parsed");
}
long now = System.currentTimeMillis();
if (timeToLive > 0 && now - dateTime > TimeUnit.SECONDS.toMillis(timeToLive)) {
throw new InvalidSignatureException("the signed Date header is older than the time to live");
}
if (futureTimeToLive >= 0 && dateTime - now > TimeUnit.SECONDS.toMillis(futureTimeToLive)) {
throw new InvalidSignatureException("the signed Date header is in the future");
}
}

private void inspectMultipleSignatureHeaders(Map<String, List<String>> responseHeaders) {
if (responseHeaders.get("Signature").size() > 1) {
throw new MultipleSignatureHeaderException("there are multiple signature headers in request");
Expand All @@ -178,4 +233,28 @@ public void setAddDefaultRequiredHeaders(boolean addDefaultRequiredHeaders) {
this.addDefaultRequiredHeaders = addDefaultRequiredHeaders;
}

public int getTimeToLive() {
return timeToLive;
}

/**
* Set the maximum age in seconds of a signed "Date" header. A message whose signed "Date" is older
* than this is rejected. The default is 300 seconds, a value of 0 or less disables the check.
*/
public void setTimeToLive(int timeToLive) {
this.timeToLive = timeToLive;
}

public int getFutureTimeToLive() {
return futureTimeToLive;
}

/**
* Set the number of seconds a signed "Date" header may be in the future, to allow for clock skew.
* The default is 60 seconds, a negative value disables the check.
*/
public void setFutureTimeToLive(int futureTimeToLive) {
this.futureTimeToLive = futureTimeToLive;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,29 @@ protected MessageVerifier createMessageVerifier() {

final String finalSignatureAlgorithm = signatureAlgorithm;
final Provider provider = Security.getProvider(DefaultSignatureConstants.SECURITY_PROVIDER);
return new MessageVerifier(keyId -> publicKey, keyId -> provider, keyId -> finalSignatureAlgorithm);
MessageVerifier verifier =
new MessageVerifier(keyId -> publicKey, keyId -> provider, keyId -> finalSignatureAlgorithm);
verifier.setTimeToLive(getContextualInteger(m,
HTTPSignatureConstants.RSSEC_HTTP_SIGNATURE_TIME_TO_LIVE, MessageVerifier.DEFAULT_TIME_TO_LIVE));
verifier.setFutureTimeToLive(getContextualInteger(m,
HTTPSignatureConstants.RSSEC_HTTP_SIGNATURE_FUTURE_TIME_TO_LIVE,
MessageVerifier.DEFAULT_FUTURE_TIME_TO_LIVE));
return verifier;
}

private static int getContextualInteger(Message m, String key, int defaultValue) {
Object value = m.getContextualProperty(key);
if (value instanceof Number) {
return ((Number)value).intValue();
}
if (value instanceof String) {
try {
return Integer.parseInt((String)value);
} catch (NumberFormatException ex) {
LOG.warning("Incorrect integer value of " + value + " specified for: " + key);
}
}
return defaultValue;
}

protected abstract void handleException(Exception ex);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@
import java.net.URI;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Clock;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import java.util.Collections;
Expand All @@ -42,8 +41,7 @@ private SignatureHeaderUtils() { }
* Add a date header at the current time using the ZoneOffset. Date format is http
*/
public static void addDateHeader(Map<String, List<String>> messageHeaders, ZoneOffset zoneOffset) {
String date = DateTimeFormatter.RFC_1123_DATE_TIME
.format(LocalDateTime.now().atZone(Clock.system(zoneOffset).getZone()));
String date = DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now(zoneOffset));
messageHeaders.put("Date", Collections.singletonList(date));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
Expand Down Expand Up @@ -285,6 +287,58 @@ public void defaultRequiredHeaderNotPresentTestNotRequired() throws IOException
headerVerifier.verifyMessage(headers, METHOD, URI, new MessageImpl(), null);
}

@Test(expected = InvalidSignatureException.class)
public void staleSignedDateHeaderFails() throws IOException {
Map<String, List<String>> headers = createMockHeaders();
headers.put("Date", Collections.singletonList(httpDate(-10)));
createAndAddSignature(headers);
messageVerifier.verifyMessage(headers, METHOD, URI, new MessageImpl(), MESSAGE_BODY.getBytes());
}

@Test(expected = InvalidSignatureException.class)
public void futureSignedDateHeaderFails() throws IOException {
Map<String, List<String>> headers = createMockHeaders();
headers.put("Date", Collections.singletonList(httpDate(10)));
createAndAddSignature(headers);
messageVerifier.verifyMessage(headers, METHOD, URI, new MessageImpl(), MESSAGE_BODY.getBytes());
}

@Test
public void signedDateHeaderWithinTimeToLivePasses() throws IOException {
Map<String, List<String>> headers = createMockHeaders();
headers.put("Date", Collections.singletonList(httpDate(-4)));
createAndAddSignature(headers);
messageVerifier.verifyMessage(headers, METHOD, URI, new MessageImpl(), MESSAGE_BODY.getBytes());
}

@Test
public void staleUnsignedDateHeaderIsIgnored() throws IOException {
Map<String, List<String>> headers = createMockHeaders();
headers.put("Date", Collections.singletonList(httpDate(-10)));
new MessageSigner(keyId -> keyPair.getPrivate(), KEY_ID, Collections.singletonList("host"))
.sign(headers, URI, METHOD);
messageVerifier.verifyMessage(headers, METHOD, URI, new MessageImpl(), MESSAGE_BODY.getBytes());
}

@Test
public void staleSignedDateHeaderPassesWithoutTimeToLive() throws IOException {
Map<String, List<String>> headers = createMockHeaders();
headers.put("Date", Collections.singletonList(httpDate(-10)));
createAndAddSignature(headers);

MessageVerifier noTtlVerifier = new MessageVerifier(keyId -> keyPair.getPublic());
noTtlVerifier.setSecurityProvider(new MockSecurityProvider());
noTtlVerifier.setAlgorithmProvider(new MockAlgorithmProvider());
noTtlVerifier.setAddDefaultRequiredHeaders(false);
noTtlVerifier.setTimeToLive(0);
noTtlVerifier.verifyMessage(headers, METHOD, URI, new MessageImpl(), MESSAGE_BODY.getBytes());
}

private static String httpDate(int minutesFromNow) {
return DateTimeFormatter.RFC_1123_DATE_TIME.format(
ZonedDateTime.now(ZoneOffset.UTC).plusMinutes(minutesFromNow));
}

private static void createAndAddSignature(Map<String, List<String>> headers) throws IOException {
messageSigner.sign(headers, URI, METHOD);
}
Expand Down
Loading