Skip to content

Commit

Permalink
Add @aws.auth#sigv4a trait (#2032)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Steven Yuan authored Dec 5, 2023
1 parent 7500a94 commit c34f984
Show file tree
Hide file tree
Showing 17 changed files with 390 additions and 8 deletions.
62 changes: 61 additions & 1 deletion docs/source-2.0/aws/aws-auth.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ Trait value
- **Required**. The signature version 4 service signing name to use
in the `credential scope`_ when signing requests. This value MUST
NOT be empty. This value SHOULD match the ``arnNamespace`` property
of the :ref:`aws.api#service-trait`.
of the :ref:`aws.api#service-trait` if present and the ``name``
property of the :ref:`aws.auth#sigv4a-trait` if present.

If a request contains the ``Authorization`` header or a query string parameter
with the name of ``X-Amz-Algorithm`` containing the value ``AWS4-HMAC-SHA256``,
Expand All @@ -59,6 +60,65 @@ unauthenticated request.
}
.. smithy-trait:: aws.auth#sigv4a
.. _aws.auth#sigv4a-trait:

-------------------------
``aws.auth#sigv4a`` trait
-------------------------

Trait summary
The ``aws.auth#sigv4a`` trait adds support for AWS Signature Version 4
Asymmetric (SigV4A), an extension of `AWS signature version 4`_ (SigV4), to
a service.
Trait selector
``service[trait|aws.auth#sigv4]``
Trait value
An ``object`` that supports the following properties:

.. list-table::
:header-rows: 1
:widths: 10 20 70

* - Property
- Type
- Description
* - name
- ``string``
- **Required**. The signature version 4a service signing name to use
in the `credential scope`_ when signing requests. This value MUST
NOT be empty. This value SHOULD match the ``arnNamespace`` property
of the :ref:`aws.api#service-trait` if present and the ``name``
property of the :ref:`aws.auth#sigv4-trait`.

SigV4A is nearly identical to SigV4, but also uses public-private keys and
asymmetric cryptographic signatures for every request. Most notably, SigV4A
supports signatures for multi-region API requests.

.. code-block:: smithy
$version: "2"
namespace aws.fooBaz
use aws.api#service
use aws.auth#sigv4
use aws.auth#sigv4a
use aws.protocols#restJson1
// This service is an AWS service that prioritizes SigV4A
// authentication before SigV4 authentication.
// Note that services that support SigV4A MUST support SigV4.
@service(sdkId: "Some Value")
@auth([sigv4a, sigv4])
@sigv4(name: "foobaz")
@sigv4a(name: "foobaz")
@restJson1
service FooBaz {
version: "2018-03-17"
}
.. smithy-trait:: aws.auth#unsignedPayload
.. _aws.auth#unsignedPayload-trait:

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.aws.traits.auth;

import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.node.ObjectNode;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.traits.AbstractTrait;
import software.amazon.smithy.model.traits.AbstractTraitBuilder;
import software.amazon.smithy.model.traits.Trait;
import software.amazon.smithy.utils.SmithyBuilder;
import software.amazon.smithy.utils.ToSmithyBuilder;

/**
* Adds AWS Signature Version 4 Asymmetric authentication to a service or operation.
*/
public final class SigV4ATrait extends AbstractTrait implements ToSmithyBuilder<SigV4ATrait> {
public static final ShapeId ID = ShapeId.from("aws.auth#sigv4a");
private static final String NAME = "name";

private final String name;

private SigV4ATrait(Builder builder) {
super(ID, builder.getSourceLocation());
this.name = SmithyBuilder.requiredState(NAME, builder.name);
}

/**
* Gets the service signing name.
*
* @return the service signing name
*/
public String getName() {
return name;
}

public static Builder builder() {
return new Builder();
}

@Override
public Builder toBuilder() {
return builder()
.sourceLocation(getSourceLocation())
.name(getName());
}

@Override
protected Node createNode() {
return Node.objectNodeBuilder()
.sourceLocation(getSourceLocation())
.withMember(NAME, getName())
.build();
}

public static final class Builder extends AbstractTraitBuilder<SigV4ATrait, Builder> {
private String name;

private Builder() {}

@Override
public SigV4ATrait build() {
return new SigV4ATrait(this);
}

public Builder name(String name) {
this.name = name;
return this;
}
}

public static final class Provider extends AbstractTrait.Provider {
public Provider() {
super(ID);
}

@Override
public Trait createTrait(ShapeId target, Node value) {
ObjectNode objectNode = value.expectObjectNode();
Builder builder = builder().sourceLocation(value);
builder.name(objectNode.expectStringMember(NAME).getValue());
SigV4ATrait result = builder.build();
result.setNodeCache(objectNode);
return result;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.aws.traits.auth;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import software.amazon.smithy.aws.traits.ServiceTrait;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.ServiceShape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.validation.AbstractValidator;
import software.amazon.smithy.model.validation.ValidationEvent;
import software.amazon.smithy.utils.SmithyInternalApi;

/**
* Validates AWS Service, SigV4, and SigV4A traits.
*/
@SmithyInternalApi
public final class SigV4TraitsValidator extends AbstractValidator {
private static final ShapeId SERVICE_ARN_NAMESPACE = ServiceTrait.ID.withMember("arnNamespace");
private static final ShapeId SIGV4_NAME = SigV4Trait.ID.withMember("name");
private static final ShapeId SIGV4A_NAME = SigV4ATrait.ID.withMember("name");

@Override
public List<ValidationEvent> validate(Model model) {
List<ValidationEvent> events = new ArrayList<>();
for (ServiceShape service : model.getServiceShapes()) {
events.addAll(validateService(model, service));
}
return events;
}

/**
* Validates Service and SigV4 traits.
*
* - service$arnNamespace, sigv4$name, and sigv4a$name SHOULD be equal. Otherwise, emits warnings.
*/
private List<ValidationEvent> validateService(Model model, ServiceShape service) {
List<ValidationEvent> events = new ArrayList<>();
Optional<ServiceTrait> serviceTraitOptional = service.getTrait(ServiceTrait.class);
Optional<SigV4Trait> sigv4TraitOptional = service.getTrait(SigV4Trait.class);
Optional<SigV4ATrait> sigv4aTraitOptional = service.getTrait(SigV4ATrait.class);
if (serviceTraitOptional.isPresent()) {
String serviceArnNamespace = serviceTraitOptional.get().getArnNamespace();
// Check service$arnNamespace with sigv4$name
if (sigv4TraitOptional.isPresent()) {
String sigv4Name = sigv4TraitOptional.get().getName();
if (!serviceArnNamespace.equals(sigv4Name)) {
events.add(createValuesShouldMatchWarning(
service,
SERVICE_ARN_NAMESPACE, serviceArnNamespace,
SIGV4_NAME, sigv4Name));
}
}
// Check service$arnNamespace with sigv4a$name
if (sigv4aTraitOptional.isPresent()) {
String sigv4aName = sigv4aTraitOptional.get().getName();
if (!serviceArnNamespace.equals(sigv4aName)) {
events.add(createValuesShouldMatchWarning(
service,
SERVICE_ARN_NAMESPACE, serviceArnNamespace,
SIGV4A_NAME, sigv4aName));
}
}
}
// Check sigv4$name with sigv4a$name
if (sigv4TraitOptional.isPresent() && sigv4aTraitOptional.isPresent()) {
String sigv4Name = sigv4TraitOptional.get().getName();
String sigv4aName = sigv4aTraitOptional.get().getName();
if (!sigv4Name.equals(sigv4aName)) {
events.add(createValuesShouldMatchWarning(
service,
SIGV4_NAME, sigv4Name,
SIGV4A_NAME, sigv4aName));
}
}
return events;
}

private ValidationEvent createValuesShouldMatchWarning(
ServiceShape service,
ShapeId member1,
String value1,
ShapeId member2,
String value2
) {
return warning(service, String.format(
"Value for `%s` \"%s\" and value for `%s` \"%s\" SHOULD match.",
member1.toString(), value1, member2.toString(), value2));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ software.amazon.smithy.aws.traits.protocols.RestXmlTrait$Provider
software.amazon.smithy.aws.traits.protocols.AwsJson1_0Trait$Provider
software.amazon.smithy.aws.traits.protocols.AwsJson1_1Trait$Provider
software.amazon.smithy.aws.traits.auth.SigV4Trait$Provider
software.amazon.smithy.aws.traits.auth.SigV4ATrait$Provider
software.amazon.smithy.aws.traits.tagging.TagEnabledTrait$Provider
software.amazon.smithy.aws.traits.tagging.TaggableTrait$Provider
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ software.amazon.smithy.aws.traits.tagging.TaggableResourceValidator
software.amazon.smithy.aws.traits.tagging.TagResourcePropertyTypeValidator
software.amazon.smithy.aws.traits.tagging.TagResourcePropertyNameValidator
software.amazon.smithy.aws.traits.ErrorRenameValidator
software.amazon.smithy.aws.traits.auth.SigV4TraitsValidator
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,40 @@ structure cognitoUserPools {
/// security, most requests to AWS must be signed with an access key, which consists
/// of an access key ID and secret access key. These two keys are commonly referred to
/// as your security credentials.
@authDefinition(traits: [unsignedPayload])
@externalDocumentation(
Reference: "https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html"
@authDefinition(
traits: [unsignedPayload]
)
@externalDocumentation(Reference: "https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html")
@trait(selector: "service")
structure sigv4 {
/// The signature version 4 service signing name to use in the credential
/// scope when signing requests. This value SHOULD match the `arnNamespace`
/// property of the `aws.api#service-trait`.
@externalDocumentation(
Reference: "https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html"
)
/// property of the `aws.api#service` trait if present and the `name`
/// property of the `aws.api#sigv4a` trait if present.
@externalDocumentation(Reference: "https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html")
@length(min: 1)
@required
name: String
}

/// Signature Version 4 Asymmetric (SigV4A), an extension of Signature Version 4 (SigV4), is the
/// process to add authentication information to AWS requests sent by HTTP. SigV4A is nearly
/// identical to SigV4, but also uses public-private keys and asymmetric cryptographic signatures
/// for every request. Most notably, SigV4A supports signatures for multi-region API requests.
@authDefinition(
traits: [unsignedPayload]
)
@externalDocumentation(
Reference: "https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html"
Examples: "https://github.com/aws-samples/sigv4a-signing-examples"
)
@trait(selector: "service[trait|aws.auth#sigv4]")
structure sigv4a {
/// The signature version 4a service signing name to use in the credential
/// scope when signing requests. This value SHOULD match the `arnNamespace`
/// property of the `aws.api#service` trait if present and the `name`
/// property of the `aws.api#sigv4` trait.
@externalDocumentation(Reference: "https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html")
@length(min: 1)
@required
name: String
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package software.amazon.smithy.aws.traits.auth;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.Optional;
import org.junit.jupiter.api.Test;
import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.node.ObjectNode;
import software.amazon.smithy.model.node.StringNode;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.traits.Trait;
import software.amazon.smithy.model.traits.TraitFactory;

public class SigV4ATraitTest {
private static final String MOCK_SIGNING_NAME = "mocksigningname";
private static final ShapeId MOCK_TARGET = ShapeId.from("ns.qux#foo");

@Test
public void loadsTrait() {
Node node = ObjectNode.builder()
.withMember("name", StringNode.from(MOCK_SIGNING_NAME))
.build();
TraitFactory provider = TraitFactory.createServiceFactory();
Optional<Trait> trait = provider.createTrait(SigV4ATrait.ID, MOCK_TARGET, node);

assertTrue(trait.isPresent());
assertThat(trait.get(), instanceOf(SigV4ATrait.class));
SigV4ATrait sigv4aTrait = (SigV4ATrait) trait.get();
assertFalse(sigv4aTrait.getName().isEmpty());
assertThat(sigv4aTrait.getName(), equalTo(MOCK_SIGNING_NAME));
assertThat(sigv4aTrait.toNode(), equalTo(node));
assertThat(sigv4aTrait.toBuilder().build(), equalTo(sigv4aTrait));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +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 number Diff line number Diff line change
@@ -0,0 +1,12 @@
$version: "2.0"

namespace smithy.example

use aws.api#service
use aws.auth#sigv4

@service(sdkId: "servicename")
@sigv4(name: "signingname")
service InvalidService {
version: "2020-07-02"
}
Original file line number Diff line number Diff line change
@@ -0,0 +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 number Diff line number Diff line change
@@ -0,0 +1,13 @@
$version: "2.0"

namespace smithy.example

use aws.auth#sigv4
use aws.auth#sigv4a

@auth([sigv4a, sigv4])
@sigv4(name: "sigv4signingname")
@sigv4a(name: "sigv4asigningname")
service InvalidService {
version: "2020-07-02"
}
Loading

0 comments on commit c34f984

Please sign in to comment.