3 # Author: Daniel Kahn Gillmor <dkg@fifthhorseman.net>
7 # WARNING: This is very rough code! the interface WILL change
8 # dramatically. The only thing I can commit to keeping stable are the
11 # Use this code to take an OpenPGP certificate (pubkey) and emit a
12 # corresponding OpenPGP-validated X.509 certificate.
14 # Usage: openpgp2x509 ssh://lair.fifthhorseman.net
20 use GnuPG::Interface 0.43;
21 use Regexp::Common qw /net/;
24 my $cert = Crypt::X509::_init('Certificate');
25 $cert->configure('encode' => { 'time' => 'raw' } );
26 my $pgpe = Crypt::X509::_init('PGPExtension');
27 $pgpe->configure('encode' => { 'time' => 'raw' } );
28 my $rsapubkeyinfo = Crypt::X509::_init('RSAPubKeyInfo');
30 my $dntypes = { 'CN' => '2.5.4.3', # common name
31 'emailAddress' => '1.2.840.113549.1.9.1', # e-mail address
32 'C' => '2.5.4.6', # country
33 'ST' => '2.5.4.8', # state
34 'L' => '2.5.4.7', # locality
35 'O' => '2.5.4.10', # organization
36 'OU' => '2.5.4.11', # organization unit (often used as a comment)
40 'RSA' => '1.2.840.113549.1.1.1',
41 'RSAwithMD2' => '1.2.840.113549.1.1.2',
42 'RSAwithMD4' => '1.2.840.113549.1.1.3',
43 'RSAwithMD5' => '1.2.840.113549.1.1.4',
44 'RSAwithSHA1' => '1.2.840.113549.1.1.5',
45 'OAEP' => '1.2.840.113549.1.1.6',
46 'RSAwithSHA256' => '1.2.840.113549.1.1.11',
47 'RSAwithSHA384' => '1.2.840.113549.1.1.12',
48 'RSAwithSHA512' => '1.2.840.113549.1.1.13',
49 'RSAwithSHA224' => '1.2.840.113549.1.1.14',
50 'NullSignatureUseOpenPGP' => '1.3.6.1.4.1.37210.1.1',
51 'OpenPGPCertificateEmbedded' => '1.3.6.1.4.1.37210.1.2',
55 # NullSignatureUseOpenPGP: this X509 certificate is not
56 # self-verifiable. It must be verified by fetching certificate
57 # material from OpenPGP keyservers or from the user's private OpenPGP
60 # The identity material and usage in the OpenPGP keyservers SHOULD be
61 # tested against the context in which the certificate is being used.
62 # If no context information is explicitly available to the
63 # implementation checking the certificate's validity, the
64 # implementation MUST assume that the context is the full set of
65 # possible contexts asserted by the X.509 material itself (is this
68 # 0) certificate validity ambiguity -- X.509 certificates are
69 # generally considered to be entirely valid or entirely invalid.
70 # OpenPGP certificates can have some User IDs that are valid, and
71 # others that are not. If an implementation is asked to return a
72 # simple boolean response to a validity inquiry, without knowing
73 # the context in which the certificate was proposed for use, it
74 # MUST validate the full conjunction of all assertions made in the
75 # X.509 certificate itself in order to return "true".
79 # OpenPGPCertificateEmbedded: the "signature" material in the X.509
80 # certificate is actually a set of OpenPGP packets corresponding to a
81 # complete "transferable public key" as specified in
82 # https://tools.ietf.org/html/rfc4880#section-11.1 , in "raw"
83 # (non-ascii-armored) form.
85 # this is the same as NullSignatureUseOpenPGP, but with the OpenPGP
86 # material transported in-band in addition.
88 # this has a few downsides:
90 # 1) data duplication -- the X.509 Subject Public Key material is
91 # repeated (either in the primary key packet, or in one of the
92 # subkey packets). The X.509 Subject material (and any
93 # subjectAltNames) are also duplicated in the User ID packets.
94 # This increases the size of the certificate. It also creates
95 # potential inconsistencies. If the X.509 Subject Public Key
96 # material is not found found in the OpenPGP Transferable Public
97 # Key (either as a primary key or as a subkey), conforming
98 # implementations MUST reject the certificate.
100 # 2) the requirement for out-of-band verification is not entirely
101 # removed, since conformant implementations may want to check the
102 # public keyservers for things like revocation certificates.
107 # this is a 5 followed by a 0. it fits into the "Parameters" section
108 # of an ASN.1 algorithmIdentifier object. what does this mean?
109 # I think it means the NULL type.
110 my $noparams = sprintf('%c%c', 5, 0);
112 my $extensions = { 'PGPExtension' => '1.3.6.1.4.1.3401.8.1.1' };
114 my $gnupg = GnuPG::Interface::->new();
115 $gnupg->options->quiet(1);
116 $gnupg->options->batch(1);
127 # see https://tools.ietf.org/html/rfc5280#section-4.1.2.5
128 return {'generalTime' => '99991231235959Z' };
130 my ($sec,$min,$hour,$mday,$mon,$year) = gmtime($ts);
132 if (($year < 1950) ||
134 return {'generalTime' => sprintf('%04d%02d%02d%02d%02d%02dZ', $year, $mon+1, $mday, $hour, $min, $sec) };
136 return {'utcTime' => sprintf('%02d%02d%02d%02d%02d%02dZ', ($year%100), $mon+1, $mday, $hour, $min, $sec) };
145 my ($sec,$min,$hour,$mday,$mon,$year) = gmtime($ts);
147 return sprintf('%04d-%02d-%02dT%02d:%02d:%02dZ', $year, $mon+1, $mday, $hour, $min, $sec);
150 sub makeX509CertForUserID {
159 if ($userid =~ /^\s+/) {
160 err("We will not process User IDs with leading whitespace\n");
163 if ($userid =~ /\s+$/) {
164 err("We will not process User IDs with trailing whitespace\n");
167 if ($userid =~ /\n/) {
168 err("We will not process User IDs containing newlines\n");
171 # FIXME: do we want to rule out any other forms of User ID?
174 if ($userid =~ /^(.*)\s+<([^><@\s]+\@$RE{net}{domain})>$/ ) {
175 # this is a typical/expected OpenPGP User ID.
180 'type' => $dntypes->{'CN'},
182 'printableString' => $humanname,
186 'type' => $dntypes->{'emailAddress'},
188 'ia5String' => $emailaddress,
192 } elsif ($userid =~ /^(https|ssh|smtps?|ike|postgresql|imaps?|submission):\/\/($RE{net}{domain})$/) {
196 'type' => $dntypes->{'CN'},
198 'printableString' => $hostname
202 # what should we do here? Maybe we just assume this is a bare Human Name?
203 err("Assuming '%s' is a bare human name.\n", $userid);
204 $humanname = $userid;
207 foreach my $gpgkey ($gnupg->get_public_keys('='.$userid)) {
210 foreach my $tryuid ($gpgkey->user_ids) {
211 if ($tryuid->as_string eq $userid) {
212 $validity = $tryuid->validity;
215 if (defined($protocol) &&
216 ($tryuid->validity =~ /^[fu]$/) &&
217 ($tryuid =~ /^$protocol\:\/\/($RE{net}{domain})/ )) {
221 if ($validity !~ /^[fu]$/) {
222 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);
226 # treat primary keys just like subkeys:
227 foreach my $subkey ($gpgkey, @{$gpgkey->subkeys}) {
228 if ($subkey->{algo_num} != 1) {
229 err("key 0x%s is algorithm %d (not RSA) -- we currently only handle RSA\n", $subkey->fingerprint->as_hex_string, $subkey->algo_num);
232 # FIXME: reject/skip over revoked/expired keys.
234 my $pubkey = { 'modulus' => @{$subkey->pubkey_data}[0],
235 'exponent' => @{$subkey->pubkey_data}[1],
237 my $vnotbefore = $subkey->creation_date;
239 my $vnotafter = $subkey->expiration_date;
240 # expiration date should be the minimum of the primary key and the subkey:
241 if (!defined($vnotafter)) {
242 $vnotafter = $gpgkey->expiration_date;
243 } elsif (defined($gpgkey->expiration_date)) {
244 $vnotafter = $gpgkey->expiration_date
245 if ($gpgkey->expiration_date < $vnotafter);
248 my $cnotbefore = ts2Time($vnotbefore);
249 my $cnotafter = ts2Time($vnotafter);
251 my $pgpeval = $pgpe->encode({ 'version' => 0, 'keyCreation' => $cnotbefore });
253 if (!defined($pgpeval));
255 my $pubkeybitstring = $rsapubkeyinfo->encode($pubkey);
256 print $rsapubkeyinfo->{error}
257 if (!defined($pubkeybitstring));
260 push(@extensions, { 'extnID' => $extensions->{'PGPExtension'},
261 'extnValue' => $pgpeval
264 # FIXME: base some keyUsage extensions on the type of User ID
265 # and on the usage flags of the key in question.
268 # if protocol =~ /^http|ssh|smtps?|postgresql|imaps?|submission$/ then set TLS server eKU + ???
269 # if protocol eq 'ike' then ??? (ask micah)
270 # if protocol =~ /^smtps?$/ then set TLS client + ???
271 # if defined($humanname) then set TLS client + ???
279 # if 'c' is present: I think we should never specify CA:TRUE or
280 # CA:FALSE in these certificates, since (a) we do not expect
281 # these keys to actually be making X.509-style certifications,
282 # but (b) we also don't want to assert that they can't make
283 # any certifications whatsoever.
286 # FIXME: add subjectAltName that matches the type of information
287 # we believe we're working with (see the cert-id draft).
289 # FIXME: if @sans is present, add them as subjectAltNames (do we
290 # want to do this? maybe this should be optional).
294 'tbsCertificate' => {
295 'version' => 2, # 0 == version 1, 1 == version 2, 2 == version 3
296 # this is a convenient way to pass the fpr too.
297 'serialNumber' => Math::BigInt->new('0x'.$subkey->fingerprint->as_hex_string),
298 'subjectPublicKeyInfo' => {
300 'parameters' => $noparams,
301 'algorithm' => $algos->{'RSA'},
303 'subjectPublicKey' => $pubkeybitstring,
306 'notAfter' => $cnotafter,
307 'notBefore' => $cnotbefore,
309 'signature' => { # maybe we should make up our own "signature algorithm" here?
310 'parameters' => $noparams,
311 'algorithm' => $algos->{'NullSignatureUseOpenPGP'}
314 'rdnSequence' => $subject,
317 'rdnSequence' => [ [ {
318 'type' => $dntypes->{'OU'},
319 'value' => { 'printableString' => sprintf('Please check the OpenPGP keyservers for certification information. (certificate generated on %s)', ts2ISO8601(time())) },
322 'extensions' => \@extensions,
324 'signature' => 'use OpenPGP',
325 'signatureAlgorithm' => {
326 'parameters' => $noparams,
327 'algorithm' => $algos->{'NullSignatureUseOpenPGP'}
331 my $dd = $cert->encode($newcert);
340 foreach $cert ( @{ makeX509CertForUserID($ARGV[0]) } ) {
341 printf("-----BEGIN CERTIFICATE-----\n%s-----END CERTIFICATE-----\n", encode_base64($cert));