Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions MANIFEST
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,6 @@ t/Net-ACME2-order-lifecycle.t
t/Net-ACME2-PromiseUtil.t
t/Net-ACME2-RetryAfter.t
t/Net-ACME2-revoke.t
t/Net-ACME2-update-fields.t
t/Net-ACME2.t
t/Net-ACME2_pre_rename.t
2 changes: 1 addition & 1 deletion lib/Net/ACME2/Authorization.pm
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ sub challenges {
sub update {
my ($self, $new_hr) = @_;

for my $name ( 'status', 'challenges' ) {
for my $name ( 'status', 'challenges', 'expires' ) {
$self->{"_$name"} = $new_hr->{$name};
}

Expand Down
9 changes: 8 additions & 1 deletion lib/Net/ACME2/Order.pm
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use constant _ACCESSORS => (
'certificate',
'finalize',
'retry_after',
'error',
);

=head1 ACCESSORS
Expand Down Expand Up @@ -57,6 +58,12 @@ The C<Retry-After> value from the most recent poll response,
or C<undef> if the server did not send one. Only populated
after C<poll_order()>.

=item * B<error()>

The error object (as a hash reference) from the ACME server when the
order's status is C<invalid>. This is an RFC 7807 problem document.
C<undef> when the order has no error. Updated by C<poll_order()>.

=back

=head2 I<OBJ>->retry_after_seconds()
Expand Down Expand Up @@ -110,7 +117,7 @@ sub identifiers {
sub update {
my ($self, $new_hr) = @_;

for my $name ( 'status', 'certificate' ) {
for my $name ( 'status', 'certificate', 'expires', 'error' ) {
$self->{"_$name"} = $new_hr->{$name};
}

Expand Down
172 changes: 172 additions & 0 deletions t/Net-ACME2-update-fields.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
#!/usr/bin/env perl

use strict;
use warnings;

use Test::More;
use Test::Deep;
use Test::FailWarnings;

use Digest::MD5;
use HTTP::Status;
use URI;
use JSON;

use Crypt::Format ();
use MIME::Base64 ();

use FindBin;
use lib "$FindBin::Bin/lib";
use Test::ACME2_Server;

#----------------------------------------------------------------------

{
package MyCA;

use parent qw( Net::ACME2 );

use constant {
HOST => 'acme.someca.net',
DIRECTORY_PATH => '/acme-directory',
};
}

my $_P256_KEY = <<END;
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIKDv8TBijBVbTYB7lfUnwLn4qjqWD0GD7XOXzdp0wb61oAoGCCqGSM49
AwEHoUQDQgAEBJIULcFadtIBc0TuNzT80UFcfkQ0U7+EPqEJNXamG1H4/z8xVgE7
3hoBfX4xbN2Hx2p26eNIptt+1jj2H/M44g==
-----END EC PRIVATE KEY-----
END

#----------------------------------------------------------------------

subtest 'Order error field populated on invalid status' => sub {
my $SERVER_OBJ = Test::ACME2_Server->new(
ca_class => 'MyCA',
);

my $acme = MyCA->new( key => $_P256_KEY );
$acme->create_account( termsOfServiceAgreed => 1 );

my $order = $acme->create_order(
identifiers => [
{ type => 'dns', value => 'example.com' },
],
);

is( $order->error(), undef, 'no error on new order' );

# Simulate the server marking the order invalid with an error
$SERVER_OBJ->{'_order_invalid'} = 1;
$SERVER_OBJ->{'_order_error'} = {
type => 'urn:ietf:params:acme:error:unauthorized',
detail => 'CAA record forbids issuance',
status => 403,
};

my $status = $acme->poll_order($order);
is( $status, 'invalid', 'poll_order returns invalid' );
is( $order->status(), 'invalid', 'order status updated' );

my $error = $order->error();
ok( $error, 'error field is populated' );
is( ref $error, 'HASH', 'error is a hashref' );
is( $error->{'type'}, 'urn:ietf:params:acme:error:unauthorized', 'error type' );
is( $error->{'detail'}, 'CAA record forbids issuance', 'error detail' );
is( $error->{'status'}, 403, 'error status' );
};

subtest 'Order error cleared when order becomes valid' => sub {
my $SERVER_OBJ = Test::ACME2_Server->new(
ca_class => 'MyCA',
);

my $acme = MyCA->new( key => $_P256_KEY );
$acme->create_account( termsOfServiceAgreed => 1 );

my $order = $acme->create_order(
identifiers => [
{ type => 'dns', value => 'example.com' },
],
);

# First, mark order invalid with error
$SERVER_OBJ->{'_order_invalid'} = 1;
$SERVER_OBJ->{'_order_error'} = {
type => 'urn:ietf:params:acme:error:unauthorized',
detail => 'temporary issue',
};

$acme->poll_order($order);
ok( $order->error(), 'error set after invalid poll' );

# Now simulate recovery (server no longer reports error)
$SERVER_OBJ->{'_order_invalid'} = 0;
$SERVER_OBJ->{'_order_finalized'} = 1;
delete $SERVER_OBJ->{'_order_error'};

$acme->poll_order($order);
is( $order->status(), 'valid', 'order recovered to valid' );
is( $order->error(), undef, 'error cleared after valid poll' );
};

subtest 'Order expires updated on poll' => sub {
my $SERVER_OBJ = Test::ACME2_Server->new(
ca_class => 'MyCA',
);

my $acme = MyCA->new( key => $_P256_KEY );
$acme->create_account( termsOfServiceAgreed => 1 );

my $order = $acme->create_order(
identifiers => [
{ type => 'dns', value => 'example.com' },
],
);

# Initially no expires (our mock doesn't set one by default)
my $initial_expires = $order->expires();

# Set expires on the server
$SERVER_OBJ->{'_order_expires'} = '2099-12-31T23:59:59Z';

$acme->poll_order($order);
is( $order->expires(), '2099-12-31T23:59:59Z', 'expires updated from poll response' );

# Change expires again
$SERVER_OBJ->{'_order_expires'} = '2100-06-15T12:00:00Z';

$acme->poll_order($order);
is( $order->expires(), '2100-06-15T12:00:00Z', 'expires updated again on subsequent poll' );
};

subtest 'Authorization expires updated on poll' => sub {
my $SERVER_OBJ = Test::ACME2_Server->new(
ca_class => 'MyCA',
);

my $acme = MyCA->new( key => $_P256_KEY );
$acme->create_account( termsOfServiceAgreed => 1 );

my $order = $acme->create_order(
identifiers => [
{ type => 'dns', value => 'example.com' },
],
);

my @authz_urls = $order->authorizations();
my $authz = $acme->get_authorization( $authz_urls[0] );

# Initially no expires
my $initial_expires = $authz->expires();

# Set expires on the server
$SERVER_OBJ->{'_authz_expires'} = '2099-12-31T23:59:59Z';

$acme->poll_authorization($authz);
is( $authz->expires(), '2099-12-31T23:59:59Z', 'authz expires updated from poll response' );
};

done_testing();
21 changes: 20 additions & 1 deletion t/lib/Test/ACME2_Server.pm
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,9 @@ sub new {

'POST:/order/1' => sub {
my $h = $self->{'ca_class'}->HOST();
my $status = $self->{'_order_finalized'} ? 'valid' : 'pending';
my $status = $self->{'_order_invalid'} ? 'invalid'
: $self->{'_order_finalized'} ? 'valid'
: 'pending';

my $order = $self->{'_orders'}{1};
$order->{'status'} = $status;
Expand All @@ -333,6 +335,17 @@ sub new {
$order->{'certificate'} = "https://$h/cert/1";
}

if ($self->{'_order_error'}) {
$order->{'error'} = $self->{'_order_error'};
}
else {
delete $order->{'error'};
}

if ($self->{'_order_expires'}) {
$order->{'expires'} = $self->{'_order_expires'};
}

my %extra_headers;
if ($self->{'_retry_after_order'}) {
$extra_headers{'retry-after'} = $self->{'_retry_after_order'};
Expand Down Expand Up @@ -477,9 +490,15 @@ sub _authz_content {
: $self->{'_challenge_accepted'} ? 'valid'
: 'pending';

my %extra;
if ($self->{'_authz_expires'}) {
$extra{'expires'} = $self->{'_authz_expires'};
}

return {
status => $status,
identifier => { type => 'dns', value => 'example.com' },
%extra,
challenges => [
{
type => 'http-01',
Expand Down
Loading