certificates ala communigate, RT#7515
[freeside.git] / FS / FS / svc_cert.pm
1 package FS::svc_cert;
2
3 use strict;
4 use base qw( FS::svc_Common );
5 #use FS::Record qw( qsearch qsearchs );
6 use FS::cust_svc;
7
8 =head1 NAME
9
10 FS::svc_cert - Object methods for svc_cert records
11
12 =head1 SYNOPSIS
13
14   use FS::svc_cert;
15
16   $record = new FS::svc_cert \%hash;
17   $record = new FS::svc_cert { 'column' => 'value' };
18
19   $error = $record->insert;
20
21   $error = $new_record->replace($old_record);
22
23   $error = $record->delete;
24
25   $error = $record->check;
26
27 =head1 DESCRIPTION
28
29 An FS::svc_cert object represents a certificate.  FS::svc_cert inherits from
30 FS::Record.  The following fields are currently supported:
31
32 =over 4
33
34 =item svcnum
35
36 primary key
37
38 =item recnum
39
40 recnum
41
42 =item privatekey
43
44 privatekey
45
46 =item csr
47
48 csr
49
50 =item certificate
51
52 certificate
53
54 =item cacert
55
56 cacert
57
58 =item common_name
59
60 common_name
61
62 =item organization
63
64 organization
65
66 =item organization_unit
67
68 organization_unit
69
70 =item city
71
72 city
73
74 =item state
75
76 state
77
78 =item country
79
80 country
81
82 =item cert_contact
83
84 contact email
85
86
87 =back
88
89 =head1 METHODS
90
91 =over 4
92
93 =item new HASHREF
94
95 Creates a new certificate.  To add the certificate to the database, see L<"insert">.
96
97 Note that this stores the hash reference, not a distinct copy of the hash it
98 points to.  You can ask the object for a copy with the I<hash> method.
99
100 =cut
101
102 # the new method can be inherited from FS::Record, if a table method is defined
103
104 sub table { 'svc_cert'; }
105
106 sub table_info {
107   my %dis = ( disable_default=>1, disable_fixed=>1, disable_inventory=>1, disable_select=>1 );
108   {
109     'name' => 'Certificate',
110     'name_plural' => 'Certificates',
111     'longname_plural' => 'Example services', #optional
112     'sorts' => 'svcnum', # optional sort field (or arrayref of sort fields, main first)
113     'display_weight' => 25,
114     'cancel_weight'  => 65,
115     'fields' => {
116       #'recnum'            => '',
117       'privatekey'        => { label=>'Private key', %dis, },
118       'csr'               => { label=>'Certificate signing request', %dis, },
119       'certificate'       => { label=>'Certificate', %dis, },
120       'cacert'            => { label=>'Certificate authority chain', %dis, },
121       'common_name'       => { label=>'Common name', %dis, },
122       'organization'      => { label=>'Organization', %dis, },
123       'organization_unit' => { label=>'Organization Unit', %dis, },
124       'city'              => { label=>'City', %dis, },
125       'state'             => { label=>'State', %dis, },
126       'country'           => { label=>'Country', %dis, },
127       'cert_contact'      => { label=>'Contact email', %dis, },
128       
129       #'another_field' => { 
130       #                     'label'     => 'Description',
131       #                     'def_label' => 'Description for service definitions',
132       #                     'type'      => 'text',
133       #                     'disable_default'   => 1, #disable switches
134       #                     'disable_fixed'     => 1, #
135       #                     'disable_inventory' => 1, #
136       #                   },
137       #'foreign_key'   => { 
138       #                     'label'        => 'Description',
139       #                     'def_label'    => 'Description for service defs',
140       #                     'type'         => 'select',
141       #                     'select_table' => 'foreign_table',
142       #                     'select_key'   => 'key_field_in_table',
143       #                     'select_label' => 'label_field_in_table',
144       #                   },
145
146     },
147   };
148 }
149
150 =item label
151
152 Returns a meaningful identifier for this example
153
154 =cut
155
156 sub label {
157   my $self = shift;
158 #  $self->label_field; #or something more complicated if necessary
159   # check privatekey, check->privatekey, more?
160   return 'Certificate';
161 }
162
163 =item insert
164
165 Adds this record to the database.  If there is an error, returns the error,
166 otherwise returns false.
167
168 =cut
169
170 # the insert method can be inherited from FS::Record
171
172 =item delete
173
174 Delete this record from the database.
175
176 =cut
177
178 # the delete method can be inherited from FS::Record
179
180 =item replace OLD_RECORD
181
182 Replaces the OLD_RECORD with this one in the database.  If there is an error,
183 returns the error, otherwise returns false.
184
185 =cut
186
187 # the replace method can be inherited from FS::Record
188
189 =item check
190
191 Checks all fields to make sure this is a valid certificate.  If there is
192 an error, returns the error, otherwise returns false.  Called by the insert
193 and replace methods.
194
195 =cut
196
197 # the check method should currently be supplied - FS::Record contains some
198 # data checking routines
199
200 sub check {
201   my $self = shift;
202
203   my $error = 
204     $self->ut_numbern('svcnum')
205     || $self->ut_numbern('recnum')
206     || $self->ut_anything('privatekey') #XXX
207     || $self->ut_anything('csr')        #XXX
208     || $self->ut_anything('certificate')#XXX
209     || $self->ut_anything('cacert')     #XXX
210     || $self->ut_textn('common_name')
211     || $self->ut_textn('organization')
212     || $self->ut_textn('organization_unit')
213     || $self->ut_textn('city')
214     || $self->ut_textn('state')
215     || $self->ut_textn('country') #XXX char(2) or NULL
216     || $self->ut_textn('cert_contact')
217   ;
218   return $error if $error;
219
220   $self->SUPER::check;
221 }
222
223 =item generate_privatekey [ KEYSIZE ]
224
225 =cut
226
227 use IPC::Run qw( run );
228 use File::Temp;
229
230 sub generate_privatekey {
231   my $self = shift;
232   my $keysize = (@_ && $_[0]) ? shift : 2048;
233   run( [qw( openssl genrsa ), $keysize], '>pipe'=>\*OUT, '2>'=>'/dev/null' )
234     or die "error running openssl: $!";
235   #XXX error checking
236   my $privatekey = join('', <OUT>);
237   $self->privatekey($privatekey);
238 }
239
240 =item check_privatekey
241
242 =cut
243
244 sub check_privatekey {
245   my $self = shift;
246   my $in = $self->privatekey;
247   run( [qw( openssl rsa -check -noout)], '<'=>\$in, '>pipe'=>\*OUT, '2>'=>'/dev/null' )
248    ;# or die "error running openssl: $!";
249
250   my $ok = <OUT>;
251   return ($ok =~ /key ok/);
252 }
253
254 my %subj = (
255   'CN' => 'common_name',
256   'O'  => 'organization',
257   'OU'  => 'organization_unit',
258   'L' => 'city',
259   'ST' => 'state',
260   'C' => 'country',
261 );
262
263 sub subj {
264   my $self = shift;
265
266   '/'. join('/', map { my $v = $self->get($subj{$_});
267                        $v =~ s/([=\/])/\\$1/;
268                        "$_=$v";
269                      }
270                      keys %subj
271            );
272 }
273
274 sub _file {
275   my $self = shift;
276   my $field = shift;
277   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc; #XXX actual cache dir
278   my $fh = new File::Temp(
279     TEMPLATE => 'cert.'. '.XXXXXXXX',
280     DIR      => $dir,
281   ) or die "can't open temp file: $!\n";
282   print $fh $self->$field;
283   close $fh;
284   $fh;
285 }
286
287 sub generate_csr {
288   my $self = shift;
289
290   my $fh = $self->_file('privatekey');
291
292   run( [qw( openssl req -new -key ), $fh->filename, '-subj', $self->subj ],
293        '>pipe'=>\*OUT, '2>'=>'/dev/null'
294      ) 
295     or die "error running openssl: $!";
296   #XXX error checking
297   my $csr = join('', <OUT>);
298   $self->csr($csr);
299 }
300
301 #sub check_csr {
302 #  my $self = shift;
303 #}
304
305 sub generate_selfsigned {
306   my $self = shift;
307
308   my $days = 730;
309
310   my $key = $self->_file('privatekey');
311   my $csr = $self->_file('csr');
312
313   run( [qw( openssl req -x509 -nodes ),
314               '-days' => $days,
315               '-key'  => $key->filename,
316               '-in'   => $csr->filename,
317        ],
318        '>pipe'=>\*OUT, '2>'=>'/dev/null'
319      ) 
320     or die "error running openssl: $!";
321   #XXX error checking
322   my $csr = join('', <OUT>);
323   $self->certificate($csr);
324 }
325
326 #openssl x509 -in cert -noout -subject -issuer -dates -serial
327 #subject= /CN=cn.example.com/ST=AK/O=Tofuy/OU=Soybean dept./C=US/L=Tofutown
328 #issuer= /CN=cn.example.com/ST=AK/O=Tofuy/OU=Soybean dept./C=US/L=Tofutown
329 #notBefore=Nov  7 05:07:42 2010 GMT
330 #notAfter=Nov  6 05:07:42 2012 GMT
331 #serial=B1DBF1A799EF207B
332
333 sub check_certificate {
334   my $self = shift;
335
336   my $in = $self->certificate;
337   run( [qw( openssl x509 -noout -subject -issuer -dates -serial )],
338        '<'=>\$in,
339        '>pipe'=>\*OUT, '2>'=>'/dev/null'
340      ) 
341     or die "error running openssl: $!";
342   #XXX error checking
343
344   my %hash = ();
345   while (<OUT>) {
346     warn $_;
347     /^\s*(\w+)=\s*(.*)\s*$/ or next;
348     $hash{$1} = $2;
349   }
350
351   %hash;
352 }
353
354 =back
355
356 =head1 BUGS
357
358 =head1 SEE ALSO
359
360 L<FS::Record>, schema.html from the base documentation.
361
362 =cut
363
364 1;
365