NAME
Fugu::OpenPGP - read an armored OpenPGP public key as bytes
SYNOPSIS
use Fugu::OpenPGP;
my ($binary, $reason) = Fugu::OpenPGP->decode_armor($armored_text);
die $reason unless defined $binary;
my $fingerprint = Fugu::OpenPGP->fingerprint($binary)
or die 'not a version 4 public key packet';
my $hash = Fugu::OpenPGP->wkd_hash('security');
# .well-known/openpgpkey/hu/$hash
DESCRIPTION
Fugu::OpenPGP decodes the armor of RFC 4880. It computes the version 4 fingerprint of a public key packet. It also computes the Web Key Directory hash of a local part.
The module runs no command, so a caller needs no gpg(1). It holds class methods only, because it holds no state. It uses core Digest::SHA and core MIME::Base64, so it adds no dependency.
The module reads a public key only. It holds no private key, it decrypts nothing, and it verifies no signature. gpg(1) owns those acts.
Every recoverable failure returns undef. In list context the second value holds the reason. The module never dies for bad input: a key file comes from outside, so bad bytes are data and not a programming error. The module never logs. The caller decides what to report.
decode_armor
decode_armor($text) returns the binary form of an armored block.
The method reads the two delimiter lines, it skips the armor headers, it decodes the base64 body, and it compares the CRC-24 checksum line against the decoded bytes.
The checksum comparison is not decoration. A decoder that skips it accepts a truncated key, and a truncated key gives a fingerprint of its own. A caller would then publish a fingerprint that no key matches.
The padding of base64 ends the data, so it may sit at the end of the last body line only. A line with interior padding decodes to a truncated key, and a crafted checksum line would still agree with the truncation. gpg(1) rejects such a block, and so does this method.
The method reads the first block of the text. A text with several blocks therefore gives the first key, and the rest stay unread. Fugu::KeyDir::keys_file writes such a text, so a caller that wants every key of a KEYS file must split the text itself.
Armor is text, so the method reads either line ending. A block that travelled through email holds CRLF, and it decodes to the same bytes. The method also trims the whitespace of each body line, because a mailer can pad one.
The method never changes the string of the caller.
Each of these is a failure:
a missing delimiter line
two delimiter lines that name two block types
an absent blank line after the armor headers
an armor header with a value and no space after the colon
a body line that is not base64
a base64 body that is not a whole number of groups
an absent checksum line, or more than one
a body line after the checksum line
a checksum mismatch
The blank line after the armor headers is necessary. RFC 4880 states it, and gpg(1) rejects a block without it. This method must not accept a block that gpg(1) rejects, because a site would then publish a key that no consumer can import.
The method trims the two ends of each body line, so a mailer that padded a line changes nothing.
The rule runs one way only. The method never accepts a block that gpg(1) rejects, so a key that this method validated always imports. The method can reject a block that gpg(1) reads, and it does so for each of these:
a body line with interior whitespace
a second checksum line
a body line after the checksum line
two delimiter lines that name two block types
a text above
MAX_ARMOR_SIZE
fingerprint
fingerprint($binary) returns the version 4 fingerprint of the first packet, in upper-case hexadecimal with no separator.
The fingerprint is the SHA-1 of the byte 0x99, the two-byte length of the public key packet body, and that body, per RFC 4880 section 12.2. The method reads the length from the packet header and writes the length again, so the same key gives the same answer in the old packet format and in the new one.
The method reads both header formats. It reads every length form that carries a whole packet. The old format has a one-byte, a two-byte and a four-byte form, and the new format has a one-byte, a two-byte and a five-byte form.
Two forms carry no whole packet, and the method rejects each one: the indeterminate length of the old format, and a partial body length of the new one. A public key packet never uses either.
The digest writes the body length in two octets. A body above MAX_PACKET_BODY therefore has no version 4 fingerprint, and it is a failure. pack would wrap the length with no warning, and the method would then answer with a confident wrong fingerprint.
A first packet that is not a public key packet, a packet version other than 4, and a truncated packet body are each a failure.
wkd_hash
wkd_hash($local) returns the Web Key Directory hash of an email local part.
The hash is the z-base-32 form of the SHA-1 of the local part in lower case. gpg --locate-keys asks for .well-known/openpgpkey/hu/<hash>, so the answer decides the publication path.
The method folds the ASCII letters of the part itself, so the case of an ASCII local part never changes the path. It leaves every byte above 127 alone, because gpg(1) does: a lowercase step that reads such a byte as Latin-1 would rewrite the bytes of a UTF-8 local part.
An undef local part and an empty one are each a failure.
zbase32
zbase32($bytes) encodes bytes in z-base-32. wkd_hash calls it, and a caller can call it for another purpose.
The alphabet is ybndrfg8ejkmcpqxot1uwisza345h769, and it is not the RFC 4648 alphabet. The order differs, so the same digest gives a different string. A caller that swaps the alphabet publishes a key at a URL that gpg(1) never asks for.
The encoding writes no padding, and it emits one character for each five bits. A byte count that is not a multiple of five therefore ends on a partial group, and the low bits of that group are zero. An undef input and an empty one each give the empty string.
CONSTANTS
MAX_PACKET_BODY-
The largest public key packet body that a version 4 fingerprint can hold, 65535 bytes, per RFC 4880 section 12.2.
MAX_ARMOR_SIZE-
The size bound of the whole text, 1 MiB. A public key of a person holds a few kilobytes. A caller that names a disk image by mistake gets a clean failure, not a decode of 500 MB.
PACKET_PUBLIC_KEY-
The tag of a public key packet, 6, per RFC 4880 section 4.3.
ZBASE32_ALPHABET-
The z-base-32 alphabet of the Web Key Directory.
CRC24_INIT,CRC24_POLY-
The initial value and the generator polynomial of the armor checksum, per RFC 4880 section 6.1.
RETURN VALUES
Each public method returns its answer in scalar context, or undef on a failure. In list context each one returns the answer and undef, or undef and the reason.
Each method takes bytes. A string that holds a code point above 255 is a failure, and never an answer. Digest::SHA dies on such a string, and a byte unpack takes the low byte of each character. A caller that holds text must encode it first.
CAVEATS
The module reads the first packet of a block only. An armored key holds a public key packet first, then the user id packets, the signature packets and the subkey packets. A caller that needs a subkey fingerprint needs a full packet walk, which this module does not hold.
The module verifies no signature and no key expiry. A fingerprint proves which bytes the block holds. It proves nothing about who made them, or whether the key is still valid.
SEE ALSO
gpg(1), Digest::SHA, MIME::Base64, Fugu::KeyDir
AUTHORS
Dick Olsson <hi@senzilla.io>
1 POD Error
The following errors were encountered while parsing the POD:
- Around line 116:
=back doesn't take any parameters, but you said =back The bound C<MAX_ARMOR_SIZE> applies to the whole text, and not to one block. A text above 1 MiB is a failure, whatever the size of its first block.