diff --git a/acme/api/identifier.go b/acme/api/identifier.go new file mode 100644 index 0000000000..27337ccba8 --- /dev/null +++ b/acme/api/identifier.go @@ -0,0 +1,27 @@ +package api + +import ( + "cmp" + "slices" + + "github.com/go-acme/lego/v4/acme" +) + +// compareIdentifiers compares 2 slices of [acme.Identifier]. +func compareIdentifiers(a, b []acme.Identifier) int { + // Clones slices to avoid modifying original slices. + right := slices.Clone(a) + left := slices.Clone(b) + + slices.SortStableFunc(right, compareIdentifier) + slices.SortStableFunc(left, compareIdentifier) + + return slices.CompareFunc(right, left, compareIdentifier) +} + +func compareIdentifier(right, left acme.Identifier) int { + return cmp.Or( + cmp.Compare(right.Type, left.Type), + cmp.Compare(right.Value, left.Value), + ) +} diff --git a/acme/api/identifier_test.go b/acme/api/identifier_test.go new file mode 100644 index 0000000000..586a879869 --- /dev/null +++ b/acme/api/identifier_test.go @@ -0,0 +1,111 @@ +package api + +import ( + "testing" + + "github.com/go-acme/lego/v4/acme" + "github.com/stretchr/testify/assert" +) + +func Test_compareIdentifiers(t *testing.T) { + testCases := []struct { + desc string + a, b []acme.Identifier + expected int + }{ + { + desc: "identical identifiers", + a: []acme.Identifier{ + {Type: "dns", Value: "example.com"}, + {Type: "dns", Value: "*.example.com"}, + }, + b: []acme.Identifier{ + {Type: "dns", Value: "example.com"}, + {Type: "dns", Value: "*.example.com"}, + }, + expected: 0, + }, + { + desc: "identical identifiers but different order", + a: []acme.Identifier{ + {Type: "dns", Value: "example.com"}, + {Type: "dns", Value: "*.example.com"}, + }, + b: []acme.Identifier{ + {Type: "dns", Value: "*.example.com"}, + {Type: "dns", Value: "example.com"}, + }, + expected: 0, + }, + { + desc: "duplicate identifiers", + a: []acme.Identifier{ + {Type: "dns", Value: "example.com"}, + {Type: "dns", Value: "*.example.com"}, + }, + b: []acme.Identifier{ + {Type: "dns", Value: "example.com"}, + {Type: "dns", Value: "example.com"}, + }, + expected: -1, + }, + { + desc: "different identifier values", + a: []acme.Identifier{ + {Type: "dns", Value: "example.com"}, + {Type: "dns", Value: "*.example.com"}, + }, + b: []acme.Identifier{ + {Type: "dns", Value: "example.com"}, + {Type: "dns", Value: "*.example.org"}, + }, + expected: -1, + }, + { + desc: "different identifier types", + a: []acme.Identifier{ + {Type: "dns", Value: "example.com"}, + {Type: "dns", Value: "*.example.com"}, + }, + b: []acme.Identifier{ + {Type: "dns", Value: "example.com"}, + {Type: "ip", Value: "*.example.com"}, + }, + expected: -1, + }, + { + desc: "different number of identifiers a>b", + a: []acme.Identifier{ + {Type: "dns", Value: "example.com"}, + {Type: "dns", Value: "*.example.com"}, + {Type: "dns", Value: "example.org"}, + }, + b: []acme.Identifier{ + {Type: "dns", Value: "example.com"}, + {Type: "dns", Value: "*.example.com"}, + }, + expected: 1, + }, + { + desc: "different number of identifiers b>a", + a: []acme.Identifier{ + {Type: "dns", Value: "example.com"}, + {Type: "dns", Value: "*.example.com"}, + }, + b: []acme.Identifier{ + {Type: "dns", Value: "example.com"}, + {Type: "dns", Value: "*.example.com"}, + {Type: "dns", Value: "example.org"}, + }, + expected: -1, + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + assert.Equal(t, test.expected, compareIdentifiers(test.a, test.b)) + }) + } +} diff --git a/acme/api/order.go b/acme/api/order.go index 4d310e0409..dd42fb4451 100644 --- a/acme/api/order.go +++ b/acme/api/order.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net" + "slices" "time" "github.com/go-acme/lego/v4/acme" @@ -85,6 +86,21 @@ func (o *OrderService) NewWithOptions(domains []string, opts *OrderOptions) (acm } } + // The elements of the "authorizations" and "identifiers" arrays are immutable once set. + // The server MUST NOT change the contents of either array after they are created. + // If a client observes a change in the contents of either array, + // then it SHOULD consider the order invalid. + // https://www.rfc-editor.org/rfc/rfc8555#section-7.1.3 + if compareIdentifiers(orderReq.Identifiers, order.Identifiers) != 0 { + // Sorts identifiers to avoid error message ambiguities about the order of the identifiers. + slices.SortStableFunc(orderReq.Identifiers, compareIdentifier) + slices.SortStableFunc(order.Identifiers, compareIdentifier) + + return acme.ExtendedOrder{}, + fmt.Errorf("order identifiers have been by the ACME server (RFC8555 ยง7.1.3): %+v != %+v", + orderReq.Identifiers, order.Identifiers) + } + return acme.ExtendedOrder{ Order: order, Location: resp.Header.Get("Location"),