2024-11-15 19:37:01 -08:00
|
|
|
use v5.38;
|
|
|
|
use feature 'class';
|
|
|
|
no warnings qw(experimental::class);
|
|
|
|
|
|
|
|
# https://perldoc.perl.org/functions/pack#pack-TEMPLATE,LIST
|
|
|
|
|
|
|
|
class KeyData {
|
|
|
|
use MIME::Base64;
|
|
|
|
use Carp qw(carp croak);
|
|
|
|
#use Smart::Comments;
|
|
|
|
|
|
|
|
field $key_data :param;
|
|
|
|
field $noisy :param;
|
|
|
|
field @packets;
|
|
|
|
|
|
|
|
ADJUST {
|
|
|
|
$self->readpgp;
|
|
|
|
}
|
|
|
|
|
|
|
|
method dearmor_pgp_key () {
|
|
|
|
if ($key_data =~ m/-----BEGIN PGP PUBLIC KEY BLOCK-----/) {
|
|
|
|
$key_data =~ s/-----BEGIN PGP PUBLIC KEY BLOCK-----\s*//s;
|
|
|
|
$key_data =~ s/Comment:(.*)\s*//g;
|
|
|
|
$key_data =~ s/Version:(.*)\s*//g;
|
|
|
|
$key_data =~ s/Hash:(.*)\s*//g;
|
|
|
|
$key_data =~ s/Charset:(.*)\s*//g;
|
|
|
|
$key_data =~ s/-----END PGP PUBLIC KEY BLOCK-----\s*//s;
|
|
|
|
$key_data = decode_base64($key_data);
|
|
|
|
}
|
|
|
|
elsif ($key_data =~ m/-----BEGIN PGP/) {
|
|
|
|
carp 'This isn\'t key data.';
|
|
|
|
return 'stop';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
method add ($Class) {
|
|
|
|
push @packets, $Class;
|
|
|
|
}
|
|
|
|
|
|
|
|
method readpgp () {
|
|
|
|
if ($key_data =~ m/-----BEGIN PGP/) {
|
|
|
|
$key_data = $self->dearmor_pgp_key();
|
|
|
|
}
|
|
|
|
|
|
|
|
return if $key_data eq 'stop';
|
|
|
|
|
|
|
|
my $offset = 0;
|
|
|
|
my $i = 0;
|
|
|
|
while ($offset < length($key_data)) {
|
|
|
|
my $packet_size = $self->parse_pgp_packet(substr($key_data, $offset));
|
|
|
|
return unless $packet_size;
|
|
|
|
$offset += $packet_size;
|
|
|
|
say "offset: $offset\n" if $noisy;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
method parse_pgp_packet ($data) {
|
|
|
|
my $byte = unpack('B8', substr($data, 0, 1)); # Packet Header
|
|
|
|
## # $byte
|
|
|
|
my @bits = split(//, $byte);
|
|
|
|
if ($bits[0] != 1) {
|
|
|
|
carp "Invalid data";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
my $format = 'legacy';
|
|
|
|
($bits[1] == 0 ? say 'Legacy Format' : (say 'OpenPGP Format' and $format = 'modern')) if $noisy;
|
|
|
|
if ($bits[1] == 1) {
|
|
|
|
$format = 'modern';
|
|
|
|
}
|
|
|
|
my $tag = '';
|
|
|
|
if ($format eq 'legacy') {
|
|
|
|
for my $b (@bits[2..5]) {
|
|
|
|
$tag .= $b;
|
|
|
|
}
|
|
|
|
$tag = scalar reverse $tag;
|
|
|
|
$tag = unpack('C', pack('b4', $tag));
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
for my $b (@bits[2..7]) {
|
|
|
|
$tag .= $b;
|
|
|
|
}
|
|
|
|
$tag = scalar reverse $tag;
|
|
|
|
$tag = unpack('C', pack('b6', $tag));
|
|
|
|
}
|
|
|
|
|
|
|
|
my $length_field;
|
|
|
|
my $length_of_length;
|
|
|
|
if ($format eq 'legacy') {
|
|
|
|
# https://www.rfc-editor.org/rfc/rfc9580#section-4.2.2
|
|
|
|
if ($bits[7] and $bits[6]) {
|
|
|
|
$length_of_length = 'bullshit';
|
|
|
|
die;
|
|
|
|
}
|
|
|
|
elsif ($bits[7]) {
|
|
|
|
$length_of_length = 2;
|
|
|
|
$length_field = unpack('n', substr($data, 1, $length_of_length));
|
|
|
|
}
|
|
|
|
elsif ($bits[6]) {
|
2024-11-15 23:48:27 -08:00
|
|
|
$length_of_length = 4;
|
2024-11-15 19:37:01 -08:00
|
|
|
$length_field = unpack('N', substr($data, 1, $length_of_length));
|
2024-11-15 23:48:27 -08:00
|
|
|
warn "Give me the key that caused this to be emited";
|
2024-11-15 19:37:01 -08:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
$length_of_length = 1;
|
|
|
|
$length_field = unpack('C', substr($data, 1, $length_of_length));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else { # modern
|
|
|
|
# https://www.rfc-editor.org/rfc/rfc9580#section-4.2.1
|
|
|
|
my $first_octet = unpack('C', substr($data, 1, 1));
|
|
|
|
my $second_octet = unpack('C', substr($data, 2, 1));
|
|
|
|
|
|
|
|
if ($first_octet < 192) {
|
|
|
|
$length_of_length = 1;
|
|
|
|
$length_field = $first_octet;
|
|
|
|
}
|
|
|
|
if ($first_octet >= 192 and $first_octet < 255) {
|
|
|
|
$length_of_length = 2;
|
|
|
|
$length_field = ((($first_octet - 192) << 8) + ($second_octet) + 192); # probably fine
|
|
|
|
}
|
|
|
|
if ($first_octet == 255) {
|
|
|
|
$length_of_length = 5;
|
|
|
|
#$subpacket_length = [4-octet scalar starting at 2nd_octet];
|
|
|
|
$length_field = (unpack('N', substr $data, 1, 4)); # idk
|
|
|
|
warn "Give me the key that caused this to be emited";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
my %tags = (
|
|
|
|
0 => 'Reserved (Lol!)',
|
|
|
|
1 => 'Public Key Encrypted Session Key Packet',
|
|
|
|
2 => 'Signature Packet',
|
|
|
|
3 => 'Symmetric Key Encrypted Session Key Packet',
|
|
|
|
4 => 'One-Pass Signature Packet',
|
|
|
|
5 => 'Secret Key Packet',
|
|
|
|
6 => 'Public Key',
|
|
|
|
7 => 'Secret Subkey Packet',
|
|
|
|
8 => 'Compressed Data Packet',
|
|
|
|
9 => 'Symmetrically Encrypted Data Packet',
|
|
|
|
10 => 'Marker Packet',
|
|
|
|
11 => 'Literal Data Packet',
|
|
|
|
12 => 'Trust Packet',
|
|
|
|
13 => 'User ID Packet',
|
|
|
|
14 => 'Public Subkey Packet',
|
|
|
|
17 => 'User Attribute Packet',
|
|
|
|
18 => 'Symmetrically Encrypted and Integrity Protected Data Packet',
|
|
|
|
19 => 'Reserved',
|
|
|
|
20 => 'Reserved',
|
|
|
|
21 => 'Padding Packet',
|
|
|
|
);
|
|
|
|
exists $tags{$tag}
|
|
|
|
and say $tags{$tag} . " with length: $length_field"
|
|
|
|
or say 'Unknown Packet' if $noisy;
|
|
|
|
|
|
|
|
if ($tag == 2) { # sigs
|
|
|
|
my $sigs = Signature->new(noisy => $noisy);
|
|
|
|
$sigs->go($data, $length_field, $length_of_length);
|
|
|
|
$self->add($sigs);
|
|
|
|
}
|
|
|
|
elsif ($tag == 6) { # pubkey
|
|
|
|
my $pubs = PublicKey->new(pub_or_sub => 'pub', noisy => $noisy);
|
|
|
|
$pubs->go($data, $length_field, $length_of_length);
|
|
|
|
$self->add($pubs);
|
|
|
|
}
|
|
|
|
elsif ($tag == 14) { # subpubkey
|
|
|
|
my $subpubs = PublicKey->new(pub_or_sub => 'sub', noisy => $noisy);
|
|
|
|
$subpubs->go($data, $length_field, $length_of_length);
|
|
|
|
$self->add($subpubs);
|
|
|
|
}
|
|
|
|
elsif ($tag == 13) { # user id
|
|
|
|
my $uid = UserID->new(uid => substr($data, 2, $length_field));
|
|
|
|
say $uid->uid() if $noisy;
|
|
|
|
$self->add($uid);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
carp "Not handling this data '$tags{$tag}' ($tag)";
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $length_field + $length_of_length + 1; # Return the number of bytes we just processed
|
|
|
|
}
|
|
|
|
|
|
|
|
# like `gpg --with-colons --list-keys --with-fingerprint --with-fingerprint` but not it.
|
|
|
|
method machine_readable_gnupg () {
|
|
|
|
# https://github.com/gpg/gnupg/blob/master/doc/DETAILS
|
|
|
|
# 1 type of record
|
|
|
|
# 2 validity
|
|
|
|
# 3 key length
|
|
|
|
# 4 pub algo
|
|
|
|
# 5 keyid (fingerprint - 16)
|
|
|
|
# 6 creation date
|
|
|
|
# 7 expiration date
|
|
|
|
# 8 Certificate S/N, UID hash, trust signature info jjakke-edition: pub/sub/sig: key-version
|
|
|
|
# 9 ownertrust
|
|
|
|
# a uid: userid jjakke-edition: sig: signature target
|
|
|
|
# b signature class
|
|
|
|
# c key capabilities
|
|
|
|
# d issuer certificate fingerprint 'or other info'
|
|
|
|
#
|
|
|
|
# below is spaced out for me to read better
|
|
|
|
#
|
|
|
|
# 1 2 3 4 5 6 7 8 9 a
|
|
|
|
# pub:u:255:22:9904E01052985080:1730221618:1856365618: :u:
|
|
|
|
# 1 23456789 a b
|
|
|
|
# fpr:::::::::50B750CC829A462D016AD9679904E01052985080:
|
|
|
|
# 1 2 3 4 5 6 7 8 9 a
|
|
|
|
# uid: : : : : : : : :Jake Thoughts (jjakke) <jake@jjakke.com>:
|
|
|
|
# 1 2 3 4 5 6 7 8 9 a b c d
|
|
|
|
# sig: : :22:9904E01052985080:1730221618: : : : :[selfsig]: :13x:
|
|
|
|
# 1 2 3 4 5 6 7 8 9 a
|
|
|
|
# sub:u:255:18:190FCA50206CE28A:1730221618:1856365618: : :
|
|
|
|
# 2 3 4 5 6 7 8 9 a
|
|
|
|
# fpr: : : : : : : : :B99D79A47141D942494EBCF2190FCA50206CE28A:
|
|
|
|
# 1 2 3 4 5 6 7 8 9 a b c d
|
|
|
|
# sig: : :22:9904E01052985080:1730221618: : : : :[keybind]: :18x:
|
|
|
|
my $string = '';
|
|
|
|
my $i = 0;
|
|
|
|
for my $class (@packets) {
|
|
|
|
if (builtin::blessed $class eq 'PublicKey') {
|
|
|
|
$string .= $class->pub_or_sub();
|
|
|
|
$string .= ":REPLACEME:".$class->key_bits().":".$class->pub_key_algo().":";
|
|
|
|
if ($class->version() == '4') {
|
|
|
|
# https://www.rfc-editor.org/rfc/rfc9580#section-5.5.4.2
|
|
|
|
$string .= substr($class->fingerprint(), -16);
|
|
|
|
} else {
|
|
|
|
# https://www.rfc-editor.org/rfc/rfc9580#section-5.5.4.3
|
|
|
|
$string .= substr($class->fingerprint(), 0, 16);
|
|
|
|
}
|
|
|
|
$string .= ":".$class->creation_time().":";
|
|
|
|
my $revoked = 0;
|
|
|
|
my $expiration = 0;
|
|
|
|
my $k = 0;
|
|
|
|
for my $Signature (@packets) {
|
|
|
|
if ($k > $i and builtin::blessed $Signature eq 'Signature') {
|
|
|
|
if ($Signature->sigtype() eq '0x13'
|
|
|
|
or $Signature->sigtype() eq '0x12'
|
|
|
|
or $Signature->sigtype() eq '0x11'
|
|
|
|
or $Signature->sigtype() eq '0x10'
|
|
|
|
or $Signature->sigtype() eq '0x18'
|
|
|
|
or $Signature->sigtype() eq '0x19'
|
|
|
|
or $Signature->sigtype() eq '0x1F')
|
|
|
|
{
|
|
|
|
if ($Signature->key_expiration_time()) {
|
|
|
|
$expiration = $class->creation_time() + $Signature->key_expiration_time();
|
|
|
|
$string .= $class->creation_time() + $Signature->key_expiration_time();
|
|
|
|
}
|
|
|
|
last;
|
|
|
|
}
|
|
|
|
if ($Signature->sigtype() eq '0x20' or $Signature->sigtype() eq '0x28') {
|
|
|
|
$revoked = 1;
|
|
|
|
last;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$k++;
|
|
|
|
next;
|
|
|
|
}
|
|
|
|
if ($revoked) {
|
|
|
|
$string =~ s/REPLACEME/r/;
|
|
|
|
}
|
|
|
|
if ($expiration and $expiration < time) {
|
|
|
|
$string =~ s/REPLACEME/e/;
|
|
|
|
}
|
|
|
|
$string =~ s/REPLACEME/-/;
|
|
|
|
$string .= ":" . $class->version()."::\n";
|
|
|
|
|
|
|
|
$string .= "fpr:::::::::" . $class->fingerprint() . ":\n";
|
|
|
|
}
|
|
|
|
elsif (builtin::blessed $class eq 'UserID') {
|
|
|
|
$string .= "uid:::::::::" . $class->uid() . ":\n";
|
|
|
|
}
|
|
|
|
elsif (builtin::blessed $class eq 'Signature') {
|
|
|
|
no warnings 'uninitialized';
|
|
|
|
my $keyid = $class->keyid_issuer(); # v6 doesn't generate these
|
|
|
|
$keyid =~ s/\s//g;
|
|
|
|
$keyid = lc $keyid;
|
|
|
|
my $fpr_issuer = $class->fpr_issuer; # so that's what this is for
|
|
|
|
$fpr_issuer =~ s/\s//g;
|
|
|
|
$fpr_issuer = lc $fpr_issuer;
|
|
|
|
# 1 2 3 4 5 6 7 8 9 a b c d
|
|
|
|
# sig: : :22:9904E01052985080:1730221618: : : : :[keybind]: :18x:
|
|
|
|
$class->sigtype eq '0x20' ? ($string .= "rev") : ($string .= 'sig');
|
|
|
|
$string .= ":::" . $class->pub_key_algo . ":" . $keyid . ":" . $class->creation . ":" .
|
|
|
|
$class->expiration . ":". $class->version . "::". $class->signature_target .":" .
|
|
|
|
$class->sigtype . "::" . $fpr_issuer . ":\n";
|
|
|
|
}
|
|
|
|
$i++;
|
|
|
|
}
|
|
|
|
return $string;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class PublicKey {
|
|
|
|
use Digest::SHA qw(sha1_hex sha256_hex);
|
|
|
|
|
|
|
|
field $version :reader; # 4/6 are supported.
|
|
|
|
field $creation_time :reader;
|
|
|
|
field $expiration_time :reader;
|
|
|
|
field $pub_key_algo :reader;
|
|
|
|
field $fingerprint :reader;
|
|
|
|
field $key_bits :reader;
|
|
|
|
field $pub_or_sub :param;
|
|
|
|
field $noisy :param;
|
|
|
|
|
|
|
|
method go ($data, $length_field, $length_of_length) {
|
|
|
|
my $offset = $length_of_length + 1;
|
|
|
|
$version = unpack('C', substr($data, $offset++, 1));
|
|
|
|
say "Key Version: $version" if $noisy;
|
|
|
|
$creation_time = $self->octet_time(substr($data, $offset, 4));
|
|
|
|
say "Creation date: " . scalar gmtime $creation_time if $noisy;
|
|
|
|
$offset += 4;
|
|
|
|
if ($version == 3 or $version == 2) {
|
|
|
|
my $_expiration = unpack('n', substr($data, $offset, 2));
|
|
|
|
# ^ 'days' so...
|
|
|
|
$self->set_expiration( $_expiration * 86400 );
|
|
|
|
$offset += 2;
|
|
|
|
}
|
|
|
|
$pub_key_algo = unpack('C', substr($data, $offset++, 1));
|
|
|
|
my $PubKeyAlgos = PubKeyAlgos->new();
|
|
|
|
say "Pubkey Algo: " . $PubKeyAlgos->valid($pub_key_algo)->[0] . " ($pub_key_algo)" if $noisy;
|
|
|
|
|
|
|
|
# https://www.rfc-editor.org/rfc/rfc9580#section-5.5.2.3 4o length of pubkey material in the next field.
|
|
|
|
if ($version == 6) {
|
|
|
|
$offset += 4; # skipping...
|
|
|
|
}
|
|
|
|
my $KeyBits = PublicKeyKeyBits->new();
|
|
|
|
$key_bits = $KeyBits->get_bits($data, $pub_key_algo, $offset);
|
|
|
|
# ^ an misspelled scalar name causes segv when compiling O_O
|
|
|
|
|
|
|
|
$self->calculate_fingerprint($data, $length_field, $length_of_length);
|
|
|
|
}
|
|
|
|
|
|
|
|
method calculate_fingerprint ($data, $length_field, $length_of_length) {
|
|
|
|
if ($version == 4) {
|
|
|
|
$fingerprint = sha1_hex(pack("C",0x99) . pack("n",$length_field) . substr($data,$length_of_length+1,$length_field));
|
|
|
|
say "Calculated fingerprint: $fingerprint" if $noisy;
|
|
|
|
}
|
|
|
|
elsif ($version == 6) {
|
|
|
|
$fingerprint = sha256_hex(pack("C",0x9B) . pack("N",$length_field) . substr($data,$length_of_length+1,$length_field));
|
|
|
|
say "Calculated fingerprint: $fingerprint" if $noisy;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
warn "Calculated fingerprint: unable to calcuate for version $version"; # v3 mainly
|
|
|
|
# v3: The fingerprint of a version 3 key is formed by hashing the body (but not the 2-octet length)
|
|
|
|
# of the MPIs that form the key material (public modulus n, followed by exponent e) with MD5
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
method pub_or_sub () {
|
|
|
|
return $pub_or_sub;
|
|
|
|
}
|
|
|
|
|
|
|
|
method octet_time ($data) {
|
|
|
|
my $_time;
|
|
|
|
$_time = (unpack('N', substr $data, 0, 4));
|
|
|
|
return $_time;
|
|
|
|
}
|
|
|
|
|
|
|
|
method set_expiration ($epoch) {
|
|
|
|
$expiration_time = $creation_time + $epoch;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class PublicKeyKeyBits {
|
|
|
|
# TODO: fix up later, good enough for now
|
|
|
|
# https://www.rfc-editor.org/rfc/rfc9580#section-3.2
|
|
|
|
# https://www.rfc-editor.org/rfc/rfc9580#section-5.5.5
|
|
|
|
method get_bits ($data, $algo, $offset) {
|
|
|
|
if ($algo == 1 or $algo == 2 or $algo == 3 or $algo == 16 or $algo == 17) { # RSA, Elgamal, DSA
|
|
|
|
return unpack('n', substr($data, $offset++, 2));
|
|
|
|
}
|
|
|
|
elsif ($algo == 18) { # ECDH
|
|
|
|
my $oid_len = unpack('C', substr($data, $offset++, 1));
|
|
|
|
my $hex = $self->get_oid_hex($data, $offset,$oid_len);
|
|
|
|
my $Curves = ECCCurves->new();
|
|
|
|
if ($Curves->valid( $hex )) {
|
|
|
|
return $Curves->octets( $hex )->[4] * 8;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
elsif ($algo == 19) { # ECDSA
|
|
|
|
my $oid_len = unpack('C', substr($data, $offset++, 1));
|
|
|
|
my $hex = $self->get_oid_hex($data, $offset,$oid_len);
|
|
|
|
my $Curves = ECCCurves->new();
|
|
|
|
if ($Curves->valid( $hex )) {
|
|
|
|
return $Curves->octets( $hex )->[4] * 8;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
elsif ($algo == 22) { # EdDSA (deprecated)
|
|
|
|
# NOTE: https://www.rfc-editor.org/rfc/rfc9580#section-11.2.3
|
|
|
|
# For example, the length of an EdDSALegacy public key for the curve Ed25519Legacy is 263 bits:
|
|
|
|
# 7 bits to represent the 0x40 prefix octet and 32 octets for the native value of the public key.
|
|
|
|
#
|
|
|
|
# I don't understand why gpg lists 255 as the bit size.
|
|
|
|
|
|
|
|
#my $oid_len = unpack('C', substr($data, $offset++, 1));
|
|
|
|
#my $hex = $self->get_oid_hex($data, $offset,$oid_len);
|
|
|
|
#my $Curves = ECCCurves->new();
|
|
|
|
#if ($Curves->valid( $hex )) {
|
|
|
|
#return $Curves->octets( $hex )->[4] * 8;
|
|
|
|
#}
|
|
|
|
return 256;
|
|
|
|
}
|
|
|
|
elsif ($algo == 25) { # X25519
|
|
|
|
# https://www.rfc-editor.org/rfc/rfc7748#section-6.1
|
|
|
|
return 32 * 8;
|
|
|
|
}
|
|
|
|
elsif ($algo == 26) { # X448
|
|
|
|
# https://www.rfc-editor.org/rfc/rfc7748#section-6.2
|
|
|
|
return 56 * 8;
|
|
|
|
}
|
|
|
|
elsif ($algo == 27) { # Ed25519
|
|
|
|
# https://www.rfc-editor.org/rfc/rfc8032#section-5.1.5
|
|
|
|
return 256;
|
|
|
|
}
|
|
|
|
elsif ($algo == 28) { # Ed448
|
|
|
|
# https://www.rfc-editor.org/rfc/rfc8032#section-5.2.5
|
|
|
|
return 456;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
method get_oid_hex ($_data, $_offset, $_count) {
|
|
|
|
my $i = 0;
|
|
|
|
my $hex = '';
|
|
|
|
while ($i != $_count) {
|
|
|
|
$hex .= sprintf("%02X", unpack('C', substr($_data, $_offset++, 1)));
|
|
|
|
$i++;
|
|
|
|
}
|
|
|
|
return $hex;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class UserID {
|
|
|
|
field $uid :param;
|
|
|
|
|
|
|
|
method uid () {
|
|
|
|
return $uid;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class Signature {
|
|
|
|
field $noisy :param;
|
|
|
|
|
|
|
|
field $version :reader; # 4/6 are supported.
|
|
|
|
field $sigtype :reader;
|
|
|
|
field $pub_key_algo :reader;
|
|
|
|
field $hash_algo :reader;
|
|
|
|
|
|
|
|
field $creation :reader;
|
|
|
|
field $expiration :reader;
|
|
|
|
field $key_expiration_time :reader; # for primary/sub key
|
|
|
|
field $keyid_issuer :reader; # likely substr($fpr_issuer, -16);
|
|
|
|
field $signature_target :reader;
|
|
|
|
field $fpr_issuer :reader;
|
|
|
|
field $key_flags :reader;
|
|
|
|
|
|
|
|
field %subpackets;
|
|
|
|
|
|
|
|
ADJUST {
|
|
|
|
%subpackets = ( # not the full list of them, just the ones *I* care about.
|
|
|
|
2 => 'Signature Creation Time', # 4o
|
|
|
|
3 => 'Signature Expiration Time', # 4o nubmer of seconds after sig creation time
|
|
|
|
9 => 'Key Expiration Time', # 4o number of seconds after key creation time
|
|
|
|
16 => 'Issuer Key ID', # 8o (not existing for v6+ keys)
|
|
|
|
27 => 'Key Flags', # N octets of flags
|
|
|
|
31 => 'Signature Target', # what signature this refers to
|
|
|
|
33 => 'Issuer Fingerprint', # 1o key ver, N octets. v4 = 20o, v6 = 32o. MUST MATCH
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
method go($data, $length_field, $length_of_length) {
|
|
|
|
my $offset = $length_of_length + 1;
|
|
|
|
$version = unpack('C', substr($data, $offset++, 1)); # version
|
|
|
|
say "version: $version" if $noisy;
|
|
|
|
|
|
|
|
$sigtype = sprintf("0x%X", unpack('C', substr($data, $offset++, 1)));
|
|
|
|
my $SignatureType = SignatureType->new();
|
|
|
|
say "signature type: ". $SignatureType->valid($sigtype) . " ($sigtype)" if $noisy;
|
|
|
|
|
|
|
|
$pub_key_algo = unpack('C', substr($data, $offset++, 1));
|
|
|
|
my $PubKeyAlgos = PubKeyAlgos->new();
|
|
|
|
say "algo: " . $PubKeyAlgos->valid($pub_key_algo)->[0] . " ($pub_key_algo)" if $noisy;
|
|
|
|
|
|
|
|
$hash_algo = unpack('C', substr($data, $offset++, 1));
|
|
|
|
my $HashAlgos = HashAlgos->new();
|
|
|
|
say "hash: " . $HashAlgos->valid($hash_algo)->[0] . " ($hash_algo)" if $noisy;
|
|
|
|
|
|
|
|
my $hashed_subpacket_count;
|
|
|
|
my $unhashed_subpacket_count;
|
|
|
|
if ($version == 4) { # 2o
|
|
|
|
$hashed_subpacket_count = unpack('n', substr($data, $offset, 2));
|
|
|
|
## $hashed_subpacket_count
|
|
|
|
$offset += 2;
|
|
|
|
$unhashed_subpacket_count = unpack('n', substr($data, $offset + $hashed_subpacket_count, 2));
|
|
|
|
## $unhashed_subpacket_count
|
|
|
|
}
|
|
|
|
else { # 6 4o
|
|
|
|
$hashed_subpacket_count = unpack('N', substr($data, $offset, 4));
|
|
|
|
## $hashed_subpacket_count
|
|
|
|
$offset += 4;
|
|
|
|
$unhashed_subpacket_count = unpack('N', substr($data, $offset + $hashed_subpacket_count, 4));
|
|
|
|
## $unhashed_subpacket_count
|
|
|
|
}
|
|
|
|
$self->subpackets(substr($data, $offset, $hashed_subpacket_count));
|
|
|
|
$offset += $hashed_subpacket_count;
|
|
|
|
if ($version == 4) {
|
|
|
|
$offset += 2;
|
|
|
|
} else {
|
|
|
|
$offset += 4;
|
|
|
|
}
|
|
|
|
$self->subpackets(substr($data, $offset, $unhashed_subpacket_count));
|
|
|
|
}
|
|
|
|
|
|
|
|
method subpackets ($data) {
|
|
|
|
my $offset = 0;
|
|
|
|
my $length = length($data);
|
|
|
|
# ## $length
|
|
|
|
while ($offset < length($data)) {
|
|
|
|
# ## $offset
|
|
|
|
my ($length_of_length, $subpacket_length, $typeid) = $self->subpacket_length($data, $offset);
|
|
|
|
# ## $length_of_length
|
|
|
|
# ## $subpacket_length
|
|
|
|
# ## $typeid
|
|
|
|
$offset += $length_of_length;
|
|
|
|
if (exists $subpackets{$typeid}) {
|
|
|
|
#print "$subpackets{$typeid} !! ";
|
|
|
|
if ($typeid == 33) {
|
|
|
|
(undef, $fpr_issuer) = $self->issuer_fingerprint(
|
|
|
|
substr($data, $offset + $length_of_length, $subpacket_length) );
|
|
|
|
say $fpr_issuer if $noisy;
|
|
|
|
}
|
|
|
|
if ($typeid == 2) {
|
|
|
|
$creation = $self->octet_time(
|
|
|
|
substr($data, $offset + $length_of_length, $subpacket_length) );
|
|
|
|
say scalar gmtime $creation if $noisy;
|
|
|
|
}
|
|
|
|
if ($typeid == 27) {
|
|
|
|
$key_flags = sprintf("%02X ",
|
|
|
|
unpack('C', substr $data, $offset+$length_of_length, $subpacket_length));
|
|
|
|
# TODO
|
|
|
|
say "0x$key_flags" if $noisy;
|
|
|
|
|
|
|
|
}
|
|
|
|
if ($typeid == 3) {
|
|
|
|
$expiration = $self->octet_time(
|
|
|
|
substr($data, $offset + $length_of_length, $subpacket_length) );
|
|
|
|
say scalar gmtime ( $creation + $expiration);
|
|
|
|
}
|
|
|
|
if ($typeid == 9) {
|
|
|
|
$key_expiration_time = $self->octet_time(
|
|
|
|
substr($data, $offset + $length_of_length, $subpacket_length) );
|
|
|
|
say $key_expiration_time if $noisy;
|
|
|
|
}
|
|
|
|
if ($typeid == 16) {
|
|
|
|
$keyid_issuer = $self->key_id(
|
|
|
|
substr($data, $offset + $length_of_length, $subpacket_length) );
|
|
|
|
say $keyid_issuer if $noisy;
|
|
|
|
}
|
|
|
|
if ($typeid == 31) {
|
|
|
|
$signature_target = $self->_signature_target(
|
|
|
|
substr($data, $offset + $length_of_length, $subpacket_length) );
|
|
|
|
say $signature_target if $noisy;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$offset += $subpacket_length + 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
method _signature_target ($data) {
|
|
|
|
my $offset = 0;
|
|
|
|
my $algo = unpack('C', substr($data, $offset++, 1));
|
|
|
|
my $hash = unpack('C', substr($data, $offset++, 1));
|
|
|
|
my $_signature_target;
|
|
|
|
|
|
|
|
# lmk when you see it (give key too)
|
|
|
|
warn "_signature_target is untested";
|
|
|
|
while ($offset < length($data)) {
|
|
|
|
$_signature_target .= sprintf("%02X ", unpack('C', substr($data, $offset++, 1)));
|
|
|
|
}
|
|
|
|
return $_signature_target;
|
|
|
|
}
|
|
|
|
|
|
|
|
# TODO doesnt verify lengths for the versions, rfc says it MUST match.
|
|
|
|
method issuer_fingerprint ($data) {
|
|
|
|
my $_fingerprint = '';
|
|
|
|
my $offset = 0;
|
|
|
|
my $_version = unpack('C', substr($data, $offset++, 1));
|
|
|
|
while ($offset < length($data)) {
|
|
|
|
$_fingerprint .= sprintf("%02X ", unpack('C', substr($data, $offset++, 1)));
|
|
|
|
}
|
|
|
|
return $_version,$_fingerprint;
|
|
|
|
}
|
|
|
|
|
|
|
|
# TODO RFC say they MUST match the repective bits on fingerprints
|
|
|
|
method key_id ($data) {
|
|
|
|
my $_keyid_issuer = '';
|
|
|
|
my $offset = 0;
|
|
|
|
while ($offset < length($data)) {
|
|
|
|
$_keyid_issuer .= sprintf("%02X ", unpack('C', substr($data, $offset++, 1)));
|
|
|
|
}
|
|
|
|
return $_keyid_issuer;
|
|
|
|
}
|
|
|
|
|
|
|
|
method octet_time ($data) {
|
|
|
|
return (unpack('N', substr $data, 0, 4));
|
|
|
|
}
|
|
|
|
|
|
|
|
# returns length_of_length, length subpacket, and typeid
|
|
|
|
method subpacket_length ($data, $offset) {
|
|
|
|
my ($length_of_length, $subpacket_length);
|
|
|
|
my $first_octet = unpack('C', substr $data, $offset, 1);
|
|
|
|
my $second_octet = unpack('C', substr $data, $offset+1, 1);
|
|
|
|
|
|
|
|
my $typeid;
|
|
|
|
|
|
|
|
if ($first_octet < 192) {
|
|
|
|
$length_of_length = 1;
|
|
|
|
$subpacket_length = $first_octet - 1; # The subpacket length field covers the encoded Subpacket
|
|
|
|
# Type ID and the subpacket-specific data, and it does not
|
|
|
|
# include the subpacket length field itself.
|
|
|
|
$typeid = $second_octet;
|
|
|
|
}
|
|
|
|
if ($first_octet >= 192 and $first_octet < 255) {
|
|
|
|
$length_of_length = 2;
|
|
|
|
$subpacket_length = ((($first_octet - 192) << 8) + ($second_octet) + 192) - 2;
|
|
|
|
$typeid = unpack('C', substr $data, $offset+2, 1);
|
|
|
|
}
|
|
|
|
if ($first_octet == 255) {
|
|
|
|
$length_of_length = 5;
|
|
|
|
#$subpacket_length = [4-octet scalar starting at 2nd_octet];
|
|
|
|
$subpacket_length = (unpack('N', substr $data, $offset+1, 4)) - 5; # idk
|
|
|
|
warn "give me the key that caused this to be emitted";
|
|
|
|
$typeid = unpack('C', substr $data, $offset+4, 1);
|
|
|
|
}
|
|
|
|
return $length_of_length, $subpacket_length, $typeid;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class SignatureType {
|
|
|
|
field %types;
|
|
|
|
ADJUST {
|
|
|
|
%types = (
|
|
|
|
'0x00' => 'Binary Signature',
|
|
|
|
'0x01' => 'Text Signature',
|
|
|
|
'0x02' => 'Standalone Signature',
|
|
|
|
'0x10' => 'Generic Certification Signature',
|
|
|
|
'0x11' => 'Persona Certification Signature',
|
|
|
|
'0x12' => 'Casual Certification Signature',
|
|
|
|
'0x13' => 'Positive Certification Signature',
|
|
|
|
'0x18' => 'Subkey Binding Signature',
|
|
|
|
'0x19' => 'Primary Key Binding Signature',
|
|
|
|
'0x1F' => 'Direct Key Signature',
|
|
|
|
'0x20' => 'Key Revocation Signature',
|
|
|
|
'0x28' => 'Subkey Revocation Signature',
|
|
|
|
'0x30' => 'Certification Revocation Signature',
|
|
|
|
'0x40' => 'Timestamp Signature',
|
|
|
|
'0x50' => 'Third-Party Confirmation Signature',
|
|
|
|
'0xFF' => 'Reserved',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
method valid ($type) {
|
|
|
|
exists $types{$type}
|
|
|
|
and return $types{$type};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class Sigs::key_flags {
|
|
|
|
field %key_flags;
|
|
|
|
field %key_flags_letters;
|
|
|
|
|
|
|
|
ADJUST {
|
|
|
|
%key_flags = (
|
|
|
|
0x01 => 'May be used to create User ID or Direct Key signatures',
|
|
|
|
0x02 => 'May be used to sign data',
|
|
|
|
0x04 => 'May be used to encrypt communications',
|
|
|
|
0x08 => 'May be used to encrypt storage',
|
|
|
|
0x10 => 'Private component of this key may have been split by a secret-sharing mechanism',
|
|
|
|
0x20 => 'May be used for authentication.',
|
|
|
|
0x80 => 'Private component may be in the pocession of more than one person',
|
|
|
|
0x0004 => 'Reserved (ADSK)',
|
|
|
|
0x0008 => 'Reserved (timestamping)',
|
|
|
|
);
|
|
|
|
%key_flags_letters = (
|
|
|
|
0x01 => 'Create UID|DK signatures',
|
|
|
|
0x02 => 'S',
|
|
|
|
0x04 => 'Ec',
|
|
|
|
0x08 => 'Es',
|
|
|
|
0x10 => 'Private component may have been split',
|
|
|
|
0x20 => 'A',
|
|
|
|
0x80 => 'Private component may be in the pocession of more than one person',
|
|
|
|
0x0004 => 'Reserved (ADSK)',
|
|
|
|
0x0008 => 'Reserved (timestamping)',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
# can just look at the bits to see what the sig says the key can do.
|
|
|
|
}
|
|
|
|
|
|
|
|
# https://www.rfc-editor.org/rfc/rfc9580#section-9.1
|
|
|
|
class PubKeyAlgos {
|
|
|
|
field %algos;
|
|
|
|
|
|
|
|
ADJUST {
|
|
|
|
%algos = ( # Algo .... Not adding the other details
|
|
|
|
0 => ['Reserved'],
|
|
|
|
1 => ['RSA (Encrypt or Sign)'],
|
|
|
|
2 => ['RSA Encrypt-Only'],
|
|
|
|
3 => ['RSA Sign-Only'],
|
|
|
|
16 => ['Elgamal (Encrypt-Only)'],
|
|
|
|
17 => ['DSA (Digital Signature Algorithm)'],
|
|
|
|
18 => ['ECDH public key algorithm'],
|
|
|
|
19 => ['ECDSA public key algorithm'],
|
|
|
|
20 => ['Reserved OR Elgamal Encrypt or Sign'],
|
|
|
|
21 => ['Reserved for Diffie-Hellman (X9.42)'],
|
|
|
|
22 => ['EdDSALegacy (deprecated)'],
|
|
|
|
23 => ['Reserved (AEDH)'],
|
|
|
|
24 => ['Reserved (AEDSA)'],
|
|
|
|
25 => ['X25519'],
|
|
|
|
26 => ['X448'],
|
|
|
|
27 => ['Ed25519'],
|
|
|
|
28 => ['Ed448'],
|
|
|
|
);
|
|
|
|
# 27 & 25 are 'MUSTs' for rfc9580
|
|
|
|
# 17 & 16 were previous 'MUSTs' for rfc4880
|
|
|
|
}
|
|
|
|
|
|
|
|
method valid ($number) {
|
|
|
|
exists $algos{$number}
|
|
|
|
and return $algos{$number};
|
|
|
|
if ($number < 110 or $number > 100) {
|
|
|
|
return ['Private or Experimental Use'];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
# https://www.rfc-editor.org/rfc/rfc9580#section-9.2
|
|
|
|
class ECCCurves {
|
|
|
|
use Storable qw(dclone);
|
|
|
|
field %helpme;
|
|
|
|
|
|
|
|
ADJUST {
|
|
|
|
%helpme = (
|
|
|
|
# ASN.1 OID OID Len Curve OID Octets Curve Name Usage Field Size
|
|
|
|
'1.2.840.10045.3.1.7' => [8, '2A8648CE3D030107', 'NIST P-256', 'ECDSA,ECDH', 32],
|
|
|
|
'1.3.132.0.34' => [5, '2B81040022', 'NIST P-384',' ECDSA,ECDH', 48],
|
|
|
|
'1.3.132.0.35' => [5, '2B81040023', 'NIST P-521', 'ECDSA,ECDH', 66],
|
|
|
|
'1.3.36.3.3.2.8.1.1.7' => [9, '2B2403030208010107', 'brainpoolP256r1','ECDSA,ECDH', 32],
|
|
|
|
'1.3.36.3.3.2.8.1.1.11' => [9, '2B240303020801010B', 'brainpoolP384r1','ECDSA,ECDH', 48],
|
|
|
|
'1.3.36.3.3.2.8.1.1.13' => [9, '2B240303020801010D', 'brainpoolP512r1','ECDSA,ECDH', 64],
|
|
|
|
'1.3.6.1.4.1.11591.15.1' => [9, '2B06010401DA470F01', 'Ed25519Legacy','EdDSALegacy', 32],
|
|
|
|
'1.3.6.1.4.1.3029.1.5.1' => [10,'2B060104019755010501', 'Curve25519Legacy', 'ECDH', 32]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
method valid ($something) {
|
|
|
|
( $self->OID($something) or $self->octets($something) ) ? 1 : 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
method OID ($oid) {
|
|
|
|
exists $helpme{$oid}
|
|
|
|
and return $helpme{oid};
|
|
|
|
}
|
|
|
|
|
|
|
|
method octets ($octets) {
|
|
|
|
my %octets;
|
|
|
|
for my $oid (keys %helpme) {
|
|
|
|
my $temp = $helpme{$oid}->[1];
|
|
|
|
$octets{$temp} = dclone($helpme{$oid});
|
|
|
|
$octets{$temp}->[1] = $oid;
|
|
|
|
}
|
|
|
|
exists $octets{$octets}
|
|
|
|
and return $octets{$octets};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
# https://www.rfc-editor.org/rfc/rfc9580#section-9.5
|
|
|
|
class HashAlgos {
|
|
|
|
field %algos;
|
|
|
|
|
|
|
|
ADJUST {
|
|
|
|
%algos = (
|
|
|
|
# Algo Text Name v6 signature salt size OID
|
|
|
|
0 => ['Reserved', 'Reserved', 0],
|
|
|
|
1 => ['MD5', 'MD5', 0],
|
|
|
|
2 => ['SHA-1', 'SHA1', 0],
|
|
|
|
3 => ['RIPEMD-160','RIPEMD160', 0],
|
|
|
|
4 => ['Reserved', 'Reserved', 0],
|
|
|
|
5 => ['Reserved', 'Reserved', 0],
|
|
|
|
6 => ['Reserved', 'Reserved', 0],
|
|
|
|
7 => ['Reserved', 'Reserved', 0],
|
|
|
|
8 => ['SHA2-256', 'SHA256', 16],
|
|
|
|
9 => ['SHA2-384', 'SHA384', 24],
|
|
|
|
10 => ['SHA2-512', 'SHA512', 32],
|
|
|
|
11 => ['SHA2-224', 'SHA224', 16],
|
|
|
|
12 => ['SHA3-256', 'SHA3-256', 16],
|
|
|
|
13 => ['Reserved', 'Reserved', 0],
|
|
|
|
14 => ['SHA3-512', 'SHA3-512', 32],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
method valid ($n) {
|
|
|
|
exists $algos{$n}
|
|
|
|
and return $algos{$n};
|
|
|
|
if ($n < 110 or $n > 100) {
|
|
|
|
return ('Private or Experimental Use','Private or Experimental Use', 0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
# https://www.rfc-editor.org/rfc/rfc9580#section-9.6
|
|
|
|
class AEADAlgos {
|
|
|
|
field %algos;
|
|
|
|
|
|
|
|
ADJUST {
|
|
|
|
%algos = ( # name, nonce length (octets), authentication tag length (octets)
|
|
|
|
0 => ['Reserved', 0, 0],
|
|
|
|
1 => ['EAX', 16, 16],
|
|
|
|
2 => ['OCB', 15, 16],
|
|
|
|
3 => ['GCM', 12, 16],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
method valid ($n) {
|
|
|
|
exists $algos{$n}
|
|
|
|
and return $algos{$n};
|
|
|
|
if ($n < 110 or $n > 100) {
|
|
|
|
return ('Private or Experimental Use','Private or Experimental Use', 0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|