File Coverage

blib/lib/VOMS/Lite/CertKeyHelper.pm
Criterion Covered Total %
statement 180 223 80.7
branch 37 86 43.0
condition 22 63 34.9
subroutine 15 15 100.0
pod 0 7 0.0
total 254 394 64.4


line stmt bran cond sub pod time code
1             package VOMS::Lite::CertKeyHelper;
2              
3 1     1   26 use 5.004;
  1         4  
  1         45  
4 1     1   5 use strict;
  1         2  
  1         43  
5 1     1   7 use VOMS::Lite::ASN1Helper qw(ASN1Index ASN1Unwrap ASN1Wrap Hex ASN1OIDtoOID);
  1         2  
  1         91  
6 1     1   795 use VOMS::Lite::RSAHelper qw(rsasign rsaverify);
  1         2  
  1         84  
7 1     1   925 use VOMS::Lite::PEMHelper qw(readCert);
  1         4  
  1         88  
8 1     1   8 use VOMS::Lite::X509;
  1         2  
  1         32  
9 1     1   6 use Digest::MD5 qw(md5);
  1         2  
  1         59  
10              
11             require Exporter;
12 1     1   6 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
  1         2  
  1         3359  
13             @ISA = qw(Exporter);
14             %EXPORT_TAGS = ( );
15             @EXPORT_OK = qw(buildchain digestSign OIDtoDNattrib DNattribToOID);
16             @EXPORT = ( );
17             $VERSION = '0.20';
18             ##################################################
19              
20             # Define some common OIDs used in Distunguished names NB we're using UID and Email not UserID and emailAddress
21             my %DNOIDs=( '2.5.4.49' => 'DN',
22             '0.9.2342.19200300.100.1.1' => 'UID',
23             '0.9.2342.19200300.100.1.25' => 'DC',
24             '1.2.840.113549.1.9.1' => 'Email',
25             '2.5.4.3' => 'CN',
26             '2.5.4.4' => 'SN',
27             '2.5.4.5' => 'serialNumber',
28             '2.5.4.6' => 'C',
29             '2.5.4.7' => 'L',
30             '2.5.4.8' => 'ST',
31             '2.5.4.9' => 'street',
32             '2.5.4.12' => 'title',
33             '2.5.4.16' => 'postalAddress',
34             '2.5.4.17' => 'postalCode',
35             '2.5.4.18' => 'postOfficeBox',
36             '2.5.4.26' => 'registeredAddress',
37             '2.5.4.11' => 'OU',
38             '2.5.4.41' => 'name',
39             '2.5.4.10' => 'O',
40             '2.5.4.42' => 'givenName',
41             '2.5.4.43' => 'initials',
42             '2.5.6.3' => 'locality',
43             '2.5.6.4' => 'organization');
44              
45             my %DNAttribs=( reverse %DNOIDs,
46             commonName => '2.5.4.3',
47             serialNumber => '2.5.4.4',
48             countryName => '2.5.4.6',
49             localityName => '2.5.4.7',
50             stateOrProvinceName => '2.5.4.8',
51             organizationName => '2.5.4.10',
52             organizationalUnitName => '2.5.4.11',
53             emailAddress => '1.2.840.113549.1.9.1',
54             UserID => '0.9.2342.19200300.100.1.1',
55             domainComponent => '0.9.2342.19200300.100.1.25'
56             );
57              
58             sub OIDtoDNattrib {
59 30 50   30 0 222 return (defined $DNOIDs{$_[0]})?$DNOIDs{$_[0]}:$_[0];
60             }
61              
62             sub DNattribToOID {
63 9 50   9 0 40 return (defined $DNAttribs{$_[0]})?$DNAttribs{$_[0]}:undef;
64             }
65              
66             ##################################################
67              
68             sub verifychain {
69 1     1 0 2 my ($lastExp,$lastMod,$lastCAPurpose,$lastKeyCertSign,$lastkeyUsageDigitalSignature,$EECDN,$EECIDN,$EEC);
70 0         0 my @Self;
71 0         0 my @Time;
72 0         0 my @LifeTime;
73 0         0 my @Signed;
74 0         0 my @SignerPurposeCA;
75 0         0 my @SignerCertSignPurpose;
76 0         0 my @PathLen;
77 0         0 my @GSIType;
78 0         0 my @ProxyPathLen;
79 1         2 my $now=time();
80              
81             # Loop over certificate starting at 'root-most'
82 1         3 foreach ( reverse @_ ) {
83 2         5 my %CI = %{ VOMS::Lite::X509::Examine( $_ , { SubjectDN=>"",
  2         57  
84             IssuerDN=>"",
85             Start=>"",
86             End=>"",
87             SignatureType=>"",
88             SignatureValue=>"",
89             X509TBSCert=>"",
90             keyUsage=>"",
91             basicConstraints=>"",
92             KeypublicExponent=>"",
93             Keymodulus=>"",
94             authorityKeyIdentifier=>"",
95             subjectKeyIdentifier=>"",
96             ProxyInfo=>""
97             }) };
98              
99             # Check if selfsigned, if first in chain set 'last' values to these values
100 2         29 push @Self,0;
101 2 100       12 if ( $CI{IssuerDN} eq $CI{SubjectDN} ) {
102 1 50 33     12 if ( ! defined $CI{authorityKeyIdentifierSkid} || $CI{subjectKeyIdentifier} eq $CI{authorityKeyIdentifierSkid} ) {
103 1         3 $Self[-1]=1;
104 1 50       6 if ( ! defined $lastMod ) {
105 1         4 $lastExp = Hex($CI{KeypublicExponent});
106 1         6 $lastMod = Hex($CI{Keymodulus});
107 1         4 $lastCAPurpose = $CI{basicConstraintsCA};
108 1         2 $lastKeyCertSign = $CI{keyUsageKeyCertSign};
109 1         3 $lastkeyUsageDigitalSignature = $CI{keyUsageDigitalSignature};
110             } } }
111              
112             # Check Validity of Time, signature, Issuer's purpose and key usage, and path length
113 2 50 33     16 push @Time, ( $CI{Start} < $now && $CI{End} > $now ) ? 1:0;
114 2 50 33     17 push @PathLen, ( defined $CI{basicConstraintsPathLen} && $CI{basicConstraintsPathLen} < ($#_-$#PathLen) ) ? 0:1;
115 2         10 push @Signed, verifySignature($CI{SignatureType},Hex($CI{SignatureValue}),$CI{X509TBSCert},$lastExp,$lastMod);
116 2         15 push @SignerPurposeCA, $lastCAPurpose;
117 2         7 push @SignerCertSignPurpose, $lastKeyCertSign;
118             # TODO Check CRL: 1 find CRL 2 In Date 3 CRL Signed 4 CA has keyUsageCRLSign 5 Cert not in List
119              
120             # Check GSIness
121 2 50 33     20 if ( ! $SignerPurposeCA[-1] && ! $SignerCertSignPurpose[-1] && $lastkeyUsageDigitalSignature) {
      33        
122 0 0 0     0 if ( $CI{SubjectDN} eq "$CI{IssuerDN}/CN=proxy" ) {
    0          
    0          
123 0 0       0 if ( $CI{IssuerDN} =~ /\/CN=limited proxy$/ ) { push @GSIType,"Bad Legacy proxy: issuer was limited" }
  0         0  
124 0         0 else { push @GSIType,"Legacy Proxy" }
125             }
126 0         0 elsif ( $CI{SubjectDN} eq "$CI{IssuerDN}/CN=limited proxy" ) { push @GSIType,"Limited Proxy"; }
127             elsif ( $CI{ProxyInfo} eq 'RFC' || $CI{ProxyInfo} eq 'Pre-RFC' ) {
128 0 0       0 if ( $CI{SubjectDN} !~ /^$CI{IssuerDN}\/CN=[0-9]+$/ ) { push @GSIType,"Bad RFC proxy: issuer name mismatch" }
  0 0       0  
129 0         0 elsif ( $CI{IssuerDN} =~ /\/CN=(?:limited )?proxy$/ ) { push @GSIType,"Bad legacy proxy: issuer was legacy" }
130 0         0 else { push @GSIType,"$CI{ProxyInfo} Proxy"; }
131             }
132 0         0 else { push @GSIType,"Bad proxy"; }
133             }
134             else {
135 2 100 66     19 if ( $CI{basicConstraintsCA} || $CI{keyUsageKeyCertSign} ) { push @GSIType,"CA" }
  1         3  
136 1         3 else { push @GSIType, "EEC";
137 1         6 ($EECDN,$EECIDN,$EEC) = ($CI{SubjectDN},$CI{IssuerDN},$_);
138             }
139             }
140              
141             # GSI PathLen
142 2 50 33     18 push @ProxyPathLen,(defined $CI{"ProxyPathlen$CI{ProxyInfo}"} && $CI{"ProxyPathlen$CI{ProxyInfo}"}<($#_-$#ProxyPathLen))?0:1;
143              
144             # Populate remaining times array
145 2         8 push @LifeTime, ( $CI{End} - $now );
146              
147             # Remember relevant parts of this certificate as issuer for next
148 2         15 $lastExp = Hex($CI{KeypublicExponent});
149 2         11 $lastMod = Hex($CI{Keymodulus});
150 2         6 $lastCAPurpose = $CI{basicConstraintsCA};
151 2         5 $lastKeyCertSign = $CI{keyUsageKeyCertSign};
152 2         25 $lastkeyUsageDigitalSignature = $CI{keyUsageDigitalSignature};
153             }
154              
155 1         3 my $i=0;
156 1         2 my @returnErrors;
157 1         5 while ( defined $Time[$i] ) {
158 2         4 my @errors=();
159 2 50 66     12 if ( $i > 0 && $GSIType[$i] =~ /^Bad/ ) { push @errors, $GSIType[$i]; }
  0         0  
160 2 50       8 if ( ! $Time[$i] ) { push @errors, "Time error in certificate $i"; }
  0         0  
161 2 0 0     7 if ( ! $Signed[$i] && ( $i!=0 || $Self[$i] )) { push @errors, "Bad signature on certificate $i"; }
  0   33     0  
162 2 50 66     10 if ( $i!=0 && $Self[$i] ) { push @errors, "Self signed certificate in at $i the chain"; }
  0         0  
163 2 50 66     16 if ( $GSIType[$i] !~ /^(?:Lega[sc]y|Limited|RFC|Pre-RFC) Proxy$/ && ( $i!=0 || $Self[$i] ) ) {
      33        
164 2 50       6 if ( ! $SignerPurposeCA[$i] ) { push @errors, "Signer of certificate $i is not a CA"; }
  0         0  
165 2 50       9 if ( ! $SignerCertSignPurpose[$i] ) { push @errors, "Signer of certificate $i may not sign certificates"; }
  0         0  
166             }
167 2 50       5 if ( ! $PathLen[$i] ) { push @errors, "Path Length exceeded at certificate $i"; }
  0         0  
168 2 50       5 if ( ! $ProxyPathLen[$i] ) { push @errors, "Proxy Path Length exceeded at certificate $i"; }
  0         0  
169 2         3 push @returnErrors,\@errors;
170 2         7 $i++;
171             }
172 1         8 return ( \@returnErrors, \@GSIType, \@LifeTime, $EECDN, $EECIDN, $EEC );
173             }
174              
175             ##################################################
176              
177             sub buildchain {
178 1     1 0 2 my $inref = shift;
179 1         2 my %in = %{$inref};
  1         4  
180              
181 1 50       4 my @CAdirs = (defined $in{'trustedCAdirs'})? @{$in{'trustedCAdirs'}}:();
  1         3  
182 1 50       5 my @certs = (defined $in{'suppliedcerts'})? @{$in{'suppliedcerts'}}:();
  1         2  
183 1 50       4 my @CAcerts = (defined $in{'trustedCAs'})? @{$in{'trustedCAs'}}:();
  0         0  
184              
185 1         2 my %cert; my %hash; my %ihash; my %skid; my %akid; my %dn; my %idn; my %dir; my %trust;
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
186 1         12 my $CertHashTemplate={authorityKeyIdentifier=>"", subjectKeyIdentifier=>"", keyUsage=>"", basicConstraints=>"",IHash=>"",Hash=>"",SubjectDN=>"",IssuerDN=>""};
187              
188             # Load Leaf certificate information
189 1         2 my @returnCerts = (shift @certs); # First one is treated as the leaf certificate/gsi-certificate
190 1         8 my $CertInfoRef = VOMS::Lite::X509::Examine( $returnCerts[0] ,$CertHashTemplate);
191 1         26 my %CERTINFO = %$CertInfoRef;
192 1         5 my @IHash = ($CERTINFO{IHash});
193 1         3 my @Hash = ($CERTINFO{Hash});
194 1         4 my @SKID = ($CERTINFO{subjectKeyIdentifier});
195 1         2 my @AKID = ($CERTINFO{authorityKeyIdentifierSkid});
196 1         3 my @DNs = ($CERTINFO{SubjectDN});
197 1         3 my @IDNs = ($CERTINFO{IssuerDN});
198 1         1 my @Trust = (0);
199              
200             #Make place holder for CA certs which reside in supplied directories (only load them if needed)
201 1         2 my @cas;
202 1         3 foreach my $dir (reverse @CAdirs) { # First directory takes presidence
203 1         63 opendir(GRIDSECURITYDIR,$dir);
204 1         824 push @cas, grep(/\.[0-9]+$/, readdir(GRIDSECURITYDIR));
205 1         16 closedir(GRIDSECURITYDIR);
206 1         4 foreach (@cas) {
207 1         4 $hash{$_}="$_";
208 1         2 $cert{$_}="";
209 1         6 $dir{$_}=$dir;
210             }
211             }
212              
213             #Load locally supplied CAcert info and peer supplied CAcert info
214 1         3 my $remainingtrusted=$#CAcerts;
215 1         3 foreach (@CAcerts,@certs) {
216 1         4 my $CertInfoRef=VOMS::Lite::X509::Examine($_,$CertHashTemplate);
217 1         22 my %CERTINFO=%$CertInfoRef;
218 1         5 my $index=0;
219 1         10 until ( ! defined $hash{"$CERTINFO{Hash}.$index"} ) { $index++; }
  1         5  
220 1         5 $cert{"$CERTINFO{Hash}.$index"} = $_;
221 1         5 $hash{"$CERTINFO{Hash}.$index"} = $CERTINFO{Hash};
222 1         3 $ihash{"$CERTINFO{Hash}.$index"} = $CERTINFO{IHash};
223 1         5 $skid{"$CERTINFO{Hash}.$index"} = $CERTINFO{subjectKeyIdentifier};
224 1         4 $akid{"$CERTINFO{Hash}.$index"} = $CERTINFO{authorityKeyIdentifierSkid};
225 1         5 $dn{"$CERTINFO{Hash}.$index"} = $CERTINFO{SubjectDN};
226 1         4 $idn{"$CERTINFO{Hash}.$index"} = $CERTINFO{IssuerDN};
227 1 50       6 $trust{"$CERTINFO{Hash}.$index"} = ( $remainingtrusted > -1 ) ? 1:0; #trust only locally supplied certs
228 1         12 $remainingtrusted--;
229             }
230              
231             #Loop round all certificate authorities until the chain is constructed or there are no found issuers.
232 1         3 my $found;
233 1         2 my $self=0;
234 1         3 for(;;) {
235 1         2 $found="no";
236 1         3 my @a;
237 1 50       5 foreach (keys(%cert)) { if ($_ =~ /^$IHash[-1].[0-9]+$/ ) { push @a, $_;} }
  2         51  
  2         7  
238 1         11 my @b=sort(@a);
239 1         5 foreach my $file ( @b ) {
240 1 50 33     19 if ( $cert{$file} eq "" && defined $dir{$file} ) { # Load in CA certificate as required
241 1         11 my $certder = readCert("$dir{$file}/$file");
242 1         7 my $CertInfoRef = VOMS::Lite::X509::Examine($certder,$CertHashTemplate);
243 1         34 my %CERTINFO = %$CertInfoRef;
244 1         5 $cert{$file} = $certder;
245 1         3 $hash{$file} = $CERTINFO{Hash};
246 1         4 $ihash{$file} = $CERTINFO{IHash};
247 1         3 $skid{$file} = $CERTINFO{subjectKeyIdentifier};
248 1         2 $akid{$file} = $CERTINFO{authorityKeyIdentifierSkid};
249 1         3 $dn{$file} = $CERTINFO{SubjectDN};
250 1         2 $idn{$file} = $CERTINFO{IssuerDN};
251 1         10 $trust{$file} = 1;
252             }
253              
254 1 50 33     17 if($IDNs[-1] eq $dn{$file} && ( ! defined $AKID[-1] || $AKID[-1] eq $skid{$file} ) ) { #Issuer names match and Key ID's match
      33        
255 1         3 $found="yes";
256 1 50 0     8 if ( $DNs[-1] eq $IDNs[-1] && ( ! defined $AKID[-1] || $SKID[-1] eq $AKID[-1] )) { #Check last cert not self-signed
      33        
257 0         0 $self=1;
258 0         0 $Trust[-1]=1;
259 0         0 last;
260             }
261             else {
262 1         3 push @returnCerts, $cert{$file};
263 1         3 push @IHash, $ihash{$file};
264 1         3 push @Hash, $hash{$file};
265 1         4 push @SKID, $skid{$file};
266 1         2 push @AKID, $akid{$file};
267 1         2 push @DNs, $dn{$file};
268 1         1 push @IDNs, $idn{$file};
269 1         3 push @Trust, $trust{$file};
270 1         3 last;
271             }
272             }
273             }
274             # Stop looping if no signer found or certificate is self signed
275 1 50       5 last if ( $found eq "no" );
276 1 50 33     12 if ( $DNs[-1] eq $IDNs[-1] && ( !defined $AKID[-1] || $SKID[-1] eq $AKID[-1] )) { $self=1; last; }
  1   33     2  
  1         3  
277             }
278 1         6 my @verify = verifychain(@returnCerts);
279 1         5 my @Errors = reverse @{ $verify[0] };
  1         4  
280 1         2 my @GSI = reverse @{ $verify[1] };
  1         4  
281 1         2 my @LifeTime = reverse @{ $verify[2] };
  1         3  
282 1         3 my $EECDN = $verify[3];
283 1         2 my $EECIDN = $verify[4];
284 1         15 my $EEC = $verify[5];
285              
286 1         65 return { Certs => \@returnCerts,
287             IssuerHashes => \@IHash,
288             SubjectHashes => \@Hash,
289             SubjectKeyIdentifiers => \@SKID,
290             AuthorityKeyIdentifiersSKIDs => \@AKID,
291             DistinguishedNames => \@DNs,
292             IssuerDistinguishedNames => \@IDNs,
293             TrustedCA => \@Trust,
294             SelfSignedInChain => $self,
295             GSIType => \@GSI,
296             EndEntityDN => $EECDN,
297             EndEntityIssuerDN => $EECIDN,
298             EndEntityCert => $EEC,
299             Lifetimes => \@LifeTime,
300             Errors => \@Errors
301             }
302             }
303              
304             ################################################################
305              
306             # Need type of digestType, BinaryData, HexKey, HexModulus,
307             sub digestTBS {
308 7     7 0 14 my ($type,$Data) = @_;
309              
310 7 100       26 if ( $type eq "md5WithRSA" ) { # 128 bit digest
    50          
    0          
311 2 50       252 if ( eval "require Digest::MD5" ) {
312             # 1.2.840.113549.2.5 for use with 1.840.113549.1.1.4 for RSA signatures
313 2         28 return ASN1Wrap("30","300c06082a864886f70d02050500".ASN1Wrap("04",Digest::MD5::md5_hex($Data)));
314             }
315             }
316             elsif ( $type eq "sha1WithRSA" ) { # 160-bit
317 5 50       598 if ( eval "require Digest::SHA1" ) {
318             # 1.3.14.3.2.26 for use with 1.840.113549.1.1.5 for RSA signatures
319 5         76 return ASN1Wrap("30","300906052b0e03021a0500".ASN1Wrap("04",Digest::SHA1::sha1_hex($Data)));
320             }
321             }
322             elsif ( $type eq "md2WithRSA" ) { # 128 bit
323 0 0       0 if ( eval "require Digest::MD2" ) {
324             # 1.840.113549.2.2 for use with 1.840.113549.1.1.2 for RSA signatures
325 0         0 return ASN1Wrap("30","300c06082a864886f70d02020500".ASN1Wrap("04",Digest::MD2::md2_hex($Data)));
326             }
327             }
328             # elsif ( $type eq "md4WithRSA" ) { #128 bit
329             # if ( eval "require Digest::MD4" ) {
330             # # 1.840.113549.2.4 for use with 1.840.113549.1.1.3 for RSA signatures
331             # return ASN1Wrap("30","300c06082a864886f70d02040500".ASN1Wrap("04",Digest::MD4::md4_hex($Data)));
332             # }
333             # }
334 0         0 return undef;
335             }
336              
337             ################################################################
338             #digestSign("md5WithRSA",$Data,$chex,$nhex);
339             sub digestSign {
340 5     5 0 24 my ($type,$Data,$chex,$nhex) = @_;
341 5         17 my $digestTBS=digestTBS($type,$Data);
342              
343 5 50       28 if ( defined $digestTBS ) { return rsasign($digestTBS,$chex,$nhex); }
  5         29  
344 0         0 return "";
345             }
346              
347             ################################################################
348              
349             sub verifySignature {
350 2     2 0 7 my ($digestType,$SignedInfo,$TBS,$chex,$nhex)=@_;
351 2 50       23 return 1 if ( digestTBS($digestType,$TBS) eq rsaverify($SignedInfo,$chex,$nhex) );
352 0           return 0;
353             }
354              
355             ################################################################
356              
357             1;
358             __END__