Skip to content

Commit

Permalink
first successful canonicalization and signature verification
Browse files Browse the repository at this point in the history
  • Loading branch information
artoonie committed Oct 19, 2023
1 parent 07b2da6 commit 0875149
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 22 deletions.
5 changes: 5 additions & 0 deletions .idea/jarRepositories.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ jlink {
'--strip-debug', '--compress', '2',
'--no-header-files', '--no-man-pages'
mergedModule {
requires "java.xml"
//requires "java.xml"

}
launcher {
// TODO Sync version number with release.yml and Main.java (github.com/BrightSpots/rcv/issues/662)
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
requires org.apache.commons.csv;
requires org.apache.poi.ooxml;
requires commons.cli;
// enable reflexive calls from network.brightspots.rcv into javafx.fxml
requires java.xml.crypto;
// enable reflexive calls from network.brightspots.rcv into javafx.fxml
opens network.brightspots.rcv;
// our main module
exports network.brightspots.rcv;
Expand Down
16 changes: 10 additions & 6 deletions src/main/java/network/brightspots/rcv/CryptographyXMLParsers.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public static class HartSignature {
@JacksonXmlProperty(localName = "SignedInfo")
SignedInfo signedInfo;

@JacksonXmlProperty(localName = "SignatureValue")
@JacksonXmlProperty(isAttribute = true, localName = "SignatureValue")
String signatureValue;

@JacksonXmlProperty(localName = "KeyInfo")
Expand All @@ -41,15 +41,19 @@ static class SignedInfo {

@JacksonXmlProperty(localName = "Reference")
Reference reference;

// Required to match Hart's implementation of canonicalization
@JacksonXmlProperty(isAttribute = true, localName = "xmlns")
String xmlns = "http://www.w3.org/2000/09/xmldsig#";
}

static class CanonicalizationMethod {
@JacksonXmlProperty(isAttribute = true)
@JacksonXmlProperty(isAttribute = true, localName = "Algorithm")
String algorithm;
}

static class SignatureMethod {
@JacksonXmlProperty(isAttribute = true)
@JacksonXmlProperty(isAttribute = true, localName = "Algorithm")
String algorithm;
}

Expand All @@ -67,7 +71,7 @@ static class Reference {
}

static class DigestMethod {
@JacksonXmlProperty(isAttribute = true)
@JacksonXmlProperty(isAttribute = true, localName = "Algorithm")
String algorithm;
}

Expand All @@ -82,10 +86,10 @@ static class KeyValue {
}

static class RSAKeyValue {
@JacksonXmlProperty(localName = "Modulus")
@JacksonXmlProperty(isAttribute = true, localName = "Modulus")
String modulus;

@JacksonXmlProperty(localName = "Exponent")
@JacksonXmlProperty(isAttribute = true, localName = "Exponent")
String exponent;
}
}
160 changes: 148 additions & 12 deletions src/main/java/network/brightspots/rcv/FileUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,52 @@

package network.brightspots.rcv;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

import javax.xml.crypto.Data;
import javax.xml.crypto.NodeSetData;
import javax.xml.crypto.OctetStreamData;
import javax.xml.crypto.XMLCryptoContext;
import javax.xml.crypto.dom.DOMCryptoContext;
import javax.xml.crypto.dsig.CanonicalizationMethod;
import javax.xml.crypto.dsig.TransformException;
import javax.xml.crypto.dsig.XMLSignatureFactory;
import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import static network.brightspots.rcv.CryptographyXMLParsers.HartSignature;
import static network.brightspots.rcv.CryptographyXMLParsers.RSAKeyValue;
import static network.brightspots.rcv.Utils.isNullOrBlank;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.math.BigInteger;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.DigestInputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
Expand Down Expand Up @@ -80,8 +112,37 @@ static <T> T readFromXML(File xmlFile, Class<T> classType) throws IOException {
}
}

static String tempMergeConflictGetHash(File file, String algorithm) {
MessageDigest digest;
try {
digest = MessageDigest.getInstance(algorithm);
} catch (NoSuchAlgorithmException e) {
Logger.severe("Failed to get algorithm " + algorithm);
return "[hash not available]";
}

try (InputStream is = Files.newInputStream(file.toPath())) {
try (DigestInputStream hashingStream = new DigestInputStream(is, digest)) {
while (hashingStream.readNBytes(1024).length > 0) {
// Read in 1kb chunks -- don't need to do anything in the body here
}
}
} catch (IOException e) {
Logger.severe("Failed to read file: %s", file.getAbsolutePath());
return "[hash not available]";
}

return Base64.getEncoder().encodeToString(digest.digest());
}

@SuppressWarnings("checkstyle:LeftCurly")
static boolean verifyPublicKeySignature(File publicKeyFile, File signatureKeyFile, File dataFile)
throws CouldNotVerifySignatureException {
// try {
// return XmlSignature.validate(signatureKeyFile);
// } catch (Exception e) {
// return false;
// }
// Load the public key and signature from their corresponding files
HartSignature hartSignature;
RSAKeyValue rsaKeyValue;
Expand All @@ -92,19 +153,53 @@ static boolean verifyPublicKeySignature(File publicKeyFile, File signatureKeyFil
throw new CouldNotVerifySignatureException("Failed to read files: " + e.getMessage());
}

EnsurePublicKeyMatchesExpectedValue(hartSignature, rsaKeyValue, signatureKeyFile, publicKeyFile);
EnsureDigestMatchesExpectedValue(dataFile, hartSignature);
EnsureSignatureMatchesExpectedValue(hartSignature, rsaKeyValue, dataFile);

return true;
}

static void EnsureDigestMatchesExpectedValue(File dataFile, HartSignature hartSignature) throws CouldNotVerifySignatureException
{
// Check if the filenames match
String actualFilename = dataFile.getName();
String expectedFilename = new File(hartSignature.signedInfo.reference.URI).getName();
if (!actualFilename.equals(expectedFilename))
{
throw new CouldNotVerifySignatureException(
"Signed file was %s but you provided %s".formatted(actualFilename, expectedFilename));
}

String actualDigest = FileUtils.tempMergeConflictGetHash(dataFile, "SHA-256");
String expectedDigest = hartSignature.signedInfo.reference.digestValue;

if (!actualDigest.equals(expectedDigest))
{
throw new CouldNotVerifySignatureException(
"Signed file had digest %s but the file you provided had %s".formatted(expectedDigest, actualDigest));
}
}

static void EnsurePublicKeyMatchesExpectedValue(HartSignature hartSignature, RSAKeyValue rsaKeyValue, File signatureKeyFile, File publicKeyFile) throws CouldNotVerifySignatureException {
// Sanity check: does the signature file match the known public key file?
// If not, the file may have been signed with a newer or older version than we support.
RSAKeyValue rsaFromFile = hartSignature.keyInfo.keyValue.rsaKeyValue;
if (!rsaFromFile.exponent.equals(rsaKeyValue.exponent)
|| !rsaFromFile.modulus.equals(rsaKeyValue.modulus)) {
rsaFromFile.exponent = rsaFromFile.exponent.trim();
rsaFromFile.modulus = rsaFromFile.modulus.trim();
if (!rsaFromFile.exponent.equals(rsaKeyValue.exponent) || !rsaFromFile.modulus.equals(rsaKeyValue.modulus))
{
throw new CouldNotVerifySignatureException("%s was signed with a different public key than %s"
.formatted(signatureKeyFile.getAbsolutePath(), publicKeyFile.getAbsolutePath()));
}
}

static void EnsureSignatureMatchesExpectedValue(HartSignature hartSignature, RSAKeyValue rsaKeyValue, File dataFile) throws CouldNotVerifySignatureException
{
// Decode Base64
byte[] modulusBytes = Base64.getDecoder().decode(rsaKeyValue.modulus);
byte[] exponentBytes = Base64.getDecoder().decode(rsaKeyValue.exponent);
byte[] signatureBytes = Base64.getDecoder().decode(hartSignature.signatureValue);
byte[] signatureBytes = Base64.getDecoder().decode(hartSignature.signatureValue.trim());

// Convert byte arrays to BigIntegers
// Use 1 as the signum to treat the bytes as positive
Expand All @@ -122,37 +217,78 @@ static boolean verifyPublicKeySignature(File publicKeyFile, File signatureKeyFil
throw new CouldNotVerifySignatureException("Failed to load signing algorithms: " + e.getMessage());
}

// Initialize the signature with the public key
try {
signature.initVerify(publicKey);
} catch (InvalidKeyException e) {
throw new CouldNotVerifySignatureException("Invalid Key: %s" + e.getMessage());
}

// Canonicalize the XML
byte[] canonicalizedBytes = CanonicalizeXml(hartSignature.signedInfo);

// Verify the signature
boolean verified;
byte[] data;
try {
// TODO read 1024 bytes at a time
data = Files.readAllBytes(dataFile.toPath());
signature.update(data);
signature.update(canonicalizedBytes);
verified = signature.verify(signatureBytes);
} catch (SignatureException e) {
throw new CouldNotVerifySignatureException("Signature failure: %s" + e.getMessage());
} catch (IOException e) {
throw new CouldNotVerifySignatureException("Failed to read data file: " + e.getMessage());
}

return verified;
if (!verified) {
throw new CouldNotVerifySignatureException("Signature verification failed");
}
}

static class UnableToCreateDirectoryException extends Exception {
static byte[] CanonicalizeXml(CryptographyXMLParsers.SignedInfo signedInfo) throws CouldNotVerifySignatureException {
// Canonicalize -- sort of. We need one change in addition to canonicalization to mirror what
// .NET Framework 4.8.1 does, which is what Hart uses.

// Build the XML mapper to serialize from the SignedInfo object
XmlMapper xmlMapper = new XmlMapper();
String xmlSignedInfo = null;
try {
xmlSignedInfo = xmlMapper.writeValueAsString(signedInfo);
} catch (JsonProcessingException e) {
throw new CouldNotVerifySignatureException("Failed to parse the signature XML file");
}

// Set up the canonicalization transform
XMLSignatureFactory sigFactory = XMLSignatureFactory.getInstance("DOM");
CanonicalizationMethod c14n;
try {
c14n = sigFactory.newCanonicalizationMethod(signedInfo.canonicalizationMethod.algorithm,
(C14NMethodParameterSpec) null);
} catch (NoSuchAlgorithmException e) {
throw new CouldNotVerifySignatureException(".sig.xml file uses an unsupported canonicalization algorithm");
} catch (InvalidAlgorithmParameterException e) {
throw new CouldNotVerifySignatureException(".sig.xml file uses an invalid canonicalization algorithm parameter");
}

// Read the serialized data into a stream and canonicalize it
InputStream xmlSignedInfoStream = new ByteArrayInputStream(xmlSignedInfo.getBytes(StandardCharsets.UTF_8));
OctetStreamData canonicalizedData;
try {
canonicalizedData = (OctetStreamData)c14n.transform(new OctetStreamData(xmlSignedInfoStream), null);
} catch (TransformException e) {
throw new CouldNotVerifySignatureException("Canonicalization failed: " + e.getMessage());
}

try {
return canonicalizedData.getOctetStream().readAllBytes();
} catch (IOException e) {
throw new CouldNotVerifySignatureException("Canonicalization returned an invalid result");
}
}

static class UnableToCreateDirectoryException extends Exception {
UnableToCreateDirectoryException(String message) {
super(message);
}
}

static class CouldNotVerifySignatureException extends Exception {

CouldNotVerifySignatureException(String message) {
super(message);
}
Expand Down
6 changes: 4 additions & 2 deletions src/test/java/network/brightspots/rcv/TabulatorTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

package network.brightspots.rcv;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
Expand Down Expand Up @@ -321,9 +322,10 @@ static void setup() {
@DisplayName("Test basic RSA signature validation")
void testRSAValidation() throws FileUtils.CouldNotVerifySignatureException {
File publicKeyTxt = new File("/Users/arminsamii/Downloads/Public Key.txt");
File signatureFile = new File("/Users/arminsamii/Downloads/CVRExport-7-27-2026 01-19-44 PM.zip.sig.xml");
File dataFile = new File("/Users/arminsamii/Downloads/CVRExport-7-27-2026 01-19-44 PM.zip");
File signatureFile = new File("/Users/arminsamii/Downloads/cvrexport/1_ddc9fb5f-2762-462b-a5da-20c083751901.xml.sig.xml");
File dataFile = new File("/Users/arminsamii/Downloads/cvrexport/1_ddc9fb5f-2762-462b-a5da-20c083751901.xml");
assertTrue(FileUtils.verifyPublicKeySignature(publicKeyTxt, signatureFile, dataFile));

}

@Test
Expand Down

0 comments on commit 0875149

Please sign in to comment.