From 05ba517febb6b2c4b80e7682d394d30c9233801b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?kleines=20Filmr=C3=B6llchen?= Date: Fri, 25 Apr 2025 14:49:54 +0200 Subject: [PATCH 1/2] LibCrypto: Add Minisign signature tooling This allows us to read and write minisign/signify-compatible keys and signatures, and create new key pairs and signatures. The next commit will make use of this utility in a CLI frontend. --- Tests/LibCrypto/CMakeLists.txt | 1 + Tests/LibCrypto/TestMinisign.cpp | 145 ++++++++++ Userland/Libraries/LibCrypto/CMakeLists.txt | 1 + Userland/Libraries/LibCrypto/Minisign.cpp | 290 ++++++++++++++++++++ Userland/Libraries/LibCrypto/Minisign.h | 160 +++++++++++ 5 files changed, 597 insertions(+) create mode 100644 Tests/LibCrypto/TestMinisign.cpp create mode 100644 Userland/Libraries/LibCrypto/Minisign.cpp create mode 100644 Userland/Libraries/LibCrypto/Minisign.h diff --git a/Tests/LibCrypto/CMakeLists.txt b/Tests/LibCrypto/CMakeLists.txt index 850ff4056006ce..7752b7b5dbc4ab 100644 --- a/Tests/LibCrypto/CMakeLists.txt +++ b/Tests/LibCrypto/CMakeLists.txt @@ -12,6 +12,7 @@ set(TEST_SOURCES TestHKDF.cpp TestHMAC.cpp TestMGF.cpp + TestMinisign.cpp TestOAEP.cpp TestPBKDF2.cpp TestPoly1305.cpp diff --git a/Tests/LibCrypto/TestMinisign.cpp b/Tests/LibCrypto/TestMinisign.cpp new file mode 100644 index 00000000000000..03a9761ee2b886 --- /dev/null +++ b/Tests/LibCrypto/TestMinisign.cpp @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2025, kleines Filmröllchen + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include + +// All variables labeled "minisign" have content created by minisign, which verifies our compatibility. +constexpr StringView public_key_file_minisign_text = R"""(untrusted comment: minisign public key FDE44BFDD77EC45A +RWRaxH7X/Uvk/etgLk05NOsAT5aNTz1d5DjHD2R3s1/URq3vnQw6R790 +)"""sv; +constexpr StringView public_key_minisign_text = "RWRaxH7X/Uvk/etgLk05NOsAT5aNTz1d5DjHD2R3s1/URq3vnQw6R790"sv; +constexpr StringView secret_key_minisign_text = R"""(untrusted comment: minisign encrypted secret key +RWQAAEIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWsR+1/1L5P3AKeEZBVWCT2g7hvHFeF8ALiRqPDSZdINZiB1uSVxyaetgLk05NOsAT5aNTz1d5DjHD2R3s1/URq3vnQw6R790AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= +)"""sv; +constexpr StringView public_key_file_text = R"""(untrusted comment: iffysign public key +RWQ7FcRc9BMU2CTaEuu+FqFwYT5OChWG7ehQgLVIVMeerG1ANDcit9Jx +)"""sv; +constexpr StringView secret_key_text = R"""(untrusted comment: iffysign unencrypted secret key +RWQAAEIyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOxXEXPQTFNiUeh0iJJbl84yimedpdQgkFsUitcDSY4S/yAsD1uFPvSTaEuu+FqFwYT5OChWG7ehQgLVIVMeerG1ANDcit9JxAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= +)"""sv; + +constexpr StringView example_data = "1234abc\n"sv; +constexpr StringView empty; + +constexpr StringView minisign_signature_text = R"""(untrusted comment: signature from minisign secret key +RURaxH7X/Uvk/Q81zoW4nzVrc1gVOQF5PZwD9vxF7TEI6lYC1qvXP4oyPcBiF0QtMDzJZvMj3/M+rm1S0nhxQA0pNtVL3VNFJAg= +trusted comment: timestamp:1745588000 file:1234abc hashed +bRVIYO+dSrQwLhTY6/kk/0qyIb7xrzPA7qq9RPIpOYBo9hnhL0L/IW5WLSiqaA9tSY+PjYjLJQ8GFqFfZyi4AA== +)"""sv; +constexpr StringView signature_text = R"""(untrusted comment: minisign-compatible signature +RUQ7FcRc9BMU2D9h28o+9Ba3QtfQHYdyLItVT6PWt1/PN66gHjumBCqje+eeLvQckrcKQGOQ8vkKXYtzWrkslGZdH/bHlI9txAE= +trusted comment: {"filename":"Tests/LibCrypto/1234abc"} +sEbAITvxLddnp9pAU3GhMO/02dCeG7V73J8JUN0qyj9z9H7B+6bajwu73sKPcTSOLu5cBxDeX8jNVNPziHTsCQ== +)"""sv; + +using namespace Crypto::Minisign; + +TEST_CASE(read_write_keys) +{ + auto const minisign_key_from_file = MUST(PublicKey::from_public_key_file(public_key_file_minisign_text)); + EXPECT_EQ(MUST(minisign_key_from_file.to_public_key_file()), public_key_file_minisign_text); + + auto const minisign_key = MUST(PublicKey::from_base64(public_key_minisign_text.bytes())); + EXPECT_EQ(minisign_key_from_file.public_key(), minisign_key.public_key()); + + auto const minisign_secret_key_from_file = MUST(SecretKey::from_secret_key_file(Core::SecretString::take_ownership(secret_key_minisign_text.to_byte_string().to_byte_buffer()))); + auto output = MUST(minisign_secret_key_from_file.to_secret_key_file()); + auto original = Core::SecretString::take_ownership(secret_key_minisign_text.to_byte_string().to_byte_buffer()); + EXPECT_EQ(output.view(), original.view()); + + auto const key_from_file = MUST(PublicKey::from_public_key_file(public_key_file_text)); + EXPECT_EQ(MUST(key_from_file.to_public_key_file()), public_key_file_text); + + auto const secret_key_from_file = MUST(SecretKey::from_secret_key_file(Core::SecretString::take_ownership(secret_key_text.to_byte_string().to_byte_buffer()))); + output = MUST(secret_key_from_file.to_secret_key_file()); + original = Core::SecretString::take_ownership(secret_key_text.to_byte_string().to_byte_buffer()); + EXPECT_EQ(output.view(), original.view()); +} + +TEST_CASE(generate) +{ + auto const secret_key = MUST(SecretKey::generate()); + PublicKey const key = secret_key; + EXPECT_EQ(key.public_key(), secret_key.public_key()); + EXPECT_EQ(key.untrusted_comment().bytes_as_string_view(), secret_key.untrusted_comment().bytes_as_string_view()); + + // Make sure both keys are identical across serialize/deserialize roundtrips. + auto const roundtrip_secret_key = MUST(SecretKey::from_secret_key_file(MUST(secret_key.to_secret_key_file()))); + EXPECT_EQ(roundtrip_secret_key.public_key(), secret_key.public_key()); + auto const roundtrip_key = MUST(PublicKey::from_public_key_file(MUST(key.to_public_key_file()))); + EXPECT_EQ(roundtrip_key.public_key(), key.public_key()); + EXPECT_EQ(roundtrip_key.untrusted_comment().bytes_as_string_view(), key.untrusted_comment().bytes_as_string_view()); + EXPECT_EQ(roundtrip_key.id(), key.id()); + + // Make sure the two key pairs actually belong to each other. + FixedMemoryStream data_stream { example_data.bytes() }; + auto const signature = MUST(secret_key.sign(data_stream, "i am not trustworthy"_string, "i can be trusted with power and responsibility"_string)); + MUST(data_stream.seek(0)); + EXPECT_EQ(MUST(key.verify(signature, data_stream)), VerificationResult::Valid); + MUST(data_stream.seek(0)); + EXPECT_EQ(MUST(roundtrip_key.verify(signature, data_stream)), VerificationResult::Valid); +} + +TEST_CASE(sign_verify) +{ + auto const minisign_key = MUST(PublicKey::from_base64(public_key_minisign_text.bytes())); + auto const minisign_secret_key = MUST(SecretKey::from_secret_key_file(Core::SecretString::take_ownership(secret_key_minisign_text.to_byte_string().to_byte_buffer()))); + auto const key = MUST(PublicKey::from_public_key_file(public_key_file_text)); + auto const secret_key = MUST(SecretKey::from_secret_key_file(Core::SecretString::take_ownership(secret_key_text.to_byte_string().to_byte_buffer()))); + + FixedMemoryStream data_stream { example_data.bytes() }; + auto signature = MUST(secret_key.sign(data_stream, "i am not trustworthy"_string, "i can be trusted with power and responsibility"_string)); + MUST(data_stream.seek(0)); + EXPECT_EQ(MUST(key.verify(signature, data_stream)), VerificationResult::Valid); + MUST(data_stream.seek(0)); + EXPECT_NE(MUST(minisign_key.verify(signature, data_stream)), VerificationResult::Valid); + + // Cannot verify signatures with the wrong key. + FixedMemoryStream minisign_data_stream { example_data.bytes() }; + auto const signature_minisign = MUST(minisign_secret_key.sign(minisign_data_stream, {}, "example trust"_string)); + MUST(minisign_data_stream.seek(0)); + EXPECT_NE(MUST(key.verify(signature_minisign, minisign_data_stream)), VerificationResult::Valid); + MUST(minisign_data_stream.seek(0)); + EXPECT_EQ(MUST(minisign_key.verify(signature_minisign, minisign_data_stream)), VerificationResult::Valid); + + // Signature from same key does not match against different data. + FixedMemoryStream empty_data_stream { empty.bytes() }; + auto const signature_empty = MUST(secret_key.sign(minisign_data_stream, {}, "empty data"_string)); + MUST(data_stream.seek(0)); + EXPECT_NE(MUST(key.verify(signature_empty, data_stream)), VerificationResult::Valid); + + // Signature does match if untrusted comment changed. + signature.untrusted_comment() = "EVIL ATTACKER SAYS HI"_string; + MUST(data_stream.seek(0)); + EXPECT_EQ(MUST(key.verify(signature, data_stream)), VerificationResult::Valid); + + // Signature does *not* match if trusted comment changed. + signature.trusted_comment() = "oh no, I changed the trusted comment!"_string; + MUST(data_stream.seek(0)); + EXPECT_EQ(MUST(key.verify(signature, data_stream)), VerificationResult::GlobalSignatureInvalid); + + // Signature does not match if key id changed. + signature = MUST(secret_key.sign(data_stream, "i am not trustworthy"_string, "i can be trusted with power and responsibility"_string)); + auto key_id_copy = signature.key_id(); + key_id_copy[0] = ~key_id_copy[0]; + signature.set_key_id(key_id_copy); + MUST(data_stream.seek(0)); + EXPECT_EQ(MUST(key.verify(signature, data_stream)), VerificationResult::Invalid); + + // Check previously prepared signatures. + auto const prepared_minisign_signature = MUST(Signature::from_signature_file(minisign_signature_text)); + MUST(data_stream.seek(0)); + EXPECT_EQ(MUST(minisign_key.verify(prepared_minisign_signature, data_stream)), VerificationResult::Valid); + auto const prepared_signature = MUST(Signature::from_signature_file(signature_text)); + MUST(data_stream.seek(0)); + EXPECT_EQ(MUST(key.verify(prepared_signature, data_stream)), VerificationResult::Valid); +} diff --git a/Userland/Libraries/LibCrypto/CMakeLists.txt b/Userland/Libraries/LibCrypto/CMakeLists.txt index 947a9aca34a6d0..934c60bc978254 100644 --- a/Userland/Libraries/LibCrypto/CMakeLists.txt +++ b/Userland/Libraries/LibCrypto/CMakeLists.txt @@ -31,6 +31,7 @@ set(SOURCES Hash/SHA1.cpp Hash/SHA2.cpp NumberTheory/ModularFunctions.cpp + Minisign.cpp PK/RSA.cpp ) diff --git a/Userland/Libraries/LibCrypto/Minisign.cpp b/Userland/Libraries/LibCrypto/Minisign.cpp new file mode 100644 index 00000000000000..2c8e6526e29d1c --- /dev/null +++ b/Userland/Libraries/LibCrypto/Minisign.cpp @@ -0,0 +1,290 @@ +/* + * Copyright (c) 2025, kleines Filmröllchen + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "Minisign.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Crypto::Minisign { + +constexpr StringView untrusted_comment_id = "untrusted comment: "sv; +constexpr StringView trusted_comment_id = "trusted comment: "sv; +// The `D` is capitalized to indicate the pre-hashed signature scheme, which is the only one we support. +constexpr StringView signature_algorithm_id = "ED"sv; +// The ID here is different from the signature algorithm ID since it wasn’t changed for the prehashed scheme (keys are valid for use with both schemes). +constexpr StringView key_signature_algorithm_id = "Ed"sv; +constexpr StringView scrypt_algorithm_id = "Sc"sv; +// BLAKE2b. +constexpr StringView checksum_algorithm_id = "B2"sv; + +// Signature file format: +// +// untrusted comment: +// base64( || || ) +// trusted_comment: +// base64() +ErrorOr Signature::from_signature_file(StringView signature_file_data) +{ + auto const lines = signature_file_data.split_view('\n', SplitBehavior::KeepEmpty); + if (lines.size() < 4) + return Error::from_string_view("Signature file has less than 4 lines"sv); + + auto const untrusted_comment_line = lines[0]; + auto const base64_file_signature_line = lines[1]; + auto const trusted_comment_line = lines[2]; + auto const base64_global_signature_line = lines[3]; + + if (!untrusted_comment_line.starts_with(untrusted_comment_id)) + return Error::from_string_view("Untrusted comment line malformed"sv); + auto const untrusted_comment = TRY(String::from_utf8(untrusted_comment_line.substring_view(untrusted_comment_id.length()))); + if (!trusted_comment_line.starts_with(trusted_comment_id)) + return Error::from_string_view("Trusted comment line malformed"sv); + auto const trusted_comment = TRY(String::from_utf8(trusted_comment_line.substring_view(trusted_comment_id.length()))); + + auto const file_signature_line = TRY(AK::decode_base64(base64_file_signature_line.trim_whitespace())); + auto const global_signature = TRY(AK::decode_base64(base64_global_signature_line.trim_whitespace())); + + auto const signature_algorithm = file_signature_line.span().trim(2); + if (StringView { signature_algorithm } != signature_algorithm_id) + return Error::from_string_view("Unknown signature ID"sv); + auto const key_id = file_signature_line.span().slice(2, 8); + auto const file_signature = file_signature_line.span().slice(10); + + return Signature { + untrusted_comment, + trusted_comment, + TRY(ByteBuffer::copy(file_signature)), + global_signature, + KeyID::from_span(key_id), + }; +} + +ErrorOr Signature::to_signature_file() const +{ + auto file_signature_data = TRY(ByteBuffer::create_uninitialized(10 + m_file_signature.size())); + signature_algorithm_id.bytes().copy_to(file_signature_data); + m_key_id.span().copy_to(file_signature_data.span().slice(2)); + m_file_signature.span().copy_to(file_signature_data.span().slice(10)); + + return ByteString::formatted("untrusted comment: {}\n{}\ntrusted comment: {}\n{}\n", m_untrusted_comment, TRY(AK::encode_base64(file_signature_data.bytes())), m_trusted_comment, TRY(AK::encode_base64(m_global_signature))); +} + +// Public key file format: +// +// untrusted comment: +// base64( || || ) +ErrorOr PublicKey::from_public_key_file(StringView key_file_data) +{ + auto const lines = key_file_data.split_view('\n', SplitBehavior::KeepEmpty); + if (lines.size() < 2) + return Error::from_string_view("Public key file has less than 2 lines"sv); + + auto const untrusted_comment_line = lines[0]; + auto const base64_public_key_line = lines[1]; + + if (!untrusted_comment_line.starts_with(untrusted_comment_id)) + return Error::from_string_view("Untrusted comment line malformed"sv); + auto const untrusted_comment = TRY(String::from_utf8(untrusted_comment_line.substring_view(untrusted_comment_id.length()))); + + auto key = TRY(from_base64(base64_public_key_line.bytes())); + key.m_untrusted_comment = untrusted_comment; + return key; +} + +ErrorOr PublicKey::from_base64(ReadonlyBytes key) +{ + auto const public_key_line = TRY(AK::decode_base64(StringView { key }.trim_whitespace())); + + auto const signature_algorithm = public_key_line.span().trim(2); + if (StringView { signature_algorithm } != key_signature_algorithm_id) + return Error::from_string_view("Unknown algorithm ID"sv); + auto const key_id = public_key_line.span().slice(2, 8); + auto const public_key = public_key_line.span().slice(10); + + return PublicKey { {}, KeyID::from_span(key_id), TRY(ByteBuffer::copy(public_key)) }; +} + +ErrorOr PublicKey::to_public_key_file() const +{ + auto key_data = TRY(ByteBuffer::create_zeroed(10 + m_public_key.size())); + key_signature_algorithm_id.bytes().copy_to(key_data); + m_id.span().copy_to(key_data.span().slice(2)); + m_public_key.span().copy_to(key_data.span().slice(10)); + + return ByteString::formatted("untrusted comment: {}\n{}\n", m_untrusted_comment, TRY(AK::encode_base64(key_data.bytes()))); +} + +ErrorOr SecretKey::from_secret_key_file(Core::SecretString const& key_file) +{ + auto const lines = key_file.view().split_view('\n', SplitBehavior::KeepEmpty); + if (lines.size() < 2) + return Error::from_string_view("Secret key file has less than 2 lines"sv); + + auto const untrusted_comment_line = lines[0]; + auto const base64_secret_key_line = lines[1]; + + if (!untrusted_comment_line.starts_with(untrusted_comment_id)) + return Error::from_string_view("Untrusted comment line malformed"sv); + auto const untrusted_comment = TRY(String::from_utf8(untrusted_comment_line.substring_view(untrusted_comment_id.length()))); + + auto const secret_key_line = TRY(AK::decode_base64(StringView { base64_secret_key_line }.trim_whitespace())); + + ReadonlyBytes reader { secret_key_line }; + + auto const signature_algorithm = reader.trim(2); + if (StringView { signature_algorithm } != key_signature_algorithm_id) + return Error::from_string_view("Unknown algorithm ID"sv); + reader = reader.slice(2); + + auto const kdf_algorithm = reader.trim(2); + if (StringView { kdf_algorithm } == scrypt_algorithm_id) + return Error::from_string_view("Scrypt KDF is not currently supported. Use a key without password protection."sv); + if (StringView { kdf_algorithm } != "\0\0"sv) + return Error::from_string_view("Unknown KDF ID"sv); + reader = reader.slice(2); + + auto const checksum_algorithm = reader.trim(2); + if (StringView { checksum_algorithm } != checksum_algorithm_id) + return Error::from_string_view("Unknown checksum algorithm ID"sv); + reader = reader.slice(2); + + auto const kdf_salt = reader.trim(32); + reader = reader.slice(32); + auto const kdf_opslimit = reader.trim(8); + reader = reader.slice(8); + auto const kdf_memlimit = reader.trim(8); + reader = reader.slice(8); + auto const key_id = reader.trim(8); + reader = reader.slice(8); + auto const temporary_secret_key = reader.trim(Curves::Ed25519 {}.key_size()); + auto secret_key = TRY(ByteBuffer::copy(temporary_secret_key)); + reader = reader.slice(Curves::Ed25519 {}.key_size()); + auto const public_key = reader.trim(Curves::Ed25519 {}.key_size()); + reader = reader.slice(Curves::Ed25519 {}.key_size()); + auto const checksum = reader.trim(Hash::BLAKE2b::DigestSize); + + // These are currently intentionally unused. When no KDF is in used, they’re zeroed out. + (void)kdf_salt; + (void)kdf_opslimit; + (void)kdf_memlimit; + + return SecretKey { + untrusted_comment, + KeyID::from_span(key_id), + TRY(ByteBuffer::copy(public_key)), + Core::SecretString::take_ownership(move(secret_key)), + Array::from_span(checksum), + }; +} + +ErrorOr SecretKey::to_secret_key_file() const +{ + auto key_data = TRY(ByteBuffer::create_zeroed(6 + 32 + 8 + 8 + 104)); + // All unnecessary fields are simply left as zero. + key_signature_algorithm_id.bytes().copy_to(key_data); + checksum_algorithm_id.bytes().copy_to(key_data.span().slice(4)); + // 32 + 8 + 8 bytes of unused KDF data + m_id.span().copy_to(key_data.span().slice(6 + 32 + 8 + 8)); + m_secret_key.view().bytes().copy_to(key_data.span().slice(6 + 32 + 8 + 8 + 8)); + m_public_key.span().copy_to(key_data.span().slice(6 + 32 + 8 + 8 + 8 + m_secret_key.length() - 1)); + // FIXME: Checksum seems to be empty for minisign created secret key files too... let’s hope that doesn’t cause any issues. + + return Core::SecretString::take_ownership(ByteString::formatted("untrusted comment: {}\n{}\n", m_untrusted_comment, TRY(AK::encode_base64(key_data.bytes()))).to_byte_buffer()); +} + +bool Signature::matches_public_key(PublicKey const& public_key) const { return public_key.id() == key_id(); } + +bool PublicKey::matches_secret_key(SecretKey const& secret_key) const { return m_public_key == secret_key.public_key(); } + +ErrorOr Signature::global_data() const +{ + // global_signature = ed25519( || ) + auto global_data = TRY(ByteBuffer::create_uninitialized(m_file_signature.size() + m_trusted_comment.byte_count())); + m_file_signature.span().copy_to(global_data); + m_trusted_comment.bytes().copy_to(global_data.span().slice(m_file_signature.size())); + return global_data; +} + +static ErrorOr stream_hash(Stream& contents) +{ + Hash::BLAKE2b hash; + // Strike some kind of balance between + // - not allocating enough buffer space, which will yield frequent calls to update() and read() and be slow (e.g. Core::File issues one syscall per read) + // - allocating too much buffer space or even reading in the entire file at once, which makes signing large files (common for software packages!) infeasible. + // There may be a better tradeoff, 256 pages (~1MB) was chosen for a buffer size that’s most definitely allocatable. + auto intermediate_buffer = TRY(ByteBuffer::create_uninitialized(256z * PAGE_SIZE)); + while (!contents.is_eof()) { + auto const read_buffer = TRY(contents.read_some(intermediate_buffer)); + if (read_buffer.is_empty()) + continue; + hash.update(read_buffer.data(), read_buffer.size()); + } + return hash.digest(); +} + +ErrorOr PublicKey::verify(Signature const& signature, Stream& contents) const +{ + if (!signature.matches_public_key(*this)) + return VerificationResult::Invalid; + + auto const calculated_hash = TRY(stream_hash(contents)); + + if (!Curves::Ed25519 {}.verify(m_public_key, signature.file_signature(), calculated_hash.bytes())) { + // Note that from a UI perspective we want to skip checking the global signature, and mark both as invalid. + // A valid trusted comment associated with an invalid file signature is basically useless. + return VerificationResult::Invalid; + } + + auto const global_data = TRY(signature.global_data()); + if (!Curves::Ed25519 {}.verify(m_public_key, signature.global_signature(), global_data.bytes())) + return VerificationResult::GlobalSignatureInvalid; + + return VerificationResult::Valid; +} + +ErrorOr SecretKey::sign(Stream& contents, String const& untrusted_comment, String const& trusted_comment) const +{ + auto const hash = TRY(stream_hash(contents)); + + // FIXME: The secret key length trim is an ugly workaround for SecretString adding a null byte to the end of all data. + auto const file_signature = TRY(Curves::Ed25519 {}.sign(m_public_key, m_secret_key.view().bytes().trim(m_secret_key.length() - 1), hash.bytes())); + + // Fill out the global signature with an empty buffer at first so we can now use Signature’s utility function to sign the global data. + Signature signature { untrusted_comment, trusted_comment, file_signature, {}, m_id }; + auto const global_data = TRY(signature.global_data()); + signature.m_global_signature = TRY(Curves::Ed25519 {}.sign(m_public_key, m_secret_key.view().bytes().trim(m_secret_key.length() - 1), global_data.bytes())); + + return signature; +} + +ErrorOr SecretKey::generate() +{ + auto private_key = TRY(Curves::Ed25519 {}.generate_private_key()); + auto const public_key = TRY(Curves::Ed25519 {}.generate_public_key(private_key.bytes())); + KeyID key_id; + fill_with_random(key_id); + + return SecretKey { + "iffysign unencrypted secret key"_string, + key_id, + public_key, + Core::SecretString::take_ownership(move(private_key)), + {}, + }; +} + +} diff --git a/Userland/Libraries/LibCrypto/Minisign.h b/Userland/Libraries/LibCrypto/Minisign.h new file mode 100644 index 00000000000000..48d3bac4eeddc1 --- /dev/null +++ b/Userland/Libraries/LibCrypto/Minisign.h @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2025, kleines Filmröllchen + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +// Minisign signature and key file format support. +// Documentation: https://jedisct1.github.io/minisign/ +namespace Crypto::Minisign { + +using KeyID = Array; + +class Signature; +class PublicKey; +class SecretKey; + +// Pre-hashed (`ED`) signature format of minisign. +class Signature final { + friend class SecretKey; + +public: + // Reads the on-disk signature file format. + static ErrorOr from_signature_file(StringView signature_file_data); + ErrorOr to_signature_file() const; + // Checks that the public key has the same ID as the one that was used to create this signature. + // This is not a guarantee that the signature was created by this key! + bool matches_public_key(PublicKey const&) const; + + constexpr String const& untrusted_comment() const { return m_untrusted_comment; } + constexpr String const& trusted_comment() const { return m_trusted_comment; } + constexpr ReadonlyBytes file_signature() const { return m_file_signature; } + constexpr ReadonlyBytes global_signature() const { return m_global_signature; } + constexpr KeyID key_id() const { return m_key_id; } + + constexpr String& untrusted_comment() { return m_untrusted_comment; } + constexpr String& trusted_comment() { return m_trusted_comment; } + constexpr void set_key_id(KeyID id) { m_key_id = id; } + + // The data signed with the global signature. + ErrorOr global_data() const; + +private: + Signature( + String untrusted_comment, + String trusted_comment, + ByteBuffer file_signature, + ByteBuffer global_signature, + KeyID key_id) + : m_untrusted_comment(move(untrusted_comment)) + , m_trusted_comment(move(trusted_comment)) + , m_file_signature(move(file_signature)) + , m_global_signature(move(global_signature)) + , m_key_id(key_id) + { + } + + String m_untrusted_comment {}; + String m_trusted_comment {}; + ByteBuffer m_file_signature; + ByteBuffer m_global_signature; + KeyID m_key_id; +}; + +// The three different kinds of result from minisig signature verification, +// as we have *two* hashes which can be valid and invalid slightly independently. +enum class VerificationResult : u8 { + // Both signatures are invalid. + Invalid, + // Global signature and file signatures are valid. + Valid, + // File signature is valid, global signature is invalid. + GlobalSignatureInvalid, +}; + +// Ed25519 public key. +class PublicKey final { + friend class SecretKey; + +public: + // Reads the on-disk public key file format. + static ErrorOr from_public_key_file(StringView key_file_data); + // The base64 public key string doesn’t have an untrusted comment, so that field will always be empty. + static ErrorOr from_base64(ReadonlyBytes key); + + ErrorOr to_public_key_file() const; + + // Verify that the signature matches the given contents with this key. + ErrorOr verify(Signature const& signature, Stream& contents) const; + + bool matches_secret_key(SecretKey const&) const; + + constexpr KeyID id() const { return m_id; } + constexpr String const& untrusted_comment() const { return m_untrusted_comment; } + void set_untrusted_comment(String comment) { m_untrusted_comment = move(comment); } + constexpr ReadonlyBytes public_key() const { return m_public_key.span(); } + +private: + PublicKey(String untrusted_comment, KeyID id, ByteBuffer public_key) + : m_untrusted_comment(move(untrusted_comment)) + , m_id(id) + , m_public_key(move(public_key)) + { + } + + String m_untrusted_comment {}; + KeyID m_id; + ByteBuffer m_public_key; +}; + +// Ed25519 Secret key without PKDF. +// FIXME: Implement Scrypt-based password encryption of the secret keys. +class SecretKey final { +public: + // Reads the on-disk secret key file format. + static ErrorOr from_secret_key_file(Core::SecretString const& key_file); + ErrorOr to_secret_key_file() const; + + // Generates a new key pair. + static ErrorOr generate(); + + ErrorOr sign(Stream& contents, String const& untrusted_comment, String const& trusted_comment) const; + + operator PublicKey() const { return { m_untrusted_comment, m_id, m_public_key }; } + + constexpr ReadonlyBytes public_key() const { return m_public_key.bytes(); } + constexpr String const& untrusted_comment() const { return m_untrusted_comment; } + +private: + SecretKey( + String untrusted_comment, + KeyID id, + ByteBuffer public_key, + Core::SecretString secret_key, + Array checksum) + : m_untrusted_comment(move(untrusted_comment)) + , m_id(id) + , m_public_key(move(public_key)) + , m_secret_key(move(secret_key)) + , m_checksum(checksum) + { + } + + String m_untrusted_comment; + KeyID m_id; + ByteBuffer m_public_key; + Core::SecretString m_secret_key; + // Checksummed using BLAKE2b-256. + // FIXME: We don’t have an implementation for this BLAKE2b variant yet. + Array m_checksum; +}; + +} From 7163e75b7b950b1c87f0e7a108deb2343a8771bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?kleines=20Filmr=C3=B6llchen?= Date: Fri, 25 Apr 2025 15:43:23 +0200 Subject: [PATCH 2/2] Utilities: Add iffysign MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a largely minisign/signify-compatible command line tool to create and verify minisign-compatible signatures and keys. It is missing some useful functionality at the moment, namely the ability to recreate public keys from private keys, more output options (quiet, verbose), and some more I/O options for stdin/stdout. It is, however, enough to provide the functionality needed for a signing and verification tool. The name was specifically chosen to not collide with either minisign or signify, to allow ports of those to coexist. (Both minisign and signify are, by their nature of self-contained unixy file I/O tools, very portable (a specifically cross-platform signify port exists), and we don’t want to prevent useful ports if we can avoid it.) --- Base/usr/share/man/man1/iffysign.md | 90 +++++++++++++++ Base/usr/share/man/man1/pkg.md | 4 + Meta/Lagom/CMakeLists.txt | 1 + Userland/Utilities/CMakeLists.txt | 4 +- Userland/Utilities/iffysign.cpp | 173 ++++++++++++++++++++++++++++ 5 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 Base/usr/share/man/man1/iffysign.md create mode 100644 Userland/Utilities/iffysign.cpp diff --git a/Base/usr/share/man/man1/iffysign.md b/Base/usr/share/man/man1/iffysign.md new file mode 100644 index 00000000000000..6f86fb251241dd --- /dev/null +++ b/Base/usr/share/man/man1/iffysign.md @@ -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 `.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 +# 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 diff --git a/Base/usr/share/man/man1/pkg.md b/Base/usr/share/man/man1/pkg.md index 0f93444c9ecec6..1ccbb5d763b891 100644 --- a/Base/usr/share/man/man1/pkg.md +++ b/Base/usr/share/man/man1/pkg.md @@ -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) diff --git a/Meta/Lagom/CMakeLists.txt b/Meta/Lagom/CMakeLists.txt index 0a9a561ef94981..5441900adac83f 100644 --- a/Meta/Lagom/CMakeLists.txt +++ b/Meta/Lagom/CMakeLists.txt @@ -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) diff --git a/Userland/Utilities/CMakeLists.txt b/Userland/Utilities/CMakeLists.txt index 11743225436636..c54ee43376e1ee 100644 --- a/Userland/Utilities/CMakeLists.txt +++ b/Userland/Utilities/CMakeLists.txt @@ -73,6 +73,7 @@ set(CMD_SOURCES_CPP iconv.cpp id.cpp ifconfig.cpp + iffysign.cpp image.cpp image2bin.cpp imgcmp.cpp @@ -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 ) @@ -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) diff --git a/Userland/Utilities/iffysign.cpp b/Userland/Utilities/iffysign.cpp new file mode 100644 index 00000000000000..3360f61adab051 --- /dev/null +++ b/Userland/Utilities/iffysign.cpp @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2025, kleines Filmröllchen . + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +enum class Operation : u8 { + Unspecified, + Sign, + Verify, + GenerateKey, +}; + +using namespace Crypto::Minisign; + +ErrorOr 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'); + + // 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 `.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()); + + auto const write_mode = force_overwrite ? Core::File::OpenMode::Write + : Core::File::OpenMode::MustBeNew | Core::File::OpenMode::Write; + + auto const get_public_key = [&] -> ErrorOr { + 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; +}