File Coverage

blib/lib/PDF/Make/Signature.pm
Criterion Covered Total %
statement 179 233 76.8
branch 49 80 61.2
condition 36 82 43.9
subroutine 58 59 98.3
pod 3 3 100.0
total 325 457 71.1


line stmt bran cond sub pod time code
1             package PDF::Make::Signature;
2              
3 45     45   184501 use strict;
  45         64  
  45         1252  
4 45     45   148 use warnings;
  45         56  
  45         1555  
5 45     45   743 use 5.010001;
  45         115  
6 45     45   893 use PDF::Make (); # XS supplies _verify, _count, _sign_doc and the SigningIdentity / Certificate accessors
  45         78  
  45         2016  
7              
8             =head1 NAME
9              
10             PDF::Make::Signature - Digital signature support for PDF documents
11              
12             =head1 SYNOPSIS
13              
14             use PDF::Make;
15            
16             my $pdf = PDF::Make->new();
17             $pdf->page;
18             $pdf->text("Signed Document", 100, 700);
19            
20             # Load signing identity from PKCS#12 file
21             my $identity = PDF::Make::Signature->load_identity(
22             file => 'signer.p12',
23             password => 'secret'
24             );
25            
26             # Sign the document
27             my $signed_pdf = $pdf->sign(
28             identity => $identity,
29             reason => 'Document approval',
30             location => 'New York, NY',
31             contact => 'signer@example.com'
32             );
33            
34             # Write signed PDF
35             open my $fh, '>', 'signed.pdf' or die;
36             binmode $fh;
37             print $fh $signed_pdf;
38             close $fh;
39            
40             # Verify a signature
41             my $result = PDF::Make::Signature->verify(
42             file => 'signed.pdf'
43             );
44            
45             if ($result->is_valid) {
46             print "Signature is valid\n";
47             print "Signed by: ", $result->signer_name, "\n";
48             print "Signed at: ", $result->signing_time, "\n";
49             }
50              
51             =head1 DESCRIPTION
52              
53             PDF::Make::Signature provides digital signature capabilities for PDF documents,
54             implementing the signature format specified in ISO 32000-2:2020 ยง12.8.
55              
56             Features:
57              
58             =over 4
59              
60             =item * RSA and ECDSA signatures with SHA-256/384/512
61              
62             =item * PKCS#7 detached signature format (adbe.pkcs7.detached)
63              
64             =item * Certificate chain embedding
65              
66             =item * Signature verification
67              
68             =item * Visual and invisible signature fields
69              
70             =item * Certification signatures (MDP)
71              
72             =back
73              
74             =cut
75              
76             our $VERSION = '0.03';
77              
78 45     45   213 use Carp qw(croak);
  45         83  
  45         2992  
79 45     45   202 use Scalar::Util qw(blessed);
  45         67  
  45         2292  
80              
81             # Hash algorithm constants
82             use constant {
83 45         3579 HASH_SHA1 => 0,
84             HASH_SHA256 => 1,
85             HASH_SHA384 => 2,
86             HASH_SHA512 => 3,
87 45     45   177 };
  45         65  
88              
89             # Signature subfilter constants
90             use constant {
91 45         2704 SUBFILTER_PKCS7_DETACHED => 0,
92             SUBFILTER_PKCS7_SHA1 => 1,
93             SUBFILTER_ETSI_CADES => 2,
94             SUBFILTER_ETSI_RFC3161 => 3,
95 45     45   202 };
  45         54  
96              
97             # MDP (Modification Detection and Prevention) levels
98             use constant {
99 45         62063 MDP_NONE => 0, # Not a certification signature
100             MDP_NO_CHANGES => 1, # No changes permitted
101             MDP_FORM_FILL => 2, # Form filling + signing allowed
102             MDP_ANNOTATE => 3, # Annotations + form fill + signing allowed
103 45     45   178 };
  45         66  
104              
105             =head1 CLASS METHODS
106              
107             =head2 load_identity
108              
109             Load a signing identity from a PKCS#12 file or separate key/certificate files.
110              
111             # From PKCS#12
112             my $identity = PDF::Make::Signature->load_identity(
113             file => 'signer.p12',
114             password => 'secret'
115             );
116            
117             # From separate files
118             my $identity = PDF::Make::Signature->load_identity(
119             key_file => 'private.pem',
120             cert_file => 'cert.pem',
121             chain_file => 'chain.pem', # optional
122             password => 'keypass' # for encrypted keys
123             );
124              
125             Returns a L object.
126              
127             =cut
128              
129             sub load_identity {
130 4     4 1 130606 my ($class, %args) = @_;
131            
132 4 100 66     43 if ($args{file}) {
    100          
133             # Load from PKCS#12
134             return PDF::Make::SigningIdentity->from_pkcs12(
135             $args{file},
136             $args{password}
137 1         9 );
138             }
139             elsif ($args{key_file} && $args{cert_file}) {
140             # Load from separate files
141             return PDF::Make::SigningIdentity->from_files(
142             key_file => $args{key_file},
143             cert_file => $args{cert_file},
144             chain_file => $args{chain_file},
145             password => $args{password}
146 1         8 );
147             }
148             else {
149 2         200 croak "load_identity requires 'file' (PKCS#12) or 'key_file' and 'cert_file'";
150             }
151             }
152              
153             =head2 verify
154              
155             Verify a digital signature in a PDF file.
156              
157             my $result = PDF::Make::Signature->verify(
158             file => 'signed.pdf',
159             index => 0, # optional, signature field index (default: 0)
160             );
161            
162             # Or verify from bytes
163             my $result = PDF::Make::Signature->verify(
164             data => $pdf_bytes,
165             );
166              
167             Returns a L object.
168              
169             =cut
170              
171             sub verify {
172 7     7 1 7071 my ($class, %args) = @_;
173            
174 7         20 my $data;
175 7 100       21 if ($args{file}) {
    100          
176 3 100       213 open my $fh, '<', $args{file} or croak "Cannot open $args{file}: $!";
177 2         5 binmode $fh;
178 2         9 local $/;
179 2         39 $data = <$fh>;
180 2         22 close $fh;
181             }
182             elsif ($args{data}) {
183 2         5 $data = $args{data};
184             }
185             else {
186 2         166 croak "verify requires 'file' or 'data'";
187             }
188            
189 4   100     20 my $index = $args{index} // 0;
190            
191             # Call the XS verification function
192 4         431 return PDF::Make::Signature::_verify($data, $index);
193             }
194              
195             =head2 count_signatures
196              
197             Count the number of signature fields in a PDF.
198              
199             my $count = PDF::Make::Signature->count_signatures(
200             file => 'document.pdf'
201             );
202              
203             =cut
204              
205             sub count_signatures {
206 7     7 1 7149 my ($class, %args) = @_;
207            
208 7         28 my $data;
209 7 100       22 if ($args{file}) {
    100          
210 3 100       189 open my $fh, '<', $args{file} or croak "Cannot open $args{file}: $!";
211 2         6 binmode $fh;
212 2         6 local $/;
213 2         33 $data = <$fh>;
214 2         23 close $fh;
215             }
216             elsif ($args{data}) {
217 2         6 $data = $args{data};
218             }
219             else {
220 2         158 croak "count_signatures requires 'file' or 'data'";
221             }
222            
223 4         309 return PDF::Make::Signature::_count($data);
224             }
225              
226             =head1 INSTANCE METHODS (for PDF::Make documents)
227              
228             These methods are called on PDF::Make document objects.
229              
230             =head2 sign
231              
232             Sign the document with a digital signature.
233              
234             my $signed_pdf = $pdf->sign(
235             identity => $identity,
236            
237             # Optional metadata
238             reason => 'Document approval',
239             location => 'New York, NY',
240             contact => 'signer@example.com',
241             name => 'John Doe', # default: from certificate
242            
243             # Signature options
244             hash => 'sha256', # sha256, sha384, sha512
245            
246             # Certification (MDP) - makes this a certification signature
247             certify => 0, # 0=none, 1=no changes, 2=form fill, 3=annotate
248            
249             # Visual signature (optional)
250             visible => 0, # default: invisible signature
251             page => 1, # page number for visible signature
252             rect => [100, 100, 300, 200], # signature rectangle
253            
254             # Timestamp (optional)
255             timestamp_url => 'http://timestamp.example.com/tsa',
256             );
257              
258             Returns the signed PDF as bytes.
259              
260             =cut
261              
262             sub _sign_document {
263 3     3   1428 my ($pdf, %args) = @_;
264            
265 3 100       76 croak "sign requires 'identity'" unless $args{identity};
266            
267             # Validate identity
268 2         3 my $identity = $args{identity};
269 2 50 66     184 croak "identity must be a PDF::Make::SigningIdentity"
270             unless blessed($identity) && $identity->isa('PDF::Make::SigningIdentity');
271            
272             # Map hash algorithm name to constant
273 0         0 my %hash_map = (
274             sha1 => HASH_SHA1,
275             sha256 => HASH_SHA256,
276             sha384 => HASH_SHA384,
277             sha512 => HASH_SHA512,
278             );
279 0   0     0 my $hash_alg = $hash_map{lc($args{hash} // 'sha256')} // HASH_SHA256;
      0        
280            
281             # Build config
282             my $config = {
283             identity => $identity,
284             hash_alg => $hash_alg,
285             subfilter => SUBFILTER_PKCS7_DETACHED,
286             reason => $args{reason},
287             location => $args{location},
288             contact => $args{contact},
289             name => $args{name},
290             mdp => $args{certify} // MDP_NONE,
291             visible => $args{visible} // 0,
292             page => $args{page} // 1,
293             rect => $args{rect} // [0, 0, 0, 0],
294             timestamp_url => $args{timestamp_url},
295             signing_time => $args{signing_time},
296             tst_token => $args{tst_token},
297 0   0     0 };
      0        
      0        
      0        
298              
299             # Build the appearance hashref handed to the XS layer.
300             #
301             # Three supported input shapes, in precedence order:
302             #
303             # appearance => sub { my ($sa) = @_; ...draw onto $sa... }
304             # โ€” user draws via a PDF::Make::Builder::SignatureAppearance
305             # helper; we capture the resulting content-stream + font map.
306             #
307             # appearance => { stream => $raw_bytes, fonts => { F1 => 'Helvetica' } }
308             # โ€” fully-custom precomputed content stream. Advanced.
309             #
310             # visible => 1 (with page/rect)
311             # โ€” use the C builder's default signer/date/reason block.
312 0         0 my $appearance;
313 0 0 0     0 if ($args{visible} || $args{appearance}) {
314 0   0     0 my $rect = $args{rect} // [0, 0, 0, 0];
315 0         0 my ($x0, $y0, $x1, $y1) = @$rect;
316 0         0 my $w = $x1 - $x0;
317 0         0 my $h = $y1 - $y0;
318              
319 0         0 my $stream;
320             my $fonts;
321 0         0 my $xobjects;
322 0 0       0 if (ref $args{appearance} eq 'CODE') {
    0          
323 0         0 require PDF::Make::Builder::SignatureAppearance;
324 0         0 my $sa = PDF::Make::Builder::SignatureAppearance->new(
325             w => $w, h => $h, doc => $pdf,
326             );
327 0         0 $args{appearance}->($sa);
328 0         0 $stream = $sa->stream;
329 0         0 $fonts = $sa->fonts;
330 0         0 $xobjects = $sa->xobjects;
331             } elsif (ref $args{appearance} eq 'HASH') {
332 0         0 $stream = $args{appearance}{stream};
333 0         0 $fonts = $args{appearance}{fonts};
334 0         0 $xobjects = $args{appearance}{xobjects};
335             }
336              
337             $appearance = {
338             visible => 1,
339             page => $args{page} // 1,
340             rect => $rect,
341             (defined $stream ? (stream => $stream) : ()),
342             (defined $fonts ? (fonts => $fonts) : ()),
343             (defined $xobjects ? (xobjects => $xobjects) : ()),
344             show_name => $args{show_name} // 1,
345             show_date => $args{show_date} // 1,
346 0 0 0     0 show_reason => $args{show_reason} // 1,
    0 0        
    0 0        
      0        
347             };
348             }
349 0         0 $config->{appearance} = $appearance;
350              
351             # Pass 1: sign without TSA (or sign once if no TSA requested).
352 0   0     0 my $signing_time = $config->{signing_time} // time();
353 0         0 $config->{signing_time} = $signing_time;
354              
355             # When a TSA is requested the CMS grows by the size of the embedded
356             # TimeStampToken (~6KB for Digicert/GlobalSign). Bump the default
357             # /Contents placeholder so the larger CMS still fits.
358 0 0 0     0 if ($config->{timestamp_url} && !$config->{placeholder_size}) {
359 0         0 $config->{placeholder_size} = 32768; # 16KB CMS headroom
360             }
361              
362 0         0 my $signed = PDF::Make::Signature::_sign($pdf, $config);
363              
364             # Pass 2: if a timestamp_url was provided, embed an RFC 3161 token.
365 0 0 0     0 if ($config->{timestamp_url} && !defined $config->{tst_token}) {
366 0         0 require Digest::SHA;
367 0         0 require HTTP::Tiny;
368              
369 0 0       0 my $cms_der = _extract_contents_cms_bytes($signed)
370             or croak "sign: failed to locate /Contents CMS in signed PDF";
371 0         0 my $sig_bytes = PDF::Make::Signature::_extract_cms_signature($cms_der);
372 0         0 my $imprint = Digest::SHA::sha256($sig_bytes);
373              
374 0         0 my $req_der = PDF::Make::Signature::_build_tsa_request(
375             $hash_alg, $imprint, 1);
376              
377             my $http = HTTP::Tiny->new(
378 0   0     0 timeout => $args{tsa_timeout} // 30,
379             verify_SSL => 1,
380             );
381             my $resp = $http->post($config->{timestamp_url}, {
382 0         0 headers => {
383             'Content-Type' => 'application/timestamp-query',
384             'Accept' => 'application/timestamp-reply',
385             },
386             content => $req_der,
387             });
388 0 0       0 unless ($resp->{success}) {
389 0         0 croak "sign: TSA HTTP error $resp->{status} $resp->{reason}"
390             . " from $config->{timestamp_url}";
391             }
392 0         0 my $token = PDF::Make::Signature::_parse_tsa_response($resp->{content});
393              
394             # Pass 2: re-sign with same signing_time + tst_token.
395 0         0 $config->{tst_token} = $token;
396 0         0 $signed = PDF::Make::Signature::_sign($pdf, $config);
397             }
398              
399 0         0 return $signed;
400             }
401              
402             # Locate /Contents <...> in a signed PDF and return the decoded DER bytes
403             # (stripping trailing zero padding). Used to fish the CMS out after
404             # pass 1 so we can timestamp the RSA signature value.
405             sub _extract_contents_cms_bytes {
406 0     0   0 my ($pdf_bytes) = @_;
407 0 0       0 return unless $pdf_bytes =~ /\/Contents \s* < ([0-9A-Fa-f]+) >/sx;
408 0         0 my $hex = $1;
409             # Strip trailing zero padding (last non-zero hex-digit boundary).
410 0         0 $hex =~ s/(?:00)+\z//;
411 0         0 return pack('H*', $hex);
412             }
413              
414             =head2 add_signature_field
415              
416             Add a signature field to the document (without signing).
417              
418             my $field = $pdf->add_signature_field(
419             name => 'Signature1',
420             page => 1,
421             rect => [100, 100, 300, 200],
422             );
423              
424             This creates an unsigned signature field that can be signed later.
425              
426             =cut
427              
428             sub _add_signature_field {
429 2     2   933 my ($pdf, %args) = @_;
430            
431 2   100     7 my $name = $args{name} // 'Signature1';
432 2   100     5 my $page = $args{page} // 1;
433 2   100     6 my $rect = $args{rect} // [0, 0, 0, 0];
434            
435             # Call the XS function to add field
436 2         4 return PDF::Make::Signature::_add_field($pdf, $name, $page, $rect);
437             }
438              
439             sub _sign {
440 1     1   453 my ($pdf, $config) = @_;
441              
442             # Use XS signing implementation
443             my $identity = $config->{identity}
444 1 50       121 or croak "Signature requires identity";
445 0   0     0 my $hash_alg = $config->{hash_alg} // 1; # SHA256
446              
447 0         0 my $signed_bytes = eval {
448             # & prefix bypasses the XS-generated prototype, which may be stale
449             # vs the XS param list during development rebuilds.
450             &PDF::Make::Signature::_sign_doc(
451             $pdf,
452             $identity,
453             $hash_alg,
454             $config->{reason},
455             $config->{location},
456             $config->{contact},
457             $config->{name},
458             $config->{signing_time},
459             $config->{tst_token},
460             $config->{placeholder_size},
461             $config->{appearance},
462 0         0 );
463             };
464 0 0       0 if ($@) {
465 0         0 croak "Signing failed: $@";
466             }
467 0         0 return $signed_bytes;
468             }
469              
470             sub _add_field {
471 3     3   257 my ($pdf, $name, $page, $rect) = @_;
472             # Stub - XS implementation pending
473 3         278 croak "add_signature_field not yet implemented";
474             }
475              
476             1;
477              
478             #============================================================================
479             # PDF::Make::SigningIdentity - Represents a signing key + certificate
480             #============================================================================
481              
482             package PDF::Make::SigningIdentity;
483              
484 45     45   320 use strict;
  45         76  
  45         1056  
485 45     45   160 use warnings;
  45         73  
  45         2019  
486 45     45   178 use Carp qw(croak);
  45         71  
  45         20938  
487              
488             =head1 NAME
489              
490             PDF::Make::SigningIdentity - Signing key and certificate pair
491              
492             =head1 DESCRIPTION
493              
494             Represents a signing identity consisting of a private key and certificate chain.
495              
496             =cut
497              
498             sub new {
499 1     1   5 my ($class, %args) = @_;
500            
501             my $self = bless {
502             privkey => $args{privkey},
503             cert => $args{cert},
504             chain => $args{chain} // [],
505             _ptr => $args{_ptr}, # XS pointer
506 1   50     6 }, $class;
507            
508 1         3 return $self;
509             }
510              
511             sub from_pkcs12 {
512 4     4   1392 my ($class, $file, $password) = @_;
513            
514 4 100       97 croak "PKCS#12 file required" unless $file;
515 3 100       377 croak "Cannot read PKCS#12 file: $file" unless -r $file;
516            
517             # Read file
518 1 50       39 open my $fh, '<', $file or croak "Cannot open $file: $!";
519 1         3 binmode $fh;
520 1         5 local $/;
521 1         38 my $data = <$fh>;
522 1         9 close $fh;
523            
524             # Call XS to parse PKCS#12
525 1   50     9 return $class->_parse_pkcs12($data, $password // '');
526             }
527              
528             sub from_files {
529 4     4   2287 my ($class, %args) = @_;
530            
531 4 100       86 my $key_file = $args{key_file} or croak "key_file required";
532 3 100       87 my $cert_file = $args{cert_file} or croak "cert_file required";
533            
534             # Read key file
535 2 50       50 open my $fh, '<', $key_file or croak "Cannot open $key_file: $!";
536 2         5 binmode $fh;
537 2         7 local $/;
538 2         31 my $key_data = <$fh>;
539 2         12 close $fh;
540            
541             # Read cert file
542 2 50       31 open $fh, '<', $cert_file or croak "Cannot open $cert_file: $!";
543 2         2 binmode $fh;
544 2         19 my $cert_data = <$fh>;
545 2         8 close $fh;
546            
547             # Read chain file if provided
548 2         4 my $chain_data = '';
549 2 100       4 if ($args{chain_file}) {
550 1 50       14 open $fh, '<', $args{chain_file} or croak "Cannot open $args{chain_file}: $!";
551 1         1 binmode $fh;
552 1         18 $chain_data = <$fh>;
553 1         6 close $fh;
554             }
555            
556             # Call XS to parse files
557 2   50     11 return $class->_parse_files($key_data, $cert_data, $chain_data, $args{password} // '');
558             }
559              
560             # XS stubs
561             sub _parse_pkcs12 {
562 1     1   3 my ($class, $data, $password) = @_;
563 1         6 require PDF::Make; # Ensure XS is loaded
564 1   50     25581 return $class->_from_pkcs12($data, $password // '');
565             }
566              
567             sub _parse_files {
568 3     3   452 my ($class, $key_data, $cert_data, $chain_data, $password) = @_;
569             # Stub - would call pdfmake_privkey_parse_pem and pdfmake_x509_parse_pem
570 3         351 croak "PEM parsing not yet implemented";
571             }
572              
573             1;
574              
575             #============================================================================
576             # PDF::Make::SignatureResult - Verification result
577             #============================================================================
578              
579             package PDF::Make::SignatureResult;
580              
581 45     45   283 use strict;
  45         86  
  45         1045  
582 45     45   180 use warnings;
  45         60  
  45         13640  
583              
584             =head1 NAME
585              
586             PDF::Make::SignatureResult - Signature verification result
587              
588             =head1 DESCRIPTION
589              
590             Represents the result of signature verification.
591              
592             =cut
593              
594             sub new {
595 5     5   138006 my ($class, %args) = @_;
596            
597             return bless {
598             valid => $args{valid} // 0,
599             signature_valid => $args{signature_valid} // 0,
600             digest_valid => $args{digest_valid} // 0,
601             cert_valid => $args{cert_valid} // 0,
602             timestamp_valid => $args{timestamp_valid},
603             document_modified => $args{document_modified} // 0,
604             signer_name => $args{signer_name},
605             signer_email => $args{signer_email},
606             signing_time => $args{signing_time},
607             cert => $args{cert},
608             chain => $args{chain},
609             error => $args{error},
610 5   100     77 }, $class;
      100        
      100        
      100        
      100        
611             }
612              
613             # Accessors
614 7     7   359 sub is_valid { $_[0]->{valid} }
615 3     3   18 sub signature_valid { $_[0]->{signature_valid} }
616 2     2   7 sub digest_valid { $_[0]->{digest_valid} }
617 3     3   13 sub cert_valid { $_[0]->{cert_valid} }
618 2     2   18 sub timestamp_valid { $_[0]->{timestamp_valid} }
619 3     3   10 sub document_modified { $_[0]->{document_modified} }
620 2     2   7 sub signer_name { $_[0]->{signer_name} }
621 2     2   7 sub signer_email { $_[0]->{signer_email} }
622 2     2   7 sub signing_time { $_[0]->{signing_time} }
623 2     2   8 sub certificate { $_[0]->{cert} }
624 2     2   8 sub certificate_chain { $_[0]->{chain} }
625 3     3   8 sub error { $_[0]->{error} }
626              
627             1;
628              
629             #============================================================================
630             # PDF::Make::Certificate - X.509 certificate wrapper
631             #============================================================================
632              
633             package PDF::Make::Certificate;
634              
635 45     45   271 use strict;
  45         75  
  45         933  
636 45     45   178 use warnings;
  45         71  
  45         1717  
637 45     45   195 use Carp qw(croak);
  45         82  
  45         24297  
638              
639             =head1 NAME
640              
641             PDF::Make::Certificate - X.509 certificate wrapper
642              
643             =head1 DESCRIPTION
644              
645             Represents an X.509 certificate for digital signatures.
646              
647             =cut
648              
649             sub new {
650 8     8   127377 my ($class, %args) = @_;
651            
652             return bless {
653             _ptr => $args{_ptr},
654             version => $args{version},
655             serial => $args{serial},
656             issuer => $args{issuer},
657             subject => $args{subject},
658             not_before => $args{not_before},
659             not_after => $args{not_after},
660             key_usage => $args{key_usage},
661             ext_key_usage => $args{ext_key_usage},
662             is_ca => $args{is_ca},
663             is_self_signed => $args{is_self_signed},
664 8         52 }, $class;
665             }
666              
667             sub load {
668 5     5   2164 my ($class, %args) = @_;
669            
670 5         7 my $data;
671 5 100       13 if ($args{file}) {
    100          
672 2 100       267 open my $fh, '<', $args{file} or croak "Cannot open $args{file}: $!";
673 1         3 binmode $fh;
674 1         4 local $/;
675 1         12 $data = <$fh>;
676 1         9 close $fh;
677             }
678             elsif ($args{data}) {
679 2         5 $data = $args{data};
680             }
681             else {
682 1         139 croak "load requires 'file' or 'data'";
683             }
684            
685             # Detect format
686 3 100       9 if ($data =~ /^-----BEGIN CERTIFICATE-----/) {
687 2         7 return $class->_parse_pem($data);
688             }
689             else {
690 1         3 return $class->_parse_der($data);
691             }
692             }
693              
694             # Accessors
695 2     2   538 sub version { $_[0]->{version} }
696 2     2   6 sub serial { $_[0]->{serial} }
697 2     2   8 sub issuer { $_[0]->{issuer} }
698 2     2   6 sub subject { $_[0]->{subject} }
699 1     1   5 sub not_before { $_[0]->{not_before} }
700 1     1   3 sub not_after { $_[0]->{not_after} }
701 1     1   4 sub key_usage { $_[0]->{key_usage} }
702 1     1   4 sub ext_key_usage { $_[0]->{ext_key_usage} }
703 2     2   8 sub is_ca { $_[0]->{is_ca} }
704 2     2   8 sub is_self_signed { $_[0]->{is_self_signed} }
705              
706             sub is_valid {
707 7     7   18 my ($self, $time) = @_;
708 7   66     26 $time //= time();
709 7   100     38 return ($time >= $self->{not_before} && $time <= $self->{not_after});
710             }
711              
712             sub can_sign_documents {
713 6     6   16 my ($self) = @_;
714            
715             # Check key usage if present
716 6 100 100     25 if (defined $self->{key_usage} && $self->{key_usage}) {
717             # Need digitalSignature (bit 0) or nonRepudiation (bit 1)
718 4 100       15 return 0 unless ($self->{key_usage} & 0x03);
719             }
720            
721             # Check extended key usage if present
722 4 50 66     25 if (defined $self->{ext_key_usage} && $self->{ext_key_usage}) {
723             # Need document signing, PDF signing, email protection, or code signing
724 2 100       7 return 0 unless ($self->{ext_key_usage} & 0xFC);
725             }
726            
727 3         8 return 1;
728             }
729              
730             # XS stubs
731             sub _parse_pem {
732 2     2   4 my ($class, $data) = @_;
733             # Would call pdfmake_x509_parse_pem
734 2         173 croak "PEM certificate parsing not yet implemented";
735             }
736              
737             sub _parse_der {
738 1     1   3 my ($class, $data) = @_;
739             # Would call pdfmake_x509_parse_der
740 1         93 croak "DER certificate parsing not yet implemented";
741             }
742              
743             1;
744              
745             __END__