1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
|
#!/usr/bin/perl
# Author: Daniel Kahn Gillmor <dkg@fifthhorseman.net>
# Copyright: 2011, 2013
# License: GPL-3+
# Usage (two examples):
# openpgp2x509 'Daniel Kahn Gillmor <dkg@fifthhorseman.net>'
# openpgp2x509 ssh://lair.fifthhorseman.net
# Each invocation will produce a series of PEM-encoded X.509
# certificates on stdout corresponding to keys that are well-bound to
# the specified OpenPGP User ID.
# This tool should detect (based on the form of the User ID) what kind
# of X.509 certificate to produce
# It only emits certificates for OpenPGP keys that are marked with the
# "Authentication" usage flag. FIXME: make the usage flag selection
# adjustable by an environment variable or something.
# WARNING: This is very rough code! the interface WILL change
# dramatically. The only thing I can commit to keeping stable are the
# OIDs.
use strict;
use warnings;
use Crypt::X509 0.50;
use Math::BigInt;
use GnuPG::Interface 0.43;
use Regexp::Common qw /net/;
use MIME::Base64;
my $cert = Crypt::X509::_init('Certificate');
$cert->configure('encode' => { 'time' => 'raw' } );
my $pgpe = Crypt::X509::_init('PGPExtension');
$pgpe->configure('encode' => { 'time' => 'raw' } );
my $san = Crypt::X509::_init('SubjectAltName');
$san->configure('encode' => { 'time' => 'raw' } );
my $rsapubkeyinfo = Crypt::X509::_init('RSAPubKeyInfo');
my $dntypes = { 'CN' => '2.5.4.3', # common name
'emailAddress' => '1.2.840.113549.1.9.1', # e-mail address -- DEPRECATED. should use subjectAltName instead.
'C' => '2.5.4.6', # country
'ST' => '2.5.4.8', # state
'L' => '2.5.4.7', # locality
'O' => '2.5.4.10', # organization
'OU' => '2.5.4.11', # organization unit (often used as a comment)
'PSEUDO' => '2.5.4.65', # pseudonym (used for the parenthetical "comment" in the conventional OpenPGP User ID)
};
my $algos = {
'RSA' => '1.2.840.113549.1.1.1',
'RSAwithMD2' => '1.2.840.113549.1.1.2',
'RSAwithMD4' => '1.2.840.113549.1.1.3',
'RSAwithMD5' => '1.2.840.113549.1.1.4',
'RSAwithSHA1' => '1.2.840.113549.1.1.5',
'OAEP' => '1.2.840.113549.1.1.6',
'RSAwithSHA256' => '1.2.840.113549.1.1.11',
'RSAwithSHA384' => '1.2.840.113549.1.1.12',
'RSAwithSHA512' => '1.2.840.113549.1.1.13',
'RSAwithSHA224' => '1.2.840.113549.1.1.14',
'NullSignatureUseOpenPGP' => '1.3.6.1.4.1.37210.1.1',
'OpenPGPCertificateEmbedded' => '1.3.6.1.4.1.37210.1.2',
};
# NullSignatureUseOpenPGP: this X509 certificate is not
# self-verifiable. It must be verified by fetching certificate
# material from OpenPGP keyservers or from the user's private OpenPGP
# keyring.
# The identity material and usage in the OpenPGP keyservers SHOULD be
# tested against the context in which the certificate is being used.
# If no context information is explicitly available to the
# implementation checking the certificate's validity, the
# implementation MUST assume that the context is the full set of
# possible contexts asserted by the X.509 material itself (is this
# doable?)
# 0) certificate validity ambiguity -- X.509 certificates are
# generally considered to be entirely valid or entirely invalid.
# OpenPGP certificates can have some User IDs that are valid, and
# others that are not. If an implementation is asked to return a
# simple boolean response to a validity inquiry, without knowing
# the context in which the certificate was proposed for use, it
# MUST validate the full conjunction of all assertions made in the
# X.509 certificate itself in order to return "true".
# OpenPGPCertificateEmbedded: the "signature" material in the X.509
# certificate is actually a set of OpenPGP packets corresponding to a
# complete "transferable public key" as specified in
# https://tools.ietf.org/html/rfc4880#section-11.1 , in "raw"
# (non-ascii-armored) form.
# If it were implemented, it would be the same as
# NullSignatureUseOpenPGP, but with the OpenPGP material transported
# in-band in addition.
## NOTE: There is no implementation of the OpenPGPCertificateEmbedded,
## and maybe there never will be. Another approach would be to
## transmitting OpenPGP signature packets in the TLS channel itself,
## with an extension comparable to OCSP stapling.
# the OpenPGPCertificateEmbedded concept has a few downsides:
# 1) data duplication -- the X.509 Subject Public Key material is
# repeated (either in the primary key packet, or in one of the
# subkey packets). The X.509 Subject material (and any
# subjectAltNames) are also duplicated in the User ID packets.
# This increases the size of the certificate. It also creates
# potential inconsistencies. If the X.509 Subject Public Key
# material is not found found in the OpenPGP Transferable Public
# Key (either as a primary key or as a subkey), conforming
# implementations MUST reject the certificate.
# 2) the requirement for out-of-band verification is not entirely
# removed, since conformant implementations may want to check the
# public keyservers for things like revocation certificates.
# this is a 5 followed by a 0. it fits into the "Parameters" section
# of an ASN.1 algorithmIdentifier object. what does this mean?
# I think it means the NULL type.
my $noparams = sprintf('%c%c', 5, 0);
my $extensions = {
'PGPExtension' => '1.3.6.1.4.1.3401.8.1.1',
'subjectAltName' => '2.5.29.17', # https://tools.ietf.org/html/rfc5280#section-4.2.1.6
};
my $gnupg = GnuPG::Interface::->new();
$gnupg->options->quiet(1);
$gnupg->options->batch(1);
sub err {
printf STDERR @_;
}
sub ts2Time {
my $ts = shift;
if (!defined($ts)) {
# see https://tools.ietf.org/html/rfc5280#section-4.1.2.5
return {'generalTime' => '99991231235959Z' };
} else {
my ($sec,$min,$hour,$mday,$mon,$year) = gmtime($ts);
$year += 1900;
if (($year < 1950) ||
($year >= 2050)) {
return {'generalTime' => sprintf('%04d%02d%02d%02d%02d%02dZ', $year, $mon+1, $mday, $hour, $min, $sec) };
} else {
return {'utcTime' => sprintf('%02d%02d%02d%02d%02d%02dZ', ($year%100), $mon+1, $mday, $hour, $min, $sec) };
}
}
}
sub ts2ISO8601 {
my $ts = shift;
$ts = time()
if (!defined($ts));
my ($sec,$min,$hour,$mday,$mon,$year) = gmtime($ts);
$year += 1900;
return sprintf('%04d-%02d-%02dT%02d:%02d:%02dZ', $year, $mon+1, $mday, $hour, $min, $sec);
};
sub makeX509CertForUserID {
my $userid = shift;
my $hostname;
my $protocol;
my $emailaddress;
my $humanname;
my $comment;
my $subject;
my $ret = [];
my @subjectAltNames;
if ($userid =~ /^\s+/) {
err("We will not process User IDs with leading whitespace\n");
return $ret;
}
if ($userid =~ /\s+$/) {
err("We will not process User IDs with trailing whitespace\n");
return $ret;
}
if ($userid =~ /\n/) {
err("We will not process User IDs containing newlines\n");
return $ret;
}
# FIXME: do we want to rule out any other forms of User ID?
if ($userid =~ /^([^()]*)\s+(\((.*)\)\s+)?<([^><@\s]+\@$RE{net}{domain})>$/ ) {
# this is a typical/expected OpenPGP User ID.
$humanname = $1;
$comment = $3;
$emailaddress = $4;
# We're stripping arbitrary amounts of whitespace between the
# name, the comment, and the e-mail address here. if that
# whitespace is anything but " " then the OpenPGP User ID will not
# be reconstructible from the string.
my $reconstructeduid;
if (defined($comment)) {
$reconstructeduid = sprintf('%s (%s) <%s>', $humanname, $comment, $emailaddress);
} else {
$reconstructeduid = sprintf('%s <%s>', $humanname, $emailaddress);
}
if ($userid ne $reconstructeduid) {
err("This OpenPGP User ID could not be reconstructed from the X.509 certificate we would generate. Maybe a whitespace issue?\n");
return $ret;
}
$subject = [
[ {
'type' => $dntypes->{'CN'},
'value' => {
'utf8String' => $humanname,
},
} ],
];
push(@{ $subject }, [ { 'type' => $dntypes->{'PSEUDO'},
'value' => { 'utf8String' => $comment } } ] )
if defined($comment);
push(@subjectAltNames, { 'rfc822Name' => $emailaddress });
} elsif ($userid =~ /^(https|ssh|smtps?|ike|postgresql|imaps?|submission):\/\/($RE{net}{domain})$/) {
$protocol = $1;
$hostname = $2;
$subject = [ [ {
'type' => $dntypes->{'CN'},
'value' => {
'printableString' => $hostname
},
} ] ];
push(@subjectAltNames, { 'dNSName' => $hostname });
} else {
# Maybe we just assume this is a bare Human Name?
# what if it's a human name plus a comment? should we treat the
# comment like a pseudonym, as above?
err("Assuming '%s' is a bare human name.\n", $userid);
$humanname = $userid;
$subject = [
[ {
'type' => $dntypes->{'CN'},
'value' => {
'printableString' => $humanname,
},
} ],
];
}
foreach my $gpgkey ($gnupg->get_public_keys('='.$userid)) {
my $validity = '-';
my @sans;
foreach my $tryuid ($gpgkey->user_ids) {
if ($tryuid->as_string eq $userid) {
$validity = $tryuid->validity;
}
if (defined($protocol) &&
($tryuid->validity =~ /^[fu]$/) &&
($tryuid =~ /^$protocol\:\/\/($RE{net}{domain})/ )) {
push(@sans, $2);
}
}
if ($validity !~ /^[fu]$/) {
err("key 0x%s only has validity %s for User ID '%s' (needs full or ultimate validity)\n", $gpgkey->fingerprint->as_hex_string, $validity, $userid);
next;
}
# treat primary keys just like subkeys:
foreach my $subkey ($gpgkey, @{$gpgkey->subkeys}) {
if ($subkey->{algo_num} != 1) {
err("key 0x%s is algorithm %d (not RSA) -- we currently only handle RSA\n", $subkey->fingerprint->as_hex_string, $subkey->algo_num);
next;
}
# FIXME: reject/skip over revoked keys.
if (defined($subkey->{expiration_date}) &&
$subkey->{expiration_date} <= time()) {
err("key 0x%s is expired -- skipping\n", $subkey->fingerprint->as_hex_string);
next;
}
if ($subkey->{usage_flags} =~ /D/) {
err("key 0x%s is disabled -- skipping\n", $subkey->fingerprint->as_hex_string);
next;
}
if ($subkey->{usage_flags} !~ /a/) {
err("key 0x%s is not authentication-capable -- skipping\n", $subkey->fingerprint->as_hex_string);
next
}
err("making certificate for key 0x%s\n", $subkey->fingerprint->as_hex_string);
my $pubkey = { 'modulus' => @{$subkey->pubkey_data}[0],
'exponent' => @{$subkey->pubkey_data}[1],
};
my $vnotbefore = $subkey->creation_date;
my $vnotafter = $subkey->expiration_date;
# expiration date should be the minimum of the primary key and the subkey:
if (!defined($vnotafter)) {
$vnotafter = $gpgkey->expiration_date;
} elsif (defined($gpgkey->expiration_date)) {
$vnotafter = $gpgkey->expiration_date
if ($gpgkey->expiration_date < $vnotafter);
}
my $cnotbefore = ts2Time($vnotbefore);
my $cnotafter = ts2Time($vnotafter);
my $pgpeval = $pgpe->encode({ 'version' => 0, 'keyCreation' => $cnotbefore });
print $pgpe->{error}
if (!defined($pgpeval));
my $pubkeybitstring = $rsapubkeyinfo->encode($pubkey);
print $rsapubkeyinfo->{error}
if (!defined($pubkeybitstring));
my @extensions;
push(@extensions, { 'extnID' => $extensions->{'PGPExtension'},
'extnValue' => $pgpeval
});
if (@subjectAltNames) {
my $saneval = $san->encode(\@subjectAltNames);
print $san->{error}
if (!defined($saneval));
push(@extensions, { 'extnID' => $extensions->{'subjectAltName'},
'extnValue' => $saneval
});
}
# FIXME: base some keyUsage extensions on the type of User ID
# and on the usage flags of the key in question.
# if 'a' is present
# if protocol =~ /^https|smtps?|postgresql|imaps?|submission$/ then set TLS server eKU + ???
# if protocol eq 'ike' then ??? (ask micah)
# if protocol =~ /^smtps?$/ then set TLS client + ???
# if defined($humanname) then set TLS client + ???
# if 'e' is present:
# ???
# if 's' is present:
# ???
# if 'c' is present: I think we should never specify CA:TRUE or
# CA:FALSE in these certificates, since (a) we do not expect
# these keys to actually be making X.509-style certifications,
# but (b) we also don't want to assert that they can't make
# any certifications whatsoever.
# FIXME: add subjectAltName that matches the type of information
# we believe we're working with (see the cert-id draft).
# if @sans is present, should we add them as subjectAltNames? i
# don't think so. this certificate should be just for the User
# ID requested. The user can always make another certificate
# for the other user IDs and use that one.
my $newcert = {
'tbsCertificate' => {
'version' => 2, # 0 == version 1, 1 == version 2, 2 == version 3
# this is a convenient way to pass the fpr too.
'serialNumber' => Math::BigInt->new('0x'.$subkey->fingerprint->as_hex_string),
'subjectPublicKeyInfo' => {
'algorithm' => {
'parameters' => $noparams,
'algorithm' => $algos->{'RSA'},
},
'subjectPublicKey' => $pubkeybitstring,
},
'validity' => {
'notAfter' => $cnotafter,
'notBefore' => $cnotbefore,
},
'signature' => { # maybe we should make up our own "signature algorithm" here?
'parameters' => $noparams,
'algorithm' => $algos->{'NullSignatureUseOpenPGP'}
},
'subject' => {
'rdnSequence' => $subject,
},
'issuer' => {
'rdnSequence' => [ [ {
'type' => $dntypes->{'OU'},
'value' => { 'printableString' => sprintf('Please check the OpenPGP keyservers for certification information. (certificate generated on %s)', ts2ISO8601(time())) },
} ] ],
},
'extensions' => \@extensions,
},
'signature' => 'use OpenPGP',
'signatureAlgorithm' => {
'parameters' => $noparams,
'algorithm' => $algos->{'NullSignatureUseOpenPGP'}
}
};
my $dd = $cert->encode($newcert);
push(@{$ret}, { 'der' => $dd, 'fpr' => $subkey->fingerprint->as_hex_string});
}
}
return $ret;
}
foreach $cert ( @{ makeX509CertForUserID($ARGV[0]) } ) {
if (defined($ENV{OPENPGP2X509_EMIT_PKCS12})) {
# FIXME: figure out how to do this with certtool instead of openssl;
# the PKCS12 files i've tried to generate from certtool --to-p12
# can't be loaded by iceweasel for some reason.
# FIXME: don't do this horrific shell nastiness. be nicer!
$ENV{CERTOUTPUT} = sprintf("-----BEGIN CERTIFICATE-----\n%s-----END CERTIFICATE-----\n",
encode_base64($cert->{'der'}));
$ENV{FPR} = $cert->{'fpr'};
$ENV{OPENPGP_UID} = $ARGV[0];
# Note that while pkcs12(1ssl) claims that the order doesn't
# matter, in fact, this doesn't work if you emit the certificate
# before you emit the key.
system('(gpg --export-options export-reset-subkey-passwd --export-secret-subkeys "0x$FPR"\! |'.
'openpgp2ssh "$FPR" && printf "%s" "$CERTOUTPUT") |'.
'openssl pkcs12 -export -name "$OPENPGP_UID"');
} else {
printf("-----BEGIN CERTIFICATE-----\n%s-----END CERTIFICATE-----\n", encode_base64($cert->{'der'}));
}
}
|