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

Fuzz testing #1712

Open
wants to merge 13 commits into
base: v6
Choose a base branch
from
Open

Fuzz testing #1712

wants to merge 13 commits into from

Conversation

hulkoba
Copy link

@hulkoba hulkoba commented Dec 14, 2023

PR description

This PR is not meant as a comprehensive Fuzz suite, just the scaffolding with a few API entries that should be easy to extend.

Relates to #1689

Adds fuzz test for

  • createCleartextMessage
  • createMessage({binary})
  • createMessage({text})
  • generateKey()
  • readKey({ armoredKey })
  • readKey({ binaryKey })
  • readMessage({ binaryMessage })
  • readMessage({ armoredMessage })

Running a fuzz test

The test/fuzz directory contains fuzz targets like for example createMessageBinary.

You can run one fuzz target:

TARGET=createMessageBinary npm run fuzz

Notice, that TARGET is the name of your fuzz target module.


(Don't) running with coverage

Jazzer.js, unfortunately, does not support coverage for esm tests, yet.

We've implemented the tests also with coverage, turned everything into cjs, but removed it because it is not super useful.
Because the imported modules were not read correctly, the result always shows, that the fuzzing function is 100% covered but not the function we are trying to fuzz.

We decided to use esm-tests and wait until jazzer.js adds coverage support for esm.
Then it can be adapted easily, by adding the --coverage option to the jazzer command.

because it is not super useul
since coverage does not work as expected at all, we can use esm. So if jazzer at some point, add esm support for esm, we can easily add it
Copy link
Collaborator

@larabr larabr left a comment

Choose a reason for hiding this comment

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

Thanks for setting this up! 🙌
I left some comments, most of them are minor, but I'm especially interested in the CI management part 😄

* @param { Buffer } inputData
*/
export function fuzz (inputData) {
const binaryKey = new Uint8Array(`-----BEGIN PGP PRIVATE KEY BLOCK-----\n ${inputData.toString('base64')} -----END PGP PRIVATE KEY BLOCK-----`);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not sure if this test was finalised or missed by mistake?
I don't think this line does what would be expected: it just takes the string as "number" and uses it as size of the array 😄

BTW, a OpenPGP binary input does not correspond to an armored one in Uint8Array format. I think we could just pass the fuzzed/random input as binaryKey.

@@ -0,0 +1,27 @@
import { FuzzedDataProvider } from '@jazzer.js/core';
Copy link
Collaborator

Choose a reason for hiding this comment

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

Minor, but for consistency, this file should be name readMessage**Armored**.js 🙂

@@ -0,0 +1,15 @@
import { FuzzedDataProvider } from '@jazzer.js/core';

import openpgp from '../initOpenpgp.js';
Copy link
Collaborator

Choose a reason for hiding this comment

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

Minor, but that entrypoint is just for mocha tests, I think it's best to use import * as openpgp from 'openpgp'; (or just import { <method> } from 'openpgp') in all these fuzz tests instead, to ensure we test the released bundle as-is 🙂

@@ -0,0 +1,22 @@
import openpgp from '../initOpenpgp.js';

const ignored = ['This message / key probably does not conform to a valid OpenPGP format'];
Copy link
Collaborator

Choose a reason for hiding this comment

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

Very minor, but I'd consider renaming ignored to expected, which as far as I understand it's the purpose it serves..? 🙂

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 this is needed as the fuzz lib expects it to exist?

Is there a way to have the reports output to stdout rather than creating files? Since we are not going to commit these reports (so this folder will always be empty), and we'd like to run some fuzz tests in the CI, I think that might be preferable.

Alternatively, if the folder is needed, I'd rather have the npm run fuzz command create it before running the fuzzer.

Copy link
Author

Choose a reason for hiding this comment

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

Exactly, this directory needs to exist. And there is no way to print the output to stout. But the naming was confusing. These are no reports, more 'artifacts'. I renamed it, to avoid confusion. The directory will be created before the fuzzer runs.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This is more of a comment/question for @twiss , but I'd be for skipping fuzzing these create* methods, as there is no parsing nor input processing being done, and I think some internal crypto functions (decryption in particular) would be a better use of "fuzzing time" ..? 😄

const asciiString = data.consumeString(MAX_COMMENT_LENGTH);
const utf8String = data.consumeString(MAX_NAME_LENGTH, 'utf-8');

return openpgp.generateKey({ userIDs: [
Copy link
Collaborator

@larabr larabr Dec 15, 2023

Choose a reason for hiding this comment

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

It seems to me this is testing the UserID parsing part more than the key generation itself (which happens to be a not-so-cheap function to run)?
For the same purpose, I'd directly fuzz UserID.fromObject().

I'm not sure we need to fuzz generateKey in general, for similar reasons as the create* methods (cc @twiss).

Copy link
Author

Choose a reason for hiding this comment

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

I've added a fuzz test for UserIDPacket.fromObject()

package.json Outdated
@@ -47,6 +47,7 @@
"prepare": "npm run build",
"test": "mocha --timeout 120000 test/unittests.js",
"test-type-definitions": "node --loader ts-node/esm test/typescript/definitions.ts",
"fuzz": "jazzer test/fuzz/$TARGET -- -artifact_prefix=test/fuzz/reports/",
Copy link
Collaborator

Choose a reason for hiding this comment

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

With CI testing in mind, is there a way to have this run on all the possible targets automatically?

Copy link
Author

Choose a reason for hiding this comment

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

Unfortunately not.
The jest integration allows to put several fuzz tests into one describe block https://github.com/CodeIntelligenceTesting/jazzer.js/blob/main/docs/jest-integration.md#executing-jest-fuzz-tests.

For the fuzz targets, there is theoretically the --includes option, but it does not work with several fuzz targets.

To fuzz-test several functions, we could have one javascript file with all fuzz tests inside. This way is hard to debug when an error occurs. Especially when we have tests for one function with different parameter types.
Another way can be found in this example: https://github.com/CodeIntelligenceTesting/jazzer.js/tree/main/examples/bug-detectors
Each fuzz test has its package.json and on the root, a run-all shell script is executed, which runs each npm run test command for each package.json

The way this works today, we envision a long-running fuzz-testing run as a pre-release procedure, rather than fuzzing on each PR, which takes a long time, or if shortened, is not very useful.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for the explanation 🙂 The script approach sounds good, but I'd be more comfortable running the fuzzing CI on every commits to main (incl. PR merging), where we can allow it to run for longer compared to PR commits.
I wouldn't want to find out issues as I am about to release, as that might add a considerable delay to something else we have planned on top of it.

cc @twiss , for another opinion.

Copy link
Member

Choose a reason for hiding this comment

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

IIUC, properly fuzzing everything would take many hours, so running it on every commit to main might not really be feasible.

Also - AIUI fuzzing is normally not primarily to find regressions, but rather to find unknown issues. Obviously the latter can lead to the former, but e.g. if we find an issue using fuzzing, we can add a test case for it to make sure it doesn't regress (once fixed).

So I think running it occasionally, e.g. after large changes have been made, is fine 😊

* @param { Buffer } inputData
*/
export function fuzz (inputData) {
const binaryMessage = new Uint8Array(`-----BEGIN PGP MESSAGE-----\n ${inputData.toString('base64')} -----END PGP MESSAGE-----`);
Copy link
Collaborator

Choose a reason for hiding this comment

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

const fuzzedText = data.consumeString(MAX_MESSAGE_LENGTH, 'utf-8');
const armoredKey = `-----BEGIN PGP PRIVATE KEY BLOCK-----\n ${fuzzedText} -----END PGP PRIVATE KEY BLOCK-----`;

return openpgp.readKey({ armoredKey })
Copy link
Collaborator

Choose a reason for hiding this comment

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

Most (if not all) of the functions being tested in this PR are actually async. I just want to make sure that the fuzzing lib is awaiting them..? Since it's not being done explicitly in this code (nor is the function declared async) 🙂

Copy link
Author

@hulkoba hulkoba Dec 18, 2023

Choose a reason for hiding this comment

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

It does.
Whenever jazzer.js detects that the fuzz target returns a promise it will await the completion before it calls the fuzz target again.
This is explained here https://youtu.be/KyIhxEiNnfc?t=1571

@hulkoba hulkoba force-pushed the fuzz-testing branch 2 times, most recently from 7e80bb1 to 99a506a Compare December 18, 2023 08:03
@hulkoba
Copy link
Author

hulkoba commented Dec 18, 2023

(Put this already as a comment, so you see the PR description edit)
This PR is not meant as a comprehensive Fuzz suite, just the scaffolding with a few API entries that should be easy to extend.

const bufferedMessage = new Uint8Array(Buffer.from(binaryMessage, 'utf8'));
return readMessage({ binaryMessage: bufferedMessage })
.catch(error => {
if (error.message && !ignoredError(error)) {
Copy link
Author

Choose a reason for hiding this comment

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

Question: This ignored error occurs each time because of this check:
(peekedBytes[0] & 0x80) === 0)
here https://github.com/openpgpjs/openpgpjs/blob/v6/src/packet/packet.js#L124

How can I avoid this?

Copy link
Member

Choose a reason for hiding this comment

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

This error means that the bytes are not a valid OpenPGP message, and should be rejected. So - for many byte arrays this error will be correct, and should indeed be ignored. In fact, I think there will be many other error messages that are (usually/hopefully) expected and correct, e.g. for malformed packets, etc.

I'm not super sure what's the easiest way to detect whether an error is "expected" or not; certainly things like TypeErrors will be unexpected, but those are also probably unlikely to happen purely based on the message contents (but perhaps not impossible).

Another option could be to check whether the message is "supposed" to be valid, e.g. with another OpenPGP implementation, though that would probably be a lot of work, and you might then run into many minor differences in parsing that may or may not be important (but might still be interesting).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants