More refactoring for scannerAPI

android-scanner
Tal Moran 2017-06-25 10:15:09 +03:00
parent 7e4260b8d5
commit cb1d2343c4
15 changed files with 296 additions and 100 deletions

View File

@ -2,7 +2,6 @@ package meerkat.pollingstation;
import com.google.common.util.concurrent.FutureCallback;
import meerkat.pollingstation.controller.PollingStationControllerInterface;
import meerkat.pollingstation.controller.callbacks.ScanDataCallback;
import meerkat.pollingstation.controller.commands.PollingStationCommand;
import meerkat.pollingstation.controller.commands.ReceivedScanCommand;
import meerkat.protobuf.PollingStation;
@ -26,7 +25,7 @@ public class PollingStationMainController implements PollingStationControllerInt
}
@Override
public void init(PollingStationScanner.Consumer server) {
public void init(PollingStationScanner.PollingStationServer server) {
server.subscribe(new FutureCallback<PollingStation.ScannedData>() {
@Override
public void onSuccess(PollingStation.ScannedData result) {

View File

@ -6,7 +6,8 @@ package meerkat.pollingstation;
import com.google.common.util.concurrent.FutureCallback;
import com.google.protobuf.BoolValue;
import meerkat.protobuf.Crypto;
import meerkat.crypto.DigitalSignature;
import meerkat.crypto.concrete.ECDSASignature;
import meerkat.protobuf.PollingStation;
import javax.annotation.PostConstruct;
@ -16,24 +17,25 @@ import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import java.io.IOException;
import static meerkat.pollingstation.PollingStationConstants.POLLING_STATION_WEB_SCANNER_CONNECT_PATH;
import static meerkat.pollingstation.PollingStationConstants.POLLING_STATION_WEB_SCANNER_ERROR_PATH;
import static meerkat.pollingstation.PollingStationConstants.POLLING_STATION_WEB_SCANNER_SCAN_PATH;
import static meerkat.rest.Constants.MEDIATYPE_PROTOBUF;
/**
* Implements a Web-App interface for {@link meerkat.pollingstation.PollingStationScanner.Producer}
* Implements a Web-App interface for {@link PollingStationScanner.ScannerClient}
* This class depends on {@link meerkat.pollingstation.PollingStationWebScanner} and works in conjunction with it
*/
@Path("/")
public class PollingStationScannerWebApp implements PollingStationScanner.Producer {
public class PollingStationScannerWebApp implements PollingStationScanner.PollingStationWebAPI {
@Context
ServletContext servletContext;
Iterable<FutureCallback<PollingStation.ScannedData>> callbacks;
final static BoolValue trueValue = BoolValue.newBuilder().setValue(true).build();
final static BoolValue falseValue = BoolValue.newBuilder().setValue(false).build();
PollingStationWebScanner.ScanRequestHandler requestHandler;
/**
* This method is called by the Jetty engine when instantiating the servlet
@ -44,7 +46,7 @@ public class PollingStationScannerWebApp implements PollingStationScanner.Produc
Object context = servletContext.getAttribute(PollingStationWebScanner.CALLBACKS_ATTRIBUTE_NAME);
try {
callbacks = (Iterable<FutureCallback<PollingStation.ScannedData>>) context;
requestHandler = (PollingStationWebScanner.ScanRequestHandler) context;
} catch (ClassCastException e) {
throw e;
}
@ -57,7 +59,7 @@ public class PollingStationScannerWebApp implements PollingStationScanner.Produc
@Produces(MEDIATYPE_PROTOBUF)
@Override
public BoolValue connect(PollingStation.ConnectionClientData clientData) {
return null;
return requestHandler.handleConnection(clientData) ? trueValue : falseValue;
}
@POST
@ -65,22 +67,8 @@ public class PollingStationScannerWebApp implements PollingStationScanner.Produc
@Consumes(MEDIATYPE_PROTOBUF)
@Produces(MEDIATYPE_PROTOBUF)
@Override
public BoolValue newScan(PollingStation.ScannedData scannedData) {
boolean handled = false;
// TODO:
for (FutureCallback<PollingStation.ScannedData> callback : callbacks){
callback.onSuccess(scannedData);
handled = true;
}
return BoolValue.newBuilder()
.setValue(handled)
.build();
public BoolValue newScan(PollingStation.SignedScannedData scannerData) {
return requestHandler.handleRequest(scannerData) ? trueValue : falseValue;
}
}

View File

@ -10,7 +10,7 @@ import org.slf4j.LoggerFactory;
public class PollingStationToyRun {
final Logger logger = LoggerFactory.getLogger(getClass());
private static PollingStationScanner.Consumer scanner;
private static PollingStationScanner.PollingStationServer scanner;
private static final String CONTEXT_PATH = "/scan";
private static final int PORT = 8080;

View File

@ -1,10 +1,16 @@
package meerkat.pollingstation;
import java.security.InvalidKeyException;
import java.security.SecureRandom;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.util.List;
import java.util.LinkedList;
import com.google.common.util.concurrent.FutureCallback;
import com.google.protobuf.ByteString;
import meerkat.crypto.DigitalSignature;
import meerkat.crypto.concrete.ECDSASignature;
import meerkat.protobuf.PollingStation;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.*;
@ -14,28 +20,128 @@ import org.glassfish.jersey.server.ResourceConfig;
import meerkat.protobuf.PollingStation.ScannedData;
import meerkat.rest.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Created by Arbel on 05/05/2016.
*/
public class PollingStationWebScanner implements PollingStationScanner.Consumer {
public class PollingStationWebScanner implements PollingStationScanner.PollingStationServer {
public final static String CALLBACKS_ATTRIBUTE_NAME = "controller";
final Logger logger = LoggerFactory.getLogger(getClass());
/**
* Expected serial number for next scan
*/
long expectedSerial;
/**
* Should a newly-connected scanner be verified using the nonce?
*/
boolean verifyNonce;
/**
* Should every scan's signature be verified?
*/
boolean verifySignatures;
/**
* The nonce we use to verify a new scanner connection.
*/
long nonce;
/**
*
*/
DigitalSignature verifier;
private final Server server;
private final List<FutureCallback<ScannedData>> callbacks;
boolean verifyScanner;
byte nonce[];
public class ScanRequestHandler {
private ScanRequestHandler() { }
boolean handleConnection(PollingStation.ConnectionClientData data) {
if (verifyNonce) {
if (data.getNonce() != nonce) {
logger.warn("Received scanner connection with bad nonce!");
return false;
}
}
if (verifySignatures) {
try {
if (data.getIdCase() != PollingStation.ConnectionClientData.IdCase.SCANNERPK)
throw new CertificateException("Connection request doesn't include certificate!");
verifier.loadVerificationCertificate(data.getScannerPK());
} catch (CertificateException e) {
logger.warn("Scanner connection with bad certificate: {}", e);
return false;
}
}
return true;
}
boolean handleRequest(PollingStation.SignedScannedData data) {
ScannedData scannedData = data.getData();
if (verifySignatures) {
ByteString scannerID = scannedData.getScannerId();
if (!scannerID.equals(verifier.getSignerID())) {
logger.warn("Scanner ID doesn't match connection public key");
return false;
}
try {
verifier.initVerify(data.getScannerSig());
verifier.updateContent(scannedData);
if (!verifier.verify()) {
logger.warn("Bad Signature");
return false;
}
} catch (CertificateException e) {
logger.warn("Certificate Exception: {}", e);
return false;
} catch (InvalidKeyException e) {
logger.warn("InvalidKey Exception: {}", e);
return false;
} catch (SignatureException e) {
logger.warn("Signature Exception: {}", e);
return false;
}
}
for (FutureCallback<ScannedData> callback : callbacks) {
callback.onSuccess(scannedData);
}
return true;
}
}
ScanRequestHandler scanRequestHandler;
public PollingStationWebScanner(int port, String contextPath) {
scanRequestHandler = new ScanRequestHandler();
callbacks = new LinkedList<>();
verifier = new ECDSASignature();
server = new Server(port);
ServletContextHandler servletContextHandler = new ServletContextHandler(server, contextPath);
servletContextHandler.setAttribute(CALLBACKS_ATTRIBUTE_NAME, (Iterable<FutureCallback<ScannedData>>) callbacks);
servletContextHandler.setAttribute(CALLBACKS_ATTRIBUTE_NAME, callbacks);
ResourceConfig resourceConfig = new ResourceConfig(PollingStationScannerWebApp.class);
resourceConfig.register(ProtobufMessageBodyReader.class);
@ -50,10 +156,8 @@ public class PollingStationWebScanner implements PollingStationScanner.Consumer
@Override
public PollingStation.ConnectionServerData start(boolean verifyScanner) throws Exception {
this.verifyScanner = verifyScanner;
nonce = new byte[32];
new SecureRandom().nextBytes(nonce);
this.verifyNonce = this.verifySignatures = verifyScanner;
nonce = new SecureRandom().nextLong();
server.start();
return null;

View File

@ -15,8 +15,6 @@ import org.eclipse.jetty.servlet.ServletHolder;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.servlet.ServletContainer;
import java.nio.ByteBuffer;
import java.time.Instant;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;
@ -24,7 +22,7 @@ import java.util.concurrent.LinkedBlockingQueue;
/**
* Created by Laura on 3/20/2017.
*/
public class ReceiverScanHandler implements PollingStationScanner.Consumer, Runnable {
public class ReceiverScanHandler implements PollingStationScanner.PollingStationServer, Runnable {
public final static String CALLBACKS_ATTRIBUTE_NAME = "controller";

View File

@ -1,6 +1,5 @@
package meerkat.pollingstation;
import com.google.common.util.concurrent.FutureCallback;
import com.google.protobuf.BoolValue;
import meerkat.pollingstation.controller.callbacks.ScanCallback;
import meerkat.pollingstation.controller.callbacks.ScanDataCallback;
@ -23,11 +22,11 @@ import static meerkat.pollingstation.PollingStationConstants.POLLING_STATION_WEB
import static meerkat.rest.Constants.MEDIATYPE_PROTOBUF;
/**
* Implements a Web-App interface for {@link meerkat.pollingstation.PollingStationScanner.Producer}
* Implements a Web-App interface for {@link PollingStationScanner.ScannerClient}
* This class depends on {@link meerkat.pollingstation.ReceiverScanHandler} and works in conjunction with it
*/
@Path("/")
public class ReceiverWebAPI implements PollingStationScanner.Producer {
public class ReceiverWebAPI implements PollingStationScanner.ScannerClient {
@Context
ServletContext servletContext;

View File

@ -11,7 +11,7 @@ public interface PollingStationControllerInterface extends Runnable {
* (see VotingBoothController)
*/
// TODO: 4/16/2017 complete with proper arguments and exceptions
public void init(PollingStationScanner.Consumer server);
public void init(PollingStationScanner.PollingStationServer server);
/**
* an asynchronous call from Admin Console (If there is such one implemented) to shut down the system

View File

@ -27,7 +27,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
*/
public class PollingStationWebScannerTest {
private PollingStationScanner.Consumer scanner;
private PollingStationScanner.PollingStationServer scanner;
private static final String ADDRESS = "http://localhost";
private static final String SUB_ADDRESS = "";
private static final int PORT = 8080;

View File

@ -27,7 +27,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
*/
public class ReceiverTest {
private PollingStationScanner.Consumer scanner;
private PollingStationScanner.PollingStationServer scanner;
private static final String ADDRESS = "http://localhost";
private static final String SUB_ADDRESS = "";
private static final int PORT = 8080;

View File

@ -5,20 +5,12 @@ import com.google.protobuf.ByteString;
import meerkat.protobuf.PollingStation;
import meerkat.protobuf.PollingStation.ErrorMsg;
import meerkat.protobuf.PollingStation.ScannedData;
import meerkat.rest.Constants;
import meerkat.rest.ProtobufMessageBodyReader;
import meerkat.rest.ProtobufMessageBodyWriter;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response;
import java.util.concurrent.Semaphore;
import static meerkat.pollingstation.PollingStationConstants.POLLING_STATION_WEB_SCANNER_ERROR_PATH;
@ -28,7 +20,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
public class Receiver_ClientTest {
final Logger logger = LoggerFactory.getLogger(getClass());
private PollingStationScanner.Consumer scanner;
private PollingStationScanner.PollingStationServer scanner;
private static final String CONTEXT_PATH = "/scan";
private Semaphore semaphore;
@ -94,7 +86,7 @@ public class Receiver_ClientTest {
scanner.subscribe(new ScanHandler(scannedData));
PollingStationScanner.Producer scannerClient = new ScannerClientAPI()
PollingStationScanner.ScannerClient scannerClient = new ScannerClientAPI()
ScannerClientAPI(PSC_URL + POLLING_STATION_WEB_SCANNER_SCAN_PATH);
scannerClient.newScan(scannedData, null);

View File

@ -15,7 +15,7 @@ public interface PollingStationScanner {
/**
* An interface for processing scans (Polling Station side)
*/
public interface Consumer {
public interface PollingStationServer {
/**
* Sets up the connection to the scanner and begins receiving scans.
@ -40,10 +40,11 @@ public interface PollingStationScanner {
public void subscribe(FutureCallback<ScannedData> scanCallback);
}
/**
* An interface for submitting scanned data (scanner side)
* An interface for handling the WebAPI (on the polling-station side)
*/
public interface Producer {
public interface PollingStationWebAPI {
/**
* Connect to the polling station server.
@ -54,11 +55,43 @@ public interface PollingStationScanner {
/**
* Sends a scan to all subscribers
* @param scannedData contains the scanned data
* @return a BoolValue containing TRUE iff the scanned data has been sent to at least one subscriber
* @param scannedData contains the scanned data.
* This can be either a ScannedBallot or a ScanError;
* On the client side, this method can be called without setting
* the signature field.
* @return true iff the scanned data has been sent to at least one subscriber
*/
public BoolValue newScan(ScannedData scannedData);
public BoolValue newScan(SignedScannedData scannedData);
}
/**
* An interface for submitting scanned data (scanner side)
*/
public interface ScannerClient {
/**
* Connect to the polling station server.
* The client data contains the scanner ID and optional verification data.
* @return
*/
public boolean connect(ConnectionServerData serverData);
/**
* Sends a scan to all subscribers
* @param scannedBallot contains the scanned data.
* This can be either a ScannedBallot or a ScanError;
* @return true iff the scanned data has been sent to at least one subscriber
*/
public boolean newScan(ScannedBallot scannedBallot);
/**
* Report a scan error to all subscribers
* @param scanError
* @return
*/
public boolean reportError(ScanError scanError);
}
}

View File

@ -1,64 +1,137 @@
package meerkat.pollingstation;
import com.google.protobuf.BoolValue;
import meerkat.protobuf.Crypto;
import com.google.protobuf.ByteString;
import meerkat.crypto.DigitalSignatureGenerator;
import meerkat.protobuf.PollingStation;
import meerkat.rest.Constants;
import meerkat.rest.ProtobufMessageBodyReader;
import meerkat.rest.ProtobufMessageBodyWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Response;
import java.security.SignatureException;
/**
* Created by Laura on 3/20/2017.
*/
public class ScannerClientAPI implements PollingStationScanner.Producer {
private Client client;
private WebTarget webTarget;
public ScannerClientAPI(String url) {
client = ClientBuilder.newClient();
client.register(ProtobufMessageBodyReader.class);
client.register(ProtobufMessageBodyWriter.class);
webTarget = client.target(url);
}
public class ScannerClientAPI implements PollingStationScanner.ScannerClient {
final Logger logger = LoggerFactory.getLogger(getClass());
DigitalSignatureGenerator signer;
ByteString scannerID;
Client client;
WebTarget webTarget;
/**
* Sends the scanned data to the polling station server
* @param scannedData
* @return boolean value: true if the data was sent successfully, false otherwise
* Serial number of request (zeroed on new connection to server)
*/
@Override
public BoolValue newScan(PollingStation.ScannedData scannedData) {
long serial;
Response response = webTarget.request(Constants.MEDIATYPE_PROTOBUF).post(Entity.entity(scannedData, Constants.MEDIATYPE_PROTOBUF),
);
BoolValue res = response.readEntity(BoolValue.class);
response.close();
return res.getValue();
protected ScannerClientAPI() {
this.signer = null;
this.scannerID = null;
this.client = ClientBuilder.newClient();
this.client.register(ProtobufMessageBodyReader.class);
this.client.register(ProtobufMessageBodyWriter.class);
}
/**
* Constructor for client that doesn't use signatures.
* @param scannerId
*/
public ScannerClientAPI(byte[] scannerId) {
this();
ByteString.copyFrom(scannerId);
}
/**
* Constructor for client that signs messages.
* @param signer
*/
public ScannerClientAPI(DigitalSignatureGenerator signer) {
this();
assert (signer != null && signer.getSignerID() != null); // Use the default constructor instead
this.signer = signer;
this.scannerID = signer.getSignerID();
}
public void close() {
client.close();
}
@Override
public BoolValue connect(PollingStation.ConnectionClientData clientData) {
return null;
public boolean connect(PollingStation.ConnectionServerData serverData) {
serial = 0;
webTarget = client.target(serverData.getServerUrl());
PollingStation.ConnectionClientData.Builder clientData = PollingStation.ConnectionClientData.newBuilder()
.setNonce(serverData.getNonce());
if (signer != null) {
clientData.setScannerPK(signer.getSignerPublicKey());
} else {
clientData.setScannerId(scannerID);
}
Response response = webTarget.request(Constants.MEDIATYPE_PROTOBUF)
.post(Entity.entity(clientData.build(), Constants.MEDIATYPE_PROTOBUF));
BoolValue res = response.readEntity(BoolValue.class);
response.close();
return res.getValue();
}
return null;
boolean sendScannedData(PollingStation.ScannedData.Builder scannedData) {
scannedData.setSerial(serial++)
.setScannerId(scannerID);
PollingStation.SignedScannedData.Builder signedDataBuilder = PollingStation.SignedScannedData.newBuilder()
.setData(scannedData);
if (signer != null) {
try {
signer.updateContent(scannedData.build());
signedDataBuilder.setScannerSig(signer.sign());
} catch (SignatureException e) {
throw new RuntimeException(e);
}
}
Response response = webTarget.request(Constants.MEDIATYPE_PROTOBUF)
.post(Entity.entity(signedDataBuilder.build(), Constants.MEDIATYPE_PROTOBUF));
BoolValue res = response.readEntity(BoolValue.class);
response.close();
return res.getValue();
}
/**
* Sends the scanned data to the polling station server
* @param scannedBallot
* @return boolean value: true if the data was sent successfully, false otherwise
*/
@Override
public boolean newScan(PollingStation.ScannedBallot scannedBallot) {
PollingStation.ScannedData.Builder scannedData = PollingStation.ScannedData.newBuilder()
.setBallot(scannedBallot);
return sendScannedData(scannedData);
}
@Override
public BoolValue reportScanError(PollingStation.ErrorMsg errorMsg, Crypto.Signature errorMsgSignature) {
return null;
public boolean reportError(PollingStation.ScanError scanError) {
PollingStation.ScannedData.Builder scannedData = PollingStation.ScannedData.newBuilder()
.setError(scanError);
return sendScannedData(scannedData);
}
}

View File

@ -8,27 +8,32 @@ import "meerkat/crypto.proto";
import "meerkat/voting.proto";
// Data sent (out-of-band) by the server that the client will require to set up a connection
message ConnectionServerData {
// Web-API URL for the server
string server_url = 1;
// random nonce used for verification of the client.
bytes nonce = 2;
uint64 nonce = 2;
}
// Data sent by the client in order to set up a connection
message ConnectionClientData {
// Key used to identify the client
SignatureVerificationKey scannerPK = 1;
oneof id {
// Unique ID of the scanner (if not using signatures)
bytes scannerId = 1;
// Public Key used to sign future reports
// If this is set, the ID is the certificate fingerprint.
SignatureVerificationKey scannerPK = 2;
}
// Nonce sent by connection-server
bytes nonce = 2;
// (used to verify pairing)
uint64 nonce = 3;
}
message ScannedBallot {
bytes channel = 1;
@ -41,6 +46,7 @@ message ScanError {
string msg = 1;
}
// Container for scanned data
message ScannedData {
oneof data {
@ -49,6 +55,10 @@ message ScannedData {
}
uint64 serial = 4; // Serial number of the message
bytes scannerId = 5; // hash of the scannerPK
Signature scannerSig = 6; // signature on the serialzed form of data.
}
message SignedScannedData {
ScannedData data = 1;
Signature scannerSig = 2; // signature on the serialzed form of data.
}

View File

@ -12,7 +12,7 @@ import java.util.concurrent.Semaphore;
*/
public class PollingStationServerToyRun {
private PollingStationScanner.Consumer scanner;
private PollingStationScanner.PollingStationServer scanner;
private static final String ADDRESS = "http://localhost";
private static final String SUB_ADDRESS = "";
private static final int PORT = 8080;

View File

@ -32,7 +32,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
public class NetworkVirtualPrinterTest {
private PollingStationScanner.Consumer scanner;
private PollingStationScanner.PollingStationServer scanner;
private static final String ADDRESS = "http://localhost";
private static final String SUB_ADDRESS = "";
private static final int PORT = 8080;