Inhaltsverzeichnis

std.pgp — OpenPGP (RFC 4880)

Zurück zur Unit-Übersicht

Das std.pgp-Paket implementiert OpenPGP-Format-Parsing und ASCII-Armor nach RFC 4880. Es ist eine reine Parser- und Codec-Bibliothek: kein Schlüssel-Erzeugen, kein Ver- oder Entschlüsseln, kein Signieren, kein Verifizieren — nur das Lesen und Kodieren von PGP-Datenstrukturen.

Typische Einsatzfälle: .asc-Dateien dekodieren, öffentliche Schlüssel aus Keyrings lesen, Key-IDs und Fingerprints extrahieren, Paketströme inspizieren.

Autor: Andreas Röne
Copyright: 2025–2026 Andreas Röne
Quelle: std/pgp/

<WRAP info> Der frühere Pufferüberlauf in PgpArmorEncode (#1454) ist behoben: Bei zu kleinem Ausgabepuffer kommt jetzt -1 zurück, und der Nachbarspeicher bleibt unberührt (nachgeprüft mit einem Wächterpuffer). Als Faustregel für die Größe weiterhin (dataLen * 4 / 3) + 200 Bytes. </WRAP>

Units

Unit Beschreibung
std.pgp.core Konstanten (Tags, Algorithmen, Signaturtypen, Armor-Typen), Paket-Struct-Offsets, PgpCrc24
std.pgp.armor ASCII-Armor kodieren, dekodieren, Typ erkennen
std.pgp.packet Paket-Iteration über binäre PGP-Daten
std.pgp.key Public-Key-Paket-Analyse: Version, Algorithmus, Zeitstempel, Fingerprint, Key-ID

Die Units bauen aufeinander auf: key und packet setzen core voraus, key zusätzlich std.crypto.sha1. In der Praxis importiert man alle vier plus std.alloc.


Funktionsreferenz

std.pgp.core

Signatur Beschreibung
PgpCrc24(data: int64, len: int64): int64 CRC-24 nach RFC 4880 §6.1 (Init 0xB704CE, Polynom 0x1864CFB). Ergebnis sind die unteren 24 Bit

std.pgp.armor

Signatur Beschreibung
PgpArmorEncode(data, dataLen, tp, out, outMax): int64 Kodiert Rohbytes als Armor-Block. tp ist eine PGP_ARMOR_*-Konstante. Rückgabe: geschriebene Zeichen ohne NUL, oder -1 wenn outMax nicht reicht
PgpArmorDecode(arm, armLen, out, outMax): int64 Dekodiert einen Armor-Block und prüft die CRC-24. Rückgabe: Bytezahl, oder -1 bei CRC-Fehler, ungültigem Block oder zu kleinem out
PgpArmorType(arm, armLen): int64 Erkennt den Blocktyp allein an der BEGIN-Zeile, ohne zu dekodieren. Rückgabe: PGP_ARMOR_*

std.pgp.packet

Signatur Beschreibung
PgpPacketFirst(buf, len, pkt): int64 Liest das erste Paket ab Offset 0 in den Struct pkt. 1 = Paket gefunden, 0 = leer oder ungültig
PgpPacketNext(buf, len, pkt): int64 Liest das Paket am gespeicherten Folge-Offset. 1 = weiteres Paket, 0 = Ende

std.pgp.key

Alle Funktionen arbeiten auf dem Paket-Body — dem Bereich, den PGP_PKT_OFF_BODY / PGP_PKT_OFF_BLEN beschreiben, ohne Tag-Byte und Längenfeld.

Signatur Beschreibung
PgpKeyVersion(body, blen): int64 Versionsbyte (3 oder 4), 0 bei zu kurzem Body
PgpKeyAlgo(body, blen): int64 Public-Key-Algorithmus (PGP_ALG_*); berücksichtigt die unterschiedliche Feldlage bei v3 und v4
PgpKeyTimestamp(body, blen): int64 Erzeugungszeit als Unix-Sekunden (4 Bytes big-endian ab Offset 1)
PgpKeyFingerprint(body, blen, out): int64 20-Byte-SHA-1-Fingerprint nach §12.2 in out. 1 = Erfolg, 0 bei v3 oder Body > 65535 Bytes
PgpKeyId(body, blen, out): int64 8-Byte-Key-ID (Bytes 12–19 des Fingerprints) in out. 1 = Erfolg

Konstanten (std.pgp.core)

Paket-Tags (RFC 4880 §4.3)

Konstante Wert Bedeutung
PGP_TAG_PKESK 1 Public-Key Encrypted Session Key
PGP_TAG_SIG 2 Signatur
PGP_TAG_SKESK 3 Symmetric-Key Encrypted Session Key
PGP_TAG_OPS 4 One-Pass Signature
PGP_TAG_SECKEY 5 Geheimer Schlüssel
PGP_TAG_PUBKEY 6 Öffentlicher Schlüssel
PGP_TAG_SECSUBKEY 7 Geheimer Unterschlüssel
PGP_TAG_COMPRESSED 8 Komprimierte Daten
PGP_TAG_SYM_ENC 9 Symmetrisch verschlüsselte Daten
PGP_TAG_MARKER 10 Marker-Paket
PGP_TAG_LITERAL 11 Literal-Daten (die eigentlichen Nutzdaten)
PGP_TAG_TRUST 12 Trust-Paket (lokal, nicht exportiert)
PGP_TAG_UID 13 User-ID
PGP_TAG_PUBSUBKEY 14 Öffentlicher Unterschlüssel
PGP_TAG_UAT 17 User-Attribute (z. B. Foto)
PGP_TAG_SEIPD 18 Symmetrisch verschlüsselt, integritätsgeschützt
PGP_TAG_MDC 19 Modification Detection Code

Public-Key-Algorithmen (RFC 4880 §9.1)

Konstante Wert Algorithmus
PGP_ALG_RSA 1 RSA (Encrypt + Sign)
PGP_ALG_RSA_E 2 RSA (nur Encrypt)
PGP_ALG_RSA_S 3 RSA (nur Sign)
PGP_ALG_ELGAMAL 16 ElGamal (nur Encrypt)
PGP_ALG_DSA 17 DSA (nur Sign)
PGP_ALG_ECDH 18 ECDH (nur Encrypt)
PGP_ALG_ECDSA 19 ECDSA (nur Sign)
PGP_ALG_EDDSA 22 EdDSA (nur Sign, z. B. Ed25519)

Hash-Algorithmen (RFC 4880 §9.4)

Konstante Wert Algorithmus
PGP_HASH_MD5 1 MD5
PGP_HASH_SHA1 2 SHA-1
PGP_HASH_RIPEMD160 3 RIPEMD-160
PGP_HASH_SHA256 8 SHA-256
PGP_HASH_SHA384 9 SHA-384
PGP_HASH_SHA512 10 SHA-512
PGP_HASH_SHA224 11 SHA-224

Symmetrische Algorithmen (RFC 4880 §9.2)

Konstante Wert Algorithmus
PGP_SYM_PLAINTEXT 0 Klartext (unverschlüsselt)
PGP_SYM_IDEA 1 IDEA
PGP_SYM_3DES 2 Triple-DES
PGP_SYM_CAST5 3 CAST5
PGP_SYM_BLOWFISH 4 Blowfish
PGP_SYM_AES128 7 AES-128
PGP_SYM_AES192 8 AES-192
PGP_SYM_AES256 9 AES-256
PGP_SYM_TWOFISH 10 Twofish

Signatur-Typen (RFC 4880 §5.2.1)

Konstante Wert Bedeutung
PGP_SIG_BINARY 0x00 Binärdokument
PGP_SIG_TEXT 0x01 Textdokument (CRLF-normalisiert)
PGP_SIG_STANDALONE 0x02 Eigenständige Signatur
PGP_SIG_CERT_GENERIC 0x10 Generische Zertifizierung
PGP_SIG_CERT_PERSONA 0x11 Zertifizierung ohne Prüfung
PGP_SIG_CERT_CASUAL 0x12 Zertifizierung nach oberflächlicher Prüfung
PGP_SIG_CERT_POSITIVE 0x13 Zertifizierung nach gründlicher Prüfung
PGP_SIG_SUBKEY_BIND 0x18 Unterschlüssel-Bindung
PGP_SIG_PRIMARY_BIND 0x19 Rückbindung an den Hauptschlüssel
PGP_SIG_DIRECT 0x1F Direkte Schlüsselsignatur
PGP_SIG_REVOKE_KEY 0x20 Schlüssel-Widerruf
PGP_SIG_REVOKE_SUBKEY 0x28 Unterschlüssel-Widerruf
PGP_SIG_REVOKE_CERT 0x30 Zertifizierungs-Widerruf
PGP_SIG_TIMESTAMP 0x40 Zeitstempel
PGP_SIG_THIRD_PARTY 0x50 Drittbestätigung

Armor-Typen

Konstante Wert BEGIN-Header
PGP_ARMOR_UNKNOWN 0 Unbekannt / kein Armor
PGP_ARMOR_MESSAGE 1 BEGIN PGP MESSAGE
PGP_ARMOR_PUBLIC_KEY 2 BEGIN PGP PUBLIC KEY BLOCK
PGP_ARMOR_PRIVATE_KEY 3 BEGIN PGP PRIVATE KEY BLOCK
PGP_ARMOR_SIGNATURE 4 BEGIN PGP SIGNATURE

Paket-Struct-Offsets

Der Aufrufer alloziert einen PGP_PKT_SIZE Bytes großen Puffer und liest die Felder mit peek64:

Konstante Offset Inhalt
PGP_PKT_OFF_TAG 0 Paket-Typ (PGP_TAG_*)
PGP_PKT_OFF_BODY 8 Absoluter Zeiger auf die Body-Bytes im Quellpuffer
PGP_PKT_OFF_BLEN 16 Body-Länge in Bytes
PGP_PKT_OFF_NEXT 24 Offset des nächsten Pakets im Quellpuffer
PGP_PKT_SIZE 32 Gesamtgröße des Structs

Die Iteration kopiert nichts: PGP_PKT_OFF_BODY zeigt direkt in den Quellpuffer. Der muss also gültig bleiben, solange mit den Bodies gearbeitet wird.


Beispiele

Alle Beispiele sind gegen einen mit gpg –quick-generate-key erzeugten Testschlüssel geprüft; die gezeigten Ausgaben stammen aus dem tatsächlichen Lauf.

Schlüsseldatei auslesen

Der vollständige Weg: Datei laden → Armor-Typ prüfen → dekodieren → Pakete durchlaufen → Schlüsseldaten lesen.

import std.pgp.core;
import std.pgp.armor;
import std.pgp.packet;
import std.pgp.key;
import std.alloc;
import std.fs;

fn HexOut(p: int64, n: int64): void {
    var hex: pchar := "0123456789ABCDEF";
    var i: int64 := 0;
    while (i < n) {
        var b: int64 := peek8(p + i);
        var s: int64 := alloc(3);
        poke8(s,     StrCharAt(hex, (b >> 4) & 15));
        poke8(s + 1, StrCharAt(hex, b & 15));
        poke8(s + 2, 0);
        Print(s as pchar);
        free(s, 3);
        i := i + 1;
    }
}

fn main(): int64 {
    var path: pchar := "key.asc";
    var fsize: int64 := FileSize(path);
    PrintLn("Armor-Datei: ", IntToStr(fsize), " Bytes");

    var arm: int64 := alloc(fsize + 1);
    ReadFile(path, arm as pchar, fsize);

    PrintLn("PgpArmorType = ", IntToStr(PgpArmorType(arm, fsize)),
            "  (PGP_ARMOR_PUBLIC_KEY = ", IntToStr(PGP_ARMOR_PUBLIC_KEY), ")");

    var raw: int64 := alloc(fsize);
    var rawLen: int64 := PgpArmorDecode(arm, fsize, raw, fsize);
    PrintLn("PgpArmorDecode -> ", IntToStr(rawLen), " Bytes binaer");
    if (rawLen < 0) { return 1; }

    var pkt: int64 := alloc(PGP_PKT_SIZE);
    var fp:  int64 := alloc(20);
    var kid: int64 := alloc(8);

    var more: int64 := PgpPacketFirst(raw, rawLen, pkt);
    while (more != 0) {
        var tag:  int64 := peek64(pkt + PGP_PKT_OFF_TAG);
        var body: int64 := peek64(pkt + PGP_PKT_OFF_BODY);
        var blen: int64 := peek64(pkt + PGP_PKT_OFF_BLEN);
        PrintLn("Paket: Tag ", IntToStr(tag), ", ", IntToStr(blen), " Bytes Body");

        if (tag == PGP_TAG_PUBKEY) {
            PrintLn("  Version   = ", IntToStr(PgpKeyVersion(body, blen)));
            PrintLn("  Algorithmus = ", IntToStr(PgpKeyAlgo(body, blen)));
            PrintLn("  Zeitstempel = ", IntToStr(PgpKeyTimestamp(body, blen)));
            if (PgpKeyFingerprint(body, blen, fp) != 0) {
                Print("  Fingerprint = "); HexOut(fp, 20); PrintLn("");
            }
            if (PgpKeyId(body, blen, kid) != 0) {
                Print("  Key-ID      = "); HexOut(kid, 8); PrintLn("");
            }
        }
        more := PgpPacketNext(raw, rawLen, pkt);
    }
    return 0;
}

Ausgabe:

Armor-Datei: 961 Bytes
PgpArmorType = 2  (PGP_ARMOR_PUBLIC_KEY = 2)
PgpArmorDecode -> 649 Bytes binaer
Paket: Tag 6, 269 Bytes Body
  Version   = 4
  Algorithmus = 1
  Zeitstempel = 1786643838
  Fingerprint = 85C6D42C76D20716A4DDF55EBBF229737744C1D3
  Key-ID      = BBF229737744C1D3
Paket: Tag 13, 35 Bytes Body
Paket: Tag 2, 337 Bytes Body

Gegenprobe mit gpg –list-keys –with-colons auf demselben Schlüssel:

fpr:::::::::85C6D42C76D20716A4DDF55EBBF229737744C1D3:

Fingerprint und Key-ID stimmen byteweise überein. Algorithmus 1 ist RSA, Tag 6 der Hauptschlüssel, Tag 13 die User-ID, Tag 2 deren Selbstsignatur.

Paketstruktur eines Schlüsselbunds

Bei einem Schlüssel mit Unterschlüssel kommen zwei weitere Pakete dazu:

import std.pgp.core;
import std.pgp.armor;
import std.pgp.packet;
import std.alloc;
import std.fs;

fn TagName(t: int64): pchar {
    if (t == PGP_TAG_PUBKEY)     { return "Hauptschluessel"; }
    if (t == PGP_TAG_UID)        { return "User-ID"; }
    if (t == PGP_TAG_SIG)        { return "Signatur"; }
    if (t == PGP_TAG_PUBSUBKEY)  { return "Unterschluessel"; }
    return "sonstiges";
}

fn scan(path: pchar): void {
    var fsize: int64 := FileSize(path);
    var arm: int64 := alloc(fsize + 1);
    ReadFile(path, arm as pchar, fsize);
    PrintLn("--- ", path, ": Typ ", IntToStr(PgpArmorType(arm, fsize)), ", ", IntToStr(fsize), " Bytes Armor");
    var raw: int64 := alloc(fsize);
    var rawLen: int64 := PgpArmorDecode(arm, fsize, raw, fsize);
    PrintLn("    dekodiert: ", IntToStr(rawLen), " Bytes");
    var pkt: int64 := alloc(PGP_PKT_SIZE);
    var more: int64 := PgpPacketFirst(raw, rawLen, pkt);
    var cnt: int64 := 0;
    while (more != 0) {
        var tag: int64 := peek64(pkt + PGP_PKT_OFF_TAG);
        PrintLn("    Tag ", IntToStr(tag), " (", TagName(tag), "), Body ", IntToStr(peek64(pkt + PGP_PKT_OFF_BLEN)));
        cnt := cnt + 1;
        more := PgpPacketNext(raw, rawLen, pkt);
    }
    PrintLn("    Pakete gesamt: ", IntToStr(cnt));
}

fn main(): int64 {
    scan("key2.asc");
    scan("sig.asc");
    return 0;
}

Ausgabe:

--- key2.asc: Typ 2, 1753 Bytes Armor
    dekodiert: 1234 Bytes
    Tag 6 (Hauptschluessel), Body 269
    Tag 13 (User-ID), Body 35
    Tag 2 (Signatur), Body 337
    Tag 14 (Unterschluessel), Body 269
    Tag 2 (Signatur), Body 310
    Pakete gesamt: 5
--- sig.asc: Typ 4, 516 Bytes Armor
    dekodiert: 332 Bytes
    Tag 2 (Signatur), Body 329
    Pakete gesamt: 1

Dieselbe Datei durch gpg –list-packets ergibt exakt dieselbe Folge — Hauptschlüssel, User-ID, Signatur, Unterschlüssel, Signatur.

Armor kodieren und wieder dekodieren

import std.pgp.core;
import std.pgp.armor;
import std.alloc;

fn main(): int64 {
    var text: pchar := "OpenPGP Rohdaten-Test 1234567890";
    var len: int64 := StrLen(text);

    var outMax: int64 := (len * 4 / 3) + 200;
    var arm: int64 := alloc(outMax);
    var n: int64 := PgpArmorEncode(text as int64, len, PGP_ARMOR_MESSAGE, arm, outMax);
    PrintLn("PgpArmorEncode -> ", IntToStr(n), " Zeichen");
    PrintLn(arm as pchar);

    PrintLn("Typ erkannt: ", IntToStr(PgpArmorType(arm, n)), " (PGP_ARMOR_MESSAGE = ", IntToStr(PGP_ARMOR_MESSAGE), ")");

    var back: int64 := alloc(len + 16);
    var m: int64 := PgpArmorDecode(arm, n, back, len + 16);
    PrintLn("PgpArmorDecode -> ", IntToStr(m), " Bytes (Original: ", IntToStr(len), ")");
    poke8(back + m, 0);
    PrintLn("Inhalt: ", back as pchar);
    return 0;
}

Ausgabe:

PgpArmorEncode -> 106 Zeichen
-----BEGIN PGP MESSAGE-----

T3BlblBHUCBSb2hkYXRlbi1UZXN0IDEyMzQ1Njc4OTA=
=WWrf
-----END PGP MESSAGE-----

Typ erkannt: 1 (PGP_ARMOR_MESSAGE = 1)
PgpArmorDecode -> 32 Bytes (Original: 32)
Inhalt: OpenPGP Rohdaten-Test 1234567890

Der Aufbau entspricht RFC 4880 §6.2: BEGIN-Zeile, Leerzeile, Base64-Block, Prüfsummenzeile =WWrf, END-Zeile. Base64-Zeilen sind 76 Zeichen lang (57 Eingangsbytes je Zeile) — RFC 4880 erlaubt bis 76, GnuPG selbst schreibt 64. Beide Formen sind gültig und werden von PgpArmorDecode gelesen.

CRC-Prüfung

PgpArmorDecode verifiziert die Prüfsumme und meldet Manipulation:

// aus dem obigen Programm: ein Zeichen der '='-Zeile kippen
var c: int64 := peek8(arm + crcPos + 1);
if (c == 65) { poke8(arm + crcPos + 1, 66); } else { poke8(arm + crcPos + 1, 65); }
var bad: int64 := PgpArmorDecode(arm, n, back, len + 16);
PrintLn("Decode mit falscher CRC -> ", IntToStr(bad));

Decode mit falscher CRC -> -1

-1 ist der einzige Fehlerwert und deckt drei Fälle ab: CRC-Fehler, fehlender oder unvollständiger BEGIN/END-Rahmen und zu kleiner Ausgabepuffer. Welcher davon vorlag, ist am Rückgabewert nicht erkennbar.

CRC-24 direkt

import std.pgp.core;

fn main(): int64 {
    var data: pchar := "Hallo OpenPGP";
    PrintLn("CRC-24 = ", IntToStr(PgpCrc24(data as int64, StrLen(data))));
    PrintLn("CRC-24 leer = ", IntToStr(PgpCrc24(data as int64, 0)));
    return 0;
}

CRC-24 = 6658813
CRC-24 leer = 11994318

Bei Länge 0 kommt der Initialwert 0xB704CE = 11994318 zurück — das ist RFC-konform. PgpArmorEncode ruft PgpCrc24 selbst auf; im normalen Armor-Ablauf braucht man die Funktion nicht direkt.


Fallstricke

Die Puffergröße muss weiterhin selbst berechnet werden

PgpArmorEncode meldet seit dem Fix von #1454 einen zu kleinen Puffer mit -1, statt darüber hinaus zu schreiben:

PgpArmorEncode(200 Bytes, outMax=40)  ->  -1
überschriebene Wächter-Bytes: 0 von 64

Der Rückgabewert ist damit zu prüfen — sonst bleibt der Puffer stillschweigend leer. Für die Größe gilt (dataLen * 4 / 3) + 200 Bytes; das deckt Base64-Aufblähung, Zeilenumbrüche, Rahmen und Prüfsummenzeile ab. Mit ausreichendem Puffer liefert der Rundlauf Encode → Decode die 200 Ausgangsbytes zurück.

Der Body zeigt in den Quellpuffer

PGP_PKT_OFF_BODY ist ein Zeiger in den dekodierten Puffer, keine Kopie. Zwei Folgen:

Das früher an dieser Stelle gezeigte poke8(body + blen, 0) überschreibt das erste Byte des Folgepakets und zerstört die weitere Iteration — bitte nicht verwenden.

Fingerprints nur für v4

PgpKeyFingerprint und PgpKeyId liefern 0, wenn das Versionsbyte nicht 4 ist oder der Body größer als 65535 Bytes wäre. v3-Schlüssel geben nur Version, Algorithmus und Zeitstempel her. Der Rückgabewert ist zu prüfen — bei 0 bleibt der Ausgabepuffer uninitialisiert.

Partial-Body-Pakete werden nicht zusammengeführt

Neuformat-Pakete mit Partial-Body-Header (Längenbyte 224–254) behandelt der Iterator als vollständiges Paket der jeweiligen Teillänge. Die Folgestücke erscheinen als eigene, aus Sicht des Formats falsch interpretierte Pakete. Das betrifft in der Praxis große komprimierte oder verschlüsselte Datenpakete, nicht die üblichen Schlüsseldateien.


Einschränkungen

Was Einschränkung
Schlüsselerzeugung Nicht vorhanden — kein RSA/DSA/ECDSA/EdDSA-KeyGen
Ver- und Entschlüsseln Nicht vorhanden
Signieren und Verifizieren Nicht vorhanden — Signaturpakete werden erkannt, aber nicht geprüft
v3-Fingerprint Nicht unterstützt
Partial-Body-Pakete Werden nicht zusammengeführt
Compressed Data (Tag 8) Wird als Paket erkannt, aber nicht entpackt
Signatur-Subpakete Werden nicht zerlegt — der Signaturbody bleibt roh

Für kryptografische Operationen sind die Bausteine in std.crypto.rsa, std.crypto.sha256 und Verwandten vorhanden; die Verbindung zum OpenPGP-Format muss man selbst herstellen.


Quelldateien

Datei Inhalt
std/pgp/core.lyx Alle Konstanten, Paket-Struct-Offsets, PgpCrc24
std/pgp/armor.lyx PgpArmorEncode, PgpArmorDecode, PgpArmorType samt Base64-Codec
std/pgp/packet.lyx PgpPacketFirst, PgpPacketNext; Alt- und Neuformat-Parser
std/pgp/key.lyx PgpKeyVersion, PgpKeyAlgo, PgpKeyTimestamp, PgpKeyFingerprint, PgpKeyId

Verwandte Units

Codebeispiele geprüft: gegen lyxc 1.2.5C übersetzt (Prüflauf 2026-09-08 über die gesamte Doku: 574 Vollprogramme, 0 echte Fehler; zusätzlich 5159 Aufrufe gegen die pub fn-Signaturen in aurum/std gehalten, 0 Abweichungen).


Letzte Aktualisierung: 2026-08-30 (Dateistand)