mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-05 10:10:22 +00:00
Merge branch 'main' into main
This commit is contained in:
22 files changed
+432
-87
No files matched your search
+3
-3
@@ -13,13 +13,13 @@ Every asset PdfCraft includes, bundles or uses to build its published material,
|
||||
| `crates/ui-egui/src/i18n/zh-hant.tsv` | Traditional Chinese (Taiwan) interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `crates/ui-egui/src/i18n/cs.tsv` | Czech interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 signature-check and standard-identification wordings are contributor-original AI-assisted translations from English, not separately native-reviewed | Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `crates/ui-egui/src/i18n/pt-br.tsv` | Brazilian Portuguese interface translation catalog | André Cardoso (andrecards), PdfCraft contributors | MIT OR Apache-2.0 | Clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources: the ~1,826-entry gist posted in issue #288 (https://gist.github.com/andrecards/ac388c25fba17312ce83207f8b7b467e), completed and reviewed by PdfCraft contributors; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 text-field-table limit messages are contributor-original AI-assisted translations from English, not separately native-reviewed | Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `crates/ui-egui/src/i18n/es.tsv` | Spanish interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; the 2026-10-20 coverage additions are AI-assisted and not separately native-reviewed; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `crates/ui-egui/src/i18n/es.tsv` | Spanish interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; the 2026-10-20 coverage additions are AI-assisted and not separately native-reviewed; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 signature-check wordings are contributor-original AI-assisted translations from English, not separately native-reviewed | Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `crates/ui-egui/src/i18n/fr.tsv` | French interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `crates/ui-egui/src/i18n/de.tsv` | German interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `crates/ui-egui/src/i18n/lo.tsv` | Lao interface translation catalog | @bounkirdni-2025 | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft English UI labels, no proprietary localisation resources; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Lao UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `crates/ui-egui/src/i18n/te.tsv` | Telugu interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `crates/ui-egui/src/i18n/it.tsv` | Italian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `crates/ui-egui/src/i18n/pl.tsv` | Polish interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `crates/ui-egui/src/i18n/pl.tsv` | Polish interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 signature-check wordings are contributor-original AI-assisted translations from English, not separately native-reviewed | Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `crates/ui-egui/src/i18n/ru.tsv` | Russian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `crates/ui-egui/src/i18n/hu.tsv` | Hungarian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `crates/ui-egui/src/i18n/bg.tsv` | Bulgarian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
@@ -285,7 +285,7 @@ Every asset PdfCraft includes, bundles or uses to build its published material,
|
||||
| `packaging/macos/dmg/background.tiff` | PdfCraft macOS DMG window background (rendered) | @XusBadia | MIT OR Apache-2.0 | Rendered from packaging/macos/dmg/background.svg by packaging/macos/dmg/generate.py | 1x + 2x HiDPI 16-colour palette TIFF copied into the macOS DMG by packaging/macos/package.sh; includes the app icon and the free LittleCMS built-in sRGB profile |
|
||||
| `crates/sign/tests/data/x509-rsa-sha1.pdf` | One-page PDF with a legacy adbe.x509.rsa_sha1 signature by the RSA test key | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: hand-written PDF objects; signature made with `openssl dgst -sha1 -sign` over its byte ranges (see crates/sign/tests/data/README.md) | Signature validation tests (crates/sign/tests/it/pdf.rs): the legacy adbe.x509.rsa_sha1 format |
|
||||
| `assets/icons/monitor.svg` | Lucide icon "monitor" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/monitor.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
||||
| `crates/ui-egui/src/i18n/vi.tsv` | Vietnamese interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels; PhotoCraft and VectorCraft Vietnamese catalogs used only as translation memory when English source and semantic context matched; original catalog reviewed by a native Vietnamese-speaking contributor; 2026-10-15 coverage additions are AI-assisted and not separately native-reviewed; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Vietnamese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `crates/ui-egui/src/i18n/vi.tsv` | Vietnamese interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels; PhotoCraft and VectorCraft Vietnamese catalogs used only as translation memory when English source and semantic context matched; original catalog reviewed by a native Vietnamese-speaking contributor; 2026-10-15 coverage additions are AI-assisted and not separately native-reviewed; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 signature-check wordings are contributor-original AI-assisted translations from English, not separately native-reviewed | Vietnamese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `crates/ui-egui/src/i18n/sl.tsv` | Slovene interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
| `vendor/vello_common/assets/probe.png` | vello_common probe reference image (rendered from probe.rgba) | Linebender vello contributors | MIT OR Apache-2.0 | vello_common 0.0.8 (crates.io), vendored in vendor/vello_common; regenerated by its vello_sparse_tests regenerate_probe_reference binary | Reference picture of the vendored vello_common probe module, for people only; the build reads probe.rgba, not this file |
|
||||
| `crates/ui-egui/src/i18n/nb.tsv` | Norwegian Bokmål interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-22 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Norwegian Bokmål UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
|
||||
|
||||
+12
-12
@@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "122325522c8619e756f3c580cecda3aed37fe6069f01c60090bf3a95b46fb94c"
|
||||
sha256 = "b08f8282923d2faaf1f658d2aff8f71fe5f2d3a33e792eeb50fb0e2b396ef24f"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/pt-br.tsv"
|
||||
@@ -63,18 +63,18 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "4fdaca243d62f3b9ddfd1909eafbe4cf96deec2c8bf40e95db4af9e00a5a4f46"
|
||||
sha256 = "df17dada3df736e75bd9b03e5ecceec164cce63767e33a1efdb98c56a121c890"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/es.tsv"
|
||||
title = "Spanish interface translation catalog"
|
||||
author = "PdfCraft contributors"
|
||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; the 2026-10-20 coverage additions are AI-assisted and not separately native-reviewed; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed"
|
||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; the 2026-10-20 coverage additions are AI-assisted and not separately native-reviewed; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 signature-check wordings are contributor-original AI-assisted translations from English, not separately native-reviewed"
|
||||
licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "c1a622dbbac7641b5e9782ef16eb66f4120ee921e535fd487e415df85280391d"
|
||||
sha256 = "8e8c0cfda0886c584e5286370ec00814206c3af3699c76665e636fa6fdb6310b"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/fr.tsv"
|
||||
@@ -135,12 +135,12 @@ sha256 = "7c7d7664df440402c4183d1df1e47dc843c52c97607a64f4c2e717499cd62924"
|
||||
path = "crates/ui-egui/src/i18n/pl.tsv"
|
||||
title = "Polish interface translation catalog"
|
||||
author = "PdfCraft contributors"
|
||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed"
|
||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 signature-check wordings are contributor-original AI-assisted translations from English, not separately native-reviewed"
|
||||
licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "5e886c7f8d234d7f2a0f12f444f4edb4071a09b2f3e5982997f40d4618ef2150"
|
||||
sha256 = "0bd75a93f8d18deb832223bd0642cc4f4abde04a322b684a143a9d4b4687a344"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/ru.tsv"
|
||||
@@ -250,7 +250,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "6be986ff819002ba6670cd31a36c02b7ef90cdb9e1bb50dc0555533a22c92a70"
|
||||
sha256 = "bbef7f4e16f83e9e0116278e7a0a175bad79757f96a18dc639516572533f6d5b"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/hi.tsv"
|
||||
@@ -292,7 +292,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Romanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "26f8f101233f04bba7e113e5440466c79322c9ef223319f0d06cea7fc344de4f"
|
||||
sha256 = "00a2153c20a191fbe04f1ce61f140fd9628cfe2f8f75dad5bce8a7406e1bd091"
|
||||
|
||||
|
||||
[[asset]]
|
||||
@@ -304,7 +304,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Finnish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "ccb789a550cac8acd8f1ef24205f68ebc2d2fb2b2dba9691ec70c3b7aa491818"
|
||||
sha256 = "609efa92054aa76de4bb6062c0fcc0a8b450519f4b253b4c0f395b3cbb34d2ba"
|
||||
|
||||
[[asset]]
|
||||
path = "assets/icons/accessibility.svg"
|
||||
@@ -3421,12 +3421,12 @@ sha256 = "d1a443233345724859de8e4fb48968ba91e885e12ddbd710701548bd91b39428"
|
||||
path = "crates/ui-egui/src/i18n/vi.tsv"
|
||||
title = "Vietnamese interface translation catalog"
|
||||
author = "PdfCraft contributors"
|
||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels; PhotoCraft and VectorCraft Vietnamese catalogs used only as translation memory when English source and semantic context matched; original catalog reviewed by a native Vietnamese-speaking contributor; 2026-10-15 coverage additions are AI-assisted and not separately native-reviewed; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed"
|
||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels; PhotoCraft and VectorCraft Vietnamese catalogs used only as translation memory when English source and semantic context matched; original catalog reviewed by a native Vietnamese-speaking contributor; 2026-10-15 coverage additions are AI-assisted and not separately native-reviewed; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed; 2026-10-22 signature-check wordings are contributor-original AI-assisted translations from English, not separately native-reviewed"
|
||||
licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Vietnamese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "b75cc4091169c015c15b86a798daed5fec31986d360fc1e98f3b819c24fe44f4"
|
||||
sha256 = "57f0dd007ad16518fbc87d8aeac840ef6d86412bb460aff9fb17b176408fb7a8"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/sl.tsv"
|
||||
@@ -3537,4 +3537,4 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Catalan UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "51d7ee5e254d1234c96746bbc283bf902f3e668a73f3cee566e16ae62aa4562d"
|
||||
sha256 = "5879ee3540ffb630fc83d0f2cb293cc6cbf2e081ea4577161a770794e8cbd22c"
|
||||
@@ -1,4 +1,17 @@
|
||||
//! Encrypt a PDF: `cargo run -p pdfcraft-cos --example protect in.pdf out.pdf rc4-40|rc4-128|aes-128|aes-256 USER OWNER`
|
||||
/// 32 fresh bytes for the file key. `RandomState` is seeded by the operating system (as the
|
||||
/// engine's `entropy` uses it), so every run encrypts with a different key.
|
||||
fn seed() -> [u8; 32] {
|
||||
use std::hash::{BuildHasher, Hasher};
|
||||
let mut out = [0u8; 32];
|
||||
for (i, chunk) in out.chunks_mut(8).enumerate() {
|
||||
let mut h = std::collections::hash_map::RandomState::new().build_hasher();
|
||||
h.write_usize(i);
|
||||
chunk.copy_from_slice(&h.finish().to_le_bytes());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let a: Vec<String> = std::env::args().skip(1).collect();
|
||||
let [input, output, alg, user, owner] = &a[..] else { panic!("usage: protect in out alg user owner") };
|
||||
@@ -15,7 +28,7 @@ fn main() {
|
||||
owner_password: owner,
|
||||
permissions: -1,
|
||||
encrypt_metadata: true,
|
||||
seed: [1; 32],
|
||||
seed: seed(),
|
||||
})
|
||||
.unwrap();
|
||||
std::fs::write(output, pdfcraft_cos::write_full(&doc, &Default::default()).unwrap()).unwrap();
|
||||
|
||||
@@ -858,7 +858,7 @@ impl Document {
|
||||
/// Decrypt a freshly parsed indirect object if the document is encrypted.
|
||||
fn decrypted(&self, id: ObjRef, o: Object) -> Object {
|
||||
match &self.security {
|
||||
Some(h) if Some(id.num) != self.encrypt_num => crate::security::transform(h, &o, id.num, id.generation, true),
|
||||
Some(h) if Some(id.num) != self.encrypt_num => crate::security::transform(h, &o, id.num, id.generation, crate::security::Pass::Decrypt),
|
||||
_ => o,
|
||||
}
|
||||
}
|
||||
|
||||
+75
-13
@@ -88,42 +88,104 @@ fn named_crypt_filter(dict: &Dict) -> Option<Vec<u8>> {
|
||||
/// whose `/Contents` is never encrypted (ISO 32000-2 §7.6.2, §12.8.1): the signer writes a
|
||||
/// placeholder, hashes the file around it and patches the value in place, so encrypting it
|
||||
/// would make signing impossible. Identified as other readers do: by `/Type`, or by a string
|
||||
/// `/Contents` alongside an array `/ByteRange` (`/Type` is optional).
|
||||
/// `/Contents` alongside an array `/ByteRange` (`/Type` is optional). A signature dictionary
|
||||
/// has no `/Subtype`; a dictionary with one (an annotation, whose `/Contents` is the comment
|
||||
/// text) or with another `/Type` is never exempt, whatever else it carries.
|
||||
fn is_signature_dict(d: &Dict) -> bool {
|
||||
matches!(d.name(b"Type"), Some(b"Sig" | b"DocTimeStamp"))
|
||||
|| (matches!(d.get(b"Contents"), Some(Object::String(_))) && matches!(d.get(b"ByteRange"), Some(Object::Array(_))))
|
||||
if d.get(b"Subtype").is_some() {
|
||||
return false;
|
||||
}
|
||||
match d.get(b"Type") {
|
||||
Some(_) => matches!(d.name(b"Type"), Some(b"Sig" | b"DocTimeStamp")),
|
||||
None => matches!(d.get(b"Contents"), Some(Object::String(_))) && matches!(d.get(b"ByteRange"), Some(Object::Array(_))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrypt (`decrypt = true`) or encrypt every string and stream in an indirect object,
|
||||
/// except a signature dictionary's `/Contents` (see [`is_signature_dict`]).
|
||||
pub(crate) fn transform(h: &SecurityHandler, o: &Object, num: u32, generation: u16, decrypt: bool) -> Object {
|
||||
/// Remove the `/Crypt` entry from a stream's filter chain, with its decode parameters.
|
||||
fn drop_crypt_filter(dict: &mut Dict) {
|
||||
let i = match dict.get(b"Filter") {
|
||||
Some(Object::Array(a)) => a.iter().position(|n| n.as_name() == Some(b"Crypt")),
|
||||
Some(Object::Name(n)) if n.as_slice() == b"Crypt" => Some(0),
|
||||
_ => None,
|
||||
};
|
||||
let Some(i) = i else { return };
|
||||
let mut filters = match dict.get(b"Filter") {
|
||||
Some(Object::Array(a)) => a.clone(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
if i < filters.len() {
|
||||
filters.remove(i);
|
||||
}
|
||||
// A lone parameter dictionary belongs to the first filter (as `named_crypt_filter` reads it).
|
||||
match dict.get(b"DecodeParms").cloned() {
|
||||
Some(Object::Array(mut parms)) if i < parms.len() => {
|
||||
parms.remove(i);
|
||||
dict.set(b"DecodeParms".to_vec(), Object::Array(parms));
|
||||
}
|
||||
Some(Object::Dict(_)) if i == 0 => {
|
||||
dict.remove(b"DecodeParms");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if filters.is_empty() {
|
||||
dict.remove(b"Filter");
|
||||
if matches!(dict.get(b"DecodeParms"), Some(Object::Array(p)) if p.is_empty()) {
|
||||
dict.remove(b"DecodeParms");
|
||||
}
|
||||
} else {
|
||||
dict.set(b"Filter".to_vec(), Object::Array(filters));
|
||||
}
|
||||
}
|
||||
|
||||
/// What [`transform`] does to an object.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum Pass {
|
||||
/// Decrypt, with the handler the document was opened with.
|
||||
Decrypt,
|
||||
/// Encrypt with the handler the document was opened with: streams keep their crypt filters.
|
||||
Encrypt,
|
||||
/// Encrypt with protection applied by `set_encryption`. A stream's crypt filter came from
|
||||
/// the source's `/Encrypt`; unless the new one defines that name it is dropped, so the
|
||||
/// stream follows the new policy instead of reading as `/Identity` and staying in clear.
|
||||
EncryptReplacing,
|
||||
}
|
||||
|
||||
/// Decrypt or encrypt every string and stream in an indirect object, except a signature
|
||||
/// dictionary's `/Contents` (see [`is_signature_dict`]).
|
||||
pub(crate) fn transform(h: &SecurityHandler, o: &Object, num: u32, generation: u16, pass: Pass) -> Object {
|
||||
let decrypt = pass == Pass::Decrypt;
|
||||
let strings = |s: &PdfString| {
|
||||
let bytes = if decrypt { h.decrypt_string(num, generation, &s.bytes) } else { h.encrypt_string(num, generation, &s.bytes) };
|
||||
PdfString { bytes, hex: s.hex || !decrypt }
|
||||
};
|
||||
walk(o, &strings, h, num, generation, decrypt)
|
||||
walk(o, &strings, h, num, generation, pass)
|
||||
}
|
||||
|
||||
fn walk(o: &Object, strings: &dyn Fn(&PdfString) -> PdfString, h: &SecurityHandler, num: u32, generation: u16, decrypt: bool) -> Object {
|
||||
fn walk(o: &Object, strings: &dyn Fn(&PdfString) -> PdfString, h: &SecurityHandler, num: u32, generation: u16, pass: Pass) -> Object {
|
||||
let decrypt = pass == Pass::Decrypt;
|
||||
match o {
|
||||
Object::String(s) => Object::String(strings(s)),
|
||||
Object::Array(a) => Object::Array(a.iter().map(|x| walk(x, strings, h, num, generation, decrypt)).collect()),
|
||||
Object::Array(a) => Object::Array(a.iter().map(|x| walk(x, strings, h, num, generation, pass)).collect()),
|
||||
Object::Dict(d) if is_signature_dict(d) => Object::Dict(
|
||||
d.iter()
|
||||
.map(|(k, v)| match (k.as_slice(), v) {
|
||||
(b"Contents", Object::String(_)) => (k.clone(), v.clone()),
|
||||
_ => (k.clone(), walk(v, strings, h, num, generation, decrypt)),
|
||||
_ => (k.clone(), walk(v, strings, h, num, generation, pass)),
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
Object::Dict(d) => Object::Dict(d.iter().map(|(k, v)| (k.clone(), walk(v, strings, h, num, generation, decrypt))).collect()),
|
||||
Object::Dict(d) => Object::Dict(d.iter().map(|(k, v)| (k.clone(), walk(v, strings, h, num, generation, pass))).collect()),
|
||||
Object::Stream(s) => {
|
||||
let dict: Dict = s.dict.iter().map(|(k, v)| (k.clone(), walk(v, strings, h, num, generation, decrypt))).collect();
|
||||
let mut dict: Dict = s.dict.iter().map(|(k, v)| (k.clone(), walk(v, strings, h, num, generation, pass))).collect();
|
||||
let ty = s.dict.name(b"Type");
|
||||
if ty == Some(b"XRef") || (ty == Some(b"Metadata") && !h.encrypts_metadata()) {
|
||||
return Object::Stream(Stream { dict, raw: s.raw.clone() });
|
||||
}
|
||||
let named = named_crypt_filter(&s.dict);
|
||||
let mut named = named_crypt_filter(&s.dict);
|
||||
if pass == Pass::EncryptReplacing && named.as_deref().is_some_and(|n| !h.dict().crypt_filters.iter().any(|(f, _)| f == n)) {
|
||||
drop_crypt_filter(&mut dict);
|
||||
named = None;
|
||||
}
|
||||
let kind = match (&named, ty) {
|
||||
(Some(n), _) => StreamKind::Named(n),
|
||||
(None, Some(b"EmbeddedFile")) => StreamKind::EmbeddedFile,
|
||||
|
||||
@@ -132,7 +132,10 @@ fn write_stream(s: &Stream, out: &mut Vec<u8>) {
|
||||
/// except the `/Encrypt` dictionary itself (`source_num` is its number in the source document).
|
||||
fn prepared<'a>(doc: &Document, source_num: u32, num: u32, generation: u16, o: &'a Object) -> Cow<'a, Object> {
|
||||
match doc.output_security() {
|
||||
(Some(h), encrypt_num) if Some(source_num) != encrypt_num => Cow::Owned(crate::security::transform(h, o, num, generation, false)),
|
||||
(Some(h), encrypt_num) if Some(source_num) != encrypt_num => {
|
||||
let pass = if doc.encryption_changed() { crate::security::Pass::EncryptReplacing } else { crate::security::Pass::Encrypt };
|
||||
Cow::Owned(crate::security::transform(h, o, num, generation, pass))
|
||||
}
|
||||
_ => Cow::Borrowed(o),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//! with our reader and with an independent one (hayro-syntax's decryption). Also editing
|
||||
//! encrypted documents incrementally, changing and removing passwords, and permissions.
|
||||
|
||||
use std::io::Write;
|
||||
use std::sync::Arc;
|
||||
|
||||
use pdfcraft_cos::{Algorithm, Auth, CosError, Document, NewEncryption, ObjRef, Object, PdfString, SaveOptions, write_full, write_incremental};
|
||||
@@ -9,11 +10,16 @@ use pdfcraft_cos::{Algorithm, Auth, CosError, Document, NewEncryption, ObjRef, O
|
||||
const SECRET: &str = "BT /F1 18 Tf 20 100 Td (Secret page) Tj ET";
|
||||
|
||||
fn fixture() -> Vec<u8> {
|
||||
fixture_with("")
|
||||
}
|
||||
|
||||
/// The fixture with `extra` added to the content stream's dictionary.
|
||||
fn fixture_with(extra: &str) -> Vec<u8> {
|
||||
let objs = [
|
||||
"<< /Type /Catalog /Pages 2 0 R /Metadata 6 0 R >>".to_string(),
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 300 200] >>".to_string(),
|
||||
"<< /Type /Page /Parent 2 0 R /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>".to_string(),
|
||||
format!("<< /Length {} >>\nstream\n{SECRET}\nendstream", SECRET.len()),
|
||||
format!("<< /Length {}{extra} >>\nstream\n{SECRET}\nendstream", SECRET.len()),
|
||||
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>".to_string(),
|
||||
"<< /Type /Metadata /Subtype /XML /Length 22 >>\nstream\n<x:xmpmeta>meta</x:xmpmeta>\nendstream".replace("22", "26"),
|
||||
"<< /Title (Quarterly numbers) >>".to_string(),
|
||||
@@ -209,11 +215,10 @@ fn unencrypted_metadata_is_left_readable() {
|
||||
#[test]
|
||||
fn qpdf_agrees() {
|
||||
let qpdf = pdfcraft_oracle::or_skip!(pdfcraft_oracle::qpdf());
|
||||
let dir = std::env::temp_dir().join(format!("pdfcraft-enc-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let dir = private_temp_dir("pdfcraft-enc");
|
||||
for alg in ALL {
|
||||
let path = dir.join(format!("{alg:?}.pdf"));
|
||||
std::fs::write(&path, protect(alg, "user", "owner", -1)).unwrap();
|
||||
std::fs::File::create_new(&path).and_then(|mut f| f.write_all(&protect(alg, "user", "owner", -1))).unwrap();
|
||||
for (pw, expect) in [("user", "user password"), ("owner", "owner password")] {
|
||||
let out = qpdf.command().arg(format!("--password={pw}")).arg("--show-encryption").arg(&path).output().unwrap();
|
||||
let text = String::from_utf8_lossy(&out.stdout);
|
||||
@@ -225,6 +230,35 @@ fn qpdf_agrees() {
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
/// A new directory for oracle files, private to this user: an unpredictable name (`RandomState`
|
||||
/// is seeded by the operating system), created by this call or not at all.
|
||||
fn private_temp_dir(prefix: &str) -> std::path::PathBuf {
|
||||
use std::hash::{BuildHasher, Hasher};
|
||||
let mut name = std::collections::hash_map::RandomState::new().build_hasher();
|
||||
name.write_u32(std::process::id());
|
||||
let dir = std::env::temp_dir().join(format!("{prefix}-{:016x}", name.finish()));
|
||||
#[cfg(unix)]
|
||||
let builder = {
|
||||
let mut builder = std::fs::DirBuilder::new();
|
||||
std::os::unix::fs::DirBuilderExt::mode(&mut builder, 0o700);
|
||||
builder
|
||||
};
|
||||
#[cfg(not(unix))]
|
||||
let builder = std::fs::DirBuilder::new();
|
||||
builder.create(&dir).unwrap();
|
||||
dir
|
||||
}
|
||||
|
||||
/// Oracle files go in a directory this test created: another local account cannot have
|
||||
/// prepared it (or symlinks inside it) in advance.
|
||||
#[test]
|
||||
fn oracle_files_go_in_a_fresh_directory() {
|
||||
let (a, b) = (private_temp_dir("pdfcraft-enc-probe"), private_temp_dir("pdfcraft-enc-probe"));
|
||||
assert_ne!(a, b);
|
||||
assert_eq!(std::fs::read_dir(&a).unwrap().count(), 0);
|
||||
let _ = (std::fs::remove_dir(a), std::fs::remove_dir(b));
|
||||
}
|
||||
|
||||
/// A signature dictionary's `/Contents` is never encrypted (§7.6.2): signers patch it in place
|
||||
/// after hashing the file around it. Its other strings, and the `/Contents` of anything that
|
||||
/// is not a signature dictionary, are encrypted as usual, and reading leaves it untouched.
|
||||
@@ -279,6 +313,75 @@ fn signature_contents_stay_unencrypted() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Only a signature dictionary keeps its `/Contents` in clear. An annotation that carries a
|
||||
/// `/ByteRange` (or even `/Type /Sig`) is still an annotation, and its comment is encrypted.
|
||||
#[test]
|
||||
fn a_byte_range_does_not_exempt_an_annotation() {
|
||||
for (ty, subtype) in [(Some("Annot"), "Text"), (None, "FreeText"), (Some("Sig"), "Text")] {
|
||||
let mut doc = Document::open(Arc::new(fixture())).unwrap();
|
||||
let mut note = pdfcraft_cos::Dict::new();
|
||||
if let Some(ty) = ty {
|
||||
note.set(b"Type".to_vec(), Object::name(ty));
|
||||
}
|
||||
note.set(b"Subtype".to_vec(), Object::name(subtype));
|
||||
note.set(b"Contents".to_vec(), PdfString::literal(b"Private note".to_vec()));
|
||||
note.set(b"ByteRange".to_vec(), Object::Array(Vec::new()));
|
||||
let note = doc.add(Object::Dict(note));
|
||||
doc.update_dict(doc.root().unwrap(), |c| c.set(b"Test".to_vec(), Object::Ref(note))).unwrap();
|
||||
doc.set_encryption(&NewEncryption {
|
||||
algorithm: Algorithm::Aes256,
|
||||
user_password: "",
|
||||
owner_password: "owner",
|
||||
permissions: -1,
|
||||
encrypt_metadata: true,
|
||||
seed: [3; 32],
|
||||
})
|
||||
.unwrap();
|
||||
let bytes = write_full(&doc, &SaveOptions { object_streams: false, ..SaveOptions::default() }).unwrap();
|
||||
assert!(!String::from_utf8_lossy(&bytes).contains("Private note"), "{ty:?} {subtype}: comment encrypted");
|
||||
let back = Document::open(Arc::new(bytes)).unwrap();
|
||||
let note = back.get(back.root().unwrap()).as_dict().unwrap().reference(b"Test").unwrap();
|
||||
assert_eq!(back.get(note).as_dict().unwrap().get(b"Contents").unwrap().as_string().unwrap().bytes, b"Private note");
|
||||
}
|
||||
}
|
||||
|
||||
/// Protecting a document puts every stream under the new handler. A stream that selects a
|
||||
/// crypt filter of its own loses the selector: the new `/Encrypt` defines only `/StdCF`, so an
|
||||
/// inherited name (or `/Identity`) would leave the stream in clear.
|
||||
#[test]
|
||||
fn protecting_drops_inherited_crypt_filter_selectors() {
|
||||
for selector in [" /Filter /Crypt /DecodeParms << /Name /MyCF >>", " /Filter [/Crypt] /DecodeParms [<< /Name /Identity >>]", " /Filter /Crypt"] {
|
||||
let mut doc = Document::open(Arc::new(fixture_with(selector))).unwrap();
|
||||
assert_eq!(content(&doc), SECRET, "{selector}");
|
||||
doc.set_encryption(&NewEncryption {
|
||||
algorithm: Algorithm::Aes256,
|
||||
user_password: "",
|
||||
owner_password: "owner",
|
||||
permissions: -1,
|
||||
encrypt_metadata: true,
|
||||
seed: [7; 32],
|
||||
})
|
||||
.unwrap();
|
||||
let bytes = write_full(&doc, &SaveOptions::default()).unwrap();
|
||||
assert!(!String::from_utf8_lossy(&bytes).contains("Secret page"), "{selector}: stream encrypted");
|
||||
assert_eq!(content(&Document::open(Arc::new(bytes.clone())).unwrap()), SECRET, "{selector}");
|
||||
assert!(hayro_syntax::Pdf::new(bytes).is_ok(), "{selector}");
|
||||
}
|
||||
// Under the handler the document was opened with, `/Identity` is the author's choice (a
|
||||
// stream meant to stay readable) and is kept.
|
||||
let mut doc = Document::open(Arc::new(protect(Algorithm::Aes128, "", "owner", -1))).unwrap();
|
||||
let catalog = doc.get(doc.root().unwrap()).as_dict().cloned().unwrap();
|
||||
let pages = doc.get(catalog.reference(b"Pages").unwrap()).as_dict().cloned().unwrap();
|
||||
let page = doc.resolve(&pages.get(b"Kids").unwrap().as_array().unwrap()[0]).as_dict().cloned().unwrap();
|
||||
let contents = page.reference(b"Contents").unwrap();
|
||||
let Object::Stream(mut s) = doc.get(contents).as_ref().clone() else { panic!("no content stream") };
|
||||
s.dict.set(b"Filter".to_vec(), Object::name("Crypt"));
|
||||
doc.set(contents, Object::Stream(s));
|
||||
let saved = write_incremental(&doc, &SaveOptions::default()).unwrap();
|
||||
assert!(String::from_utf8_lossy(&saved).contains("Secret page"), "written as the stream asks");
|
||||
assert_eq!(content(&Document::open(Arc::new(saved)).unwrap()), SECRET);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_nonpositive_crypt_filter_length_reads_as_the_default_key_length() {
|
||||
// A V4 crypt filter's /Length counts bytes (≤ 32) or bits. A zero or negative value is
|
||||
|
||||
+22
-7
@@ -378,6 +378,7 @@ pub const WORDINGS: &[&str] = &[
|
||||
"The signer's certificate has critical extensions PdfCraft does not recognize ({}).",
|
||||
// Why a certificate that matched the issuer was not used to build the chain.
|
||||
"The certificate of {} is not a CA certificate that may issue certificates, so it is not used to vouch for {}.",
|
||||
"The certificate of {} has critical extensions PdfCraft does not recognize, so it is not used to vouch for {}.",
|
||||
"The certificate of {} does not allow this many CA certificates below it (path length), so it is not used to vouch for {}.",
|
||||
"The certificate of {} was not valid at the time of signing, so it is not used to vouch for {}.",
|
||||
"The signer's identity is unknown because it has not been included in your list of trusted certificates and none of its parent certificates are trusted certificates.",
|
||||
@@ -400,6 +401,7 @@ pub const WORDINGS: &[&str] = &[
|
||||
"The timestamp token could not be read ({}).",
|
||||
"The document has been altered or corrupted since the timestamp was applied.",
|
||||
"The timestamp authority's certificate was not valid at the time of timestamping.",
|
||||
"The timestamp authority's certificate is not issued for time stamping.",
|
||||
"This document has not been modified since this timestamp was applied.",
|
||||
"The document has been modified since this timestamp was applied, but the changes are permitted ({}).",
|
||||
"The document has been altered since this timestamp was applied in ways it does not permit ({}).",
|
||||
@@ -770,13 +772,16 @@ fn validate_into(doc: &Document, bytes: &[u8], trust: &TrustStore, v: &Dict, inf
|
||||
// own claimed time, as without a timestamp.
|
||||
let mut pool = crate::timestamp::token_certs(raw);
|
||||
pool.extend(trust.certs.iter().cloned());
|
||||
let trusted_tsa = t
|
||||
.signer_certificate()
|
||||
.is_some_and(|c| c.valid_at(t.gen_time) && build_chain(&c, &pool, Some(t.gen_time)).iter().any(|x| trust.trusts(x)));
|
||||
if trusted_tsa {
|
||||
let tsa = t.signer_certificate();
|
||||
let trusted_tsa = tsa
|
||||
.as_ref()
|
||||
.is_some_and(|c| c.valid_at(t.gen_time) && build_chain(c, &pool, Some(t.gen_time)).iter().any(|x| trust.trusts(x)));
|
||||
if !trusted_tsa {
|
||||
unverified_time = Some(t.gen_time);
|
||||
} else if tsa.as_ref().is_some_and(Certificate::may_timestamp) {
|
||||
info.timestamp_time = Some(t.gen_time);
|
||||
} else {
|
||||
unverified_time = Some(t.gen_time);
|
||||
info.details.push(Detail::plain("The timestamp authority's certificate is not issued for time stamping."));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1084,6 +1089,10 @@ fn validate_doc_timestamp(doc: &Document, bytes: &[u8], v: &Dict, info: &mut Sig
|
||||
info.status = Status::Unknown;
|
||||
info.details.push(Detail::plain("The timestamp authority's certificate was not valid at the time of timestamping."));
|
||||
}
|
||||
if !cert.may_timestamp() {
|
||||
info.status = Status::Unknown;
|
||||
info.details.push(Detail::plain("The timestamp authority's certificate is not issued for time stamping."));
|
||||
}
|
||||
}
|
||||
info.modification = if covered == bytes.len() || bytes[covered..].iter().all(|b| b.is_ascii_whitespace() || *b == 0) {
|
||||
Modification::None
|
||||
@@ -1111,9 +1120,10 @@ fn validate_doc_timestamp(doc: &Document, bytes: &[u8], v: &Dict, info: &mut Sig
|
||||
pool.extend(trust.certs.iter().cloned());
|
||||
// A trusted authority whose certificate was not valid at the time it stamped proves
|
||||
// nothing either (its detail is already recorded above): never upgrade that to Valid.
|
||||
let in_validity = token.signer_certificate().is_some_and(|c| c.valid_at(token.gen_time));
|
||||
// Nor does one whose certificate is not issued for time stamping.
|
||||
let may_stamp = token.signer_certificate().is_some_and(|c| c.valid_at(token.gen_time) && c.may_timestamp());
|
||||
let trusted_tsa = token.signer_certificate().is_some_and(|c| build_chain(&c, &pool, Some(token.gen_time)).iter().any(|x| trust.trusts(x)));
|
||||
if trusted_tsa && in_validity {
|
||||
if trusted_tsa && may_stamp {
|
||||
info.status = Status::Valid;
|
||||
info.details.push(Detail::plain("The timestamp token is valid and its authority is trusted."));
|
||||
} else if trusted_tsa {
|
||||
@@ -1286,6 +1296,11 @@ fn classify_changes(doc: &Document, old: Option<Document>, p: Option<u8>) -> Mod
|
||||
disallowed.push(what);
|
||||
}
|
||||
}
|
||||
// The trailer chooses the catalog. A later revision that points it at another catalog (one
|
||||
// already in the signed file, so no object changes) shows another document.
|
||||
if doc.root() != old.root() {
|
||||
disallowed.push(kind::STRUCTURE);
|
||||
}
|
||||
for num in doc.object_numbers() {
|
||||
if same_place(num) {
|
||||
continue;
|
||||
|
||||
@@ -259,26 +259,27 @@ impl OcspResponse {
|
||||
/// response window.
|
||||
pub fn check(&self, cert: &Certificate, issuer: &Certificate, at: Time) -> RevocationStatus {
|
||||
// Who signed the response? The issuer identified by responderID, or a delegated,
|
||||
// embedded responder identified the same way.
|
||||
let responder: Option<&Certificate> = match &self.responder_id {
|
||||
// embedded responder identified the same way. Which of the two it is comes from that
|
||||
// choice, never from the embedded certificate's names, which anyone can copy.
|
||||
let (responder, delegated): (Option<&Certificate>, bool) = match &self.responder_id {
|
||||
ResponderId::ByName(name) => {
|
||||
if *name == issuer.subject.raw {
|
||||
Some(issuer)
|
||||
(Some(issuer), false)
|
||||
} else {
|
||||
self.responder_cert.as_ref().filter(|c| *name == c.subject.raw)
|
||||
(self.responder_cert.as_ref().filter(|c| *name == c.subject.raw), true)
|
||||
}
|
||||
}
|
||||
ResponderId::ByKey(hash) => {
|
||||
if *hash == DigestAlg::Sha1.digest(&[&issuer.public_key.key_bits()]) {
|
||||
Some(issuer)
|
||||
(Some(issuer), false)
|
||||
} else {
|
||||
self.responder_cert.as_ref().filter(|c| *hash == DigestAlg::Sha1.digest(&[&c.public_key.key_bits()]))
|
||||
(self.responder_cert.as_ref().filter(|c| *hash == DigestAlg::Sha1.digest(&[&c.public_key.key_bits()])), true)
|
||||
}
|
||||
}
|
||||
};
|
||||
let Some(responder) = responder else { return RevocationStatus::Unknown };
|
||||
// A delegated responder must carry the OCSP-signing EKU and be issued by the issuer.
|
||||
if responder.subject.raw != issuer.subject.raw {
|
||||
if delegated {
|
||||
if responder.issuer.raw != issuer.subject.raw || !responder.signed_by(&issuer.public_key) {
|
||||
return RevocationStatus::Unknown;
|
||||
}
|
||||
|
||||
+21
-1
@@ -165,6 +165,9 @@ const PROCESSED_EXTENSIONS: [&str; 11] = [
|
||||
"1.3.6.1.5.5.7.1.3",
|
||||
];
|
||||
|
||||
/// id-kp-timeStamping (RFC 5280 §4.2.1.12), required of a timestamp authority (RFC 3161 §2.3).
|
||||
const EKU_TIME_STAMPING: &str = "1.3.6.1.5.5.7.3.8";
|
||||
|
||||
/// Extended key usages that allow signing documents: anyExtendedKeyUsage, emailProtection,
|
||||
/// codeSigning, documentSigning (RFC 9336), Adobe Authentic Documents and Microsoft document
|
||||
/// signing.
|
||||
@@ -384,6 +387,18 @@ impl Certificate {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this certificate may stamp trusted time (RFC 3161 §2.3): its extended key usage
|
||||
/// includes timeStamping, its key usage (if any) allows digital signatures or
|
||||
/// non-repudiation, and it has no critical extension PdfCraft doesn't process. A certificate
|
||||
/// issued for another purpose proves who signed, not when.
|
||||
pub fn may_timestamp(&self) -> bool {
|
||||
/// keyUsage bits 0 (digitalSignature) and 1 (nonRepudiation).
|
||||
const SIGNING_BITS: u16 = 0b11;
|
||||
self.extended_key_usage.as_ref().is_some_and(|e| e.iter().any(|o| o == EKU_TIME_STAMPING))
|
||||
&& self.key_usage.is_none_or(|u| u & SIGNING_BITS != 0)
|
||||
&& self.unknown_critical.is_empty()
|
||||
}
|
||||
|
||||
/// [`signing_problem`](Self::signing_problem) as a clause that follows the certificate's
|
||||
/// name ("it is not issued for signing documents (…)"), for lists of digital IDs.
|
||||
pub fn signing_problem_clause(&self) -> Option<String> {
|
||||
@@ -492,7 +507,8 @@ pub fn subject_of(raw: &[u8]) -> Option<Name> {
|
||||
}
|
||||
|
||||
/// The chain from `leaf` up through `pool`, as far as issuers can be found and may issue: each
|
||||
/// issuer's key verifies the certificate below it, it is a CA allowed to sign certificates, its
|
||||
/// issuer's key verifies the certificate below it, it is a CA allowed to sign certificates, it
|
||||
/// has no critical extension PdfCraft doesn't process (unless it is self-signed), its
|
||||
/// `pathLenConstraint` allows the CA certificates below it, and (when `at` is given) it was valid
|
||||
/// then. Stops at a self-signed certificate. Certificates embedded in a document are in `pool`
|
||||
/// too, so an ordinary end-entity certificate must never be accepted as an issuer.
|
||||
@@ -522,6 +538,10 @@ pub fn build_chain_noted<'a>(leaf: &'a Certificate, pool: &'a [Certificate], at:
|
||||
// two names where it needs them.
|
||||
let why = if !c.may_issue() {
|
||||
Some("The certificate of {} is not a CA certificate that may issue certificates, so it is not used to vouch for {}.")
|
||||
} else if !c.unknown_critical.is_empty() && !c.is_self_signed() {
|
||||
// What such an extension limits (name constraints, policy constraints) would
|
||||
// go unchecked. A self-signed root's extensions are the trust store's business.
|
||||
Some("The certificate of {} has critical extensions PdfCraft does not recognize, so it is not used to vouch for {}.")
|
||||
} else if c.path_len.is_some_and(|n| below > n as usize) {
|
||||
Some("The certificate of {} does not allow this many CA certificates below it (path length), so it is not used to vouch for {}.")
|
||||
} else if at.is_some_and(|t| !c.valid_at(t)) {
|
||||
|
||||
@@ -340,3 +340,25 @@ fn an_unreadable_extended_key_usage_allows_no_purpose() {
|
||||
verdict(signer_with("Good EKU", &r, &[key_usage(DIGITAL_SIGNATURE), extended_key_usage(&["2.5.29.37.0"])]), vec![], &r.cert);
|
||||
assert_eq!(status, Status::Valid, "{details:?}");
|
||||
}
|
||||
|
||||
/// A CA certificate with a critical extension PdfCraft does not enforce (here name constraints)
|
||||
/// vouches for nothing: what that extension limits would go unchecked. A self-signed root's
|
||||
/// extensions are the trust store's business, so such a root still anchors.
|
||||
#[test]
|
||||
fn an_issuer_with_an_unprocessed_critical_extension_does_not_vouch() {
|
||||
let name_constraints = ext("2.5.29.30", &der::seq(&[]));
|
||||
let r = root();
|
||||
let constrained =
|
||||
issue("Name Constrained CA", Some(&r), FOREVER, &[basic_constraints(true, None), key_usage(KEY_CERT_SIGN), name_constraints.clone()]);
|
||||
let l = end_entity("Outside Its Names", &constrained);
|
||||
let pool = [constrained.cert.clone(), r.cert.clone()];
|
||||
let (chain, why) = build_chain_noted(&l.cert, &pool, None);
|
||||
assert_eq!(names(&chain), ["Outside Its Names"]);
|
||||
assert!(why.is_some_and(|w| w.message.contains("critical extensions")), "the reason is reported");
|
||||
let constrained_root =
|
||||
issue("Name Constrained Root", None, FOREVER, &[basic_constraints(true, None), key_usage(KEY_CERT_SIGN), name_constraints]);
|
||||
let i = ca("Under Constrained Root", &constrained_root, None);
|
||||
let l = end_entity("Leaf", &i);
|
||||
let pool = [i.cert.clone(), constrained_root.cert.clone()];
|
||||
assert_eq!(names(&build_chain(&l.cert, &pool, None)), ["Leaf", "Under Constrained Root", "Name Constrained Root"]);
|
||||
}
|
||||
@@ -355,10 +355,19 @@ fn ocsp(issuer: &pkcs12::DigitalId, status: RevocationStatus, this: Time, next:
|
||||
};
|
||||
let single = der::seq(&[&cert_id, &status, &this.encode(), &der::tlv(tag::ctx(0), &next.encode())]);
|
||||
let responder_id = der::tlv(tag::ctx(1), &issuer.certificate.subject.raw);
|
||||
let tbs = der::seq(&[&responder_id, &this.encode(), &der::seq(&[&single])]);
|
||||
let alg = issuer.key.signature_algorithm(DigestAlg::Sha256);
|
||||
let sig = issuer.key.sign(DigestAlg::Sha256, &tbs).unwrap();
|
||||
let basic = der::seq(&[&tbs, &alg, &der::bit_string(&sig)]);
|
||||
signed_ocsp(&issuer.key, &responder_id, None, &single, this)
|
||||
}
|
||||
|
||||
/// A successful OCSP response holding `single`, signed with `key` and naming its responder by
|
||||
/// `responder_id`, with `certs` (the encoded `[0]` field) if given.
|
||||
fn signed_ocsp(key: &pdfcraft_sign::PrivateKey, responder_id: &[u8], certs: Option<&[u8]>, single: &[u8], at: Time) -> Vec<u8> {
|
||||
let tbs = der::seq(&[responder_id, &at.encode(), &der::seq(&[single])]);
|
||||
let alg = key.signature_algorithm(DigestAlg::Sha256);
|
||||
let sig = key.sign(DigestAlg::Sha256, &tbs).unwrap();
|
||||
let basic = match certs {
|
||||
Some(c) => der::seq(&[&tbs, &alg, &der::bit_string(&sig), c]),
|
||||
None => der::seq(&[&tbs, &alg, &der::bit_string(&sig)]),
|
||||
};
|
||||
let basic_type = der::oid("1.3.6.1.5.5.7.48.1.1");
|
||||
der::seq(&[&der::int(0), &der::explicit(0, &der::seq(&[&basic_type, &der::octets(&basic)]))])
|
||||
}
|
||||
@@ -465,6 +474,33 @@ fn ocsp_responses_verify_and_match_the_certificate() {
|
||||
assert!(OcspResponse::parse(&error).is_err());
|
||||
}
|
||||
|
||||
/// An embedded responder certificate answers for an issuer only when the issuer signed it for
|
||||
/// OCSP signing. One that merely copies the issuer's name, found by its key hash, does not.
|
||||
#[test]
|
||||
fn a_responder_that_copies_the_issuers_name_does_not_answer_for_it() {
|
||||
let id = pkcs12::open(&data("ec-p256.p12"), "test").unwrap();
|
||||
let issuer = &id.certificate;
|
||||
let (this, next) = (mid_june(), Time { year: 2026, month: 7, day: 15, hour: 12, minute: 0, second: 0 });
|
||||
let key = pdfcraft_sign::PrivateKey::generate_p256().unwrap();
|
||||
let impostor =
|
||||
Certificate::self_signed(&issuer.subject, &key, Time { year: 2026, month: 1, day: 1, hour: 0, minute: 0, second: 0 }, 5, &[9]).unwrap();
|
||||
assert_eq!(impostor.subject.raw, issuer.subject.raw);
|
||||
let cert_id = der::seq(&[
|
||||
&DigestAlg::Sha1.algorithm(),
|
||||
&der::octets(&DigestAlg::Sha1.digest(&[&issuer.issuer.raw])),
|
||||
&der::octets(&DigestAlg::Sha1.digest(&[&issuer.public_key.key_bits()])),
|
||||
&der::uint(&issuer.serial),
|
||||
]);
|
||||
let revoked = der::tlv(tag::ctx(1), &Time { year: 2026, month: 6, day: 2, hour: 9, minute: 0, second: 0 }.encode());
|
||||
let single = der::seq(&[&cert_id, &revoked, &this.encode(), &der::tlv(tag::ctx(0), &next.encode())]);
|
||||
let by_key = der::tlv(tag::ctx(2), &der::octets(&DigestAlg::Sha1.digest(&[&impostor.public_key.key_bits()])));
|
||||
// RFC 6960's `[0] EXPLICIT SEQUENCE OF Certificate`, and the certificate directly in `[0]`.
|
||||
for certs in [der::explicit(0, &der::seq(&[&impostor.raw])), der::explicit(0, &impostor.raw)] {
|
||||
let forged = OcspResponse::parse(&signed_ocsp(&key, &by_key, Some(&certs), &single, this)).unwrap();
|
||||
assert_eq!(forged.check(issuer, issuer, mid_june()), RevocationStatus::Unknown);
|
||||
}
|
||||
}
|
||||
|
||||
// ── tolerance for the encodings found in old and unusual signers ───────────────────────────────
|
||||
// RSA signatures over SHA-256("hello") by the key in rsa-aes.p12, made with OpenSSL 3 (see
|
||||
// tests/data/README.md): the standard DigestInfo, DigestInfo without the NULL parameter, the bare
|
||||
|
||||
+85
-33
@@ -384,6 +384,38 @@ struct TestTsa {
|
||||
time: Time,
|
||||
}
|
||||
|
||||
/// A timestamp authority's digital ID: a self-signed certificate valid from October 2026 for
|
||||
/// ten years, whose critical extended key usage is time stamping alone, as RFC 3161 requires.
|
||||
fn tsa_id() -> DigitalId {
|
||||
tsa_id_with(&["1.3.6.1.5.5.7.3.8"])
|
||||
}
|
||||
|
||||
/// [`tsa_id`] with these extended key usages.
|
||||
fn tsa_id_with(ekus: &[&str]) -> DigitalId {
|
||||
let key = pdfcraft_sign::PrivateKey::generate_p256().unwrap();
|
||||
let name = Name::build("Test TSA", "", "PdfCraft Tests", "", "US");
|
||||
let alg = key.preferred_digest();
|
||||
let sig_alg = key.signature_algorithm(alg);
|
||||
let from = Time { year: 2026, month: 10, day: 2, hour: 0, minute: 0, second: 0 };
|
||||
let until = Time { year: 2036, ..from };
|
||||
let oids: Vec<Vec<u8>> = ekus.iter().map(|o| der::oid(o)).collect();
|
||||
let oids: Vec<&[u8]> = oids.iter().map(Vec::as_slice).collect();
|
||||
let eku = der::seq(&[&der::oid("2.5.29.37"), &der::boolean(true), &der::octets(&der::seq(&oids))]);
|
||||
let tbs = der::seq(&[
|
||||
&der::explicit(0, &der::int(2)),
|
||||
&der::uint(&[0x31]),
|
||||
&sig_alg,
|
||||
&name.raw,
|
||||
&der::seq(&[&from.encode(), &until.encode()]),
|
||||
&name.raw,
|
||||
&key.public_key().spki(),
|
||||
&der::explicit(3, &der::seq(&[&eku])),
|
||||
]);
|
||||
let signature = key.sign(alg, &tbs).unwrap();
|
||||
let certificate = Certificate::parse(&der::seq(&[&tbs, &sig_alg, &der::bit_string(&signature)])).unwrap();
|
||||
DigitalId { key, certificate, chain: Vec::new(), friendly_name: None }
|
||||
}
|
||||
|
||||
impl TimestampAuthority for TestTsa {
|
||||
fn timestamp(&self, request: &[u8]) -> Result<Vec<u8>, SignError> {
|
||||
let q = pdfcraft_sign::timestamp::parse_request(request)?;
|
||||
@@ -403,10 +435,7 @@ impl TimestampAuthority for TestTsa {
|
||||
#[test]
|
||||
fn signing_with_a_timestamp_embeds_a_verified_rfc3161_token() {
|
||||
let id = pkcs12::open(&data("ec-p256.p12"), "test").unwrap();
|
||||
let tsa = TestTsa {
|
||||
id: pkcs12::open(&data("rsa-aes.p12"), "test").unwrap(),
|
||||
time: Time { year: 2026, month: 10, day: 6, hour: 12, minute: 0, second: 0 },
|
||||
};
|
||||
let tsa = TestTsa { id: tsa_id(), time: Time { year: 2026, month: 10, day: 6, hour: 12, minute: 0, second: 0 } };
|
||||
let signed = pdfcraft_sign::sign_with_timestamp(&open(&fixture()), &id, &opts(), &tsa).unwrap();
|
||||
assert!(signed.starts_with(&fixture()), "still an incremental update");
|
||||
let anchor = id.chain.first().cloned().unwrap_or_else(|| id.certificate.clone());
|
||||
@@ -442,10 +471,7 @@ fn a_malformed_timestamp_response_fails_signing_without_a_file() {
|
||||
|
||||
#[test]
|
||||
fn a_document_timestamp_covers_the_file_and_validates() {
|
||||
let tsa = TestTsa {
|
||||
id: pkcs12::open(&data("rsa-aes.p12"), "test").unwrap(),
|
||||
time: Time { year: 2026, month: 10, day: 6, hour: 12, minute: 0, second: 0 },
|
||||
};
|
||||
let tsa = TestTsa { id: tsa_id(), time: Time { year: 2026, month: 10, day: 6, hour: 12, minute: 0, second: 0 } };
|
||||
let stamped = pdfcraft_sign::timestamp_document(&open(&fixture()), &tsa, "D:20261006120000Z").unwrap();
|
||||
assert!(stamped.starts_with(&fixture()), "an incremental update");
|
||||
let trust_tsa = TrustStore { certs: vec![tsa.id.certificate.clone()] };
|
||||
@@ -479,10 +505,7 @@ fn a_document_timestamp_covers_the_file_and_validates() {
|
||||
|
||||
#[test]
|
||||
fn cached_discovery_still_validates_current_timestamp_bytes_ranges_and_trust() {
|
||||
let tsa = TestTsa {
|
||||
id: pkcs12::open(&data("rsa-aes.p12"), "test").unwrap(),
|
||||
time: Time { year: 2026, month: 10, day: 6, hour: 12, minute: 0, second: 0 },
|
||||
};
|
||||
let tsa = TestTsa { id: tsa_id(), time: Time { year: 2026, month: 10, day: 6, hour: 12, minute: 0, second: 0 } };
|
||||
let mut source = open(&fixture());
|
||||
source
|
||||
.update_dict(source.root().unwrap(), |d| {
|
||||
@@ -558,10 +581,7 @@ fn embedding_ltv_evidence_adds_a_dss_and_keeps_signatures_valid() {
|
||||
fn sign_then_ltv_then_timestamp_makes_a_b_lta_file() {
|
||||
use pdfcraft_sign::dss::{self, Evidence};
|
||||
let id = pkcs12::open(&data("ec-p256.p12"), "test").unwrap();
|
||||
let tsa = TestTsa {
|
||||
id: pkcs12::open(&data("rsa-aes.p12"), "test").unwrap(),
|
||||
time: Time { year: 2026, month: 10, day: 6, hour: 12, minute: 0, second: 0 },
|
||||
};
|
||||
let tsa = TestTsa { id: tsa_id(), time: Time { year: 2026, month: 10, day: 6, hour: 12, minute: 0, second: 0 } };
|
||||
let signed = pdfcraft_sign::sign(&open(&fixture()), &id, &opts()).unwrap();
|
||||
let ltv = dss::embed(&open(&signed), &Evidence { certs: vec![id.certificate.raw.clone()], ocsps: Vec::new(), crls: Vec::new() }).unwrap();
|
||||
let lta = pdfcraft_sign::timestamp_document(&open(<v), &tsa, "D:20261006120000Z").unwrap();
|
||||
@@ -1531,10 +1551,7 @@ fn an_untrusted_timestamp_does_not_set_the_validation_time() {
|
||||
let id = pkcs12::open(&data("ec-p256.p12"), "test").unwrap();
|
||||
// The TSA claims 2027 (inside the signer's validity) but is not trusted; the signer's own
|
||||
// clock says 2040, after the certificate expired.
|
||||
let tsa = TestTsa {
|
||||
id: pkcs12::open(&data("rsa-aes.p12"), "test").unwrap(),
|
||||
time: Time { year: 2027, month: 1, day: 1, hour: 0, minute: 0, second: 0 },
|
||||
};
|
||||
let tsa = TestTsa { id: tsa_id(), time: Time { year: 2027, month: 1, day: 1, hour: 0, minute: 0, second: 0 } };
|
||||
let late = SignOptions { date: "D:20400101120000Z".into(), ..opts() };
|
||||
let signed = pdfcraft_sign::sign_with_timestamp(&open(&fixture()), &id, &late, &tsa).unwrap();
|
||||
let anchor = id.chain.first().cloned().unwrap_or_else(|| id.certificate.clone());
|
||||
@@ -1753,10 +1770,7 @@ fn fixture_timestamped_in_a_field(tsa: &TestTsa) -> Vec<u8> {
|
||||
|
||||
#[test]
|
||||
fn a_document_timestamp_held_by_a_signature_field_is_checked_as_a_timestamp() {
|
||||
let tsa = TestTsa {
|
||||
id: pkcs12::open(&data("rsa-aes.p12"), "test").unwrap(),
|
||||
time: Time { year: 2026, month: 10, day: 6, hour: 12, minute: 0, second: 0 },
|
||||
};
|
||||
let tsa = TestTsa { id: tsa_id(), time: Time { year: 2026, month: 10, day: 6, hour: 12, minute: 0, second: 0 } };
|
||||
let stamped = fixture_timestamped_in_a_field(&tsa);
|
||||
let trust = TrustStore { certs: vec![tsa.id.certificate.clone()] };
|
||||
let all = signatures(&open(&stamped), &stamped, &trust);
|
||||
@@ -1786,7 +1800,7 @@ fn a_document_timestamp_held_by_a_signature_field_is_checked_as_a_timestamp() {
|
||||
#[test]
|
||||
fn a_field_timestamp_from_a_trusted_tsa_outside_its_validity_is_not_valid() {
|
||||
let tsa = TestTsa {
|
||||
id: pkcs12::open(&data("rsa-aes.p12"), "test").unwrap(),
|
||||
id: tsa_id(),
|
||||
// Before the TSA certificate's validity (it starts in October 2026).
|
||||
time: Time { year: 2020, month: 1, day: 1, hour: 0, minute: 0, second: 0 },
|
||||
};
|
||||
@@ -1801,10 +1815,7 @@ fn a_field_timestamp_from_a_trusted_tsa_outside_its_validity_is_not_valid() {
|
||||
|
||||
#[test]
|
||||
fn a_standalone_timestamp_typed_sig_by_an_older_writer_is_still_a_timestamp() {
|
||||
let tsa = TestTsa {
|
||||
id: pkcs12::open(&data("rsa-aes.p12"), "test").unwrap(),
|
||||
time: Time { year: 2026, month: 10, day: 6, hour: 12, minute: 0, second: 0 },
|
||||
};
|
||||
let tsa = TestTsa { id: tsa_id(), time: Time { year: 2026, month: 10, day: 6, hour: 12, minute: 0, second: 0 } };
|
||||
let stamped = pdfcraft_sign::timestamp_document(&open(&fixture()), &tsa, "D:20261006120000Z").unwrap();
|
||||
let s = signatures(&open(&stamped), &stamped, &TrustStore::default()).into_iter().find(|s| s.doc_timestamp).unwrap();
|
||||
assert_eq!(s.status, Status::Unknown, "{:?}", s.details);
|
||||
@@ -2035,10 +2046,7 @@ fn signing_waits_for_a_pending_security_change_to_be_saved() {
|
||||
|
||||
#[test]
|
||||
fn encrypted_documents_take_a_document_timestamp() {
|
||||
let tsa = TestTsa {
|
||||
id: pkcs12::open(&data("rsa-aes.p12"), "test").unwrap(),
|
||||
time: Time { year: 2026, month: 10, day: 6, hour: 12, minute: 0, second: 0 },
|
||||
};
|
||||
let tsa = TestTsa { id: tsa_id(), time: Time { year: 2026, month: 10, day: 6, hour: 12, minute: 0, second: 0 } };
|
||||
let base = protected(pdfcraft_cos::Algorithm::Aes128, FILL_AND_SIGN_ONLY);
|
||||
let stamped = pdfcraft_sign::timestamp_document(&open_pw(&base, None), &tsa, "D:20261006120000Z").unwrap();
|
||||
assert!(stamped.starts_with(&base));
|
||||
@@ -2471,3 +2479,47 @@ fn rewritten_page_resources_are_page_content_whatever_keys_they_carry() {
|
||||
s.modification
|
||||
);
|
||||
}
|
||||
|
||||
/// A later revision that only points the trailer at another catalog already in the signed file
|
||||
/// shows a different document while every signed object is unchanged. That is not permitted.
|
||||
#[test]
|
||||
fn switching_the_trailer_to_another_signed_catalog_is_not_permitted() {
|
||||
let base = fixture_with(
|
||||
b"<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [6 0 R] >> >>",
|
||||
&[
|
||||
b"<< /Type /Catalog /Pages 8 0 R /AcroForm << /Fields [6 0 R] >> >>",
|
||||
b"<< /Type /Pages /Kids [9 0 R] /Count 1 >>",
|
||||
b"<< /Type /Page /Parent 8 0 R /MediaBox [0 0 300 300] /Contents 10 0 R /Resources << /Font << /F1 5 0 R >> >> /Annots [6 0 R] >>",
|
||||
b"<< /Length 44 >>\nstream\nBT /F1 14 Tf 20 250 Td (Contract void) Tj ET\nendstream",
|
||||
],
|
||||
);
|
||||
let id = pkcs12::open(&data("ec-p256.p12"), "test").unwrap();
|
||||
let trust = TrustStore { certs: vec![id.certificate.clone()] };
|
||||
let signed = pdfcraft_sign::sign(&open(&base), &id, &SignOptions { field: Some("Approval".into()), rect: None, ..opts() }).unwrap();
|
||||
let s = signatures(&open(&signed), &signed, &trust).into_iter().find(|s| s.signed).unwrap();
|
||||
assert_eq!(s.status, Status::Valid, "{:?}", s.details);
|
||||
let switched = edit_after(&signed, |doc| doc.trailer_mut().set(b"Root".to_vec(), oref(7)));
|
||||
let s = signatures(&open(&switched), &switched, &trust).into_iter().find(|s| s.signed).expect("the signature is still listed");
|
||||
assert!(matches!(s.modification, Modification::Disallowed(_)), "{:?}", s.modification);
|
||||
assert_eq!(s.status, Status::Invalid, "{:?}", s.details);
|
||||
}
|
||||
|
||||
/// Only a certificate issued for time stamping makes trusted time. A trusted certificate issued
|
||||
/// for something else (here e-mail) stamps nothing trusted: neither a document timestamp nor
|
||||
/// a signature's embedded one.
|
||||
#[test]
|
||||
fn a_timestamp_from_a_certificate_not_issued_for_time_stamping_is_not_trusted_time() {
|
||||
let tsa = TestTsa { id: tsa_id_with(&["1.3.6.1.5.5.7.3.4"]), time: Time { year: 2026, month: 10, day: 6, hour: 12, minute: 0, second: 0 } };
|
||||
let stamped = pdfcraft_sign::timestamp_document(&open(&fixture()), &tsa, "D:20261006120000Z").unwrap();
|
||||
let trust = TrustStore { certs: vec![tsa.id.certificate.clone()] };
|
||||
let s = signatures(&open(&stamped), &stamped, &trust).into_iter().find(|s| s.doc_timestamp).unwrap();
|
||||
assert_eq!(s.status, Status::Unknown, "{:?}", s.details);
|
||||
assert!(s.details.iter().any(|d| d.message.contains("not issued for time stamping")), "{:?}", s.details);
|
||||
let id = pkcs12::open(&data("ec-p256.p12"), "test").unwrap();
|
||||
let signed = pdfcraft_sign::sign_with_timestamp(&open(&fixture()), &id, &opts(), &tsa).unwrap();
|
||||
let trust = TrustStore { certs: vec![id.certificate.clone(), tsa.id.certificate.clone()] };
|
||||
let s = signatures(&open(&signed), &signed, &trust).into_iter().find(|s| s.signed).unwrap();
|
||||
assert!(s.timestamp);
|
||||
assert_eq!(s.timestamp_time, None, "{:?}", s.details);
|
||||
assert!(s.details.iter().any(|d| d.message.contains("not issued for time stamping")), "{:?}", s.details);
|
||||
}
|
||||
@@ -2283,6 +2283,7 @@ action wizard Start Inicia
|
||||
The signer's certificate is not issued for signing documents (its extended key usage has no document-signing purpose). El certificat del signant no s'ha emès per signar documents (el seu ús estès de clau no inclou el propòsit de signatura de documents).
|
||||
The signer's certificate has critical extensions PdfCraft does not recognize ({}). El certificat del signant té extensions crítiques que PdfCraft no reconeix ({}).
|
||||
The certificate of {} is not a CA certificate that may issue certificates, so it is not used to vouch for {}. El certificat de {} no és un certificat de CA que pugui emetre certificats, de manera que no s'utilitza per avalar {}.
|
||||
The certificate of {} has critical extensions PdfCraft does not recognize, so it is not used to vouch for {}. The certificate of {} has critical extensions PdfCraft does not recognize, so it is not used to vouch for {}.
|
||||
The certificate of {} does not allow this many CA certificates below it (path length), so it is not used to vouch for {}. El certificat de {} no permet tants certificats de CA per sota seu (longitud de camí), de manera que no s'utilitza per avalar {}.
|
||||
The certificate of {} was not valid at the time of signing, so it is not used to vouch for {}. El certificat de {} no era vàlid en el moment de signar, de manera que no s'utilitza per avalar {}.
|
||||
The signer's identity is unknown because it has not been included in your list of trusted certificates and none of its parent certificates are trusted certificates. La identitat del signant és desconeguda perquè no s'ha inclòs a la llista de certificats de confiança i cap dels seus certificats pare és un certificat de confiança.
|
||||
@@ -2302,6 +2303,7 @@ action wizard Start Inicia
|
||||
The timestamp token could not be read ({}). No s'ha pogut llegir el testimoni de marca de temps ({}).
|
||||
The document has been altered or corrupted since the timestamp was applied. El document ha estat alterat o s'ha corromput des que es va aplicar la marca de temps.
|
||||
The timestamp authority's certificate was not valid at the time of timestamping. El certificat de l'autoritat de marca de temps no era vàlid en el moment de marcar el temps.
|
||||
The timestamp authority's certificate is not issued for time stamping. The timestamp authority's certificate is not issued for time stamping.
|
||||
This document has not been modified since this timestamp was applied. Aquest document no s'ha modificat des que es va aplicar aquesta marca de temps.
|
||||
The document has been modified since this timestamp was applied, but the changes are permitted ({}). El document s'ha modificat des que es va aplicar aquesta marca de temps, però els canvis són permesos ({}).
|
||||
The document has been altered since this timestamp was applied in ways it does not permit ({}). El document ha estat alterat des que es va aplicar aquesta marca de temps d'una manera que no permet ({}).
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 7 and column 3.
|
@@ -2635,6 +2635,7 @@ animation effects Off Vypnuto
|
||||
The RSA signature's DigestInfo has no NULL parameter (older signers write it so); it was read anyway. DigestInfo podpisu RSA nemá parametr NULL (starší podepisovací programy jej tak zapisují); přesto byl načten.
|
||||
The certificate of {} does not allow this many CA certificates below it (path length), so it is not used to vouch for {}. Certifikát {} nepovoluje pod sebou tolik certifikátů CA (délka cesty), proto se nepoužije k ručení za {}.
|
||||
The certificate of {} is not a CA certificate that may issue certificates, so it is not used to vouch for {}. Certifikát {} není certifikát CA, který smí vydávat certifikáty, proto se nepoužije k ručení za {}.
|
||||
The certificate of {} has critical extensions PdfCraft does not recognize, so it is not used to vouch for {}. Certifikát {} má kritická rozšíření, která PdfCraft nezná, proto se nepoužije k ručení za {}.
|
||||
The certificate of {} was not valid at the time of signing, so it is not used to vouch for {}. Certifikát {} nebyl v době podpisu platný, proto se nepoužije k ručení za {}.
|
||||
The checker found problems which may prevent the document from being fully accessible. Kontrola našla problémy, které mohou bránit plné přístupnosti dokumentu.
|
||||
The document has been altered or corrupted since the signature was applied. Dokument byl od připojení podpisu změněn nebo poškozen.
|
||||
@@ -2677,6 +2678,7 @@ animation effects Off Vypnuto
|
||||
The signer's identity is unknown because it has not been included in your list of trusted certificates and none of its parent certificates are trusted certificates. Identita podepisující osoby je neznámá, protože není ve vašem seznamu důvěryhodných certifikátů a žádný z jejích nadřazených certifikátů není důvěryhodný.
|
||||
The signer's identity is valid. Identita podepisující osoby je platná.
|
||||
The timestamp authority's certificate was not valid at the time of timestamping. Certifikát autority časových razítek nebyl v době vystavení razítka platný.
|
||||
The timestamp authority's certificate is not issued for time stamping. Certifikát autority časových razítek není vystaven pro vydávání časových razítek.
|
||||
The timestamp contents are not hexadecimal. Obsah časového razítka není šestnáctkový.
|
||||
The timestamp has no valid byte range. Časové razítko nemá platný rozsah bajtů.
|
||||
The timestamp token could not be read ({}). Časové razítko nebylo možné načíst ({}).
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2366,6 +2366,7 @@ action wizard Start Iniciar
|
||||
The signer's certificate has critical extensions PdfCraft does not recognize ({}). El certificado del firmante tiene extensiones críticas que PdfCraft no reconoce ({}).
|
||||
# Why a certificate that matched the issuer was not used to build the chain.
|
||||
The certificate of {} is not a CA certificate that may issue certificates, so it is not used to vouch for {}. El certificado de {} no es un certificado de CA que pueda emitir certificados, así que no se usa para avalar a {}.
|
||||
The certificate of {} has critical extensions PdfCraft does not recognize, so it is not used to vouch for {}. El certificado de {} tiene extensiones críticas que PdfCraft no reconoce, así que no se usa para avalar a {}.
|
||||
The certificate of {} does not allow this many CA certificates below it (path length), so it is not used to vouch for {}. El certificado de {} no admite tantos certificados de CA por debajo (longitud de ruta), así que no se usa para avalar a {}.
|
||||
The certificate of {} was not valid at the time of signing, so it is not used to vouch for {}. El certificado de {} no era válido en el momento de la firma, así que no se usa para avalar a {}.
|
||||
The signer's identity is unknown because it has not been included in your list of trusted certificates and none of its parent certificates are trusted certificates. La identidad del firmante es desconocida porque no está en tu lista de certificados de confianza y ninguno de los certificados superiores es de confianza.
|
||||
@@ -2385,6 +2386,7 @@ action wizard Start Iniciar
|
||||
The timestamp token could not be read ({}). No se ha podido leer el sello de tiempo ({}).
|
||||
The document has been altered or corrupted since the timestamp was applied. El documento se ha alterado o se ha dañado desde que se aplicó el sello de tiempo.
|
||||
The timestamp authority's certificate was not valid at the time of timestamping. El certificado de la autoridad de sellado de tiempo no era válido en el momento del sellado.
|
||||
The timestamp authority's certificate is not issued for time stamping. El certificado de la autoridad de sellado de tiempo no está emitido para sellar el tiempo.
|
||||
This document has not been modified since this timestamp was applied. Este documento no se ha modificado desde que se aplicó este sello de tiempo.
|
||||
The document has been modified since this timestamp was applied, but the changes are permitted ({}). El documento se ha modificado desde que se aplicó este sello de tiempo, pero son cambios que permite ({}).
|
||||
The document has been altered since this timestamp was applied in ways it does not permit ({}). El documento se ha alterado desde que se aplicó este sello de tiempo de maneras que no permite ({}).
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 60.
|
@@ -2496,6 +2496,7 @@ animation effects Off Ei käytössä
|
||||
The signer's certificate is not issued for signing documents (its extended key usage has no document-signing purpose). Allekirjoittajan varmennetta ei ole myönnetty asiakirjojen allekirjoittamiseen (sen laajennettu avaimen käyttö ei sisällä asiakirjojen allekirjoitusta).
|
||||
The signer's certificate has critical extensions PdfCraft does not recognize ({}). Allekirjoittajan varmenteessa on kriittisiä laajennuksia, joita PdfCraft ei tunnista ({}).
|
||||
The certificate of {} is not a CA certificate that may issue certificates, so it is not used to vouch for {}. Kohteen {} varmenne ei ole varmenteita myöntävä CA-varmenne, joten sitä ei käytetä kohteen {} vahvistamiseen.
|
||||
The certificate of {} has critical extensions PdfCraft does not recognize, so it is not used to vouch for {}. The certificate of {} has critical extensions PdfCraft does not recognize, so it is not used to vouch for {}.
|
||||
The certificate of {} does not allow this many CA certificates below it (path length), so it is not used to vouch for {}. Kohteen {} varmenne ei salli näin montaa alempaa CA-varmennetta (polun pituus), joten sitä ei käytetä kohteen {} vahvistamiseen.
|
||||
The certificate of {} was not valid at the time of signing, so it is not used to vouch for {}. Kohteen {} varmenne ei ollut voimassa allekirjoitushetkellä, joten sitä ei käytetä kohteen {} vahvistamiseen.
|
||||
The signer's identity is unknown because it has not been included in your list of trusted certificates and none of its parent certificates are trusted certificates. Allekirjoittajan henkilöllisyys on tuntematon, koska sitä ei ole lisätty luotettujen varmenteiden luetteloon eikä mikään sen ylemmistä varmenteista ole luotettu varmenne.
|
||||
@@ -2515,6 +2516,7 @@ animation effects Off Ei käytössä
|
||||
The timestamp token could not be read ({}). Aikaleimatunnistetta ei voitu lukea ({}).
|
||||
The document has been altered or corrupted since the timestamp was applied. Asiakirjaa on muutettu tai se on vioittunut aikaleiman lisäämisen jälkeen.
|
||||
The timestamp authority's certificate was not valid at the time of timestamping. Aikaleiman myöntäjän varmenne ei ollut voimassa aikaleimaushetkellä.
|
||||
The timestamp authority's certificate is not issued for time stamping. The timestamp authority's certificate is not issued for time stamping.
|
||||
This document has not been modified since this timestamp was applied. Asiakirjaa ei ole muokattu tämän aikaleiman lisäämisen jälkeen.
|
||||
The document has been modified since this timestamp was applied, but the changes are permitted ({}). Asiakirjaa on muokattu tämän aikaleiman lisäämisen jälkeen, mutta muutokset ovat sallittuja ({}).
|
||||
The document has been altered since this timestamp was applied in ways it does not permit ({}). Asiakirjaa on muutettu tämän aikaleiman lisäämisen jälkeen tavoilla, joita se ei salli ({}).
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 47.
|
@@ -1735,6 +1735,7 @@
|
||||
The RSA signature's DigestInfo has no NULL parameter (older signers write it so); it was read anyway. RSA parašo „DigestInfo“ neturi parametro NULL (taip įrašo senesnės pasirašymo programos); jis vis tiek perskaitytas.
|
||||
The certificate of {} does not allow this many CA certificates below it (path length), so it is not used to vouch for {}. Subjekto {} sertifikatas neleidžia tiek žemesnio lygio sertifikavimo įstaigų sertifikatų (grandinės ilgio riba), todėl juo nepatvirtinamas {} sertifikatas.
|
||||
The certificate of {} is not a CA certificate that may issue certificates, so it is not used to vouch for {}. Subjekto {} sertifikatas nėra sertifikavimo įstaigos sertifikatas, leidžiantis išduoti sertifikatus, todėl juo nepatvirtinamas {} sertifikatas.
|
||||
The certificate of {} has critical extensions PdfCraft does not recognize, so it is not used to vouch for {}. The certificate of {} has critical extensions PdfCraft does not recognize, so it is not used to vouch for {}.
|
||||
The certificate of {} was not valid at the time of signing, so it is not used to vouch for {}. Subjekto {} sertifikatas pasirašymo metu negaliojo, todėl juo nepatvirtinamas {} sertifikatas.
|
||||
The checker found problems which may prevent the document from being fully accessible. Tikrintuvas aptiko problemų, dėl kurių dokumentas gali būti nevisiškai pritaikytas prieinamumui.
|
||||
The document has been altered or corrupted since the signature was applied. Po pasirašymo dokumentas buvo pakeistas arba pažeistas.
|
||||
@@ -1783,6 +1784,7 @@
|
||||
The signer's identity is unknown because it has not been included in your list of trusted certificates and none of its parent certificates are trusted certificates. Pasirašiusiojo tapatybė nežinoma, nes jo sertifikato nėra patikimų sertifikatų sąraše ir nė vienas aukštesnio lygio sertifikatas nėra patikimas.
|
||||
The signer's identity is valid. Pasirašiusiojo tapatybė patvirtinta.
|
||||
The timestamp authority's certificate was not valid at the time of timestamping. Laiko žymą išdavusios įstaigos sertifikatas žymos sukūrimo metu negaliojo.
|
||||
The timestamp authority's certificate is not issued for time stamping. The timestamp authority's certificate is not issued for time stamping.
|
||||
The timestamp contents are not hexadecimal. Laiko žymos turinys nėra šešioliktainis.
|
||||
The timestamp has no valid byte range. Laiko žyma neturi tinkamo baitų intervalo.
|
||||
The timestamp token could not be read ({}). Nepavyko perskaityti laiko žymos žetono ({}).
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 406 and column 9.
|
@@ -2523,6 +2523,7 @@ animation effects Off Wyłączone
|
||||
The signer's certificate is not issued for signing documents (its extended key usage has no document-signing purpose). Certyfikat podpisującego nie jest wystawiony do podpisywania dokumentów (jego rozszerzone użycie klucza nie obejmuje podpisywania dokumentów).
|
||||
The signer's certificate has critical extensions PdfCraft does not recognize ({}). Certyfikat podpisującego ma rozszerzenia krytyczne, których PdfCraft nie rozpoznaje ({}).
|
||||
The certificate of {} is not a CA certificate that may issue certificates, so it is not used to vouch for {}. Certyfikat podmiotu {} nie jest certyfikatem urzędu certyfikacji, który może wystawiać certyfikaty, dlatego nie poświadcza certyfikatu {}.
|
||||
The certificate of {} has critical extensions PdfCraft does not recognize, so it is not used to vouch for {}. Certyfikat podmiotu {} ma rozszerzenia krytyczne, których PdfCraft nie rozpoznaje, dlatego nie poświadcza certyfikatu {}.
|
||||
The certificate of {} does not allow this many CA certificates below it (path length), so it is not used to vouch for {}. Certyfikat podmiotu {} nie dopuszcza tylu certyfikatów urzędów certyfikacji poniżej siebie (długość ścieżki), dlatego nie poświadcza certyfikatu {}.
|
||||
The certificate of {} was not valid at the time of signing, so it is not used to vouch for {}. Certyfikat podmiotu {} nie był ważny w chwili podpisania, dlatego nie poświadcza certyfikatu {}.
|
||||
The signer's identity is unknown because it has not been included in your list of trusted certificates and none of its parent certificates are trusted certificates. Tożsamość podpisującego jest nieznana, ponieważ jego certyfikatu nie ma na liście zaufanych certyfikatów i żaden z certyfikatów nadrzędnych nie jest zaufany.
|
||||
@@ -2542,6 +2543,7 @@ animation effects Off Wyłączone
|
||||
The timestamp token could not be read ({}). Nie udało się odczytać tokenu znacznika czasu ({}).
|
||||
The document has been altered or corrupted since the timestamp was applied. Dokument został zmieniony lub uszkodzony po dodaniu znacznika czasu.
|
||||
The timestamp authority's certificate was not valid at the time of timestamping. Certyfikat urzędu znacznika czasu nie był ważny w chwili oznaczania czasem.
|
||||
The timestamp authority's certificate is not issued for time stamping. Certyfikat urzędu znakowania czasem nie jest wystawiony do znakowania czasem.
|
||||
This document has not been modified since this timestamp was applied. Dokument nie był modyfikowany od dodania tego znacznika czasu.
|
||||
The document has been modified since this timestamp was applied, but the changes are permitted ({}). Dokument zmodyfikowano po dodaniu tego znacznika czasu, ale zmiany są dozwolone ({}).
|
||||
The document has been altered since this timestamp was applied in ways it does not permit ({}). Dokument zmieniono po dodaniu tego znacznika czasu w sposób, na który znacznik nie pozwala ({}).
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2653,6 +2653,7 @@ animation effects Off Desativados
|
||||
The RSA signature's DigestInfo has no NULL parameter (older signers write it so); it was read anyway. O DigestInfo da assinatura RSA não tem o parâmetro NULL (assinantes mais antigos o incluem); mesmo assim, ele foi lido.
|
||||
The certificate of {} does not allow this many CA certificates below it (path length), so it is not used to vouch for {}. O certificado de {} não permite essa quantidade de certificados de AC abaixo dele (comprimento do caminho), então não é usado para atestar a validade de {}.
|
||||
The certificate of {} is not a CA certificate that may issue certificates, so it is not used to vouch for {}. O certificado de {} não é um certificado de AC autorizado a emitir certificados, então não é usado para atestar a validade de {}.
|
||||
The certificate of {} has critical extensions PdfCraft does not recognize, so it is not used to vouch for {}. The certificate of {} has critical extensions PdfCraft does not recognize, so it is not used to vouch for {}.
|
||||
The certificate of {} was not valid at the time of signing, so it is not used to vouch for {}. O certificado de {} não era válido no momento da assinatura, então não é usado para atestar a validade de {}.
|
||||
The checker found problems which may prevent the document from being fully accessible. A verificação encontrou problemas que podem impedir que o documento seja totalmente acessível.
|
||||
The document has been altered or corrupted since the signature was applied. O documento foi alterado ou corrompido desde que a assinatura foi aplicada.
|
||||
@@ -2699,6 +2700,7 @@ animation effects Off Desativados
|
||||
The signer's identity is unknown because it has not been included in your list of trusted certificates and none of its parent certificates are trusted certificates. A identidade do signatário é desconhecida porque não foi incluída na sua lista de certificados confiáveis e nenhum dos certificados na cadeia acima dela é confiável.
|
||||
The signer's identity is valid. A identidade do signatário é válida.
|
||||
The timestamp authority's certificate was not valid at the time of timestamping. O certificado da autoridade de carimbo de data e hora não era válido no momento da emissão do carimbo.
|
||||
The timestamp authority's certificate is not issued for time stamping. The timestamp authority's certificate is not issued for time stamping.
|
||||
The timestamp contents are not hexadecimal. O conteúdo do carimbo de data e hora não está em hexadecimal.
|
||||
The timestamp has no valid byte range. O carimbo de data e hora não tem um intervalo de bytes válido.
|
||||
The timestamp token could not be read ({}). Não foi possível ler o token de carimbo de data e hora ({}).
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -1684,6 +1684,7 @@
|
||||
The RSA signature's DigestInfo has no NULL parameter (older signers write it so); it was read anyway. DigestInfo din semnătura RSA nu are parametrul NULL (semnatarii mai vechi îl scriu așa); a fost citită oricum.
|
||||
The certificate of {} does not allow this many CA certificates below it (path length), so it is not used to vouch for {}. Certificatul lui {} nu permite atâtea certificate CA sub el (lungimea căii), așa că nu este folosit pentru a garanta pentru {}.
|
||||
The certificate of {} is not a CA certificate that may issue certificates, so it is not used to vouch for {}. Certificatul lui {} nu este un certificat CA care poate emite certificate, așa că nu este folosit pentru a garanta pentru {}.
|
||||
The certificate of {} has critical extensions PdfCraft does not recognize, so it is not used to vouch for {}. The certificate of {} has critical extensions PdfCraft does not recognize, so it is not used to vouch for {}.
|
||||
The certificate of {} was not valid at the time of signing, so it is not used to vouch for {}. Certificatul lui {} nu era valabil în momentul semnării, așa că nu este folosit pentru a garanta pentru {}.
|
||||
The checker found problems which may prevent the document from being fully accessible. Verificatorul a găsit probleme care pot împiedica accesibilitatea completă a documentului.
|
||||
The document has been altered or corrupted since the signature was applied. Documentul a fost modificat sau deteriorat după aplicarea semnăturii.
|
||||
@@ -1729,6 +1730,7 @@
|
||||
The signer's identity is unknown because it has not been included in your list of trusted certificates and none of its parent certificates are trusted certificates. Identitatea semnatarului este necunoscută, deoarece nu a fost inclusă în lista certificatelor de încredere și niciunul dintre certificatele părinte nu este de încredere.
|
||||
The signer's identity is valid. Identitatea semnatarului este validă.
|
||||
The timestamp authority's certificate was not valid at the time of timestamping. Certificatul autorității de marcare temporală nu era valabil în momentul marcării.
|
||||
The timestamp authority's certificate is not issued for time stamping. The timestamp authority's certificate is not issued for time stamping.
|
||||
The timestamp contents are not hexadecimal. Conținutul marcajului temporal nu este hexazecimal.
|
||||
The timestamp has no valid byte range. Marcajul temporal nu are un interval de octeți valid.
|
||||
The timestamp token could not be read ({}). Tokenul de marcaj temporal nu a putut fi citit ({}).
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3 and column 110.
|
@@ -1724,6 +1724,7 @@ theme Appearance Giao diện
|
||||
The RSA signature's DigestInfo has no NULL parameter (older signers write it so); it was read anyway. DigestInfo của chữ ký RSA không có tham số NULL (các trình ký cũ ghi như vậy); nó vẫn được đọc.
|
||||
The certificate of {} does not allow this many CA certificates below it (path length), so it is not used to vouch for {}. Chứng chỉ của {} không cho phép có nhiều chứng chỉ CA ở dưới nó như vậy (độ dài đường dẫn), nên nó không được dùng để bảo chứng cho {}.
|
||||
The certificate of {} is not a CA certificate that may issue certificates, so it is not used to vouch for {}. Chứng chỉ của {} không phải là chứng chỉ CA được phép cấp chứng chỉ, nên nó không được dùng để bảo chứng cho {}.
|
||||
The certificate of {} has critical extensions PdfCraft does not recognize, so it is not used to vouch for {}. Chứng chỉ của {} có các phần mở rộng quan trọng mà PdfCraft không nhận ra, nên nó không được dùng để bảo chứng cho {}.
|
||||
The certificate of {} was not valid at the time of signing, so it is not used to vouch for {}. Chứng chỉ của {} không hợp lệ tại thời điểm ký, nên nó không được dùng để bảo chứng cho {}.
|
||||
The checker found problems which may prevent the document from being fully accessible. Trình kiểm tra tìm thấy các vấn đề có thể khiến tài liệu không thể tiếp cận đầy đủ.
|
||||
The document has been altered or corrupted since the signature was applied. Tài liệu đã bị thay đổi hoặc hỏng kể từ khi chữ ký được áp dụng.
|
||||
@@ -1772,6 +1773,7 @@ theme Appearance Giao diện
|
||||
The signer's identity is unknown because it has not been included in your list of trusted certificates and none of its parent certificates are trusted certificates. Danh tính của người ký không rõ vì nó chưa được đưa vào danh sách chứng chỉ tin cậy của bạn và không chứng chỉ cấp trên nào của nó là chứng chỉ tin cậy.
|
||||
The signer's identity is valid. Danh tính của người ký hợp lệ.
|
||||
The timestamp authority's certificate was not valid at the time of timestamping. Chứng chỉ của cơ quan cấp dấu thời gian không hợp lệ tại thời điểm đóng dấu thời gian.
|
||||
The timestamp authority's certificate is not issued for time stamping. Chứng chỉ của cơ quan cấp dấu thời gian không được cấp cho mục đích đóng dấu thời gian.
|
||||
The timestamp contents are not hexadecimal. Nội dung dấu thời gian không phải là thập lục phân.
|
||||
The timestamp has no valid byte range. Dấu thời gian không có dải byte hợp lệ.
|
||||
The timestamp token could not be read ({}). Không thể đọc mã thông báo dấu thời gian ({}).
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 493 and column 12.
|
Reference in new issue
Block a user