line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Net::ACME; |
2
|
|
|
|
|
|
|
|
3
|
|
|
|
|
|
|
=encoding utf-8 |
4
|
|
|
|
|
|
|
|
5
|
|
|
|
|
|
|
=head1 NAME |
6
|
|
|
|
|
|
|
|
7
|
|
|
|
|
|
|
Net::ACME - Client for the ACME protocol (e.g., L) |
8
|
|
|
|
|
|
|
|
9
|
|
|
|
|
|
|
=head1 SYNOPSIS |
10
|
|
|
|
|
|
|
|
11
|
|
|
|
|
|
|
package MyACME::SomeService; |
12
|
|
|
|
|
|
|
|
13
|
|
|
|
|
|
|
sub _HOST { } #return the name of the ACME host |
14
|
|
|
|
|
|
|
|
15
|
|
|
|
|
|
|
#See below for full examples. |
16
|
|
|
|
|
|
|
|
17
|
|
|
|
|
|
|
=head1 DESCRIPTION |
18
|
|
|
|
|
|
|
|
19
|
|
|
|
|
|
|
This module implements client logic (including SSL certificate issuance) |
20
|
|
|
|
|
|
|
for the ACME protocol, the system for automated issuance of SSL |
21
|
|
|
|
|
|
|
certificates used by Let’s Encrypt. |
22
|
|
|
|
|
|
|
|
23
|
|
|
|
|
|
|
The methods of this class return objects that correspond to the |
24
|
|
|
|
|
|
|
respective ACME resource: |
25
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
=over 4 |
27
|
|
|
|
|
|
|
|
28
|
|
|
|
|
|
|
=item * C: C |
29
|
|
|
|
|
|
|
|
30
|
|
|
|
|
|
|
=item * C: C |
31
|
|
|
|
|
|
|
|
32
|
|
|
|
|
|
|
=item * C: C or C |
33
|
|
|
|
|
|
|
|
34
|
|
|
|
|
|
|
=back |
35
|
|
|
|
|
|
|
|
36
|
|
|
|
|
|
|
=head1 WHY USE THIS MODULE? |
37
|
|
|
|
|
|
|
|
38
|
|
|
|
|
|
|
=over 4 |
39
|
|
|
|
|
|
|
|
40
|
|
|
|
|
|
|
=item * Closely based on cPanel’s widely used Let’s Encrypt plugin. |
41
|
|
|
|
|
|
|
|
42
|
|
|
|
|
|
|
=item * Support for both RSA and ECDSA encryption (via L). |
43
|
|
|
|
|
|
|
|
44
|
|
|
|
|
|
|
=item * Thorough error-checking: any deviation from what the ACME protocol |
45
|
|
|
|
|
|
|
expects is reported immediately via an exception. |
46
|
|
|
|
|
|
|
|
47
|
|
|
|
|
|
|
=item * Well-defined object system, including typed, queryable exceptions. |
48
|
|
|
|
|
|
|
|
49
|
|
|
|
|
|
|
=item * Extensive test coverage. |
50
|
|
|
|
|
|
|
|
51
|
|
|
|
|
|
|
=item * Light memory footprint - no Moose/Moo/etc. |
52
|
|
|
|
|
|
|
|
53
|
|
|
|
|
|
|
=item * No careless overwriting of globals like C<$@>, C<$!>, and C<$?>. |
54
|
|
|
|
|
|
|
(Hopefully your code isn’t susceptible to this anyway, but it’s just a good |
55
|
|
|
|
|
|
|
precaution.) |
56
|
|
|
|
|
|
|
|
57
|
|
|
|
|
|
|
=item * All dependencies are either core or pure Perl. Net::ACME will run |
58
|
|
|
|
|
|
|
anywhere that Perl runs! |
59
|
|
|
|
|
|
|
|
60
|
|
|
|
|
|
|
=back |
61
|
|
|
|
|
|
|
|
62
|
|
|
|
|
|
|
=head1 STATUS |
63
|
|
|
|
|
|
|
|
64
|
|
|
|
|
|
|
This module is now well-tested and should be safe for use in your application. |
65
|
|
|
|
|
|
|
|
66
|
|
|
|
|
|
|
=head1 CUSTOMIZATION |
67
|
|
|
|
|
|
|
|
68
|
|
|
|
|
|
|
B: This module uses C for its network operations. |
69
|
|
|
|
|
|
|
In some instances it is desirable to specify custom C in that |
70
|
|
|
|
|
|
|
module’s constructor; to do this, populate |
71
|
|
|
|
|
|
|
C<@Net::ACME::HTTP_Tiny::SSL_OPTIONS>. |
72
|
|
|
|
|
|
|
|
73
|
|
|
|
|
|
|
=head1 URI vs. URL |
74
|
|
|
|
|
|
|
|
75
|
|
|
|
|
|
|
This module uses “uri” for ACME-related objects and “url” for |
76
|
|
|
|
|
|
|
HTTP-related ones. This apparent conflict is a result of maintaining |
77
|
|
|
|
|
|
|
consistency with both the ACME specification (“uri”) and L (“url”). |
78
|
|
|
|
|
|
|
|
79
|
|
|
|
|
|
|
=head1 EXAMPLES |
80
|
|
|
|
|
|
|
|
81
|
|
|
|
|
|
|
See the C directory in the distribution for complete, interactive |
82
|
|
|
|
|
|
|
example scripts that also illustrate a bit of how ACME works. |
83
|
|
|
|
|
|
|
|
84
|
|
|
|
|
|
|
See below for cut-paste-y examples. |
85
|
|
|
|
|
|
|
|
86
|
|
|
|
|
|
|
=head1 EXAMPLE: REGISTRATION |
87
|
|
|
|
|
|
|
|
88
|
|
|
|
|
|
|
my $tos_url = Net::ACME::LetsEncrypt->get_terms_of_service(); |
89
|
|
|
|
|
|
|
|
90
|
|
|
|
|
|
|
my $acme = Net::ACME::LetsEncrypt->new( key => $reg_rsa_pem ); |
91
|
|
|
|
|
|
|
|
92
|
|
|
|
|
|
|
#Use this method any time you want to update contact information, |
93
|
|
|
|
|
|
|
#not just when you set up a new account. |
94
|
|
|
|
|
|
|
my $reg = $acme->register('mailto:who.am@i.tld', 'mailto:who@else.tld'); |
95
|
|
|
|
|
|
|
|
96
|
|
|
|
|
|
|
$acme->accept_tos( $reg->uri(), $tos_url ); |
97
|
|
|
|
|
|
|
|
98
|
|
|
|
|
|
|
=head1 EXAMPLE: DOMAIN AUTHORIZATION & CERTIFICATE PROCUREMENT |
99
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
for my $domain (@domains) { |
101
|
|
|
|
|
|
|
my $authz_p = $acme->start_domain_authz($domain); |
102
|
|
|
|
|
|
|
|
103
|
|
|
|
|
|
|
for my $cmb_ar ( $authz_p->combinations() ) { |
104
|
|
|
|
|
|
|
|
105
|
|
|
|
|
|
|
#$cmb_ar is a set of challenges that the ACME server will |
106
|
|
|
|
|
|
|
#accept as proof of domain control. As of November 2016, these |
107
|
|
|
|
|
|
|
#sets all contain exactly one challenge each: “http-01”, etc. |
108
|
|
|
|
|
|
|
|
109
|
|
|
|
|
|
|
#Each member of @$cmb_ar is an instance of |
110
|
|
|
|
|
|
|
#Net::ACME::Challenge::Pending--maybe a subclass thereof such as |
111
|
|
|
|
|
|
|
#Net::ACME::Challenge::Pending::http_01. |
112
|
|
|
|
|
|
|
|
113
|
|
|
|
|
|
|
#At this point, you examine $cmb_ar and determine if this |
114
|
|
|
|
|
|
|
#combination is one that you’re interested in. You might try |
115
|
|
|
|
|
|
|
#something like: |
116
|
|
|
|
|
|
|
# |
117
|
|
|
|
|
|
|
# next if @$cmb_ar > 1; |
118
|
|
|
|
|
|
|
# next if $cmb_ar->[0]->type() ne 'http-01'; |
119
|
|
|
|
|
|
|
|
120
|
|
|
|
|
|
|
#Once you’ve examined $cmb_ar and set up the appropriate response(s), |
121
|
|
|
|
|
|
|
#it’s time to tell the ACME server to send its challenge query. |
122
|
|
|
|
|
|
|
$acme->do_challenge($_) for @$cmb_ar; |
123
|
|
|
|
|
|
|
|
124
|
|
|
|
|
|
|
while (1) { |
125
|
|
|
|
|
|
|
if ( $authz_p->is_time_to_poll() ) { |
126
|
|
|
|
|
|
|
my $poll = $authz_p->poll(); |
127
|
|
|
|
|
|
|
|
128
|
|
|
|
|
|
|
last if $poll->status() eq 'valid'; |
129
|
|
|
|
|
|
|
|
130
|
|
|
|
|
|
|
if ( $poll->status() eq 'invalid' ) { |
131
|
|
|
|
|
|
|
my @failed = map { $_->error() } $poll->challenges(); |
132
|
|
|
|
|
|
|
|
133
|
|
|
|
|
|
|
warn $_->to_string() . $/ for @failed; |
134
|
|
|
|
|
|
|
|
135
|
|
|
|
|
|
|
die "Failed authorization for “$domain”!"; |
136
|
|
|
|
|
|
|
} |
137
|
|
|
|
|
|
|
|
138
|
|
|
|
|
|
|
} |
139
|
|
|
|
|
|
|
|
140
|
|
|
|
|
|
|
sleep 1; |
141
|
|
|
|
|
|
|
} |
142
|
|
|
|
|
|
|
} |
143
|
|
|
|
|
|
|
} |
144
|
|
|
|
|
|
|
|
145
|
|
|
|
|
|
|
#Make a key and CSR. |
146
|
|
|
|
|
|
|
#Creation of CSRs is well-documented so won’t be discussed here. |
147
|
|
|
|
|
|
|
|
148
|
|
|
|
|
|
|
my $cert = $acme->get_certificate($csr_pem); |
149
|
|
|
|
|
|
|
|
150
|
|
|
|
|
|
|
#This shouldn’t actually be necessary for Let’s Encrypt, |
151
|
|
|
|
|
|
|
#but the ACME protocol describes it. |
152
|
|
|
|
|
|
|
while ( !$cert->pem() ) { |
153
|
|
|
|
|
|
|
sleep 1; |
154
|
|
|
|
|
|
|
next if !$cert->is_time_to_poll(); |
155
|
|
|
|
|
|
|
$cert = $cert->poll() || $cert; |
156
|
|
|
|
|
|
|
} |
157
|
|
|
|
|
|
|
|
158
|
|
|
|
|
|
|
=head1 TODO |
159
|
|
|
|
|
|
|
|
160
|
|
|
|
|
|
|
=over 4 |
161
|
|
|
|
|
|
|
|
162
|
|
|
|
|
|
|
=item * Once the L |
163
|
|
|
|
|
|
|
is finalized, update this module to take advantage of the full specification. |
164
|
|
|
|
|
|
|
As Let’s Encrypt’s L is currently |
165
|
|
|
|
|
|
|
the only widely-used ACME server, and that software is compatible with |
166
|
|
|
|
|
|
|
L, |
167
|
|
|
|
|
|
|
there’s little reason to update for the time being. |
168
|
|
|
|
|
|
|
|
169
|
|
|
|
|
|
|
=back |
170
|
|
|
|
|
|
|
|
171
|
|
|
|
|
|
|
=head1 THANKS |
172
|
|
|
|
|
|
|
|
173
|
|
|
|
|
|
|
=over 4 |
174
|
|
|
|
|
|
|
|
175
|
|
|
|
|
|
|
=item * cPanel, Inc. for permission to adapt their ACME framework for |
176
|
|
|
|
|
|
|
public consumption. |
177
|
|
|
|
|
|
|
|
178
|
|
|
|
|
|
|
=item * Stephen Ludin for developing and maintaining L, from which |
179
|
|
|
|
|
|
|
this module took its inspiration. |
180
|
|
|
|
|
|
|
|
181
|
|
|
|
|
|
|
=back |
182
|
|
|
|
|
|
|
|
183
|
|
|
|
|
|
|
=head1 SEE ALSO |
184
|
|
|
|
|
|
|
|
185
|
|
|
|
|
|
|
I am aware of the following additional CPAN modules that implement this protocol: |
186
|
|
|
|
|
|
|
|
187
|
|
|
|
|
|
|
=over 4 |
188
|
|
|
|
|
|
|
|
189
|
|
|
|
|
|
|
=item * L |
190
|
|
|
|
|
|
|
|
191
|
|
|
|
|
|
|
=item * L |
192
|
|
|
|
|
|
|
|
193
|
|
|
|
|
|
|
=item * L |
194
|
|
|
|
|
|
|
|
195
|
|
|
|
|
|
|
=item * L |
196
|
|
|
|
|
|
|
|
197
|
|
|
|
|
|
|
=back |
198
|
|
|
|
|
|
|
|
199
|
|
|
|
|
|
|
=head1 REPOSITORY (FEEDBACK/BUGS) |
200
|
|
|
|
|
|
|
|
201
|
|
|
|
|
|
|
L |
202
|
|
|
|
|
|
|
|
203
|
|
|
|
|
|
|
=head1 AUTHOR |
204
|
|
|
|
|
|
|
|
205
|
|
|
|
|
|
|
Felipe Gasper (FELIPE) |
206
|
|
|
|
|
|
|
|
207
|
|
|
|
|
|
|
=head1 LICENSE |
208
|
|
|
|
|
|
|
|
209
|
|
|
|
|
|
|
This module is licensed under the same terms as Perl. |
210
|
|
|
|
|
|
|
|
211
|
|
|
|
|
|
|
=cut |
212
|
|
|
|
|
|
|
|
213
|
2
|
|
|
2
|
|
89000
|
use strict; |
|
2
|
|
|
|
|
3
|
|
|
2
|
|
|
|
|
44
|
|
214
|
2
|
|
|
2
|
|
7
|
use warnings; |
|
2
|
|
|
|
|
2
|
|
|
2
|
|
|
|
|
37
|
|
215
|
|
|
|
|
|
|
|
216
|
2
|
|
|
2
|
|
797
|
use Crypt::Format (); |
|
2
|
|
|
|
|
712
|
|
|
2
|
|
|
|
|
27
|
|
217
|
2
|
|
|
2
|
|
590
|
use JSON (); |
|
2
|
|
|
|
|
9846
|
|
|
2
|
|
|
|
|
32
|
|
218
|
2
|
|
|
2
|
|
414
|
use MIME::Base64 (); |
|
2
|
|
|
|
|
504
|
|
|
2
|
|
|
|
|
28
|
|
219
|
|
|
|
|
|
|
|
220
|
2
|
|
|
2
|
|
694
|
use Net::ACME::Authorization::Pending (); |
|
2
|
|
|
|
|
5
|
|
|
2
|
|
|
|
|
33
|
|
221
|
2
|
|
|
2
|
|
653
|
use Net::ACME::Certificate (); |
|
2
|
|
|
|
|
3
|
|
|
2
|
|
|
|
|
27
|
|
222
|
2
|
|
|
2
|
|
631
|
use Net::ACME::Certificate::Pending (); |
|
2
|
|
|
|
|
2
|
|
|
2
|
|
|
|
|
26
|
|
223
|
2
|
|
|
2
|
|
7
|
use Net::ACME::Constants (); |
|
2
|
|
|
|
|
2
|
|
|
2
|
|
|
|
|
19
|
|
224
|
2
|
|
|
2
|
|
702
|
use Net::ACME::Challenge::Pending::http_01 (); |
|
2
|
|
|
|
|
5
|
|
|
2
|
|
|
|
|
30
|
|
225
|
2
|
|
|
2
|
|
9
|
use Net::ACME::HTTP (); |
|
2
|
|
|
|
|
2
|
|
|
2
|
|
|
|
|
19
|
|
226
|
2
|
|
|
2
|
|
807
|
use Net::ACME::Registration (); |
|
2
|
|
|
|
|
3
|
|
|
2
|
|
|
|
|
27
|
|
227
|
2
|
|
|
2
|
|
7
|
use Net::ACME::Utils (); |
|
2
|
|
|
|
|
2
|
|
|
2
|
|
|
|
|
21
|
|
228
|
2
|
|
|
2
|
|
5
|
use Net::ACME::X (); |
|
2
|
|
|
|
|
2
|
|
|
2
|
|
|
|
|
1619
|
|
229
|
|
|
|
|
|
|
|
230
|
|
|
|
|
|
|
our $VERSION; |
231
|
|
|
|
|
|
|
*VERSION = \$Net::ACME::Constants::VERSION; |
232
|
|
|
|
|
|
|
|
233
|
|
|
|
|
|
|
*_to_base64url = \&MIME::Base64::encode_base64url; |
234
|
|
|
|
|
|
|
|
235
|
|
|
|
|
|
|
sub new { |
236
|
5
|
|
|
5
|
0
|
737192
|
my ( $class, %opts ) = @_; |
237
|
|
|
|
|
|
|
|
238
|
|
|
|
|
|
|
my $self = { |
239
|
|
|
|
|
|
|
_host => $class->_HOST(), |
240
|
5
|
|
|
|
|
20
|
_key => $opts{'key'}, |
241
|
|
|
|
|
|
|
}; |
242
|
|
|
|
|
|
|
|
243
|
5
|
|
|
|
|
26
|
bless $self, $class; |
244
|
|
|
|
|
|
|
|
245
|
5
|
|
|
|
|
16
|
$self->_set_ua(); |
246
|
|
|
|
|
|
|
|
247
|
5
|
|
|
|
|
28
|
return $self; |
248
|
|
|
|
|
|
|
} |
249
|
|
|
|
|
|
|
|
250
|
0
|
|
|
0
|
|
0
|
sub _HOST { die 'Not Implemented!' } |
251
|
|
|
|
|
|
|
|
252
|
|
|
|
|
|
|
sub accept_tos { |
253
|
1
|
|
|
1
|
0
|
2
|
my ( $self, $reg_uri, $tos_url ) = @_; |
254
|
|
|
|
|
|
|
|
255
|
1
|
|
|
|
|
6
|
my $resp = $self->_post_url( |
256
|
|
|
|
|
|
|
$reg_uri, |
257
|
|
|
|
|
|
|
{ |
258
|
|
|
|
|
|
|
resource => 'reg', |
259
|
|
|
|
|
|
|
agreement => $tos_url, |
260
|
|
|
|
|
|
|
}, |
261
|
|
|
|
|
|
|
); |
262
|
|
|
|
|
|
|
|
263
|
1
|
50
|
|
|
|
21
|
$resp->die_because_unexpected() if $resp->status() != 202; |
264
|
|
|
|
|
|
|
|
265
|
1
|
|
|
|
|
10
|
return; |
266
|
|
|
|
|
|
|
} |
267
|
|
|
|
|
|
|
|
268
|
|
|
|
|
|
|
#Returns a Net::ACME::Registration instance whose |
269
|
|
|
|
|
|
|
#terms_of_service() will be current/useful. |
270
|
|
|
|
|
|
|
sub register { |
271
|
1
|
|
|
1
|
1
|
13
|
my ( $self, @contacts ) = @_; |
272
|
|
|
|
|
|
|
|
273
|
1
|
|
|
|
|
3
|
my $payload = { |
274
|
|
|
|
|
|
|
resource => 'new-reg', |
275
|
|
|
|
|
|
|
}; |
276
|
|
|
|
|
|
|
|
277
|
1
|
50
|
|
|
|
4
|
if (@contacts) { |
278
|
1
|
|
|
|
|
2
|
$payload->{'contact'} = \@contacts; |
279
|
|
|
|
|
|
|
} |
280
|
|
|
|
|
|
|
|
281
|
1
|
|
|
|
|
2
|
my ( $resp, $reg_uri ); |
282
|
|
|
|
|
|
|
|
283
|
1
|
|
|
|
|
5
|
$resp = $self->_post( 'new-reg', $payload ); |
284
|
|
|
|
|
|
|
|
285
|
1
|
50
|
|
|
|
19
|
if ( $resp->status() != 201 ) { |
286
|
0
|
|
|
|
|
0
|
$resp->die_because_unexpected(); |
287
|
|
|
|
|
|
|
} |
288
|
|
|
|
|
|
|
|
289
|
1
|
|
|
|
|
7
|
$reg_uri = $resp->header('location'); |
290
|
|
|
|
|
|
|
|
291
|
|
|
|
|
|
|
#We don’t save the terms-of-service here because the terms |
292
|
|
|
|
|
|
|
#of service might be updated between now and the next time we |
293
|
|
|
|
|
|
|
#load this data. It’s better to make the caller call |
294
|
|
|
|
|
|
|
#get_terms_of_service() each time. |
295
|
|
|
|
|
|
|
my @metadata = ( |
296
|
|
|
|
|
|
|
uri => $reg_uri, |
297
|
1
|
|
|
|
|
5
|
%{ $resp->content_struct() }, |
|
1
|
|
|
|
|
5
|
|
298
|
|
|
|
|
|
|
); |
299
|
|
|
|
|
|
|
|
300
|
|
|
|
|
|
|
#Even though we didn’t save the “terms-of-service” URL from |
301
|
|
|
|
|
|
|
#this registration object, we might as well hold onto it |
302
|
|
|
|
|
|
|
#for the current process to save a call to get_terms_of_service(). |
303
|
|
|
|
|
|
|
return Net::ACME::Registration->new( |
304
|
|
|
|
|
|
|
@metadata, |
305
|
1
|
|
|
|
|
15
|
terms_of_service => { $resp->links() }->{'terms-of-service'}, |
306
|
|
|
|
|
|
|
); |
307
|
|
|
|
|
|
|
} |
308
|
|
|
|
|
|
|
|
309
|
|
|
|
|
|
|
#NOTE: This doesn’t actually seem to work with Let’s Encrypt. |
310
|
|
|
|
|
|
|
#The POST keeps coming back with a 202 status rather than 200. |
311
|
|
|
|
|
|
|
#(Looks like Boulder doesn’t handle this function yet?) |
312
|
|
|
|
|
|
|
#sub rollover_key { |
313
|
|
|
|
|
|
|
# my ($self, $reg_uri) = @_; |
314
|
|
|
|
|
|
|
# |
315
|
|
|
|
|
|
|
# my $new_key = $self->create_key_pem(); |
316
|
|
|
|
|
|
|
# |
317
|
|
|
|
|
|
|
# my $sub_payload = { |
318
|
|
|
|
|
|
|
# resource => 'reg', |
319
|
|
|
|
|
|
|
# oldKey => $self->jwk_thumbprint(), |
320
|
|
|
|
|
|
|
# }; |
321
|
|
|
|
|
|
|
# |
322
|
|
|
|
|
|
|
# my $resp = $self->_post_url( |
323
|
|
|
|
|
|
|
# $reg_uri, |
324
|
|
|
|
|
|
|
# { |
325
|
|
|
|
|
|
|
# resource => 'reg', |
326
|
|
|
|
|
|
|
# newKey => Net::ACME::Utils::get_jws_data( |
327
|
|
|
|
|
|
|
# $new_key, |
328
|
|
|
|
|
|
|
# undef, |
329
|
|
|
|
|
|
|
# JSON::encode_json($sub_payload), |
330
|
|
|
|
|
|
|
# ), |
331
|
|
|
|
|
|
|
# }, |
332
|
|
|
|
|
|
|
# ); |
333
|
|
|
|
|
|
|
# |
334
|
|
|
|
|
|
|
# if ($resp->status() != 200) { |
335
|
|
|
|
|
|
|
# die "Incorrect status: " . $resp->status() . $/ . $resp->content(); |
336
|
|
|
|
|
|
|
# } |
337
|
|
|
|
|
|
|
# |
338
|
|
|
|
|
|
|
# $self->{'_account_key'} = $new_key; |
339
|
|
|
|
|
|
|
# $self->_set_ua(); |
340
|
|
|
|
|
|
|
# |
341
|
|
|
|
|
|
|
# return $new_key; |
342
|
|
|
|
|
|
|
#} |
343
|
|
|
|
|
|
|
|
344
|
|
|
|
|
|
|
sub start_domain_authz { |
345
|
1
|
|
|
1
|
1
|
21
|
my ( $self, $domain_name ) = @_; |
346
|
|
|
|
|
|
|
|
347
|
1
|
|
|
|
|
8
|
my $resp = $self->_post( |
348
|
|
|
|
|
|
|
'new-authz', |
349
|
|
|
|
|
|
|
{ |
350
|
|
|
|
|
|
|
resource => 'new-authz', |
351
|
|
|
|
|
|
|
identifier => { |
352
|
|
|
|
|
|
|
type => 'dns', |
353
|
|
|
|
|
|
|
value => $domain_name, |
354
|
|
|
|
|
|
|
}, |
355
|
|
|
|
|
|
|
}, |
356
|
|
|
|
|
|
|
); |
357
|
|
|
|
|
|
|
|
358
|
1
|
50
|
|
|
|
35
|
$resp->die_because_unexpected() if $resp->status() != 201; |
359
|
|
|
|
|
|
|
|
360
|
1
|
|
|
|
|
13
|
my $content = $resp->content_struct(); |
361
|
|
|
|
|
|
|
|
362
|
|
|
|
|
|
|
return Net::ACME::Authorization::Pending->new( |
363
|
|
|
|
|
|
|
uri => $resp->header('location'), |
364
|
|
|
|
|
|
|
combinations => $content->{'combinations'}, |
365
|
|
|
|
|
|
|
challenges => [ |
366
|
|
|
|
|
|
|
map { |
367
|
2
|
|
|
|
|
3
|
my $class = 'Net::ACME::Challenge::Pending'; |
368
|
2
|
100
|
|
|
|
6
|
if ( $_->{'type'} eq 'http-01' ) { |
369
|
1
|
|
|
|
|
3
|
$class .= '::http_01'; |
370
|
|
|
|
|
|
|
} |
371
|
2
|
|
|
|
|
16
|
$class->new(%$_); |
372
|
1
|
|
|
|
|
18
|
} @{ $content->{'challenges'} }, |
|
1
|
|
|
|
|
9
|
|
373
|
|
|
|
|
|
|
], |
374
|
|
|
|
|
|
|
); |
375
|
|
|
|
|
|
|
} |
376
|
|
|
|
|
|
|
|
377
|
|
|
|
|
|
|
#NOTE: This doesn’t actually work with Boulder (Let’s Encrypt) because |
378
|
|
|
|
|
|
|
#that server implements acme-01. Deletion of an authz was added in acme-02. |
379
|
|
|
|
|
|
|
# |
380
|
|
|
|
|
|
|
#It is critical, though, that when this doesn’t work we still request the |
381
|
|
|
|
|
|
|
#challenge against the authz so that the LE account doesn’t exceed a rate |
382
|
|
|
|
|
|
|
#limit. (cf. COBRA-3273) |
383
|
|
|
|
|
|
|
sub delete_authz { |
384
|
1
|
|
|
1
|
0
|
11
|
my ( $self, $authz ) = @_; |
385
|
|
|
|
|
|
|
|
386
|
|
|
|
|
|
|
#sanity |
387
|
1
|
50
|
|
|
|
5
|
if ( !Net::ACME::Utils::thing_isa($authz, 'Net::ACME::Authorization::Pending') ) { |
388
|
0
|
|
|
|
|
0
|
die "Must be a pending authz object, not “$authz”!"; |
389
|
|
|
|
|
|
|
} |
390
|
|
|
|
|
|
|
|
391
|
1
|
|
|
|
|
7
|
my $resp = $self->_post_url( |
392
|
|
|
|
|
|
|
$authz->uri(), |
393
|
|
|
|
|
|
|
{ |
394
|
|
|
|
|
|
|
resource => 'authz', |
395
|
|
|
|
|
|
|
delete => JSON::true(), |
396
|
|
|
|
|
|
|
}, |
397
|
|
|
|
|
|
|
); |
398
|
|
|
|
|
|
|
|
399
|
1
|
50
|
|
|
|
22
|
$resp->die_because_unexpected() if $resp->status() != 200; |
400
|
|
|
|
|
|
|
|
401
|
1
|
|
|
|
|
7
|
return; |
402
|
|
|
|
|
|
|
} |
403
|
|
|
|
|
|
|
|
404
|
|
|
|
|
|
|
sub do_challenge { |
405
|
1
|
|
|
1
|
0
|
13
|
my ( $self, $challenge_obj ) = @_; |
406
|
|
|
|
|
|
|
|
407
|
1
|
|
|
|
|
3
|
my ( $token, $uri ) = map { $challenge_obj->$_() } qw( token uri ); |
|
2
|
|
|
|
|
15
|
|
408
|
|
|
|
|
|
|
|
409
|
1
|
|
|
|
|
5
|
my $key_obj = Net::ACME::Crypt::parse_key($self->{'_key'}); |
410
|
|
|
|
|
|
|
|
411
|
1
|
|
33
|
|
|
82997
|
$self->{'_key_jwk'} ||= $key_obj->get_struct_for_public_jwk(); |
412
|
|
|
|
|
|
|
|
413
|
|
|
|
|
|
|
my $resp = $self->_post_url( |
414
|
|
|
|
|
|
|
$uri, |
415
|
|
|
|
|
|
|
{ |
416
|
|
|
|
|
|
|
resource => 'challenge', |
417
|
1
|
|
|
|
|
3991
|
keyAuthorization => $challenge_obj->make_key_authz( $self->{'_key_jwk'} ), |
418
|
|
|
|
|
|
|
}, |
419
|
|
|
|
|
|
|
); |
420
|
|
|
|
|
|
|
|
421
|
1
|
50
|
|
|
|
21
|
$resp->die_because_unexpected() if $resp->status() != 202; |
422
|
|
|
|
|
|
|
|
423
|
1
|
|
|
|
|
8
|
return; |
424
|
|
|
|
|
|
|
} |
425
|
|
|
|
|
|
|
|
426
|
|
|
|
|
|
|
sub get_certificate { |
427
|
1
|
|
|
1
|
1
|
12
|
my ( $self, $csr_pem ) = @_; |
428
|
|
|
|
|
|
|
|
429
|
1
|
|
|
|
|
6
|
my $csr_der = Crypt::Format::pem2der($csr_pem); |
430
|
|
|
|
|
|
|
|
431
|
1
|
|
|
|
|
66
|
my $resp = $self->_post( |
432
|
|
|
|
|
|
|
'new-cert', |
433
|
|
|
|
|
|
|
{ |
434
|
|
|
|
|
|
|
resource => 'new-cert', |
435
|
|
|
|
|
|
|
csr => _to_base64url($csr_der), |
436
|
|
|
|
|
|
|
}, |
437
|
|
|
|
|
|
|
); |
438
|
|
|
|
|
|
|
|
439
|
1
|
|
|
|
|
28
|
my $status = $resp->status(); |
440
|
|
|
|
|
|
|
|
441
|
|
|
|
|
|
|
#NB: Let’s Encrypt doesn’t seem to need this, |
442
|
|
|
|
|
|
|
#but per the ACME spec it *could* work this way. |
443
|
1
|
50
|
|
|
|
7
|
if ( $status == 202 ) { |
444
|
0
|
|
|
|
|
0
|
my $pcert = Net::ACME::Certificate::Pending->new( |
445
|
|
|
|
|
|
|
uri => $resp->header('location'), |
446
|
|
|
|
|
|
|
retry_after => $resp->header('retry-after'), |
447
|
|
|
|
|
|
|
); |
448
|
|
|
|
|
|
|
|
449
|
0
|
|
|
|
|
0
|
while (1) { |
450
|
0
|
0
|
|
|
|
0
|
if ( $pcert->is_time_to_poll() ) { |
451
|
0
|
|
|
|
|
0
|
my $c = $pcert->poll(); |
452
|
0
|
0
|
|
|
|
0
|
return $c if $c; |
453
|
|
|
|
|
|
|
} |
454
|
0
|
|
|
|
|
0
|
sleep 1; |
455
|
|
|
|
|
|
|
} |
456
|
|
|
|
|
|
|
} |
457
|
|
|
|
|
|
|
|
458
|
1
|
50
|
|
|
|
3
|
if ( $status == 201 ) { |
459
|
|
|
|
|
|
|
return Net::ACME::Certificate->new( |
460
|
|
|
|
|
|
|
content => $resp->content(), |
461
|
|
|
|
|
|
|
type => $resp->header('content-type'), |
462
|
1
|
|
|
|
|
16
|
issuer_cert_uri => { $resp->links() }->{'up'}, |
463
|
|
|
|
|
|
|
); |
464
|
|
|
|
|
|
|
} |
465
|
|
|
|
|
|
|
|
466
|
0
|
|
|
|
|
0
|
$resp->die_because_unexpected(); |
467
|
|
|
|
|
|
|
|
468
|
0
|
|
|
|
|
0
|
return; |
469
|
|
|
|
|
|
|
} |
470
|
|
|
|
|
|
|
|
471
|
|
|
|
|
|
|
#This isn’t needed yet, nor is it useful because |
472
|
|
|
|
|
|
|
#Let’s Encrypt (i.e., Boulder) doesn’t support it. |
473
|
|
|
|
|
|
|
#Once Boulder supports this, we should switch to it |
474
|
|
|
|
|
|
|
#in favor of the LE-specific logic in LetsEncrypt.pm. |
475
|
|
|
|
|
|
|
# |
476
|
|
|
|
|
|
|
#cf. https://ietf-wg-acme.github.io/acme/#rfc.section.6.1.1 |
477
|
|
|
|
|
|
|
#sub get_terms_of_service { |
478
|
|
|
|
|
|
|
# my ($self) = @_; |
479
|
|
|
|
|
|
|
# |
480
|
|
|
|
|
|
|
# my $dir = $self->_get_directory(); |
481
|
|
|
|
|
|
|
# my $url = $self->_get_directory()->{'meta'} or die 'No “meta” in directory!'; |
482
|
|
|
|
|
|
|
# $url = $url->{'terms-of-service'} or die 'No “terms-of-service” in directory metadata!'; |
483
|
|
|
|
|
|
|
# |
484
|
|
|
|
|
|
|
# return $url; |
485
|
|
|
|
|
|
|
#} |
486
|
|
|
|
|
|
|
|
487
|
|
|
|
|
|
|
#---------------------------------------------------------------------- |
488
|
|
|
|
|
|
|
|
489
|
|
|
|
|
|
|
sub _set_ua { |
490
|
5
|
|
|
5
|
|
7
|
my ($self) = @_; |
491
|
|
|
|
|
|
|
$self->{'_ua'} = Net::ACME::HTTP->new( |
492
|
5
|
|
|
|
|
38
|
key => $self->{'_key'}, |
493
|
|
|
|
|
|
|
); |
494
|
|
|
|
|
|
|
|
495
|
5
|
|
|
|
|
14
|
return; |
496
|
|
|
|
|
|
|
} |
497
|
|
|
|
|
|
|
|
498
|
|
|
|
|
|
|
#TODO: cache |
499
|
|
|
|
|
|
|
sub _get_directory { |
500
|
9
|
|
|
9
|
|
15
|
my ($self) = @_; |
501
|
|
|
|
|
|
|
|
502
|
9
|
|
66
|
|
|
56
|
return $self->{'_directory'} ||= $self->{'_ua'}->get("https://$self->{'_host'}/directory")->content_struct(); |
503
|
|
|
|
|
|
|
} |
504
|
|
|
|
|
|
|
|
505
|
|
|
|
|
|
|
sub _post { |
506
|
3
|
|
|
3
|
|
23
|
my ( $self, $link_name, $data ) = @_; |
507
|
|
|
|
|
|
|
|
508
|
3
|
50
|
|
|
|
10
|
my $url = $self->_get_directory()->{$link_name} or die "Unknown link name: “$link_name”"; |
509
|
|
|
|
|
|
|
|
510
|
3
|
|
|
|
|
45
|
return $self->_post_url( $url, $data ); |
511
|
|
|
|
|
|
|
} |
512
|
|
|
|
|
|
|
|
513
|
|
|
|
|
|
|
#mocked in tests |
514
|
|
|
|
|
|
|
sub _post_url { |
515
|
6
|
|
|
6
|
|
43
|
my ( $self, $url, $data ) = @_; |
516
|
|
|
|
|
|
|
|
517
|
|
|
|
|
|
|
#Do this in case we haven’t initialized the directory yet. |
518
|
|
|
|
|
|
|
#Initializing the directory is necessary to get a nonce. |
519
|
6
|
|
|
|
|
16
|
$self->_get_directory(); |
520
|
|
|
|
|
|
|
|
521
|
6
|
|
|
|
|
41
|
return $self->{'_ua'}->post( $url, $data ); |
522
|
|
|
|
|
|
|
} |
523
|
|
|
|
|
|
|
|
524
|
|
|
|
|
|
|
1; |