#key #ed25519-key #archive-format #layer #curve25519 #parser #openssl

curve25519-parser

Curve25519 解析器 - OpenSSL Ed25519 / X25519 密钥的 DER/PEM 解析器

4 个版本 (重大更改)

0.4.0 2023年9月29日
0.3.0 2022年8月19日
0.2.0 2020年9月14日
0.1.0 2020年8月5日

#1385密码学

Download history 9/week @ 2024-04-28 25/week @ 2024-05-05 209/week @ 2024-05-12 95/week @ 2024-05-19 73/week @ 2024-05-26 84/week @ 2024-06-02 69/week @ 2024-06-09 132/week @ 2024-06-16 111/week @ 2024-06-23 94/week @ 2024-06-30 115/week @ 2024-07-07 97/week @ 2024-07-14 102/week @ 2024-07-21 125/week @ 2024-07-28 131/week @ 2024-08-04 104/week @ 2024-08-11

每月463次下载
3 个crate 中使用

LGPL-3.0-only

46KB
294

Build & test Cargo MLA Documentation MLA Cargo Curve25519-Parser Documentation Curve25519-Parser Cargo MLAR

多层存档(MLA)

MLA 是一种存档文件格式,具有以下特性:

  • 支持压缩(基于 rust-brotli
  • 支持使用非对称密钥进行认证加密(基于 Curve25519 的 ECIES 方案,AES256-GCM,基于 Rust-Crypto aes-ctrDalekCryptography x25519-dalek
  • 高效、架构无关且可移植(完全使用 Rust 编写)
  • 存档创建过程中的内存占用小
  • 可流式存档创建
    • 甚至可以通过数据隔离器构建存档
    • 可以通过数据块添加文件,无需事先知道最终大小
    • 文件块可以交错(可以在添加一个文件的开始后,开始第二个文件,然后继续添加第一个文件的部分)
  • 存档文件是可定位的,即使已压缩或加密。可以在存档的中间访问文件,而无需从开头读取
  • 如果被截断,存档可以修复。仍在存档中的文件以及缺少末尾的文件的开始将被恢复
  • 更不容易出错,特别是在解析不受信任的存档时(Rust 安全性)

仓库

此仓库包含以下内容:

  • mla:实现 MLA 读取器和写入器的 Rust 库
  • mlar:用于常见操作的 Rust 工具,包装 mla(创建、列出、提取等)
  • curve25519-parser:解析 DER/PEM 公钥和私钥 Ed25519 密钥以及 X25519 密钥的 Rust 库(由 openssl 创建)
  • mla-fuzz-afl:用于模糊测试 mla 的 Rust 工具
  • bindings:其他语言的绑定
  • .github:持续集成需求

快速命令行使用

以下是一些使用 mlar 命令以处理 MLA 格式存档的命令。

# Generate an X25519 key pair {key, key.pub} (OpenSSL could also be used)
mlar keygen key

# Create an archive with some files, using the public key
mlar create -p key.pub -o my_archive.mla /etc/os-release /etc/issue

# List the content of the archive, using the private key
mlar list -k key -i my_archive.mla

# Extract the content of the archive into a new directory
# In this example, this creates two files:
# extracted_content/etc/issue and extracted_content/etc/os-release
mlar extract -k key -i my_archive.mla -o extracted_content

# Display the content of a file in the archive
mlar cat -k key -i my_archive.mla /etc/os-release

# Convert the archive to a long-term one, removing encryption and using the best
# and slower compression level
mlar convert -k key -i my_archive.mla -o longterm.mla -l compress -q 11

# Create an archive with multiple recipient
mlar create -p archive.pub -p client1.pub -o my_archive.mla ...

mlar 可以通过以下方式获得

  • Cargo: cargo install mlar
  • 使用支持的操作系统上的最新版本

快速 API 使用

  • 创建一个带压缩和加密的存档
use curve25519_parser::parse_openssl_25519_pubkey;
use mla::config::ArchiveWriterConfig;
use mla::ArchiveWriter;

const PUB_KEY: &[u8] = include_bytes!("samples/test_x25519_pub.pem");

fn main() {
    // Load the needed public key
    let public_key = parse_openssl_25519_pubkey(PUB_KEY).unwrap();

    // Create an MLA Archive - Output only needs the Write trait
    let mut buf = Vec::new();
    // Default is Compression + Encryption, to avoid mistakes
    let mut config = ArchiveWriterConfig::default();
    // The use of multiple public keys is supported
    config.add_public_keys(&vec![public_key]);
    // Create the Writer
    let mut mla = ArchiveWriter::from_config(&mut buf, config).unwrap();

    // Add a file
    mla.add_file("filename", 4, &[0, 1, 2, 3][..]).unwrap();

    // Complete the archive
    mla.finalize().unwrap();
}
  • 分部分地添加文件,以“并发”的方式
...
// A file is tracked by an id, and follows this API's call order:
// 1. id = start_file(filename);
// 2. append_file_content(id, content length, content (impl Read))
// 2-bis. repeat 2.
// 3. end_file(id)

// Start a file and add content
let id_file1 = mla.start_file("fname1").unwrap();
mla.append_file_content(id_file1, file1_part1.len() as u64, file1_part1.as_slice()).unwrap();
// Start a second file and add content
let id_file2 = mla.start_file("fname2").unwrap();
mla.append_file_content(id_file2, file2_part1.len() as u64, file2_part1.as_slice()).unwrap();
// Add a file as a whole
mla.add_file("fname3", file3.len() as u64, file3.as_slice()).unwrap();
// Add new content to the first file
mla.append_file_content(id_file1, file1_part2.len() as u64, file1_part2.as_slice()).unwrap();
// Mark still opened files as finished
mla.end_file(id_file1).unwrap();
mla.end_file(id_file2).unwrap();
  • 从存档中读取文件
use curve25519_parser::parse_openssl_25519_privkey;
use mla::config::ArchiveReaderConfig;
use mla::ArchiveReader;
use std::io;

const PRIV_KEY: &[u8] = include_bytes!("samples/test_x25519_archive_v1.pem");
const DATA: &[u8] = include_bytes!("samples/archive_v1.mla");

fn main() {
    // Get the private key
    let private_key = parse_openssl_25519_privkey(PRIV_KEY).unwrap();

    // Specify the key for the Reader
    let mut config = ArchiveReaderConfig::new();
    config.add_private_keys(&[private_key]);

    // Read from buf, which needs Read + Seek
    let buf = io::Cursor::new(DATA);
    let mut mla_read = ArchiveReader::from_config(buf, config).unwrap();

    // Get a file
    let mut file = mla_read
        .get_file("simple".to_string())
        .unwrap() // An error can be raised (I/O, decryption, etc.)
        .unwrap(); // Option(file), as the file might not exist in the archive

    // Get back its filename, size, and data
    println!("{} ({} bytes)", file.filename, file.size);
    let mut output = Vec::new();
    std::io::copy(&mut file.data, &mut output).unwrap();

    // Get back the list of files in the archive:
    for fname in mla_read.list_files().unwrap() {
        println!("{}", fname);
    }
}

⚠️ 文件名是 String,可能包含路径分隔符(/\..等)。请在使用 API 时考虑这一点,以避免路径遍历问题。

用其他语言使用 MLA

以下语言提供了绑定

设计

正如其名所示,MLA 存档由多个独立层组成。以下部分介绍了 MLA 背后的设计理念。有关更正式的描述,请参阅 FORMAT.md

每一层都作为 Unix PIPE,输入字节并在下一层输出。层由以下部分组成

  • 一个 Writer,实现了 Write trait。它在创建新存档时负责发出字节
  • 一个 Reader,实现了 ReadSeek trait。它在读取存档时负责读取字节
  • 一个 FailSafeReader,仅实现了 Read trait。它在修复存档时负责读取字节

层是考虑到可修复性而设计的。读取它们时决不能需要来自脚本的详细信息,但可以使用脚本来优化读取。例如,使用脚本来定位存档内的文件可以优化到文件的开头,但仍然可以通过读取整个存档直到找到文件来获取信息。

层是可选的,但它们的顺序是强制的。用户可以选择启用或禁用它们。当前顺序如下

  1. 文件存储抽象(不是层)
  2. 原始层(强制)
  3. 压缩层
  4. 加密层
  5. 位置层(强制)
  6. 存储的字节

概述

+----------------+-------------------------------------------------------------------------------------------------------------+
| Archive Header |                                                                                                             | => Final container (File / Buffer / etc.)
+------------------------------------------------------------------------------------------------------------------------------+
                 +-------------------------------------------------------------------------------------------------------------+
                 |                                                                                                             | => Raw layer
                 +-------------------------------------------------------------------------------------------------------------+
                 +-----------+---------+------+---------+------+---------------------------------------------------------------+
                 | E. header | Block 1 | TAG1 | Block 2 | TAG2 | Block 3 | TAG3 | ...                                          | => Encryption layer
                 +-----------+---------+------+---------+------+---------------------------------------------------------------+
                             |         |      |         |      |         |      |                                              |
                             +-------+--      --+-------       -----------      ----+---------+------+---------+ +-------------+
                             | Blk 1 |          | Blk 2                             | Block 3 | ...  | Block n | |    Footer   | => Compression Layer
                             +-------+--      --+-------       -----------      ----+---------+------+---------+ +-------------+
                            /         \                                                             /           \
                           /           \                                                           /             \
                          /             \                                                         /               \
                         +-----------------------------------------------------------------------------------------+
                         |                                                                                         |             => Position layer
                         +-----------------------------------------------------------------------------------------+
                         +-------------+-------------+-------------+-------------+-----------+-------+-------------+
                         | File1 start | File1 data1 | File2 start | File1 data2 | File1 end |  ...  | Files index |             => Files information and content
                         +-------------+-------------+-------------+-------------+-----------+-------+-------------+

层描述

原始层

RawLayer* 中实现(即 RawLayerWriterRawLayerReaderRawLayerFailSafeReader)。

这是最简单的层。它要求在层和最终输出世界之间提供一个 API。它也用于保持数据的起始位置。

位置层

PositionLayer* 中实现。

类似于 RawLayer,这是一个非常简单、实用的层。它跟踪已写入子层的字节数。

例如,文件存储层需要它来跟踪文件流中的位置,以进行索引。

加密层

EncryptionLayer* 中实现。

此层使用对称认证加密与关联数据 (AEAD) 算法 AES-GCM 256 加密数据,并使用基于 Curve25519 的 ECIES 架构加密对称密钥。

ECIES方案被扩展以支持多个公钥:生成一个公钥,然后使用它来与n个用户的公钥执行n次Diffie-Hellman交换。生成的公钥也记录在头部(以便用户重新执行DH交换)。根据ECIES推导后,我们得到n个密钥。这些密钥随后用于加密一个公共密钥k,生成的n个密文存储在层头部。此密钥k将用于存档的对称加密。

除了密钥之外,每个存档还生成一个nonce(8字节)。使用固定的相关数据。

生成过程使用来自crate randOsRng,它使用crate getrandom中的getrandom()getrandom为许多系统提供了实现,列表在此。在Linux上,它使用getrandom()系统调用,并回退到/dev/urandom。在Windows上,它使用RtlGenRandom API(自Windows XP/Windows Server 2003以来可用)。

为了“宁可信其有,不可信其无”,使用OsRng生成的字节对ChaChaRng进行初始化,以构建一个CSPRNG(密码学安全的伪随机数生成器)。此ChaChaRng提供用于密钥和非ce生成中实际使用的字节。

层数据由几个加密块组成,除了最后一个块外,每个块的大小都是固定的。每个块使用包括基本nonce和计数器的IV进行加密。这种构造与STREAM非常相似,除了last_block位。选择不使用它,因为

  • 在编写本文时,存档编写器并不知道当前块是最后一个块。因此,它不能使用特定的IV。为了绕过它,必须在末尾添加一个虚拟的脚部块,从而导致检测最后一个块时增加额外的复杂性。
  • 在STREAM中,使用last_block位来防止未检测到的截断。在MLA中,它已经是文件层级的EndOfArchiveData标签的角色。

因此,要定位和读取给定位置,层将解密包含此位置的块,并在返回解密数据之前验证标签。

作者决定使用椭圆曲线而非RSA,因为

  • 在写作日期之前,尚未找到现成的基于Rust的库
  • 已经存在一个经过安全审计的Curve25519 Rust库
  • Curve25519被广泛使用,并且满足多个标准
  • 常见的论点,如Trail of bits的论点

AES-GCM被使用,因为它是最常用的AEAD算法之一,使用它避免了整个攻击类。此外,它使我们能够依赖硬件加速(如AES-NI)以保持合理的性能。

已审查外部加密库

压缩层

CompressionLayer*中实现。

此层基于Brotli压缩算法(RFC 7932)。每个4MB的明文数据存储在一个单独压缩的数据块中。

此算法,使用大小为1的窗口,能够读取每个数据块,并在获得4MB的明文数据时停止。然后它被重置,并开始解压缩下一个数据块。

为了加快解压缩速度,并使层可寻址,使用了一个尾随信息。它保存压缩大小。知道解压缩大小后,可以通过定位到正确的压缩块的开始,然后解压缩前几个字节,直到达到所需位置来在明文位置进行查找。

尾随信息还用于允许更宽的窗口,以实现更快的解压缩。最后,它还记录了最后一个块的大小,以计算压缩数据和尾随信息之间的边界。

4MB的大小是在更好的压缩(值更高)和更快的查找(值更小)之间的一种权衡。它是在对代表性数据进行基准测试的基础上选择的。通过将压缩质量参数设置得更高(导致过程变慢)也可以实现更好的压缩。

文件存储

文件以一系列存档文件块的形式保存。第一个特殊类型的块指示文件的开始,以及其文件名和文件ID。第二个特殊类型的块指示当前文件的结束。

块包含文件数据,前面是当前块大小和相应的文件ID。即使该格式可以处理流式文件,在写入之前也必须知道文件块的大小。文件ID允许不同文件的块交错。

文件结束块标记了给定文件的结束,并包含其完整内容的SHA256。因此,即使在修复操作中也可以检查文件的完整性。

层尾随信息包含每个文件的大小、其结束块偏移量和其块位置索引。块位置索引允许直接访问。结束块偏移量允许快速获取哈希值,而文件大小简化了需要文件大小在数据之前的格式(如Tar)的转换。

如果此尾随信息不可用,则从开始处读取存档以恢复文件信息。

API指南

存档格式为每个文件提供

  • 一个文件名,它是一个Unicode字符串
  • 数据,它是一系列字节

还计算了一些元数据,例如

  • 文件大小
  • 内容的SHA256哈希值

没有额外的元数据(权限、所有权等),除非给出非常有力的论据,否则可能不会添加。目标是保持文件格式足够简单,并将复杂性留给使用它的代码。例如,权限、所有权等在多个操作系统和文件系统上很难保证;并且会导致更高的复杂性,例如在tar中。出于同样的原因,/\在文件名中没有任何意义;用户必须自己选择如何处理它们(是否存在命名空间?Windows风格的目录等)。

如果仍然希望为其自己的用例具有关联的元数据,推荐的方法是在存档中嵌入一个包含所需元数据的附加文件。

此外,预计文件格式在将来会有所变化,以保持更容易的向后兼容性,至少是版本转换和简单支持。

库提供的API非常简单

  • 添加文件
  • 开始/添加文件块/结束
  • 列出存档中的文件(无序)
  • 获取文件
  • 获取文件哈希值

由于可能需要更通用的API,因此提供了一些辅助器,例如在mla::helpers中,例如

  • StreamWriter:在ArchiveWriter文件上提供Write接口(可能用于不知道文件块大小的场景,例如与io::copy一起使用)
  • linear_extract:线性提取归档。通过减少昂贵的seek操作,提取整个归档的更快方式

是否真的需要新的格式?

由于现有的归档格式众多,可能不需要。

但据作者所知,其中没有一个支持上述功能(但当然,更适用于其他目的)。

例如(根据作者的理解)

  • tar格式在添加文件之前需要知道文件的大小,并且不可寻址
  • zip格式如果删除了尾部,可能会丢失关于文件的信息
  • 7zip格式在向其中添加文件时需要重建整个归档(不可流式传输)。它也非常复杂,因此在解压缩未知归档时更难以审计/信任
  • journald格式不可流式传输。此外,这里不需要一个写者/多个读者,因此释放了journald格式的一些约束
  • 任何归档 + age:可以使用age与归档格式一起提供加密,但可能缺乏与内部归档格式的集成
  • 备份格式通常被编写以避免诸如重复之类的事情,因此它们需要保留更大的结构在内存中,或者它们不可流式传输

调整这些格式可能会产生类似的功能。选择已经做出,以更好地控制格式能够做什么,并(尝试)KISS。

测试

存储库包含

  • 单元测试(对于mlacurve25519-parser),分别测试预期行为
  • 集成测试(对于mlar),测试常见场景,例如createlistto-tar,或create→truncate→repair
  • 基准测试场景(对于mla
  • AFL场景(对于mla
  • 一个格式为v1的已提交归档,以确保随着时间的推移可向后读取

性能

可以通过嵌入式基准测试来评估性能,基于Criterion

已经嵌入了一些场景,例如

  • 文件添加,具有不同的大小和层配置
  • 文件添加,压缩质量不同
  • 文件读取,具有不同的大小和层配置
  • 随机文件读取,具有不同的大小和层配置
  • 线性归档提取,具有不同的大小和层配置

在“Intel(R) Core(TM) i5-7200U CPU @ 2.50GHz”上

$ cd mla/
$ cargo bench
...
multiple_layers_multiple_block_size/Layers ENCRYPT | COMPRESS | DEFAULT/1048576                                                                           
                        time:   [28.091 ms 28.259 ms 28.434 ms]
                        thrpt:  [35.170 MiB/s 35.388 MiB/s 35.598 MiB/s]
...
chunk_size_decompress_mutilfiles_random/Layers ENCRYPT | COMPRESS | DEFAULT/4194304                                                                          
                        time:   [126.46 ms 129.54 ms 133.42 ms]
                        thrpt:  [29.980 MiB/s 30.878 MiB/s 31.630 MiB/s]
...
linear_vs_normal_extract/LINEAR / Layers DEBUG | EMPTY/2097152                        
                        time:   [145.19 us 150.13 us 153.69 us]
                        thrpt:  [12.708 GiB/s 13.010 GiB/s 13.453 GiB/s]
...

criterion.rs文档解释了如何获取HTML报告,比较结果等。

AES-NI扩展在编译工具链中启用,适用于支持的架构,这导致加密层在读取操作中获得巨大的性能提升。由于crate aesni静态启用它,如果用户的架构不支持它,可能会导致错误。它可以在编译时禁用,或者在.cargo/config中注释掉相关部分。

模糊测试

mla-fuzz-afl中提供了一个使用afl.rs制作的模糊测试场景。该场景能够

  • 创建包含交错文件的存档,并启用不同层
  • 读取它们以检查其内容
  • 无截断地修复存档,并验证它
  • 更改存档原始数据,并确保读取它不会引发恐慌(只会失败)
  • 修复更改后的存档,并确保恢复不会失败(仅报告检测到的错误)

启动它

  1. 通过取消注释 produce_samples()mla-fuzz-afl/src/main.rs 中生成初始样本
cd mla-fuzz-afl
# ... uncomment `produces_samples()` ...
mkdir in
mkdir out
cargo run
  1. 构建并启动AFL
cargo afl build
cargo afl run -i in -o out ../target/debug/mla-fuzz-afl

如果您发现了崩溃,尝试使用以下任一方式重放它们

  • AFL的秘鲁兔模式: cargo afl run -i - -o out -C ../target/debug/mla-fuzz-afl
  • 直接重放: ../target/debug/mla-fuzz-afl < out/crashes/crash_id
  • 调试:取消注释 mla-fuzz-afl/src/main.rs 中的“重放样本”部分,并在需要时添加 dbg!()

⚠️ 稳定性相当低,可能是由于用于场景的处理过程(从AFL提供的数据反序列化)以及内部算法(如brotli)的可变性。如果有的话,崩溃可能无法重现或由于 mla-fuzz-afl 的内部工作方式较为复杂(因此可能存在错误)。可以通过在 mla-fuzz-afl/src/main.rs 中注释掉不相关的部分来确保更好的体验。

常见问题解答(FAQ)

MLAArchiveWriter 是否是 Send 类型?

默认情况下,MLAArchiveWriter 不是 Send 类型。如果内部可写类型也是 Send,可以在 Cargo.toml 中为 mla 启用 send 功能,例如

[dependencies]
mla = { version = "...", default-features = false, features = ["send"]}

如何确定性地生成一个密钥对?

mlar keygen--seed 选项可以用来确定性地生成一个密钥对。例如,它可以用于可重复测试或安全地存档密钥。

⚠️ 如果不知道为什么这么做,不推荐使用 seed。结果的私钥安全性依赖于种子安全性。特别是

  • 如果攻击者知道了 seed,他就知道了私钥
  • 结果的私钥熵最多与 seed 相同

用于生成的算法如下

  1. 给定一个 seed,将其编码为UTF8字节的UTF8序列 bytes
  2. prng_seed= SHA512(bytes)[0..32]
  3. secret=ChaCha-20rounds(prng_seed)
  4. secret 在经过Curve-25519参考指定的clamp处理后,用作私钥

如何设置“分层密钥基础设施”?

mlar 提供了一个子命令 keyderive,可以从给定的密钥和派生路径(类似于BIP-32,除了子公钥不能从父密钥派生)确定性地派生子密钥。

例如,如果想要派生以下方案

root_key
    ├──["App X"]── key_app_x
    │   └──["v1.2.3"]── key_app_x_v1.2.3
    └──["App Y"]── key_app_y

可以使用以下命令

# Create the root key (--seed can be used if this key must be created deterministically, see above)
mlar keygen root_key
# Create App keys
mlar keyderive root_key key_app_x --path "App X"
mlar keyderive root_key key_app_y --path "App Y"
# Create the v1.2.3 key of App X
mlar keyderive key_app_x key_app_x_v1.2.3 --path "v1.2.3"

在此阶段,假设发生了故障并且密钥已丢失。

可以从私钥 root_key 中恢复所有密钥。例如,要恢复 key_app_v1.2.3

mlar keyderive root_key recovered_key --path "App X" --path "v1.2.3"

因此,如果 App X 的所有者只知道 key_app_x,则他可以恢复其所有子密钥,包括 key_app_v1.2.3,但排除 key_app_y

⚠️ 此方案不提供任何撤销机制。如果父密钥被泄露,其子树中的所有密钥都必须被视为已泄露(即从它可以获得的过去和未来的所有密钥)。反之则不然:如果其任一子密钥被泄露,父密钥仍然安全。

用于生成的算法如下

  1. 给定一个 私钥,将其 密钥 提取为 32 字节值(Curve 25519 的钳位私钥)

  2. 对于每个 路径 以 UTF8 编码

    1. 使用以下 HKDF-SHA512 函数(RFC5869)从密钥中派生一个种子:HKDF-SHA512(salt="PATH DERIVATION" ASCII-encoded, ikm=从父密钥提取的密钥, info=派生路径)
    2. 将前 32 字节作为 ChaCha-20 轮的 PRNG 的种子
    3. ChaCha 的第一个 32 字节输出(根据 Curve-25519 参考规范进行钳位)用作新的私钥
  3. 使用最后计算的私钥作为结果密钥

依赖项

~4–5MB
~105K SLoC