Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/signed canon units #6659

Open
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

Scoppio
Copy link
Collaborator

@Scoppio Scoppio commented Mar 6, 2025

What it does?

Functional and full feature proof of concept for digitally signing canon units to allow players to have always a list of canon units without worry that a custom unit may overwrite their units, this also does not depend on the MUL ID being present and differently from the OUL it don't suffer with name collisions.

It also has functionalities like generating signing keys, signing the files using github actions, etc.

How it works?

The current way to say a unit is canon is looking up it's name in the Oficial Units List, it works fine but has a few edge cases that it can't deal, like a canonical unit being customized, which should remove its canonicity but it does not, chassis and model collision-If the custom unit has the same model and chassis it may be read as canon. The OUL can also be generated at any moment and it will add to it any custom units you have as canonical.

The signing of the files can make sure that only the files that were signed with the private key used during this dist build are recognized as canon. If the unit is customized in any way its signature becomes invalid since it will change the checksum of the unit file. This feature deals with the problems where a non-canonical unit would by mistake be considered canonical, it does not solve the problem of people with malicious intent as it.

This serves specifically to tell players "Hey, this list of units are those that the copyright holders of the game have released as their own original designs, if you wan't to play using only this kind of unit, feel free, the option to show only canonical units will only show those they released, ok?". We are just stamping those as we curate the list of units, nothing else.

Ok, but HOW it works?!

The unit file is hashed, the hash is signed by a private key, the resulting binary is converted to base64 and saved as the <signature>...</signature> at the bottom of the file.

When loading a file, the signature is striped of it and hold in a temporary variable, then the file is hashed and this hash is loaded into the public key, the public key then compares it to the signature and says if it matches or not. If there is any change to the file then the check will fail. If the file has no signature or an invalid signature it will also fail.

There is a gradel task added in this PR that generates the private public key pair, you have to run it in your own computer, it will generate one binary public key, you add it into the resources folder as public.key, and it will generate two private keys, one private.key and one private.key.b64, you will copy the contents of the private.key.b64 and add it into the repository settings > security > secrets and variables > actions and inside of it you click the button new repository secret, you name it FILE_SIGNING_PRIVATE_KEY and then paste the contents of the private.key.b64 in there.

If you want to run it locally, sure, just add the private key in a place you wont publish it by mistake in the repo.

Help! I published the private key by mistake!!!

Ouch, ok... generate a new one, substitute it in the github repo secrets, add the public key to the repo substituting the old one, generate a new release to sign all files again, publish the new release.

Future improvements?

Allows players in a match during the handshake/connection to exchange/verify the public keys in each others client, and flag any player that has a different public key as using a non-compliant client. This is to my point of view imporant since people do play tournaments and disputes over prize money and imaginary internet points, so a modicum of security is welcome.

We could even do a kind of "only play with same versions" where we can use the public key of each new release to be different, this way two different version programs can't play in the same lobby because their keys are different.

Performance

With signed units feature

Cold opening the game - no cache
12 seconds

Cold opening the game - with cache
1 second

Master branch

Cold opening the game - no cache
8 seconds

Cold opening the game - with cache
1 second

This load process is async and dont block anything in the UI while it happens, so this extra 4 second isn't necessarily something the user ever experiences except for a slightly slower load the first time they open the game after installing

@Scoppio Scoppio added Refactoring For New Dev Cycle This PR should be merged at the beginning of a dev cycle Utility Improvement to Existing Feature Used with the RFE tag to indicate an improvement to an existing feature labels Mar 6, 2025
@Scoppio Scoppio self-assigned this Mar 6, 2025
@Scoppio Scoppio requested review from rjhancock and Sleet01 March 6, 2025 01:59
Comment on lines +10972 to +10978
/**
* Get the source file for this unit.
* @return The source file for this unit, null if it does not exists.
*/
public @Nullable File getSourceFile() {
return MekSummary.getSourceFile(getChassis() + " " + getModel());
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

turns out this piece of code is unnecessary

@@ -151,7 +153,18 @@
return m_entity;
}

public void parse(InputStream is, String fileName) throws Exception {
public void parse(InputStream ns, String fileName) throws Exception {
File tempFile = File.createTempFile("temp", ".tmp");

Check warning

Code scanning / CodeQL

Local information disclosure in a temporary directory Medium

Local information disclosure vulnerability due to use of file readable by other local users.
writer.newLine();
}
}
InputStream is = new FileInputStream(tempFile);

Check warning

Code scanning / CodeQL

Potential input resource leak Warning

This FileInputStream is not always closed on method exit.
Comment on lines +157 to +167
File tempFile = File.createTempFile("temp", ".tmp");
tempFile.deleteOnExit();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(ns));
BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile))) {
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.newLine();
}
}
InputStream is = new FileInputStream(tempFile);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hate it as much as everybody else... best way would be to move this to its own small method and nobody talks about it ever again

Comment on lines +804 to +814
// canonicity of the unit was set during the basic data setup with the new method
// Check if it's canon; if it is, mark it as such.
ent.setCanon(false);// Guilty until proven innocent
if (canonUnitNames == null) {
initCanonUnitNames();
}

int index = Collections.binarySearch(canonUnitNames, ent.getShortNameRaw());
if (index >= 0) {
ent.setCanon(true);
}
// ent.setCanon(false);// Guilty until proven innocent
// if (canonUnitNames == null) {
// initCanonUnitNames();
// }

// int index = Collections.binarySearch(canonUnitNames, ent.getShortNameRaw());
// if (index >= 0) {
// ent.setCanon(true);
// }
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a proof of concept, this would be removed when integrating it in the project.

@@ -1382,6 +1382,18 @@ public String formatSUA(BattleForceSUA sua, String delimiter, ASSpecialAbilityCo
return null;
}

public static @Nullable File getSourceFile(String fullName) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing javadoc


File privateKeyFile = new File(args[0]);
if (!privateKeyFile.exists()) {
System.out.println("Private key file not found: " + privateKeyFile.getAbsolutePath());
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have to go with system out here, otherwise it wont print in the github actions

/**
* Generate a new key pair and save to files.
*/
public static void generateKeyPair(File privateKeyFile, File publicKeyFile) throws Exception {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried generating the key pairs using only linux commands, but I could not, after 5+ hours of multiple tries, make it generate a pair that would work.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably better to not introduce a platform dependency anyhow.
This seems fine.

Comment on lines +42 to +51
- name: Setup private key for file signing
run: |
mkdir -p megamek/keystore
echo "${{ secrets.FILE_SIGNING_PRIVATE_KEY }}" > megamek/keystore/private.key
chmod 600 megamek/keystore/private.key

- name: Setup private key for file signing
run: |
mkdir -p megamek/megamek/keystore
echo "${{ secrets.FILE_SIGNING_PRIVATE_KEY }}" | base64 --decode > megamek/megamek/keystore/private.key
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume these should not have the same name.

@@ -14,6 +14,11 @@ Thumbs.db
/megamek/classes/
/megamek/bin/

# Sigining keys
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/Sigining/Signing/

Copy link
Collaborator

@Sleet01 Sleet01 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple spots need updating, and the POC section that's commented out would need to go.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
For New Dev Cycle This PR should be merged at the beginning of a dev cycle Improvement to Existing Feature Used with the RFE tag to indicate an improvement to an existing feature Refactoring Utility
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants