Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions Base/usr/share/man/man1/iffysign.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
## Name

iffysign - Sign and verify

## Synopsis

```**sh
$ iffysign [--generate] [--sign] [--verify] [--pubkey-file FILE] [--pubkey PUBLIC_KEY] [--secret-key-file FILE] [--signature FILE] [--file FILE] [--force] [--untrusted-comment COMMENT] [--comment COMMENT]
```

## Description

### Introduction

`iffysign` is a dead simple tool for signing files and verifying these signatures, including trusted file comments. It is able to both verify the _integrity_ of files (that is, they haven’t been modified during transmission, e.g. while downloading them from the internet), as well as the _authenticity_ of the files (that is, they have been signed by the person holding a certain known key).

iffysign uses the [Ed25519](https://ed25519.cr.yp.to/) system, which is a state-of-the-art elliptic asymmetric signature system. iffysign has a secret key, which is used to create signatures, and a public key, which can be shared freely and is used to verify signatures created with the secret key. In comparison to similarly-strong schemes like RSA-4096, Ed25519 keys are very short. Therefore, iffysign public keys fit in 56 characters of Base64, allowing them to be transmitted via chat applications, social media posts, QR codes, NFC tags, and other low-data and/or text-only transmission mediums. This makes the keys easier to distribute via a multitude of mediums, decreasing the likelihood of an attacker intercepting and replacing a public key across all transmission paths.

iffysign is based on minisign (see below for history). The command-line options are almost completely compatible, mostly except for option defaults. The key and signature file formats are completely compatible, with two main exceptions: First, iffysign does not support password-encrypted secret keys and can only create and read unencrypted secret keys. Second, iffysign only supports the new pre-hashed signing scheme, using the identifier `ED` (capital D).

To sign a file, iffysign hashes the file’s contents using [BLAKE2b-512](https://www.blake2.net/), a modern hashing function at least as secure as SHA-3. The hashing provides the aforementioned integrity protection. The hash is then signed using the Ed25519 secret key, creating the _file signature_. Furthermore, the concatenation of the trusted comment and this signature are signed again, creating the _global signature_. This verifies the authenticity of the trusted comment _in combination with_ the file (it is not possible to transfer a trusted comment and the global signature onto another file signed with the same key). Both signatures are stored in Base64 on disk.

### Usage

iffysign provides three primary operations invoked with the `-G`, `-V`, and `-S` options: generate keys, verify signatures, and sign files, respectively. Files to be signed or verified are specified with the `-m` option. Signatures by default are named the same as the operand file, with the extension `.iffy` appended.

When signing, the `-c` and `-t` options can be used to change the default untrusted and trusted comments. The untrusted comments of newly generated key pairs cannot be modified, but freely edited in the files afterwards.

Public keys can be provided as files with the `-p` option, or as the Base64 key string directly on the command line with the `-P` option.

iffysign returns 0 on successful operation, including a valid signature. It returns 1 if the signature is invalid or some other issue occurred. It returns 2 on invalid CLI arguments.

## Options

- `--help` Display help message and exit.
- `--version` Print version.
- `-G`, `--generate` Generate a new key pair.
- `-S`, `--sign` Sign a file.
- `-V`, `--verify` Verify that a file's signature is valid.
- `-p FILE`, `--pubkey-file FILE` Path to the public key file, default `iffysign.pub`.
- `-P PUBLIC_KEY`, `--pubkey PUBLIC_KEY` Public key as base64. This is the same as the second line of public key files.
- `-s FILE`, `--secret-key-file FILE` Secret key file, default `~/.config/iffysign/iffysign.sec`.
- `-x FILE`, `--signature FILE` Signature file, default `<file>.iffy`.
- `-m FILE`, `--file FILE` File to sign or verify.
- `-f`, `--force` Force overwrite files if they already exist. This option applies to all files iffysign can write (signatures and keys).
- `-c COMMENT`, `--untrusted-comment COMMENT` **Untrusted** (not signed) comment to add when signing. **Do not use this option** unless you know what you’re doing. iffysign will emit a warning if you only use this option to avoid accidentally attaching unverifiable (and therefore mostly worthless) comments.
- `-t COMMENT`, `--comment COMMENT` Trusted comment to add when signing.

## Files

iffysign deals in three kinds of files: signatures, public keys (and public key files), and secret keys. All three file formats are text-only and use [Base64](help://man/1/base64) to encode binary data. The [minisign website](https://jedisct1.github.io/minisign/) documents most parts of the file formats including how to create them.

For unencrypted secret keys, the `kdf_algorithm` is `\0\0` (two null bytes instead of `Sc`), and salt, opslimit and memlimit are zeroed out. The BLAKE2b-256 checksum of the secret key can be all zeroes, meaning it was not filled in and should not be verified.

All files provide untrusted comments in their first line, which are not signed and may be changed arbitrarily without invalidating signatures or keys. Untrusted comments are for end user information, such as to distinguish a large collection of public key files you have received from various sources. While useful, untrusted comments, as their name implies, are not signed or verified, and may not be relied upon for authoritative information about the keys or signatures. For instance, if the untrusted comment of a public key specifies the owner’s email address, this address’s authenticity cannot be verified and an attacker may arbitrarily change the email address without changing the public key itself. This is in contrast to protocols like OpenPGP, which provide verified proof of identity. The inability to verify any data associated with the public key, including the key ID, is by design; if you wish to sign and verify identity information with the keys, you can use trusted signature comments or identity file formats with associated signatures.

The trusted comment of a signature is verified in the global signature below it. It cannot be changed without the file failing verification.

All keys used by iffysign have randomly generated key IDs. Key IDs are not signed and may be changed arbitrarily. They are mostly a convenience feature to avoid accidentally verifying signatures with an incorrect key. Key IDs are inherited from `minisign` and `signify` mostly to provide compatibility.

The default secret key is `~/.config/iffysign/iffysign.sec`. This directory (`~/.config/iffysign`) is recommended for public and secret keys alike. The default public key is `iffysign.pub` (in the current directory).

## Examples

```sh
# Generate a new key pair in the default locations (iffysign.pub and ~/.config/iffysign/iffysign.sec)
$ minisign -G

# Sign the myfile.txt file, using your default secret key at ~/.config/iffysign/iffysign.sec
$ minisign -Sm myfile.txt
# Include a trusted comment into the signature, which will also be signed
$ minisign -Sm myfile.txt -t "This comment will be signed as well"

# Verify a file’s signature residing in myfile.txt.iffy using the base64 public key string
$ minisign -Vm myfile.txt -P <pubkey>
# Verify the file with a public key file instead
$ minisign -Vm myfile.txt -p signature.pub
```

## History

iffysign is based on [minisign(1)](https://jedisct1.github.io/minisign/) and provides a mostly compatible command-line interface as well as compatible key and signature file formats. Minisign and iffysign are based on and partially compatible with OpenBSD’s [signify(1)](https://man.openbsd.org/signify), which introduced the concept of “dead simple” signing and verification with modern cryptography. signify was written in 2014 by Ted Unangst to provide package signing for OpenBSD, and more rationale has been provided in a [transcribed talk of his](https://www.openbsd.org/papers/bsdcan-signify.html). Minisign’s major advancement over signify is the introduction of trusted (i.e. signed) comments, where signify only provides untrusted comments.

iffysign’s name was chosen like signify’s as a portmanteau of the terms “sign” and “verify”.

## See Also

- [`pkg`(1)](help://man/1/pkg)
- [`base64`(1)](help://man/1/base64)
- [`checksum`(1)](help://man/1/checksum): to only verify the integrity of files, not their authenticity
4 changes: 4 additions & 0 deletions Base/usr/share/man/man1/pkg.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ It does not currently support installing and uninstalling packages. To install t
# Query the ports database for the serenity-theming package
$ pkg -q serenity-theming
```

## See Also

- [`iffysign`(1)](help://man/1/iffysign)
1 change: 1 addition & 0 deletions Meta/Lagom/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,7 @@ if (BUILD_LAGOM)

lagom_utility(icc SOURCES ../../Userland/Utilities/icc.cpp LIBS LibGfx LibMain LibURL)
lagom_utility(iconv SOURCES ../../Userland/Utilities/iconv.cpp LIBS LibTextCodec LibMain)
lagom_utility(iffysign SOURCES ../../Userland/Utilities/iffysign.cpp LIBS LibCrypto LibMain)
lagom_utility(image SOURCES ../../Userland/Utilities/image.cpp LIBS LibGfx LibMain)
lagom_utility(imgcmp SOURCES ../../Userland/Utilities/imgcmp.cpp LIBS LibGfx LibMain)
lagom_utility(isobmff SOURCES ../../Userland/Utilities/isobmff.cpp LIBS LibGfx LibMain)
Expand Down
4 changes: 3 additions & 1 deletion Userland/Utilities/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ set(CMD_SOURCES_CPP
iconv.cpp
id.cpp
ifconfig.cpp
iffysign.cpp
image.cpp
image2bin.cpp
imgcmp.cpp
Expand Down Expand Up @@ -238,7 +239,7 @@ list(APPEND REQUIRED_TARGETS
touch tr true umount uname uniq uptime w watchfs wc which whoami xargs yes
)
list(APPEND RECOMMENDED_TARGETS
aconv adjtime aplay abench asctl bt checksum chres cksum copy fortune gzip install keymap lsdev lsirq lsof lspci lzcat man mkfs.fat mknod mktemp
aconv adjtime aplay abench asctl bt checksum chres cksum copy fortune gzip iffysign install keymap lsdev lsirq lsof lspci lzcat man mkfs.fat mknod mktemp
nc netstat notify ntpquery open passwd pixelflut pls printf pro shot strings tar tt unzip wallpaper xzcat zip
)

Expand Down Expand Up @@ -330,6 +331,7 @@ target_link_libraries(headless-browser PRIVATE LibCrypto LibFileSystem LibGemini
target_link_libraries(hiddump PRIVATE LibHID)
target_link_libraries(icc PRIVATE LibGfx LibMedia LibURL)
target_link_libraries(iconv PRIVATE LibTextCodec)
target_link_libraries(iffysign PRIVATE LibCrypto)
target_link_libraries(image PRIVATE LibGfx)
target_link_libraries(image2bin PRIVATE LibGfx)
target_link_libraries(imgcmp PRIVATE LibGfx)
Expand Down
173 changes: 173 additions & 0 deletions Userland/Utilities/iffysign.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
* Copyright (c) 2025, kleines Filmröllchen <[email protected]>.
*
* SPDX-License-Identifier: BSD-2-Clause
*/

#include <AK/Assertions.h>
#include <AK/ByteString.h>
#include <AK/Format.h>
#include <AK/JsonObject.h>
#include <AK/JsonValue.h>
#include <AK/String.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/File.h>
#include <LibCore/SecretString.h>
#include <LibCore/StandardPaths.h>
#include <LibCrypto/Minisign.h>
#include <LibMain/Main.h>

enum class Operation : u8 {
Unspecified,
Sign,
Verify,
GenerateKey,
};

using namespace Crypto::Minisign;

ErrorOr<int> serenity_main(Main::Arguments arguments)
{
Operation operation { Operation::Unspecified };
ByteString public_key_file;
ByteString public_key;
ByteString secret_key_file;
ByteString signature_file;
ByteString operand_file;
String untrusted_comment;
String trusted_comment;
bool force_overwrite { false };

// As noted in the manpage, all options that are supported by minisign(1) as well are (almost) compatible with it.
// Some of our defaults are different to reflect the missing functionality and how iffysign is used in Serenity.
// minisign doesn’t use long options, so our long options are freely chosen.
Core::ArgsParser args_parser;
args_parser.set_general_help("Sign files and verify signatures. iffysign has a partially minisign-compatible command line interface and key/signature file formats.");

// Base operations.
args_parser.add_option(operation, Operation::GenerateKey, "Generate a new key pair", "generate", 'G');
args_parser.add_option(operation, Operation::Sign, "Sign a file", "sign", 'S');
args_parser.add_option(operation, Operation::Verify, "Verify that a file's signature is valid", "verify", 'V');
Comment on lines +48 to +50
Copy link
Contributor

Choose a reason for hiding this comment

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

ArgsParser seems kind of broken in this case, since operation always ended up being Operation::Verify when I tested this. I'd just add a fixme and replace the enum with a triplet of booleans to work around this for now (or fix ArgsParser, but I couldn't really figure out why it does this).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For me it worked? Not sure what command line you used

Copy link
Contributor

Choose a reason for hiding this comment

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

So the issue I was having was apparently with a dangling reference in ArgsParser. The accept_value lambda in the Enum specialization of add_option takes a reference to new_value, and since that's just a local copy, the reference becomes invalid after add_option returns.

This fixes it, so feel free to add it (or something similar) to this PR if you want. (I reworked this to explicitly construct an Option to be more consistent with other overloads while at it)

--- a/Userland/Libraries/LibCore/ArgsParser.h
+++ b/Userland/Libraries/LibCore/ArgsParser.h
@@ -91,15 +91,19 @@ public:
     template<Enum T>
     void add_option(T& value, T new_value, char const* help_string, char const* long_name, char short_name = 0, OptionHideMode hide_mode = OptionHideMode::None)
     {
-        add_option({ .argument_mode = Core::ArgsParser::OptionArgumentMode::None,
-            .help_string = help_string,
-            .long_name = long_name,
-            .short_name = short_name,
-            .accept_value = [&](StringView) {
+        Option option {
+            OptionArgumentMode::None,
+            help_string,
+            long_name,
+            short_name,
+            nullptr,
+            [&value, new_value](StringView) -> ErrorOr<bool> {
                 value = new_value;
                 return true;
             },
-            .hide_mode = hide_mode });
+            hide_mode
+        };
+        add_option(move(option));
     }
 
     template<Integral I>


// Options needed by some or all operations.
args_parser.add_option(public_key_file, "Path to the public key file, default `iffysign.pub`", "pubkey-file", 'p', "FILE");
args_parser.add_option(public_key, "Public key as base64", "pubkey", 'P', "PUBLIC_KEY");
args_parser.add_option(secret_key_file, "Secret key file, default `~/.config/iffysign/iffysign.sec`", "secret-key-file", 's', "FILE");
args_parser.add_option(signature_file, "Signature file, default `<file>.iffy`", "signature", 'x', "FILE");
args_parser.add_option(operand_file, "File to sign or verify", "file", 'm', "FILE");
args_parser.add_option(force_overwrite, "Force overwrite files if they already exist.", "force", 'f');

// Comment options.
args_parser.add_option(untrusted_comment, "UNTRUSTED (not signed) comment to add when signing. DO NOT USE THIS OPTION unless you know what you’re doing.", "untrusted-comment", 'c', "COMMENT");
args_parser.add_option(trusted_comment, "Trusted comment to add when signing.", "comment", 't', "COMMENT");

args_parser.parse(arguments);

if (operation == Operation::Unspecified) {
warnln("iffysign: error: no operation specified, use one of -G, -V, -S.");
return 2;
}
if (!public_key_file.is_empty() && !public_key.is_empty()) {
warnln("iffysign: error: only one of -p, -P is allowed");
return 2;
}
if (public_key_file.is_empty() && public_key.is_empty())
public_key_file = "./iffysign.pub";

// Users may be used to the `-c` option, which is not really what you should use.
if (!untrusted_comment.is_empty() && trusted_comment.is_empty())
warnln("iffysign: warning: Only untrusted comment provided. This comment is not signed and recipients of the signature can not validate its authenticity! Consider providing a trusted comment with the `-t` option.");
if (untrusted_comment.is_empty())
untrusted_comment = "minisign-compatible signature"_string;

if (signature_file.is_empty() && !operand_file.is_empty())
signature_file = ByteString::formatted("{}.minisig", operand_file);

if (secret_key_file.is_empty())
secret_key_file = ByteString::formatted("{}/.config/iffysign/iffysign.sec", Core::StandardPaths::home_directory());
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: the .config/iffysign directory doesn't exist by default, so maybe we should be create it if it's missing.


auto const write_mode = force_overwrite ? Core::File::OpenMode::Write
: Core::File::OpenMode::MustBeNew | Core::File::OpenMode::Write;

auto const get_public_key = [&] -> ErrorOr<PublicKey> {
if (!public_key_file.is_empty()) {
auto public_key_file_object = TRY(Core::File::open(public_key_file, Core::File::OpenMode::Read));
auto const public_key_data = TRY(public_key_file_object->read_until_eof());
return TRY(PublicKey::from_public_key_file({ public_key_data }));
}
if (!public_key.is_empty()) {
// We made sure earlier that in this case, a literal key must have been given.
return TRY(PublicKey::from_base64(public_key.bytes()));
}

warnln("iffysign: error: no public key specified");
exit(1);
};

switch (operation) {
case Operation::Verify: {
auto signature_file_object = TRY(Core::File::open(signature_file, Core::File::OpenMode::Read));
auto const signature_data = TRY(signature_file_object->read_until_eof());
auto const signature = TRY(Signature::from_signature_file(StringView { signature_data }));

outln("untrusted comment is: {}", signature.untrusted_comment());

auto const public_key = TRY(get_public_key());
auto operand = TRY(Core::File::open_file_or_standard_stream(operand_file, Core::File::OpenMode::Read));
auto const signature_validity = TRY(public_key.verify(signature, *operand));

switch (signature_validity) {
case VerificationResult::Invalid:
outln("iffysign: error: invalid signature for file {}!", operand_file);
return 1;
case VerificationResult::Valid:
outln("valid signature for file {}\ntrusted comment is: {}\n", operand_file, signature.trusted_comment());
return 0;
case VerificationResult::GlobalSignatureInvalid:
outln("iffysign: error: file signature is valid for file {} but trusted comment signature is not valid!", operand_file);
return 1;
}
break;
}

case Operation::Sign: {
if (trusted_comment.is_empty()) {
JsonObject info_structure;
// TODO: Add more information, like signing and file modification timestamps, file hash, etc.
info_structure.set("filename", JsonValue { operand_file.view() });
trusted_comment = TRY(TRY(String::from_byte_string(info_structure.to_byte_string())).replace("\n"sv, " "sv, ReplaceMode::All));
}

auto secret_key_file_object = TRY(Core::File::open(secret_key_file, Core::File::OpenMode::Read));
auto temporary_secret_key_data = TRY(secret_key_file_object->read_until_eof());
auto const secret_key_data = Core::SecretString::take_ownership(move(temporary_secret_key_data));
auto const secret_key = TRY(SecretKey::from_secret_key_file(secret_key_data));

auto operand = TRY(Core::File::open_file_or_standard_stream(operand_file, Core::File::OpenMode::Read));
auto const signature = TRY(secret_key.sign(*operand, untrusted_comment, trusted_comment));
auto const signature_data = TRY(signature.to_signature_file());
auto const signature_file_object = TRY(Core::File::open(signature_file, write_mode));
TRY(signature_file_object->write_until_depleted(signature_data.bytes()));
break;
}
case Operation::GenerateKey: {
auto const secret_key = TRY(SecretKey::generate());
auto const secret_key_text = TRY(secret_key.to_secret_key_file());
auto public_key = PublicKey { secret_key };
public_key.set_untrusted_comment(TRY(String::formatted("iffysign public key")));
auto const public_key_text = TRY(public_key.to_public_key_file());

auto secret_key_file_object = TRY(Core::File::open(secret_key_file, write_mode));
auto public_key_file_object = TRY(Core::File::open(public_key_file, write_mode));
TRY(secret_key_file_object->write_until_depleted(secret_key_text.view().trim("\0"sv, TrimMode::Right)));
TRY(public_key_file_object->write_until_depleted(public_key_text));
outln("Generated new key pair to {} and {}.", secret_key_file, public_key_file);

break;
}
case Operation::Unspecified:
VERIFY_NOT_REACHED();
}

return 0;
}
Loading