Skip to content

Commit 8bbfb87

Browse files
Steven Yuanrchache
Steven Yuan
authored andcommitted
Add @aws.auth#sigv4a trait (smithy-lang#2032)
* Add `@aws.auth#sigv4a` trait * Add `@aws.auth#sigv4a` trait Java implementation * Add `SigV4TraitsValidator` validation * Add `@aws.auth#sigv4a` trait tests * Add `@aws.auth#sigv4a` trait documentation
1 parent 9f0d350 commit 8bbfb87

17 files changed

+390
-8
lines changed

docs/source-2.0/aws/aws-auth.rst

+61-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ Trait value
3333
- **Required**. The signature version 4 service signing name to use
3434
in the `credential scope`_ when signing requests. This value MUST
3535
NOT be empty. This value SHOULD match the ``arnNamespace`` property
36-
of the :ref:`aws.api#service-trait`.
36+
of the :ref:`aws.api#service-trait` if present and the ``name``
37+
property of the :ref:`aws.auth#sigv4a-trait` if present.
3738

3839
If a request contains the ``Authorization`` header or a query string parameter
3940
with the name of ``X-Amz-Algorithm`` containing the value ``AWS4-HMAC-SHA256``,
@@ -59,6 +60,65 @@ unauthenticated request.
5960
}
6061
6162
63+
.. smithy-trait:: aws.auth#sigv4a
64+
.. _aws.auth#sigv4a-trait:
65+
66+
-------------------------
67+
``aws.auth#sigv4a`` trait
68+
-------------------------
69+
70+
Trait summary
71+
The ``aws.auth#sigv4a`` trait adds support for AWS Signature Version 4
72+
Asymmetric (SigV4A), an extension of `AWS signature version 4`_ (SigV4), to
73+
a service.
74+
Trait selector
75+
``service[trait|aws.auth#sigv4]``
76+
Trait value
77+
An ``object`` that supports the following properties:
78+
79+
.. list-table::
80+
:header-rows: 1
81+
:widths: 10 20 70
82+
83+
* - Property
84+
- Type
85+
- Description
86+
* - name
87+
- ``string``
88+
- **Required**. The signature version 4a service signing name to use
89+
in the `credential scope`_ when signing requests. This value MUST
90+
NOT be empty. This value SHOULD match the ``arnNamespace`` property
91+
of the :ref:`aws.api#service-trait` if present and the ``name``
92+
property of the :ref:`aws.auth#sigv4-trait`.
93+
94+
SigV4A is nearly identical to SigV4, but also uses public-private keys and
95+
asymmetric cryptographic signatures for every request. Most notably, SigV4A
96+
supports signatures for multi-region API requests.
97+
98+
.. code-block:: smithy
99+
100+
$version: "2"
101+
102+
namespace aws.fooBaz
103+
104+
use aws.api#service
105+
use aws.auth#sigv4
106+
use aws.auth#sigv4a
107+
use aws.protocols#restJson1
108+
109+
// This service is an AWS service that prioritizes SigV4A
110+
// authentication before SigV4 authentication.
111+
// Note that services that support SigV4A MUST support SigV4.
112+
@service(sdkId: "Some Value")
113+
@auth([sigv4a, sigv4])
114+
@sigv4(name: "foobaz")
115+
@sigv4a(name: "foobaz")
116+
@restJson1
117+
service FooBaz {
118+
version: "2018-03-17"
119+
}
120+
121+
62122
.. smithy-trait:: aws.auth#unsignedPayload
63123
.. _aws.auth#unsignedPayload-trait:
64124

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.aws.traits.auth;
7+
8+
import software.amazon.smithy.model.node.Node;
9+
import software.amazon.smithy.model.node.ObjectNode;
10+
import software.amazon.smithy.model.shapes.ShapeId;
11+
import software.amazon.smithy.model.traits.AbstractTrait;
12+
import software.amazon.smithy.model.traits.AbstractTraitBuilder;
13+
import software.amazon.smithy.model.traits.Trait;
14+
import software.amazon.smithy.utils.SmithyBuilder;
15+
import software.amazon.smithy.utils.ToSmithyBuilder;
16+
17+
/**
18+
* Adds AWS Signature Version 4 Asymmetric authentication to a service or operation.
19+
*/
20+
public final class SigV4ATrait extends AbstractTrait implements ToSmithyBuilder<SigV4ATrait> {
21+
public static final ShapeId ID = ShapeId.from("aws.auth#sigv4a");
22+
private static final String NAME = "name";
23+
24+
private final String name;
25+
26+
private SigV4ATrait(Builder builder) {
27+
super(ID, builder.getSourceLocation());
28+
this.name = SmithyBuilder.requiredState(NAME, builder.name);
29+
}
30+
31+
/**
32+
* Gets the service signing name.
33+
*
34+
* @return the service signing name
35+
*/
36+
public String getName() {
37+
return name;
38+
}
39+
40+
public static Builder builder() {
41+
return new Builder();
42+
}
43+
44+
@Override
45+
public Builder toBuilder() {
46+
return builder()
47+
.sourceLocation(getSourceLocation())
48+
.name(getName());
49+
}
50+
51+
@Override
52+
protected Node createNode() {
53+
return Node.objectNodeBuilder()
54+
.sourceLocation(getSourceLocation())
55+
.withMember(NAME, getName())
56+
.build();
57+
}
58+
59+
public static final class Builder extends AbstractTraitBuilder<SigV4ATrait, Builder> {
60+
private String name;
61+
62+
private Builder() {}
63+
64+
@Override
65+
public SigV4ATrait build() {
66+
return new SigV4ATrait(this);
67+
}
68+
69+
public Builder name(String name) {
70+
this.name = name;
71+
return this;
72+
}
73+
}
74+
75+
public static final class Provider extends AbstractTrait.Provider {
76+
public Provider() {
77+
super(ID);
78+
}
79+
80+
@Override
81+
public Trait createTrait(ShapeId target, Node value) {
82+
ObjectNode objectNode = value.expectObjectNode();
83+
Builder builder = builder().sourceLocation(value);
84+
builder.name(objectNode.expectStringMember(NAME).getValue());
85+
SigV4ATrait result = builder.build();
86+
result.setNodeCache(objectNode);
87+
return result;
88+
}
89+
}
90+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.aws.traits.auth;
7+
8+
import java.util.ArrayList;
9+
import java.util.List;
10+
import java.util.Optional;
11+
import software.amazon.smithy.aws.traits.ServiceTrait;
12+
import software.amazon.smithy.model.Model;
13+
import software.amazon.smithy.model.shapes.ServiceShape;
14+
import software.amazon.smithy.model.shapes.ShapeId;
15+
import software.amazon.smithy.model.validation.AbstractValidator;
16+
import software.amazon.smithy.model.validation.ValidationEvent;
17+
import software.amazon.smithy.utils.SmithyInternalApi;
18+
19+
/**
20+
* Validates AWS Service, SigV4, and SigV4A traits.
21+
*/
22+
@SmithyInternalApi
23+
public final class SigV4TraitsValidator extends AbstractValidator {
24+
private static final ShapeId SERVICE_ARN_NAMESPACE = ServiceTrait.ID.withMember("arnNamespace");
25+
private static final ShapeId SIGV4_NAME = SigV4Trait.ID.withMember("name");
26+
private static final ShapeId SIGV4A_NAME = SigV4ATrait.ID.withMember("name");
27+
28+
@Override
29+
public List<ValidationEvent> validate(Model model) {
30+
List<ValidationEvent> events = new ArrayList<>();
31+
for (ServiceShape service : model.getServiceShapes()) {
32+
events.addAll(validateService(model, service));
33+
}
34+
return events;
35+
}
36+
37+
/**
38+
* Validates Service and SigV4 traits.
39+
*
40+
* - service$arnNamespace, sigv4$name, and sigv4a$name SHOULD be equal. Otherwise, emits warnings.
41+
*/
42+
private List<ValidationEvent> validateService(Model model, ServiceShape service) {
43+
List<ValidationEvent> events = new ArrayList<>();
44+
Optional<ServiceTrait> serviceTraitOptional = service.getTrait(ServiceTrait.class);
45+
Optional<SigV4Trait> sigv4TraitOptional = service.getTrait(SigV4Trait.class);
46+
Optional<SigV4ATrait> sigv4aTraitOptional = service.getTrait(SigV4ATrait.class);
47+
if (serviceTraitOptional.isPresent()) {
48+
String serviceArnNamespace = serviceTraitOptional.get().getArnNamespace();
49+
// Check service$arnNamespace with sigv4$name
50+
if (sigv4TraitOptional.isPresent()) {
51+
String sigv4Name = sigv4TraitOptional.get().getName();
52+
if (!serviceArnNamespace.equals(sigv4Name)) {
53+
events.add(createValuesShouldMatchWarning(
54+
service,
55+
SERVICE_ARN_NAMESPACE, serviceArnNamespace,
56+
SIGV4_NAME, sigv4Name));
57+
}
58+
}
59+
// Check service$arnNamespace with sigv4a$name
60+
if (sigv4aTraitOptional.isPresent()) {
61+
String sigv4aName = sigv4aTraitOptional.get().getName();
62+
if (!serviceArnNamespace.equals(sigv4aName)) {
63+
events.add(createValuesShouldMatchWarning(
64+
service,
65+
SERVICE_ARN_NAMESPACE, serviceArnNamespace,
66+
SIGV4A_NAME, sigv4aName));
67+
}
68+
}
69+
}
70+
// Check sigv4$name with sigv4a$name
71+
if (sigv4TraitOptional.isPresent() && sigv4aTraitOptional.isPresent()) {
72+
String sigv4Name = sigv4TraitOptional.get().getName();
73+
String sigv4aName = sigv4aTraitOptional.get().getName();
74+
if (!sigv4Name.equals(sigv4aName)) {
75+
events.add(createValuesShouldMatchWarning(
76+
service,
77+
SIGV4_NAME, sigv4Name,
78+
SIGV4A_NAME, sigv4aName));
79+
}
80+
}
81+
return events;
82+
}
83+
84+
private ValidationEvent createValuesShouldMatchWarning(
85+
ServiceShape service,
86+
ShapeId member1,
87+
String value1,
88+
ShapeId member2,
89+
String value2
90+
) {
91+
return warning(service, String.format(
92+
"Value for `%s` \"%s\" and value for `%s` \"%s\" SHOULD match.",
93+
member1.toString(), value1, member2.toString(), value2));
94+
}
95+
}

smithy-aws-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.traits.TraitService

+1
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,6 @@ software.amazon.smithy.aws.traits.protocols.RestXmlTrait$Provider
2121
software.amazon.smithy.aws.traits.protocols.AwsJson1_0Trait$Provider
2222
software.amazon.smithy.aws.traits.protocols.AwsJson1_1Trait$Provider
2323
software.amazon.smithy.aws.traits.auth.SigV4Trait$Provider
24+
software.amazon.smithy.aws.traits.auth.SigV4ATrait$Provider
2425
software.amazon.smithy.aws.traits.tagging.TagEnabledTrait$Provider
2526
software.amazon.smithy.aws.traits.tagging.TaggableTrait$Provider

smithy-aws-traits/src/main/resources/META-INF/services/software.amazon.smithy.model.validation.Validator

+1
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ software.amazon.smithy.aws.traits.tagging.TaggableResourceValidator
1111
software.amazon.smithy.aws.traits.tagging.TagResourcePropertyTypeValidator
1212
software.amazon.smithy.aws.traits.tagging.TagResourcePropertyNameValidator
1313
software.amazon.smithy.aws.traits.ErrorRenameValidator
14+
software.amazon.smithy.aws.traits.auth.SigV4TraitsValidator

smithy-aws-traits/src/main/resources/META-INF/smithy/aws.auth.smithy

+29-7
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,40 @@ structure cognitoUserPools {
1919
/// security, most requests to AWS must be signed with an access key, which consists
2020
/// of an access key ID and secret access key. These two keys are commonly referred to
2121
/// as your security credentials.
22-
@authDefinition(traits: [unsignedPayload])
23-
@externalDocumentation(
24-
Reference: "https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html"
22+
@authDefinition(
23+
traits: [unsignedPayload]
2524
)
25+
@externalDocumentation(Reference: "https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html")
2626
@trait(selector: "service")
2727
structure sigv4 {
2828
/// The signature version 4 service signing name to use in the credential
2929
/// scope when signing requests. This value SHOULD match the `arnNamespace`
30-
/// property of the `aws.api#service-trait`.
31-
@externalDocumentation(
32-
Reference: "https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html"
33-
)
30+
/// property of the `aws.api#service` trait if present and the `name`
31+
/// property of the `aws.api#sigv4a` trait if present.
32+
@externalDocumentation(Reference: "https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html")
33+
@length(min: 1)
34+
@required
35+
name: String
36+
}
37+
38+
/// Signature Version 4 Asymmetric (SigV4A), an extension of Signature Version 4 (SigV4), is the
39+
/// process to add authentication information to AWS requests sent by HTTP. SigV4A is nearly
40+
/// identical to SigV4, but also uses public-private keys and asymmetric cryptographic signatures
41+
/// for every request. Most notably, SigV4A supports signatures for multi-region API requests.
42+
@authDefinition(
43+
traits: [unsignedPayload]
44+
)
45+
@externalDocumentation(
46+
Reference: "https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html"
47+
Examples: "https://github.com/aws-samples/sigv4a-signing-examples"
48+
)
49+
@trait(selector: "service[trait|aws.auth#sigv4]")
50+
structure sigv4a {
51+
/// The signature version 4a service signing name to use in the credential
52+
/// scope when signing requests. This value SHOULD match the `arnNamespace`
53+
/// property of the `aws.api#service` trait if present and the `name`
54+
/// property of the `aws.api#sigv4` trait.
55+
@externalDocumentation(Reference: "https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html")
3456
@length(min: 1)
3557
@required
3658
name: String
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.aws.traits.auth;
7+
8+
import static org.hamcrest.MatcherAssert.assertThat;
9+
import static org.hamcrest.Matchers.equalTo;
10+
import static org.hamcrest.Matchers.instanceOf;
11+
import static org.junit.jupiter.api.Assertions.assertFalse;
12+
import static org.junit.jupiter.api.Assertions.assertTrue;
13+
14+
import java.util.Optional;
15+
import org.junit.jupiter.api.Test;
16+
import software.amazon.smithy.model.node.Node;
17+
import software.amazon.smithy.model.node.ObjectNode;
18+
import software.amazon.smithy.model.node.StringNode;
19+
import software.amazon.smithy.model.shapes.ShapeId;
20+
import software.amazon.smithy.model.traits.Trait;
21+
import software.amazon.smithy.model.traits.TraitFactory;
22+
23+
public class SigV4ATraitTest {
24+
private static final String MOCK_SIGNING_NAME = "mocksigningname";
25+
private static final ShapeId MOCK_TARGET = ShapeId.from("ns.qux#foo");
26+
27+
@Test
28+
public void loadsTrait() {
29+
Node node = ObjectNode.builder()
30+
.withMember("name", StringNode.from(MOCK_SIGNING_NAME))
31+
.build();
32+
TraitFactory provider = TraitFactory.createServiceFactory();
33+
Optional<Trait> trait = provider.createTrait(SigV4ATrait.ID, MOCK_TARGET, node);
34+
35+
assertTrue(trait.isPresent());
36+
assertThat(trait.get(), instanceOf(SigV4ATrait.class));
37+
SigV4ATrait sigv4aTrait = (SigV4ATrait) trait.get();
38+
assertFalse(sigv4aTrait.getName().isEmpty());
39+
assertThat(sigv4aTrait.getName(), equalTo(MOCK_SIGNING_NAME));
40+
assertThat(sigv4aTrait.toNode(), equalTo(node));
41+
assertThat(sigv4aTrait.toBuilder().build(), equalTo(sigv4aTrait));
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[WARNING] smithy.example#InvalidService: Value for `aws.api#service$arnNamespace` "invalidservice" and value for `aws.auth#sigv4$name` "signingname" SHOULD match. | SigV4Traits
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
$version: "2.0"
2+
3+
namespace smithy.example
4+
5+
use aws.api#service
6+
use aws.auth#sigv4
7+
8+
@service(sdkId: "servicename")
9+
@sigv4(name: "signingname")
10+
service InvalidService {
11+
version: "2020-07-02"
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[WARNING] smithy.example#InvalidService: Value for `aws.auth#sigv4$name` "sigv4signingname" and value for `aws.auth#sigv4a$name` "sigv4asigningname" SHOULD match. | SigV4Traits
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
$version: "2.0"
2+
3+
namespace smithy.example
4+
5+
use aws.auth#sigv4
6+
use aws.auth#sigv4a
7+
8+
@auth([sigv4a, sigv4])
9+
@sigv4(name: "sigv4signingname")
10+
@sigv4a(name: "sigv4asigningname")
11+
service InvalidService {
12+
version: "2020-07-02"
13+
}

0 commit comments

Comments
 (0)