Add gpg key generation from rfc822 template file.
[monkeysphere-validation-agent.git] / openpgp2x509
1 #!/usr/bin/perl
2
3 # Author: Daniel Kahn Gillmor <dkg@fifthhorseman.net>
4 # Copyright: 2011
5 # License: GPL-3+
6
7 # WARNING: This is very rough code!  the interface WILL change
8 # dramatically.  The only thing I can commit to keeping stable are the
9 # OIDs.
10
11 # Use this code to take an OpenPGP certificate (pubkey) and emit a
12 # corresponding OpenPGP-validated X.509 certificate.
13
14 # Usage: openpgp2x509 ssh://lair.fifthhorseman.net
15
16 use strict;
17 use warnings;
18 use Crypt::X509 0.50;
19 use Math::BigInt;
20 use GnuPG::Interface 0.43;
21 use Regexp::Common qw /net/;
22 use MIME::Base64;
23
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');
29
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)
37             };
38
39 my $algos = {
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',
52
53             };
54
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
58 # keyring.
59
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
66 # doable?)
67
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".
76
77
78
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.
84
85 # If it were implemented, it would be the same as
86 # NullSignatureUseOpenPGP, but with the OpenPGP material transported
87 # in-band in addition.
88
89 ## NOTE: There is no implementation of the OpenPGPCertificateEmbedded,
90 ## and maybe there never will be.  Another approach would be to
91 ## transmitting OpenPGP signature packets in the TLS channel itself,
92 ## with an extension comparable to OCSP stapling.
93
94 # the OpenPGPCertificateEmbedded concept has a few downsides:
95
96 # 1) data duplication -- the X.509 Subject Public Key material is
97 #    repeated (either in the primary key packet, or in one of the
98 #    subkey packets).  The X.509 Subject material (and any
99 #    subjectAltNames) are also duplicated in the User ID packets.
100 #    This increases the size of the certificate.  It also creates
101 #    potential inconsistencies.  If the X.509 Subject Public Key
102 #    material is not found found in the OpenPGP Transferable Public
103 #    Key (either as a primary key or as a subkey), conforming
104 #    implementations MUST reject the certificate.
105
106 # 2) the requirement for out-of-band verification is not entirely
107 #    removed, since conformant implementations may want to check the
108 #    public keyservers for things like revocation certificates.
109
110
111
112
113 # this is a 5 followed by a 0.  it fits into the "Parameters" section
114 # of an ASN.1 algorithmIdentifier object. what does this mean?
115 # I think it means the NULL type.
116 my $noparams = sprintf('%c%c', 5, 0);
117
118 my $extensions = { 'PGPExtension' => '1.3.6.1.4.1.3401.8.1.1' };
119
120 my $gnupg = GnuPG::Interface::->new();
121 $gnupg->options->quiet(1);
122 $gnupg->options->batch(1);
123
124 sub err {
125   printf STDERR @_;
126 }
127
128
129 sub ts2Time {
130   my $ts = shift;
131
132   if (!defined($ts)) {
133     # see https://tools.ietf.org/html/rfc5280#section-4.1.2.5
134     return {'generalTime' => '99991231235959Z' };
135   } else {
136     my ($sec,$min,$hour,$mday,$mon,$year) = gmtime($ts);
137     $year += 1900;
138     if (($year < 1950) ||
139         ($year >= 2050)) {
140       return {'generalTime' => sprintf('%04d%02d%02d%02d%02d%02dZ', $year, $mon+1, $mday, $hour, $min, $sec) };
141     } else {
142       return {'utcTime' => sprintf('%02d%02d%02d%02d%02d%02dZ', ($year%100), $mon+1, $mday, $hour, $min, $sec) };
143     }
144   }
145 }
146
147 sub ts2ISO8601 {
148   my $ts = shift;
149   $ts = time()
150     if (!defined($ts));
151   my ($sec,$min,$hour,$mday,$mon,$year) = gmtime($ts);
152   $year += 1900;
153   return sprintf('%04d-%02d-%02dT%02d:%02d:%02dZ', $year, $mon+1, $mday, $hour, $min, $sec);
154 };
155
156 sub makeX509CertForUserID {
157   my $userid = shift;
158   my $hostname;
159   my $protocol;
160   my $emailaddress;
161   my $humanname;
162   my $subject;
163   my $ret = [];
164
165   if ($userid =~ /^\s+/) {
166     err("We will not process User IDs with leading whitespace\n");
167     return $ret;
168   }
169   if ($userid =~ /\s+$/) {
170     err("We will not process User IDs with trailing whitespace\n");
171     return $ret;
172   }
173   if ($userid =~ /\n/) {
174     err("We will not process User IDs containing newlines\n");
175     return $ret;
176   }
177   # FIXME: do we want to rule out any other forms of User ID?
178
179
180   if ($userid =~ /^(.*)\s+<([^><@\s]+\@$RE{net}{domain})>$/ ) {
181     # this is a typical/expected OpenPGP User ID.
182     $humanname = $1;
183     $emailaddress = $2;
184     $subject = [
185                 [ {
186                    'type' => $dntypes->{'CN'},
187                    'value' => {
188                                'printableString' => $humanname,
189                               },
190                   } ],
191                 [ {
192                    'type' => $dntypes->{'emailAddress'},
193                    'value' => {
194                                'ia5String' => $emailaddress,
195                               },
196                   } ],
197                ];
198   } elsif ($userid =~ /^(https|ssh|smtps?|ike|postgresql|imaps?|submission):\/\/($RE{net}{domain})$/) {
199     $protocol = $1;
200     $hostname = $2;
201     $subject = [ [ {
202                     'type' => $dntypes->{'CN'},
203                     'value' => {
204                                 'printableString' => $hostname
205                                },
206                    } ] ];
207   } else {
208     # what should we do here?  Maybe we just assume this is a bare Human Name?
209     err("Assuming '%s' is a bare human name.\n", $userid);
210     $humanname = $userid;
211   }
212
213   foreach my $gpgkey ($gnupg->get_public_keys('='.$userid)) {
214     my $validity = '-';
215     my @sans;
216     foreach my $tryuid ($gpgkey->user_ids) {
217       if ($tryuid->as_string eq $userid) {
218         $validity = $tryuid->validity;
219       }
220
221       if (defined($protocol) &&
222           ($tryuid->validity =~ /^[fu]$/) &&
223           ($tryuid =~ /^$protocol\:\/\/($RE{net}{domain})/ )) {
224         push(@sans, $2);
225       }
226     }
227     if ($validity !~ /^[fu]$/) {
228       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);
229       next;
230     }
231
232     # treat primary keys just like subkeys:
233     foreach my $subkey ($gpgkey, @{$gpgkey->subkeys}) {
234       if ($subkey->{algo_num} != 1) {
235         err("key 0x%s is algorithm %d (not RSA) -- we currently only handle RSA\n", $subkey->fingerprint->as_hex_string, $subkey->algo_num);
236         next;
237       }
238       # FIXME: reject/skip over revoked/expired keys.
239
240       my $pubkey = { 'modulus' => @{$subkey->pubkey_data}[0],
241                      'exponent' => @{$subkey->pubkey_data}[1],
242                    };
243       my $vnotbefore = $subkey->creation_date;
244
245       my $vnotafter = $subkey->expiration_date;
246       # expiration date should be the minimum of the primary key and the subkey:
247       if (!defined($vnotafter)) {
248         $vnotafter = $gpgkey->expiration_date;
249       } elsif (defined($gpgkey->expiration_date)) {
250         $vnotafter = $gpgkey->expiration_date
251           if ($gpgkey->expiration_date < $vnotafter);
252       }
253
254       my $cnotbefore = ts2Time($vnotbefore);
255       my $cnotafter = ts2Time($vnotafter);
256
257       my $pgpeval = $pgpe->encode({ 'version' => 0, 'keyCreation' => $cnotbefore });
258       print $pgpe->{error}
259         if (!defined($pgpeval));
260
261       my $pubkeybitstring = $rsapubkeyinfo->encode($pubkey);
262       print $rsapubkeyinfo->{error}
263         if (!defined($pubkeybitstring));
264
265       my @extensions;
266       push(@extensions, { 'extnID' => $extensions->{'PGPExtension'},
267                           'extnValue' => $pgpeval
268                         });
269
270       # FIXME: base some keyUsage extensions on the type of User ID
271       # and on the usage flags of the key in question.
272
273       # if 'a' is present
274       # if protocol =~ /^http|ssh|smtps?|postgresql|imaps?|submission$/ then set TLS server eKU + ???
275       # if protocol eq 'ike' then ??? (ask micah)
276       # if protocol =~ /^smtps?$/ then set TLS client + ???
277       # if defined($humanname) then set TLS client + ???
278
279       # if 'e' is present:
280       # ???
281
282       # if 's' is present:
283       # ???
284
285       # if 'c' is present: I think we should never specify CA:TRUE or
286       # CA:FALSE in these certificates, since (a) we do not expect
287       # these keys to actually be making X.509-style certifications,
288       # but (b) we also don't want to assert that they can't make
289       # any certifications whatsoever.
290
291
292       # FIXME: add subjectAltName that matches the type of information
293       # we believe we're working with (see the cert-id draft).
294
295       # FIXME: if @sans is present, add them as subjectAltNames (do we
296       # want to do this? maybe this should be optional).
297
298
299       my $newcert = {
300                      'tbsCertificate' => {
301                                           'version' => 2, # 0 == version 1, 1 == version 2, 2 == version 3
302                                           # this is a convenient way to pass the fpr too.
303                                           'serialNumber' => Math::BigInt->new('0x'.$subkey->fingerprint->as_hex_string),
304                                           'subjectPublicKeyInfo' => {
305                                                                      'algorithm' => {
306                                                                                      'parameters' => $noparams,
307                                                                                      'algorithm' => $algos->{'RSA'},
308                                                                                     },
309                                                                      'subjectPublicKey' => $pubkeybitstring,
310                                                                     },
311                                           'validity' => {
312                                                          'notAfter' => $cnotafter,
313                                                          'notBefore' => $cnotbefore,
314                                                         },
315                                           'signature' => { # maybe we should make up our own "signature algorithm" here?
316                                                           'parameters' => $noparams,
317                                                           'algorithm' => $algos->{'NullSignatureUseOpenPGP'}
318                                                          },
319                                           'subject' => {
320                                                         'rdnSequence' => $subject,
321                                                        },
322                                           'issuer' => {
323                                                        'rdnSequence' => [ [ {
324                                                                            'type' => $dntypes->{'OU'},
325                                                                            'value' => { 'printableString' => sprintf('Please check the OpenPGP keyservers for certification information. (certificate generated on %s)', ts2ISO8601(time())) },
326                                                                           } ] ],
327                                                       },
328                                           'extensions' => \@extensions,
329                                          },
330                      'signature' => 'use OpenPGP',
331                      'signatureAlgorithm' => {
332                                               'parameters' => $noparams,
333                                               'algorithm' => $algos->{'NullSignatureUseOpenPGP'}
334                                              }
335                     };
336
337       my $dd = $cert->encode($newcert);
338
339       push(@{$ret}, $dd);
340     }
341   }
342   return $ret;
343 }
344
345
346 foreach $cert ( @{ makeX509CertForUserID($ARGV[0]) } ) {
347   printf("-----BEGIN CERTIFICATE-----\n%s-----END CERTIFICATE-----\n", encode_base64($cert));
348 }