Skip to main content

Finding vulnerabilities in Swiss Post's future e-voting system - Part 2


Earlier this year I published the Part I of this series of blog posts on vulnerabilities in Swiss Post's future e-voting system. That publication comprehensively explains the context, methodology and attack surface for the Swiss Post e-voting system, so it is highly recommended to go through it before reading this post, if you're really interested in getting the whole picture.

This second round of bugs (reported during December '21 and January '22 ) includes multiple cryptographic vulnerabilities and a deserialization issue.  

For me, the most interesting issue is '#YWH-PGM2323-65', not only because it would have prevented ballot boxes from being decrypted during the tally phase, but also due to the potential design weaknesses that I'm coming across as a result of its analysis. 

Let's briefly discuss the reported issues before going into detail:

IDTitleReward (€)Attack Surface Areas*CVSS
#YWH-PGM2323-53Multiple unchecked length values during SafeStreamDeserialization may crash Control Components35003 & 45.4 - Medium
CVSS:3.0/AV:A/AC:H/PR:H/UI:N/S:C/C:N/I:N/A:H
#YWH-PGM2323-64Verifier does not properly verify the signature of NodeContributions50002 & 35.1 - Medium
CVSS:3.0/AV:A/AC:H/PR:H/UI:R/S:C/C:N/I:H/A:N
#YWH-PGM2323-65Generation of 'Choice Return Codes encryption' Public Key and 'Election' Public Key may be influenced by a malicious voting server.450035.4 - Medium
CVSS:3.0/AV:A/AC:H/PR:H/UI:N/S:C/C:N/I:N/A:H
#YWH-PGM2323-{59,60,61}Multiple improper signature verification issues.N/A (Duplicated)2 & 3 & 47.3 - High
CVSS:3.0/AV:A/AC:H/PR:H/UI:R/S:C/C:H/I:H/A:H

* The 'attack surface areas' column refers to the top 5 priorities previously elaborated on 'Finding vulnerabilities in Swiss Post's future e-voting system - Part I '.

1.  Multiple unchecked length values during SafeStreamDeserialization may crash Control Components

As it was explained in the Part I, the orchestrator service, part of the untrusted Voting Server, communicates with the Control Components using AMQP messages through a RabbitMQ cluster.

The control components implement a specific deserialization logic for some of those messages. A lack of proper length validation during the deserialization of potentially attacker-controlled messages can force an out of memory error, thus crashing the affected component

2. 'Verifier' does not properly verify the signature of NodeContributions

This vulnerability targets the Verifier, a component that is not a priority yet as it is still missing part of its functionality, but nonetheless a key component intended to facilitate auditors (including 3rd parties) the task of validating an election event.

When checking the Node Contributions resulting from the 'GenEncLongCodeShares', the verifier uses data, coming from the Control Components, that has not been yet verified in order to set up the own verification logic, which usually is a bad thing to do. Additionally, it does not properly verify the consistency of these contributions, however consistence checks are still being developed so that may be considered a known-issue.

3. Generation of 'Choice Return Codes encryption' Public Key and 'Election' Public Key may be influenced by a malicious voting server.

This vulnerability involves a malicious voting server (note that the system specification considers the voting server untrustworthy and potentially compromised) providing a specific combination of Control Components public keys to the SDM, so that when 'Choice Return Codes' and the 'Election' Public Keys are generated, they will not comply with the actual contributions from the four different Control Components.

When targeting the  'Election' Public key (ELpk),  this attack would have prevented the ballot boxes from being decrypted during the tally phase, which renders the election event basically useless as votes cannot be decrypted.

If we were talking about a regular election, this issue would be similar to be directly voting into a paper shredding machine.

To be honest, I don't completely agree with the assessment Swiss Post has performed on this vulnerability due to the following reasons:

 - Swiss Post narrows down the impact of this vulnerability to the availability of the system, while I consider there is also an impact on its integrity.  

-  They didn't consider this attack may qualify for the 'Vote Corruptiondomain-specific scenario, because it targets all votes instead of individual votes, which, from my perspective, may be questionable at least.

- They acknowledge that the mitigation adduced for this attack, which is the ability of the cantons to decrypt 'test ballot boxes' using the same cryptographic materials than in the real election event (so they would see votes are not being decrypted), is not currently part of the system specification, neither had been documented anywhere when the attack was reported. In addition to this, I think that ability presents a high risk attack vector but as that functionality has not been formally described nor implemented, so far there is little to add.

- Swiss Post states that attacks against availability do not violate their security objectives, which is  a concern. Actually they acknowledge the system design and specification are vulnerable by default to this kind of attack: for instance, if a malicious Control Component destroys (3 of 4 Control Components are assumed to be malicious) its election private key. 

Just to put things into context, Ransomware is essentially an attack against availability. I see a troubled future for e-voting if the election results depend on whether the election authorities are willing to pay in order to receive the keys that would allow to decrypt votes. 

4. Multiple improper signature verification issues.

The signature validation for payloads sent back and forth between the SDM and Control Components (through the Voting Server's Orchestrator) was broken due to an improper validation of the certificate chain.  Basically, the signature validation algorithm was just checking whether the payload was properly signed, but not who really signed it. As there were different certificate chains hanging from the same Platform Root CA, untrustworthy components would be able to leverage their intermediate certificates to impersonate others. 

My reports were accepted but discarded as 'Duplicated'. It turned out these, and similar, issues had been previously uncovered by Thomas Haines, Vanessa Tiague and Oliver Pereira: the experts who have reported most of the serious cryptographic vulnerabilities in the system. 

Although there is a little difference: in Thomas' paper he mentions a malicious Control Component is required to perform this kind of attack but I found a malicious Voting Server is enough, due to the Voting Server's 'Election Information 100' certificate chain.

Vulnerabilities

#YWH-PGM2323-53  - Multiple unchecked length values during SafeStreamDeserialization may crash Control Components

Description

The Orchestrator service, part of the untrusted Voting Server, communicates with the Control Components using AMQP messages through a RabbitMQ cluster.

The control components implement specific deserialization logic for some of those messages. A lack of proper length validation during the deserialization of potentially attacker-controlled messages can force an out of memory error, thus crashing the affected component.

Technical  Details

The 'unpackArrayHeader()' (or similar unpack* functions) is used multiple times without enforcing any bounds checking (see lines 156, 101, 115, 129, 100 in the following files). As a result, an overly large signed integer value is used to initialize lists and allocate arrays, which may result in a 'java.lang.OutOfMemoryError: Java heap space' error, thus crashing the affecting component.

(Version: 0.12.0.0)

File: e-voting-master/domain/src/main/java/ch/post/it/evoting/domain/returncodes/KeyCreationDTO.java

144:    @Override
145:    public void deserialize(MessageUnpacker unpacker) throws SafeStreamDeserializationException {
146:        try {
147:            setCorrelationId(UUID.fromString(StreamSerializableUtil.retrieveStringValueWithNullCheck(unpacker)));
148:            this.requestId = StreamSerializableUtil.retrieveStringValueWithNullCheck(unpacker);
149:            this.signature = StreamSerializableUtil.retrieveStringValueWithNullCheck(unpacker);
150:            this.resourceId = StreamSerializableUtil.retrieveStringValueWithNullCheck(unpacker);
151:            this.encryptionParameters = StreamSerializableUtil.retrieveStringValueWithNullCheck(unpacker);
152:            this.electionEventId = StreamSerializableUtil.retrieveStringValueWithNullCheck(unpacker);
153:            this.from = StreamSerializableUtil.retrieveDateValueWithNullCheck(unpacker);
154:            this.to = StreamSerializableUtil.retrieveDateValueWithNullCheck(unpacker);
155:            if (!unpacker.tryUnpackNil()) {
156:                int listSize = unpacker.unpackArrayHeader();
157:                this.publicKeys = new ArrayList<>(listSize);
158:                for (int i = 0; i < listSize; i++) {
159:                    CCPublicKey key = new CCPublicKey();
160:                    key.deserialize(unpacker);
161:                    publicKeys.add(key);
162:                }
163:            } else {
164:                publicKeys = null;
165:            }
166:        } catch (IOException e) {
167:            throw new SafeStreamDeserializationException(e);
168:        }
169:    }

File: e-voting-master/domain/src/main/java/ch/post/it/evoting/domain/returncodes/ChoiceCodesVerificationDecryptResPayload.java

096:    public void deserialize(MessageUnpacker unpacker) throws SafeStreamDeserializationException {
097:        try {
098:            if (unpacker.tryUnpackNil()) {
099:                decryptContributionResult = null;
100:            } else {
101:                int listSize = unpacker.unpackArrayHeader();
102:                decryptContributionResult = new ArrayList<>(listSize);
103:                for (int i = 0; i < listSize; i++) {
104:                    decryptContributionResult.add(StreamSerializableUtil.retrieveStringValueWithNullCheck(unpacker));
105:                }
106:            }
107:            exponentiationProofJson = StreamSerializableUtil.retrieveStringValueWithNullCheck(unpacker);
108:            publicKeyJson = StreamSerializableUtil.retrieveStringValueWithNullCheck(unpacker);
109:            if (unpacker.tryUnpackNil()) {
110:                this.signature = null;
111:            } else {
112:                int arraySize = unpacker.unpackArrayHeader();
113:                X509Certificate[] certs = new X509Certificate[arraySize];

File: e-voting-master/domain/src/main/java/ch/post/it/evoting/domain/returncodes/ReturnCodesExponentiationResponsePayload.java

110:    public void deserialize(MessageUnpacker unpacker) throws SafeStreamDeserializationException {
111:        try {
112:            if (unpacker.tryUnpackNil()) {
113:                pccOrCkToLongReturnCodeShare = null;
114:            } else {
115:                int mapSize = unpacker.unpackMapHeader();
116:                pccOrCkToLongReturnCodeShare = new LinkedHashMap<>(mapSize);
117:                for (int i = 0; i < mapSize; i++) {
118:                    BigInteger key = retrieveBigIntegerValueWithNullCheck(unpacker);
119:                    BigInteger value = retrieveBigIntegerValueWithNullCheck(unpacker);
120:                    pccOrCkToLongReturnCodeShare.put(key, value);
121:                }
122:            }
123:            exponentiationProofJson = retrieveStringValueWithNullCheck(unpacker);
124:            voterChoiceReturnCodeGenerationPublicKeyJson = retrieveStringValueWithNullCheck(unpacker);
125:            voterVoteCastReturnCodeGenerationPublicKeyJson = retrieveStringValueWithNullCheck(unpacker);
126:            if (unpacker.tryUnpackNil()) {
127:                this.signature = null;
128:            } else {
129:                int arraySize = unpacker.unpackArrayHeader();
130:                X509Certificate[] certs = new X509Certificate[arraySize];

File: e-voting-master/domain/src/main/java/ch/post/it/evoting/domain/returncodes/ReturnCodesInput.java

095:    public void deserialize(MessageUnpacker unpacker) throws SafeStreamDeserializationException {
096:        try {
097:            if (unpacker.tryUnpackNil()) {
098:                this.returnCodesInputElements = null;
099:            } else {
100:                int listSize = unpacker.unpackArrayHeader();
101:                returnCodesInputElements = new ArrayList<>(listSize);
102:                for (int i = 0; i < listSize; i++) {
103:                    returnCodesInputElements.add(StreamSerializableUtil.retrieveBigIntegerValueWithNullCheck(unpacker));
104:                }
105:            }

The deserialization of the received messages relies on the open-source 'msgpack' code, the {unpack*Header} functions end up invoking 'getInt', so there is no safeguard either at that component level.

https://github.com/msgpack/msgpack-java/blob/651a2a02fd5f269d91183cf70e162dea7a5d9caa/msgpack-core/src/main/java/org/msgpack/core/buffer/MessageBuffer.java#L459

 public int getInt(int index)
    {
        // Reading little-endian value
        int i = unsafe.getInt(base, address + index);
        // Reversing the endian
        return Integer.reverseBytes(i);
    }

The RabbitMQ message size limit mitigation is also out of the scope of this issue. We're not sending an overly large RabbitMQ message, we are sending a especially crafted serialized message, that once deserialized by MessagePack provides the large integer values that are consumed by the e-voting code but the actual RabbitMQ message may be just several KBs. 


#YWH-PGM2323-64  - Verifier does not properly verify the signature of NodeContributions

Description

The verifier is using potentially malicious data to dynamically adjust the values that will determine whether a successful verification has been performed.

When checking the Node Contributions resulting from the 'GenEncLongCodeShares', the verifier uses data, coming from the Control Components, that has not been yet verified in order to set up its own verification logic.

Technical  Details

(v 0.12.3)

At line 165 the NodeContributions are persisted by the SDM, once they have been collected from the Orchestrator.

File: e-voting-master/secure-data-manager/secure-data-manager-backend/services/src/main/java/ch/post/it/evoting/sdm/application/service/VotingCardSetService.java

133:    /**
134:     * Download the computed values for a votingCardSet
135:     *
136:     * @throws InvalidStatusTransitionException if the original status does not allow the download
137:     */
138:    public void download(String votingCardSetId, String electionEventId)
139:            throws ResourceNotFoundException, InvalidStatusTransitionException, IOException {
140: 
141:        if (!idleStatusService.getIdLock(votingCardSetId)) {
142:            return;
143:        }
144: 
145:        try {
146:            Status fromStatus = Status.COMPUTED;
147:            Status toStatus = Status.VCS_DOWNLOADED;
148: 
149:            checkVotingCardSetStatusTransition(electionEventId, votingCardSetId, fromStatus, toStatus);
150: 
151:            JsonObject votingCardSetJson = votingCardSetRepository.getVotingCardSetJson(electionEventId, votingCardSetId);
152:            String verificationCardSetId = getVerificationCardSetId(votingCardSetJson);
153: 
154:            deleteNodeContributions(electionEventId, verificationCardSetId);
155: 
156:            int chunkCount;
157:            try {
158:                chunkCount = returnCodeGenerationRequestPayloadRepository.getCount(electionEventId, verificationCardSetId);
159:            } catch (PayloadStorageException e) {
160:                throw new IllegalStateException("Failed to get the chunk count.", e);
161:            }
162: 
163:            for (int i = 0; i < chunkCount; i++) {
164:                try (InputStream contributions = votingCardSetChoiceCodesService.download(electionEventId, verificationCardSetId, i)) {
165:                    writeNodeContributions(electionEventId, verificationCardSetId, i, contributions);
166:                }
167:            }
168: 
169:            configurationEntityStatusService.update(toStatus.name(), votingCardSetId, votingCardSetRepository);
170: 
171:        } finally {
172:            idleStatusService.freeIdLock(votingCardSetId);
173:        }
174: 
175:    }

During verification, the persisted Node contributions are then loaded, deserialized and verified. However, at line 190 we can see that the verifier use 'payload.getNodeId()', a value belonging to a payload for which its signature has not been yet validated, to choose the CCN CA that will be used to validate the certificate chain of the signature. This can be used by an attacker to trick the verifier into using a certificate chain, which may belong to a malicious Control Component, to validate a tampered contribution of the honest Control Component.

File: verifier-master/verifier-block1/src/main/java/ch/post/it/evoting/verifier/block/block1/verifications/CheckSigNodeContributions.java

172:    // Data class that represent node contributions signature to verify
173:    static class NodeOutputSignature {
174:        private final byte[] signature;
175:        private final byte[] payloadHash;
176:        private final X509Certificate signingCertificate;
177:        private final List<X509Certificate> intermediateCertificates;
178:        private final X509Certificate rootCertificate;
179: 
180:        // Massage the data to get it into the expected format for the verification algorithm
181:        NodeOutputSignature(final ReturnCodeGenerationResponsePayload payload, final NodeCertificates nodeCertificates, HashService hashService,
182:                CertificateLoader certificateLoader) {
183:            // Signature
184:            this.signature = payload.getSignature().getSignatureContents();
185:            this.payloadHash = hashService.recursiveHash(payload);
186: 
187:            // Certificates chain
188:            this.signingCertificate = payload.getSignature().getCertificateChain()[0];
189:            final List<Path> filteredCertificates = nodeCertificates.nodeCertificatesPaths.stream()
190:                    .filter(ccPath -> ccPath.getFileName().toString().equals("cc" + payload.getNodeId() + "_CA.pem"))
191:                    .collect(Collectors.toList());
192:            this.intermediateCertificates = Collections.singletonList(certificateLoader.loadCertificate(filteredCertificates.get(0)));
193:            this.rootCertificate = nodeCertificates.rootCertificate;
194:        }
195:    }

The verifier should limit the use of potentially attacker-controlled data while performing the different verifications. Before verification, the data to be validated should not be part of the values involved in the own validation. Otherwise, there is a risk to validate data that may have been tampered in the component that persisted that data, thus populating any potential issue abused in a component providing inputs to the verifier into the own verifier.

In this specific case, if we are expecting to verify contributions from 4 nodes, for which their CCN CA are known, the verifier should check whether the node Id n (without using the nodeId from the not yet verified payload) is actually using the CCN CA n, but also that all the contributions come from four different nodes, and that the same node Id has not been used to sign another node's contribution.

#YWH-PGM2323-65  - Generation of 'Choice Return Codes encryption' Public Key and 'Election' Public Key may be influenced by a malicious voting server

Description

ELpk and pkCCR are generated by combining the public key contributions of the CCM and CCR nodes. These contributions are collected from the Control Components by the Voting Server and sent to the Setup Component, as can be seen in the 'System Specification' document.







These public keys are individually signed by the corresponding CCR/CCM Signing certificate, which later on are verified by the Setup Component before combining them to generate the final public keys. However, the Setup Component does not validate the number of public keys that have been received from the Voting Server matches the amount of public keys defined in the protocol specification. Thus, a malicious voting server can just return to the Setup Component an arbitrary number of properly signed public keys, that will be validated by the Setup Component and then combined.

As a result, this combination of an arbitrary number of public keys can be abused to maliciously influence the resulting ELpk and pkCCR keys, in such a way that it may compromise the ability to decrypt all votes in an election event.

It's important to note that this attack does not rely on being able to bypass the implemented signature logic, but it is based on the ability to increase the number of valid public keys that are used during the generation of ELpk and pkCCR

Technical  Details

The following Runnable lambda 'serializePublicKey' triggers the logic that generates the ELpk

File: secure-data-manager/secure-data-manager-backend/services/src/main/java/ch/post/it/evoting/sdm/application/service/ElectoralAuthorityService.java

305:    public void writeShare(final String electionEventId, final String electoralAuthorityId, final Integer shareNumber, final String pin)
306:            throws IOException, GeneralCryptoLibException, SharesException, ResourceNotFoundException {
307: 
308:        CreateSharesHandler createSharesHandlerElGamal = getHandler(electoralAuthorityId);
309: 
310:        JsonObject electoralAuthority = getElectoralAuthorityJsonObject(electoralAuthorityId);
311:        JsonArray electoralAuthorityMembers = electoralAuthority.getJsonArray(Constants.ELECTORAL_BOARD_LABEL);
312: 
313:        Runnable serializePublicKey = () -> {
314: 
315:            try {
316:                JsonArray mixingKeysJsonArray = controlComponentKeysAccessorService.downloadMixingKeys(electoralAuthorityId);
317: 
318:                controlComponentKeysAccessorService.writeMixingKeys(electionEventId, electoralAuthorityId, mixingKeysJsonArray);
319: 
320:                serializePublicKeysAndVerifyThatTheyWereWritten(electionEventId, electoralAuthorityId, createSharesHandlerElGamal);
321: 
322:                updateElectoralAuthorityStatus(electoralAuthorityId, electoralAuthority);
323: 
324:                createSharesHandlerElGamalMap.remove(electionEventId);
325:            } catch (ResourceNotFoundException | SharesException | IOException e) {
326:                throw new LambdaException(e);
327:            }
328:        };
329: 

It will be executed by 'processShare' when the last share for the EBsk has been written (line 308)

File: secure-data-manager/config-generator/config-shares/src/main/java/ch/post/it/evoting/sdm/config/shares/handler/CreateSharesHandler.java

294:    private void processShare(final int i, final String name, final String oldPinPuk, final String newPinPuk,
295:            final PrivateKey privateKeyToBeUsedToSign, Runnable finalOperation) throws SharesException {
296: 
297:        Share share = shares.get(i);
298: 
299:        try {
300:            smartcardService.write(share, name, oldPinPuk, newPinPuk, privateKeyToBeUsedToSign);
301:        } catch (SmartcardException e) {
302:            throw new SharesException("An error occured while trying to write a share", e);
303:        }
304: 
305:        numSharesWritten++;
306: 
307:        if (numSharesWritten == shares.size()) {
308:            finalOperation.run();
309:            wipeAllSharesFromMemory();
310:        }
311:    }

At line 320 from the previous 'serializePublicKey' lambda, we can see how the CCM election keys are going to be verified, combined and persisted at 'serializePublicKeysAndVerifyThatTheyWereWritten'

File: secure-data-manager/secure-data-manager-backend/services/src/main/java/ch/post/it/evoting/sdm/application/service/ElectoralAuthorityService.java

512:    private void serializePublicKeysAndVerifyThatTheyWereWritten(final String electionEventId, final String electoralAuthorityId,
513:            final CreateSharesHandler createSharesHandler) throws SharesException, ResourceNotFoundException {
514: 
515:        CreateElectoralBoardKeyPairInput createEbKeyPairInput = electoralAuthorityDataGeneratorServiceImpl
516:                .generate(electoralAuthorityId, electionEventId);
517:        Path outputFolder = pathResolver.resolve(createEbKeyPairInput.getOutputFolder());
518: 
519:        final ElGamalPublicKey electoralAuthorityPublicKey = getElectoralAuthorityPublicKey(createSharesHandler);
520: 
521:        JsonArray mixingKeysJsonArray = controlComponentKeysAccessorService.downloadMixingKeys(electoralAuthorityId);
522: 
523:        final List<ElGamalPublicKey> mixingPublicKeys = getMixingElGamalPublicKeys(electionEventId, electoralAuthorityId, mixingKeysJsonArray);
524: 
525:        ElGamalPublicKey electionPublicKey = combineUsingCompression(electoralAuthorityPublicKey, mixingPublicKeys);
526: 
527:        boolean areElectoralAuthorityKeysSerialized = createEBKeysSerializer
528:                .serializeElectionPublicKeys(outputFolder, electoralAuthorityId, electionPublicKey, electoralAuthorityPublicKey);
529:        if (!areElectoralAuthorityKeysSerialized) {
530:            throw new IllegalStateException(
531:                    "The serialization of the Electoral Authority public keys failed. They might not be written to file. Stopping the process.");
532:        }
533: 
534:    }
535: 

At line 523 'getMixingElGamalPublicKeys' is invoked, which basically iterates over all the existent 'publicKey' objects, without following the specification of 'SetupTallyEB' which clearly defines the number of nodes that should contribute to this operation.


File: secure-data-manager/secure-data-manager-backend/services/src/main/java/ch/post/it/evoting/sdm/application/service/ElectoralAuthorityService.java

548:    private List<ElGamalPublicKey> getMixingElGamalPublicKeys(String electionEventId, String electoralAuthorityId, JsonArray mixingKeysJsonArray) {
549:        List<ElGamalPublicKey> mixingPublicKeys = new ArrayList<>();
550:        try {
551:            X509Certificate rootCACertificate = platformRootCAService.load();
552:            for (JsonObject jsonObject : jsonArrayToJsonObjects(mixingKeysJsonArray)) {
553:                ElGamalPublicKey publicKey = ElGamalPublicKey.fromJson(jsonObject.get("publicKey").toString());
554:                byte[] signature = Base64.getDecoder().decode(jsonObject.getString("signature"));
555:                X509Certificate signingCertificate = (X509Certificate) PemUtils.certificateFromPem(jsonObject.getString("signerCertificate"));
556:                X509Certificate nodeCACertificate = (X509Certificate) PemUtils.certificateFromPem(jsonObject.getString("nodeCACertificate"));
557:                X509Certificate[] chain = { signingCertificate, nodeCACertificate, rootCACertificate };
558:                keySignatureValidator.checkMixingKeySignature(signature, chain, publicKey, electionEventId, electoralAuthorityId);
559:                mixingPublicKeys.add(publicKey);
560:            }
561:        } catch (SignatureException | GeneralCryptoLibException | CertificateManagementException e) {
562:            throw new IllegalStateException("Failed to get mixing ElGamal public keys", e);
563:        }
564:        return mixingPublicKeys;
565:    }

File: secure-data-manager/secure-data-manager-backend/services/src/main/java/ch/post/it/evoting/sdm/application/service/ElectoralAuthorityService.java

567:    private List<JsonObject> jsonArrayToJsonObjects(JsonArray array) {
568:        List<JsonObject> jsonObjects = new ArrayList<>(array.size());
569:        for (int i = 0; i < array.size(); i++) {
570:            jsonObjects.add(array.getJsonObject(i));
571:        }
572:        return jsonObjects;
573:    }

Then 'combineUsingCompression' is invoked to combine the EBsk with the CCMj public keys, thus yielding the final ELpk

File: secure-data-manager/secure-data-manager-backend/services/src/main/java/ch/post/it/evoting/sdm/application/service/ElectoralAuthorityService.java

536:    public ElGamalPublicKey combineUsingCompression(ElGamalPublicKey electoralAuthorityPublicKey, List<ElGamalPublicKey> mixingPublicKeys) {
537: 
538:        ElGamalPublicKey combinedPublicKey;
539:        try {
540:            combinedPublicKey = new ElGamalPublicKeyCombinerWithCompression().combine(electoralAuthorityPublicKey, mixingPublicKeys);
541:        } catch (GeneralCryptoLibException e) {
542:            throw new ElectoralAuthorityServiceException("Exception when trying to combine public keys: " + e.getMessage(), e);
543:        }
544: 
545:        return combinedPublicKey;
546:    }

We can see how 'combine' doesn't check the number of contributions either, iterating over the entire list of public keys.

File: secure-data-manager/secure-data-manager-backend/services/src/main/java/ch/post/it/evoting/sdm/domain/service/utils/ElGamalPublicKeyCombinerWithCompression.java
40:     public ElGamalPublicKey combine(ElGamalPublicKey primaryElGamalPublicKey, List<ElGamalPublicKey> keysToBeCombined)
41:             throws GeneralCryptoLibException {
42: 
43:         validateInputs(primaryElGamalPublicKey, keysToBeCombined);
44: 
45:         int numRequiredElements = primaryElGamalPublicKey.getKeys().size();
46: 
47:         ElGamalPublicKey combinedKey = primaryElGamalPublicKey;
48: 
49:         GroupElementsCompressor<ZpGroupElement> compressor = new GroupElementsCompressor<>();
50: 
51:         for (ElGamalPublicKey key : keysToBeCombined) {
52: 
53:             List<ZpGroupElement> subkeys = key.getKeys();
54: 
55:             if (subkeys.size() > numRequiredElements) {
56: 
57:                 List<ZpGroupElement> compressedList = compressor.buildListWithCompressedFinalElement(numRequiredElements, subkeys);
58: 
59:                 key = new ElGamalPublicKey(compressedList, key.getGroup());
60:             }
61: 
62:             combinedKey = combinedKey.multiply(key);
63:         }
64: 
65:         return combinedKey;
66:     }

We find a similar scenario during the generation of pkCCR based on the CCMj  Choice Return Codes encryption keys

The number of contributions is not validated as specified in 'GenVerCardSetKeys'

File: secure-data-manager/secure-data-manager-backend/services/src/main/java/ch/post/it/evoting/sdm/domain/service/impl/VotingCardSetDataGeneratorServiceImpl.java

276:    private void validateChoiceCodesEncryptionKey(String electionEventId, String verificationCardId, List<String> keys) {
277:        try {
278:            X509Certificate rootCACertificate = platformRootCAService.load();
279:            Decoder decoder = Base64.getDecoder();
280:            for (String string : keys) {
281:                JsonObject object = JsonUtils.getJsonObject(string);
282:                ElGamalPublicKey key = ElGamalPublicKey.fromJson(object.get("publicKey").toString());
283:                byte[] signature = decoder.decode(object.getString("signature"));
284:                X509Certificate signingCertificate = (X509Certificate) PemUtils.certificateFromPem(object.getString("signerCertificate"));
285:                X509Certificate nodeCACertificate = (X509Certificate) PemUtils.certificateFromPem(object.getString("nodeCACertificate"));
286:                X509Certificate[] chain = { signingCertificate, nodeCACertificate, rootCACertificate };
287:                keySignatureValidator.checkChoiceCodesEncryptionKeySignature(signature, chain, key, electionEventId, verificationCardId);
288:            }
289:        } catch (SignatureException | GeneralCryptoLibException | CertificateManagementException e) {
290:            throw new IllegalStateException("Invalid choice codes encryption keys.", e);
291:        }
292:    }

Then at line 199 the List of pkCCRj,i keys is assigned as a parameter for the upcoming job task.

File: secure-data-manager/secure-data-manager-backend/services/src/main/java/ch/post/it/evoting/sdm/domain/service/impl/VotingCardSetDataGeneratorServiceImpl.java

List<String> choiceCodeEncryptionKey = getChoiceCodesEncryptionKey(choiceCodeKeysJsonArray);
176:            validateChoiceCodesEncryptionKey(electionEventId, verificationCardSetId, choiceCodeEncryptionKey);
177: 
178:            controlComponentKeysAccessorService.writeChoiceCodeKeys(electionEventId, verificationCardSetId, choiceCodeKeysJsonArray);
179: 
180:            CreateVotingCardSetInput createVotingCardSetInput = new CreateVotingCardSetInput();
181:            createVotingCardSetInput.setStart(ballotBox.getString(JsonConstants.DATE_FROM));
182:            createVotingCardSetInput.setElectoralAuthorityID(electoralAuthorityId);
183:            createVotingCardSetInput.setEnd(ballotBox.getString(JsonConstants.DATE_TO));
184:            createVotingCardSetInput
185:                    .setValidityPeriod(electionEvent.getJsonObject(JsonConstants.SETTINGS).getInt(JsonConstants.CERTIFICATES_VALIDITY_PERIOD));
186:            createVotingCardSetInput.setBasePath(configElectionEventPath.toString());
187:            createVotingCardSetInput.setBallotBoxID(ballotBoxId);
188:            createVotingCardSetInput.setBallotID(ballotId);
189:            createVotingCardSetInput.setBallotPath(destinationBallotFilePath.toString());
190:            createVotingCardSetInput.setEeID(electionEventId);
191:            createVotingCardSetInput.setNumberVotingCards(votingCardSet.getInt(JSON_PARAM_NAME_NR_OF_VC_TO_GENERATE));
192:            createVotingCardSetInput.setVerificationCardSetID(verificationCardSetId);
193:            createVotingCardSetInput.setVotingCardSetID(id);
194: 
195:            // INCLUDE ALIAS INSIDE THE OBJECT...
196:            createVotingCardSetInput.setVotingCardSetAlias(votingCardSet.getString(JsonConstants.ALIAS, ""));
197: 
198:            createVotingCardSetInput.setKeyForProtectingKeystorePassword(getPublicKeyForProtectingKeystorePassword());
199:            createVotingCardSetInput.setChoiceCodesEncryptionKey(choiceCodeEncryptionKey);
200: 
201:            createVotingCardSetInput.setPlatformRootCACertificate(PemUtils.certificateToPem(platformRootCAService.load()));
202: 
203:            createVotingCardSetInput.setCreateVotingCardSetCertificateProperties(getCertificateProperties());
204: 
205:            final ResponseEntity<StartVotingCardGenerationJobResponse> startJobResponse;
206:            try {
207:                startJobResponse = sendStartJobRequest(tenantId, electionEventId, createVotingCardSetInput);

Eventually, the list of pkCCRj,i is combined without checking the allowed number of contributions.

File: secure-data-manager/config-generator/config-engine/src/main/java/ch/post/it/evoting/sdm/config/commands/voters/datapacks/generators/VerificationCardSetCredentialDataPackGenerator.java

103:            combinedChoiceCodesEncryptionPublicKey = choiceCodesEncryptionPublicKey;
104:            for (int i = 1; i < choiceCodesEncryptionKeys.length; i++) {
105:                jsonNode = mapper.readTree(choiceCodesEncryptionKeys[i]);
106:                choiceCodesEncryptionPublicKeyJson = jsonNode.get("publicKey").toString();
107:                choiceCodesEncryptionPublicKey = ElGamalPublicKey.fromJson(choiceCodesEncryptionPublicKeyJson);
108:                nonCombinedChoiceCodesEncryptionPublicKeys[i] = choiceCodesEncryptionPublicKey;
109:                combinedChoiceCodesEncryptionPublicKey = combinedChoiceCodesEncryptionPublicKey.multiply(choiceCodesEncryptionPublicKey);
110:            }
111: 
112:            LOGGER.info(ConfigGeneratorLogEvents.GENVCD_SUCCESS_GENERATING_CHOICES_CODES_KEYPAIR.getInfo(), inputDataPack.getEeid(),
113:                    Constants.ADMIN_ID, Constants.VERIFCS_ID, verificationCardSetID);
114: 
115:        } catch (Exception e) {
116: 
117:            LOGGER.error(ConfigGeneratorLogEvents.GENVCD_ERROR_GENERATING_CHOICES_CODES_KEYPAIR.getInfo(), inputDataPack.getEeid(),
118:                    Constants.ADMIN_ID, Constants.VERIFCS_ID, verificationCardSetID, Constants.ERR_DESC, e.getMessage());
119: 
120:            throw new CreateVotingCardSetException("An error occurred while trying to set the choices codes ElGamal public key", e);
121:        }
122: 
123:        dataPack.setChoiceCodesEncryptionPublicKey(combinedChoiceCodesEncryptionPublicKey);


#YWH-PGM2323-59,60,61  - Multiple improper signature verification issues.

As these issues have been marked as 'Duplicated' I will just include one of the examples (#60, affecting 'MixDecOnline') to illustrate the problem.

Description

The MixNet 'initial' payload sent from the Voting Server to the first Control Component is signed using a private key belonging to the 'Election Information' certificate chain, made available in a keystore (by design) to the Voting Server. The subsequent 'shuffle' payloads generated by the different Control Components are then signed using a signing key belonging to each of the CCN certificate chains.

 However, it's important to note that both certificate chains share the same trusted cert as shown in the image.


The vulnerability can be found in how the signature validation of these payloads ('initial' and 'shuffle') has been implemented in the CCN's 'MixDecryptMessageConsumer', as the signature for both kinds of payloads is validated using the same logic, which only relies on checking whether the certificate chain is validated by the trusted certificate (Platform Root CA).

As a result, a malicious Voting Server can modify any 'shuffle' payload that will be accepted by a honest control component. In addition to this, as the signature validation logic is the same in 'ReturnCodesGenerationConsumer' the voting server's 'Election Information' certificate chain can be used to perform the impersonation attack without even requiring the collusion of a malicious Control Component.

Technical Details




The online Mixing control components CCM shuffle (and re-encrypt) the previous control component’s ciphertexts and perform partial decryption. The payload of the input list of ciphertexts cdec,j-1 corresponds to the cleansed encrypted votes cdec,0 and the other input, the remaining election public key ELpk,j-1 equals the election public key ELpk.

The initial mixnet payload created after the 'Cleansing' process is signed (line 175) by the Voting Server using the Election Information Signing key. The Certificate chain (line 158) is set to the 'Election Information' as shown in the above diagram.


File: e-voting-master/voting-server/election-information/src/main/java/ch/post/it/evoting/votingserver/electioninformation/services/domain/service/ballotbox/CleansedBallotBoxServiceImpl.java
110:    @Override
111:    public MixnetInitialPayload getMixnetInitialPayload(final BallotBoxId ballotBoxId)
112:            throws ResourceNotFoundException, CleansedBallotBoxServiceException {
113: 
114:        checkNotNull(ballotBoxId);
115: 
116:        // Find out how many vote sets fit the ballot box.
117:        final int voteCount;
118:        try {
119:            voteCount = cleansedBallotBoxRepository.count(ballotBoxId);
120:        } catch (CleansedBallotBoxRepositoryException e) {
121:            throw new CleansedBallotBoxServiceException(String.format("Failed to count votes for ballot box %s.", ballotBoxId), e);
122:        }
123: 
124:        // Get the encryption parameters from the ballot box information.
125:        final JsonObject ballotBoxInformation = getBallotBoxInformationJson(ballotBoxId);
126:        final JsonObject encryptionParametersJson = ballotBoxInformation.getJsonObject(ENCRYPTION_PARAMETERS_JSON_FIELD);
127: 
128:        final BigInteger p = new BigInteger(encryptionParametersJson.getString(P_JSON_FIELD));
129:        final BigInteger q = new BigInteger(encryptionParametersJson.getString(Q_JSON_FIELD));
130:        final BigInteger g = new BigInteger(encryptionParametersJson.getString(G_JSON_FIELD));
131:        final GqGroup encryptionParameters = new GqGroup(p, q, g);
132: 
133:        // Convert the EncryptedVotes to ElGamalMultiRecipientCiphertext.
134:        final List<ElGamalMultiRecipientCiphertext> encryptedVotes = cleansedBallotBoxRepository.getVoteSet(ballotBoxId, 0, voteCount)
135:                .map(vote -> ElGamalMultiRecipientCiphertext.create(GqElement.create(vote.getGamma(), encryptionParameters),
136:                        vote.getPhis().stream().map(bi -> GqElement.create(bi, encryptionParameters)).collect(Collectors.toList())))
137:                .collect(Collectors.toList());
138: 
139:        // Get the election public key.
140:        final ElGamalMultiRecipientPublicKey electionPublicKey;
141:        try {
142:            // Get the electoral authority identifier.
143:            final String electoralAuthorityId = ballotBoxInformation.getString(ELECTORAL_AUTHORITY_ID_JSON_FIELD);
144: 
145:            // Get the vote encryption key, which at this stage is the electoral authority public key.
146:            final ElGamalPublicKey voteEncryptionKey = getVoteEncryptionKey(TENANT_ID, ballotBoxId.getElectionEventId(), electoralAuthorityId);
147:            final List<ZpGroupElement> keys = voteEncryptionKey.getKeys();
148: 
149:            // Convert cryptolib public key to crypto-primitives public key.
150:            electionPublicKey = keys.stream().map(k -> GqElement.create(k.getValue(), encryptionParameters))
151:                    .collect(Collectors.collectingAndThen(Collectors.toList(), ElGamalMultiRecipientPublicKey::new));
152:        } catch (GeneralCryptoLibException | IOException e) {
153:            throw new CleansedBallotBoxServiceException("Failed to retrieve election public key.");
154:        }
155: 
156:        // Get the certificate chain for the election information public key.
157:        LOGGER.info("Finding the validation key certificate chain for ballot box {}...", ballotBoxId);
158:        final X509Certificate[] fullCertificateChain = eiTenantSystemKeys.getSigningCertificateChain(TENANT_ID);
159:        if (null == fullCertificateChain) {
160:            throw new CleansedBallotBoxServiceException("No certificate chain was found for tenant " + TENANT_ID);
161:        }
162:        final X509Certificate[] certificateChain = new X509Certificate[fullCertificateChain.length - 1];
163:        System.arraycopy(fullCertificateChain, 0, certificateChain, 0, fullCertificateChain.length - 1);
164:        LOGGER.info("Obtained the validation key certificate for tenant {} with {} elements", TENANT_ID, certificateChain.length);
165: 
166:        // Create the initial payload to send.
167:        final MixnetInitialPayload mixnetInitialPayload = new MixnetInitialPayload(encryptionParameters, encryptedVotes, electionPublicKey);
168: 
169:        // Hash the payload.
170:        final byte[] payloadHash = hashService
171:                .recursiveHash(mixnetInitialPayload.getEncryptionGroup(), HashableList.from(mixnetInitialPayload.getEncryptedVotes()),
172:                        mixnetInitialPayload.getElectionPublicKey());
173: 
174:        // Get the election information system key to sign the payload.
175:        final PrivateKey signingKey = eiTenantSystemKeys.getSigningPrivateKey(TENANT_ID);
176:        LOGGER.info("Obtained the signing key for tenant {}, signing the initial payload...", TENANT_ID);
177: 
178:        // Sign the payload hash.
179:        byte[] signature;
180:        try {
181:            signature = asymmetricService.sign(signingKey, payloadHash);
182:        } catch (GeneralCryptoLibException e) {
183:            throw new CleansedBallotBoxServiceException("Failed to sign the initial payload.", e);
184:        }
185:        final CryptoPrimitivesPayloadSignature payloadSignature = new CryptoPrimitivesPayloadSignature(signature, certificateChain);
186:        mixnetInitialPayload.setSignature(payloadSignature);
187:        LOGGER.info("Initial payload signed successfully.");
188: 
189:        return mixnetInitialPayload;
190:    }

The 'mixnetInitialPayload' is sent through the Orchestrator to be consumed by the Control Components

File: e-voting-master/control-components/distributed-mixing-service/src/main/java/ch/post/it/evoting/controlcomponents/mixing/service/MixDecryptMessageConsumer.java

122:    @RabbitListener(queues = "${partialMixingDecryptionRequestQueue}", autoStartup = "false")
123:    public void onMessage(Message message) throws IOException, KeyManagementException, PayloadSignatureException {
124:        byte[] messageBody = message.getBody();
125:        byte[] mixnetStateBytes = new byte[messageBody.length - 1];
126:        System.arraycopy(messageBody, 1, mixnetStateBytes, 0, messageBody.length - 1);
127: 
128:        MixnetState mixnetState = objectMapper.readValue(mixnetStateBytes, MixnetState.class);
129: 
130:        List<String> validationErrors = validateData(mixnetState);
131:        if (!validationErrors.isEmpty()) {
132:            sendWithError(mixnetState, "The following fields present validation errors: " + validationErrors);
133:            return;
134:        }

'ValidateData' extracts the payload and verifies its signature by using 'CryptolibPayloadSignatureService' (line 216) in the same vulnerable way.

File: e-voting-master/control-components/distributed-mixing-service/src/main/java/ch/post/it/evoting/controlcomponents/mixing/service/MixDecryptMessageConsumer.java

192:    private List<String> validateData(final MixnetState mixnetState) {
193:        List<String> errors = new ArrayList<>();
194: 
195:        final MixnetPayload payload = mixnetState.getPayload();
196: 
197:        if (mixnetState.getNodeToVisit() != nodeID) {
198:            String errorMessage = String.format("Node to visit is expected to be %d, but was %d", nodeID, mixnetState.getNodeToVisit());
199:            errors.add(errorMessage);
200:            LOGGER.error(errorMessage);
201:        }
202:        if (payload == null) {
203:            String errorMessage = "No payload provided";
204:            errors.add(errorMessage);
205:            LOGGER.error(errorMessage);
206:        } else if (containsNullFields(payload)) {
207:            String errorMessage = "The payload contains null objects.";
208:            errors.add(errorMessage);
209:            LOGGER.error(errorMessage);
210:        } else {
211:            LOGGER.info("Verifying signature...");
212:            final CryptoPrimitivesPayloadSignature signature = payload.getSignature();
213:            final byte[] payloadHash = hashPayload(payload);
214: 
215:            try {
216:                final boolean validSignature = signatureService.verify(signature, ccmjKeyRepository.getPlatformCACertificate(), payloadHash);
217: 
218:                if (!validSignature) {
219:                    String errorMessage = "Invalid signature.";
220:                    errors.add(errorMessage);
221:                    LOGGER.error(errorMessage);
222:                } else {
223:                    LOGGER.info("The signature is valid.");
224:                }
225:            } catch (PayloadVerificationException e) {
226:                String errorMessage = "Signature verification failed.";
227:                errors.add(errorMessage);
228:                LOGGER.error(errorMessage);
229:            }
230:        }

As a result, the Voting Server's 'Election Information' certificate chain can be abused to impersonate the 'SetupComponent' during the configuration phase.

Popular posts from this blog

SATCOM terminals under attack in Europe: a plausible analysis.

------ Update 03/12/2022 Reuters has published new information on this incident, which initially matches the proposed scenario. You can find the  update  at the bottom of this post. ------ February 24th: at the same time Russia initiated a full-scale attack on Ukraine, tens of thousands of KA-SAT SATCOM terminals suddenly  stopped  working in several european countries: Germany, Ukraine, Greece, Hungary, Poland...Germany's Enercon moved forward and acknowledged that approximately 5800 of its wind turbines, presumably those remotely operated via a SATCOM link in central Europe, had lost contact with their  SCADA server .  In the affected countries, a significant part of the customers of Eutelsat's domestic broadband service were also unable to access Internet.  From the very beginning Eutelsat and its parent company Viasat, stated that the issue was being investigated as a cyberattack. Since then, details have been scarcely provided but few days ago I came across a really inter

VIASAT incident: from speculation to technical details.

  34 days after the incident, yesterday Viasat published a statement providing some technical details about the attack that affected tens of thousands of its SATCOM terminals. Also yesterday, I eventually had access to two Surfbeam2 modems: one was targeted during the attack and the other was in a working condition. Thank you so much to the person who disinterestedly donated the attacked modem. I've been closely covering this issue since the beginning, providing a  plausible theory based on the information that was available at that time, and my experience in this field. Actually, it seems that this theory was pretty close to what really happened. Fortunately, now we can move from just pure speculation into something more tangible, so I dumped the flash memory for both modems (Spansion S29GL256P90TFCR2 ) and the differences were pretty clear. In the following picture you can see 'attacked1.bin', which belongs to the targeted modem and 'fw_fixed.bin', coming from t

Reversing 'France Identité': the new French digital ID.

  -------------- Update from 06/10/2023 : following my publication, I’ve been in contact with France Identité CISO and they could provide more information on the measures they have taken in the light of these findings: We would like to thank you for your in-depth technical research work on “France Identite” app that was launched in beta a year ago and for which you were rewarded. As you know, the app is now generally available on iOS and Android through their respective app stores. Your work, alongside French cybersecurity agency (ANSSI) research, made us update and modify deeply the E2EE Secure Channel used between the app and our backend. It is now mostly based on TLS1.3. Those modifications were released only a few weeks after you submitted your work through our private BugBounty program with YesWeHack. That released version also fixes the three other vulnerabilities you submitted. From the beginning of “France Identite” program, it was decided to implicate cybersecurity community,