DTLSProfileRequests.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.coap.client;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.logging.Logger;

import org.eclipse.californium.core.CoapClient;
import org.eclipse.californium.core.CoapResponse;
import org.eclipse.californium.core.network.CoapEndpoint;
import org.eclipse.californium.elements.Connector;
import org.eclipse.californium.elements.UDPConnector;
import org.eclipse.californium.scandium.DTLSConnector;
import org.eclipse.californium.elements.auth.RawPublicKeyIdentity;
import org.eclipse.californium.elements.config.CertificateAuthenticationMode;
import org.eclipse.californium.elements.config.Configuration;
import org.eclipse.californium.elements.exception.ConnectorException;
import org.eclipse.californium.scandium.config.DtlsConfig;
import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
import org.eclipse.californium.scandium.dtls.CertificateType;
import org.eclipse.californium.scandium.dtls.PskPublicInformation;
import org.eclipse.californium.scandium.dtls.cipher.CipherSuite;
import org.eclipse.californium.scandium.dtls.pskstore.AdvancedMultiPskStore;
import org.eclipse.californium.scandium.dtls.x509.AsyncNewAdvancedCertificateVerifier;
import org.eclipse.californium.scandium.dtls.x509.SingleCertificateProvider;

import com.upokecenter.cbor.CBORObject;

import org.eclipse.californium.cose.CoseException;
import org.eclipse.californium.cose.KeyKeys;
import org.eclipse.californium.cose.OneKey;
import se.sics.ace.AceException;
import se.sics.ace.Constants;
import se.sics.ace.Util;

/**
 * Implements getting a token from the /token endpoint for a client 
 * using the DTLS profile.
 * 
 * Also implements POSTing the token to the /authz-info endpoint at the 
 * RS.
 * 
 * Clients are expected to create an instance of this class when the want to
 * perform token requests from a specific AS.
 * 
 * @author Ludwig Seitz and Marco Tiloca
 *
 */
public class DTLSProfileRequests {
    
    /**
     * The logger
     */
    private static final Logger LOGGER 
        = Logger.getLogger(DTLSProfileRequests.class.getName() ); 

    /**
     * Sends a POST request to the /token endpoint of the AS to request an
     * access token. If the DTLS connection uses pre-shared symmetric keys 
     * we will use the key identifier (COSE kid) as psk_identity.
     * 
     * @param asAddr  the full address of the /token endpoint
     *  (including scheme and hostname, and port if not default)
     * @param payload  the payload of the request.  Use the GetToken 
     *  class to construct this payload
     * @param key  the key to be used to secure the connection to the AS. 
     *  This MUST have a kid.
     * 
     * @return  the response 
     *
     * @throws AceException 
     */
    public static CoapResponse getToken(String asAddr, CBORObject payload, 
            OneKey key) throws AceException {
    	Configuration dtlsConfig = Configuration.getStandard();
    	dtlsConfig.set(DtlsConfig.DTLS_USE_SERVER_NAME_INDICATION,  false);
    	dtlsConfig.set(DtlsConfig.DTLS_CLIENT_AUTHENTICATION_MODE, CertificateAuthenticationMode.NEEDED);

        CBORObject type = key.get(KeyKeys.KeyType);
    	if (type.equals(KeyKeys.KeyType_Octet)) {
        	dtlsConfig.set(DtlsConfig.DTLS_CIPHER_SUITES, Collections.singletonList(CipherSuite.TLS_PSK_WITH_AES_128_CCM_8));    		
    	} else if (type.equals(KeyKeys.KeyType_EC2) || type.equals(KeyKeys.KeyType_OKP)) {
    		dtlsConfig.set(DtlsConfig.DTLS_CIPHER_SUITES, Collections.singletonList(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8));
    	}
    	
        DtlsConnectorConfig.Builder builder
            = new DtlsConnectorConfig.Builder(dtlsConfig).setAddress(
                    new InetSocketAddress(0));

        if (type.equals(KeyKeys.KeyType_Octet)) {
            String keyId = new String(
                    key.get(KeyKeys.KeyId).GetByteString(),
                    Constants.charset);
            AdvancedMultiPskStore pskStore = new AdvancedMultiPskStore();
            pskStore.setKey(keyId, key.get(KeyKeys.Octet_K).GetByteString());
            builder.setAdvancedPskStore(pskStore);
        } else if (type.equals(KeyKeys.KeyType_EC2) || type.equals(KeyKeys.KeyType_OKP)){
            try {
                builder.setCertificateIdentityProvider(
                        new SingleCertificateProvider(key.AsPrivateKey(), key.AsPublicKey()));
            } catch (CoseException e) {
                LOGGER.severe("Failed to transform key: " + e.getMessage());
                throw new AceException(e.getMessage());
            }
        } else {
            LOGGER.severe("Unknwon key type used for getting a token");
            throw new AceException("Unknown key type");
        }

        DTLSConnector dtlsConnector = new DTLSConnector(builder.build());      
        CoapEndpoint ep = new CoapEndpoint.Builder()
                .setConnector(dtlsConnector)
                .setConfiguration(Configuration.getStandard())
                .build();
        CoapClient client = new CoapClient(asAddr);
        client.setEndpoint(ep);
        try {
            dtlsConnector.start();
        } catch (IOException e) {
            LOGGER.severe("Failed to start DTLSConnector: " + e.getMessage());
            throw new AceException(e.getMessage());
        }
        try {
            return client.post(
                    payload.EncodeToBytes(), 
                    Constants.APPLICATION_ACE_CBOR);
        } catch (ConnectorException | IOException e) {
            LOGGER.severe("DTLSConnector error: " + e.getMessage());
            throw new AceException(e.getMessage());
        }
    }
    
    /**
     * Sends a POST request to the /authz-info endpoint of the RS to submit an
     * access token.
     * 
     * @param rsAddr  the full address of the /authz-info endpoint
     *  (including scheme and hostname, and port if not default)
     * @param payload  the token received from the getToken() method
     * @param key  an asymmetric key-pair to use with DTLS in a raw-public 
     *  key handshake
     * 
     * @return  the response 
     *
     * @throws AceException 
     */
    public static CoapResponse postToken(String rsAddr, CBORObject payload, OneKey key) throws AceException {
        if (payload == null) {
            throw new AceException(
                    "Payload cannot be null when POSTing to authz-info");
        }
        Connector c = null;
        if (key != null) {
        	Configuration dtlsConfig = Configuration.getStandard();
        	dtlsConfig.set(DtlsConfig.DTLS_USE_SERVER_NAME_INDICATION,  false);
        	dtlsConfig.set(DtlsConfig.DTLS_CIPHER_SUITES, Collections.singletonList(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8));
        	dtlsConfig.set(DtlsConfig.DTLS_CLIENT_AUTHENTICATION_MODE, CertificateAuthenticationMode.NEEDED);
            DtlsConnectorConfig.Builder builder 
                = new DtlsConnectorConfig.Builder(dtlsConfig).setAddress(
                        new InetSocketAddress(0));

            try {
                builder.setCertificateIdentityProvider(
                        new SingleCertificateProvider(key.AsPrivateKey(), key.AsPublicKey()));
            } catch (CoseException e) {
                LOGGER.severe("Key is invalid: " + e.getMessage());
               throw new AceException("Aborting, key invalid: " 
                       + e.getMessage());
            }

            ArrayList<CertificateType> certTypes = new ArrayList<CertificateType>();
            certTypes.add(CertificateType.RAW_PUBLIC_KEY);
            AsyncNewAdvancedCertificateVerifier verifier = new AsyncNewAdvancedCertificateVerifier(
                    new X509Certificate[0], new RawPublicKeyIdentity[0], certTypes);
            builder.setAdvancedCertificateVerifier(verifier);

            c = new DTLSConnector(builder.build());
        } else {
            c = new UDPConnector(new InetSocketAddress(0), Configuration.getStandard());
        }
        CoapEndpoint e = new CoapEndpoint.Builder().setConnector(c)
                .setConfiguration(Configuration.getStandard()).build();
        CoapClient client = new CoapClient(rsAddr);
        client.setEndpoint(e);   
        try {
            e.start();
        } catch (IOException ex) {
            LOGGER.severe("Failed to start DTLSConnector: " + ex.getMessage());
            throw new AceException(ex.getMessage());
        }
               LOGGER.finest("Sending request payload: " + payload);
        CoapResponse r = null;
        try {
            r = client.post(
                    payload.EncodeToBytes(), 
                    Constants.APPLICATION_ACE_CBOR);
        } catch (ConnectorException | IOException ex) {
            LOGGER.severe("DTLSConnector error: " + ex.getMessage());
            throw new AceException(ex.getMessage());
        }
        e.stop();
        return r;
    }
    
    /**
     * Sends a POST request to the /authz-info endpoint of the RS to submit an
     * access token for updating access rights.
     * 
     * @param rsAddr  the full address of the /authz-info endpoint
     *  (including scheme and hostname, and port if not default)
     * @param payload  the token received from the getToken() method
     * @param key  an asymmetric key-pair to use with DTLS in a raw-public 
     *  key handshake
     * 
     * @return  the response 
     *
     * @throws AceException 
     */
    public static CoapResponse postTokenUpdate(String rsAddr, CBORObject payload, CoapClient c) throws AceException {
        if (payload == null) {
            throw new AceException(
                    "Payload cannot be null when POSTing to authz-info");
        }

        //Submit the new token
        c.setURI(rsAddr);
        CoapResponse tokenPostResp = null;
        try {
        	tokenPostResp = c.post(payload.EncodeToBytes(), Constants.APPLICATION_ACE_CBOR);
        } catch (ConnectorException | IOException ex) {
            LOGGER.severe("DTLSConnector error: " + ex.getMessage());
            throw new AceException(ex.getMessage());
        }
        
        return tokenPostResp;
    }
        
    /**
     * Generates a Coap client for sending requests to an RS that will pass the
     *  access token through psk-identity in the DTLS handshake.
     * @param serverAddress  the address of the server and resource this client
     *  should talk to
     * @param token  the access token this client should use towards the server
     * @param key  the pre-shared key for use with this server.
     * 
     * @return  a CoAP client configured to pass the access token through the
     *  psk-identity in the handshake 
     */
    public static CoapClient getPskClient(InetSocketAddress serverAddress,
            CBORObject token, OneKey key) {
        if (serverAddress == null || serverAddress.getHostString() == null) {
            throw new IllegalArgumentException(
                    "Client requires a non-null server address");
        }
        if (token == null) {
            throw new IllegalArgumentException(
                    "PSK client requires a non-null access token");
        }
        if (key == null || key.get(KeyKeys.Octet_K) == null) {
            throw new IllegalArgumentException(
                    "PSK  client requires a non-null symmetric key");
        }
        
        Configuration dtlsConfig = Configuration.getStandard();
        dtlsConfig.set(DtlsConfig.DTLS_USE_SERVER_NAME_INDICATION,  false);
        dtlsConfig.set(DtlsConfig.DTLS_CIPHER_SUITES, Collections.singletonList(CipherSuite.TLS_PSK_WITH_AES_128_CCM_8));
        dtlsConfig.set(DtlsConfig.DTLS_SIGNATURE_AND_HASH_ALGORITHMS, null);
        dtlsConfig.set(DtlsConfig.DTLS_CURVES, null);
    	
        DtlsConnectorConfig.Builder builder 
            = new DtlsConnectorConfig.Builder(dtlsConfig).setAddress(
                    new InetSocketAddress(0));
        
        AdvancedMultiPskStore store = new AdvancedMultiPskStore();
        
        LOGGER.finest("Adding key for: " + serverAddress.toString());
        
        byte[] identityBytes = token.EncodeToBytes();
        String identityStr = Base64.getEncoder().encodeToString(identityBytes);
        PskPublicInformation pskInfo = new PskPublicInformation(identityStr, identityBytes);
        store.addKnownPeer(serverAddress, pskInfo, key.get(KeyKeys.Octet_K).GetByteString());
                
        builder.setAdvancedPskStore(store);
        Connector c = new DTLSConnector(builder.build());
        CoapEndpoint e = new CoapEndpoint.Builder().setConnector(c)
                .setConfiguration(Configuration.getStandard()).build();
        CoapClient client = new CoapClient(serverAddress.getHostString());
        client.setEndpoint(e);   

        return client;    
    }
    
    
    /**
     * Generates a Coap client for sending requests to an RS that will use
     * a symmetric PoP key to connect to the server.
     * 
     * @param serverAddress  the address of the server and resource this client
     *  should talk to
     * @param kid  the kid that the client should use as PSK in the handshake
     * @param key  the pre-shared key for use with this server.
     * 
     * @return  a CoAP client configured to pass the access token through the
     *  psk-identity in the
     *  handshake 
     */
    public static CoapClient getPskClient(InetSocketAddress serverAddress,
            byte[] kid, OneKey key) {
        if (serverAddress == null || serverAddress.getHostString() == null) {
            throw new IllegalArgumentException(
                    "Client requires a non-null server address");
        }
        if (kid == null) {
            throw new IllegalArgumentException(
                    "PSK client requires a non-null kid");
        }
        if (key == null || key.get(KeyKeys.Octet_K) == null) {
            throw new IllegalArgumentException(
                    "PSK  client requires a non-null symmetric key");
        }
        
        Configuration dtlsConfig = Configuration.getStandard();
        dtlsConfig.set(DtlsConfig.DTLS_USE_SERVER_NAME_INDICATION, false);
        dtlsConfig.set(DtlsConfig.DTLS_CIPHER_SUITES, Arrays.asList(CipherSuite.TLS_PSK_WITH_AES_128_CCM_8));
        dtlsConfig.set(DtlsConfig.DTLS_SIGNATURE_AND_HASH_ALGORITHMS, null);
        dtlsConfig.set(DtlsConfig.DTLS_CURVES, null);
        
        DtlsConnectorConfig.Builder builder 
            = new DtlsConnectorConfig.Builder(dtlsConfig).setAddress(
                new InetSocketAddress(0));
        
        AdvancedMultiPskStore store = new AdvancedMultiPskStore();

        LOGGER.finest("Adding key for: " + serverAddress.toString());
        
        byte[] identityBytes = Util.buildDtlsPskIdentity(kid);
        String identityStr = Base64.getEncoder().encodeToString(identityBytes);
        PskPublicInformation pskInfo = new PskPublicInformation(identityStr, identityBytes);
        store.addKnownPeer(serverAddress, pskInfo, key.get(KeyKeys.Octet_K).GetByteString());
        
        builder.setAdvancedPskStore(store);
        Connector c = new DTLSConnector(builder.build());
        CoapEndpoint e = new CoapEndpoint.Builder().
                setConfiguration(Configuration.getStandard()).setConnector(c).build();
        CoapClient client = new CoapClient(serverAddress.getHostString());
        client.setEndpoint(e);   

        return client;    
    }
    
    /**
     * Generates a Coap client for sending requests to an RS that will use
     * a raw public key to connect to the server.
     * 
     * @param clientKey  the raw asymmetric key of the client
     * @param rsPublicKey  the raw public key of the RS
     * @return   the CoAP client
     * @throws CoseException 
     */
    public static CoapClient getRpkClient(OneKey clientKey, OneKey rsPublicKey) 
            throws CoseException {
    	
        Configuration dtlsConfig = Configuration.getStandard();
        dtlsConfig.set(DtlsConfig.DTLS_USE_SERVER_NAME_INDICATION,  false);
        dtlsConfig.set(DtlsConfig.DTLS_CIPHER_SUITES, Collections.singletonList(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8));
    	
        DtlsConnectorConfig.Builder builder 
            = new DtlsConnectorConfig.Builder(dtlsConfig).setAddress(
                    new InetSocketAddress(0));

        builder.setCertificateIdentityProvider(
                new SingleCertificateProvider(clientKey.AsPrivateKey(), clientKey.AsPublicKey()));
        if (rsPublicKey != null) {

            RawPublicKeyIdentity[] identities = new RawPublicKeyIdentity[1];
            identities[0] = new RawPublicKeyIdentity(rsPublicKey.AsPublicKey());
            AsyncNewAdvancedCertificateVerifier verifier = new AsyncNewAdvancedCertificateVerifier(
                    new X509Certificate[0], identities, null);

            builder.setAdvancedCertificateVerifier(verifier);
        }
        
        Connector c = new DTLSConnector(builder.build());
        CoapEndpoint e = new CoapEndpoint.Builder().setConnector(c)
                .setConfiguration(Configuration.getStandard()).build();
        CoapClient client = new CoapClient();
        client.setEndpoint(e);   
        
        return client;    
    }    
}