CnonceHandler.java
/*******************************************************************************
* Copyright (c) 2019, RISE AB
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/
package se.sics.ace.rs;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Map;
import java.util.logging.Logger;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import com.upokecenter.cbor.CBORObject;
import com.upokecenter.cbor.CBORType;
import se.sics.ace.AceException;
import se.sics.ace.Constants;
/**
* This class handles the freshness verification using client-nonces
* (see Section 5.3.1 of RFC 9200).
*
* @author Ludwig Seitz
*
*/
public class CnonceHandler {
/**
* The singleton instance
*/
private static CnonceHandler singleton = null;
/**
* The default window size
*/
private static int defaultWindowSize = 30;
/**
* The counter used to generate the cnonces.
* -1 means we don't use cnonces.
*/
private Integer cnonceCounter = -1;
/**
* The last seen nonce
*/
private int cnonceSeen;
/**
* The size of the replay window
*/
private int cnonceWindowSize;
/**
* Cnonce replay window,
*/
private int cnonceWindow;
/**
* Cnonce HMAC key (32 bytes)
*/
private byte[] cnonceKey;
/**
* The logger
*/
private static final Logger LOGGER
= Logger.getLogger(CnonceHandler.class.getName());
/**
* Create the cnonce handler.
*
* @param cnonceReplayWindowSize the cnonce replay window size (or null to
* use the default)
*/
protected CnonceHandler() {
this.cnonceCounter = 1;
this.cnonceSeen = 0;
this.cnonceKey = new byte[32];
SecureRandom sr = new SecureRandom();
sr.nextBytes(this.cnonceKey);
this.cnonceWindow = 0;
this.cnonceWindowSize = defaultWindowSize;
}
/**
* The singleton getter
* @return the singleton repository
* @throws AceException if the handler is not initialized
*/
public static CnonceHandler getInstance() {
if (singleton == null) {
singleton = new CnonceHandler();
}
return singleton;
}
/**
* Set the default window size for the replay window.
* Will only have effect before the singleton is created.
*
* @param size the size of the replay window
*/
public static void setDefaultWindowSize(int size) {
if (singleton != null) {
throw new RuntimeException(
"Cannot window size after singleton was created");
}
if (size < 0 || size > 32) {
throw new IllegalArgumentException(
"cnonceWindow size must be between 0 and 32");
}
defaultWindowSize = size;
}
/**
* Implements the nonce checking for a token received at authz-info.
*
* @param claims the claims of the token to check
* @throws AceException
*/
public void checkNonce(Map<Short, CBORObject> claims) throws AceException {
if (this.cnonceCounter == -1) {//Means we are not using the client nonces
return;
}
CBORObject cnonce = claims.get(Constants.CNONCE);
if (cnonce == null) {
LOGGER.info("Expected a cnonce but found none");
throw new AceException("cnonce expected but not found");
}
if (!cnonce.getType().equals(CBORType.ByteString)) {
throw new AceException("Invalid cnonce type");
}
byte[] cnonceB = cnonce.GetByteString();
if (cnonceB.length != 4+32) {//4 byte for the int counter, 16 bytes HMAC
throw new AceException("Invalid cnonce length");
}
byte[] mac = new byte[32];
byte[] counter = new byte[4];
mac = Arrays.copyOfRange(cnonceB, 0, 32);
counter = Arrays.copyOfRange(cnonceB, 32, 36);
byte[] macExpected;
//Verify MAC
try {
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(
this.cnonceKey, "HmacSHA256");
sha256_HMAC.init(secret_key);
macExpected = sha256_HMAC.doFinal(counter);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
LOGGER.severe("Error while verifying cnonce: " + e.getMessage());
throw new AceException("Nonce verification failed");
}
if (!Arrays.equals(mac, macExpected)) {
throw new AceException("cnonce invalid");
}
//Check if nonce is in the replay window
ByteBuffer b = ByteBuffer.wrap(counter);
int counterI = b.getInt();
checkIncomingCounter(counterI);
}
/**
* Check an incoming cnonce counter
* @param counter
* @throws AceException
*/
private synchronized void checkIncomingCounter(int counter) throws AceException {
if (counter > this.cnonceSeen) {
// Update the replay window
int shift = counter - this.cnonceSeen;
this.cnonceWindow = this.cnonceWindow << shift;
this.cnonceSeen = counter;
} else if (counter == this.cnonceSeen) {
throw new AceException("cnonce replayed");
} else { // counter < this.cnonceSeen
if (counter + this.cnonceWindowSize < this.cnonceSeen) {
LOGGER.severe("cnonce too old");
throw new AceException("cnonce expired");
}
// seq+replay_window_size > recipient_seq
int shift = this.cnonceSeen - counter;
int pattern = 1 << shift;
int verifier = this.cnonceWindow & pattern;
verifier = verifier >> shift;
if (verifier == 1) {
throw new AceException("cnonce replayed");
}
this.cnonceWindow = this.cnonceWindow | pattern;
}
}
/**
* Create a client-nonce to ensure freshness of access tokens, when the
* RS has no synchronzied clock with the AS.
*
* @return a nonce
*
* @throws NoSuchAlgorithmException
* @throws InvalidKeyException
*/
public byte[] createNonce()
throws NoSuchAlgorithmException, InvalidKeyException {
if (this.cnonceCounter == -1) {
LOGGER.info("cnonce requested but not configured to handle them");
return null;
}
if (this.cnonceCounter == Integer.MAX_VALUE) {
LOGGER.info("cnonce counter wrapped");
this.cnonceCounter = 1;
this.cnonceSeen = 0;
this.cnonceWindow = 0;
//Generate a new key to invalidate the old cnonces
this.cnonceKey = new byte[32];
new SecureRandom().nextBytes(this.cnonceKey);
}
byte[] mac = null;
byte[] counter = ByteBuffer.allocate(4).putInt(
this.cnonceCounter).array();
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(
this.cnonceKey, "HmacSHA256");
sha256_HMAC.init(secret_key);
mac = sha256_HMAC.doFinal(counter);
byte[] nonce = new byte[mac.length + counter.length];
System.arraycopy(mac,0, nonce, 0, mac.length);
System.arraycopy(counter, 0, nonce , mac.length, counter.length);
this.cnonceCounter++;
return nonce;
}
}