Repository: smithy-lang/smithy
Pull request #1988 (2023-09-22 18:48:54)

Give range and default validation a specific ID
When the default value for a shape is incompatible with the range trait, it will now include an `InvalidRange` suffix on the event ID.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.



Patch:
diff --git a/.github/workflows/update-brew.yml b/.github/workflows/update-brew.yml
index 51542ef78..7f9f78cf9 100644
--- a/.github/workflows/update-brew.yml
+++ b/.github/workflows/update-brew.yml
@@ -63,6 +63,7 @@ jobs:
           repository: 'smithy-lang/homebrew-tap'
           path: 'homebrew-tap'
           ref: 'main'
+          token: ${{ secrets.PR_TOKEN }}
       - name: Update version
         run: |
           version="${GITHUB_REF##*/}"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6924c0b86..de7e10f46 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,13 @@
 # Smithy Changelog
 
-## 1.39.0 (2023-09-20)
+## 1.39.1 (2023-09-26)
+
+### Bug Fixes
+
+* Fix several issues with validating `authSchemes` configurations for endpoints in the
+  `smithy-rules-engine` package for both AWS and non-AWS issues. ([#1990](https://github.com/smithy-lang/smithy/pull/1990))
+
+## 1.39.0 (2023-09-25)
 
 ### Features
 
diff --git a/VERSION b/VERSION
index 5edffce6d..0c11aad2f 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-1.39.0
+1.39.1
diff --git a/docs/Makefile b/docs/Makefile
index 39e53b7b0..b0a870ca9 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -16,7 +16,7 @@ html1:
 html2:
 	@$(SPHINXBUILD) -M html "source-2.0" "build/2.0" $(SPHINXOPTS) -W --keep-going -n $(O)
 
-html: html1 html2 merge-versions
+html: html2 html1 merge-versions
 
 merge-versions:
 	mkdir -p build/html
diff --git a/docs/source-2.0/additional-specs/rules-engine/specification.rst b/docs/source-2.0/additional-specs/rules-engine/specification.rst
index 7b7ed5dd2..148170f66 100644
--- a/docs/source-2.0/additional-specs/rules-engine/specification.rst
+++ b/docs/source-2.0/additional-specs/rules-engine/specification.rst
@@ -290,7 +290,7 @@ of rule conditions to that point.
 An endpoint MAY return a set of endpoint properties using the ``properties``
 field. This can be used to provide a grab-bag set of metadata associated with
 an endpoint that an endpoint resolver implementation MAY use. For example, the
-``authSchemes`` property is used to specify the priority listed order of
+``authSchemes`` property is used to specify the priority ordered list of
 authentication schemes and their configuration supported by the endpoint.
 Properties MAY contain arbitrary nested maps and arrays of strings and
 booleans.
@@ -299,6 +299,40 @@ booleans.
     To prevent ambiguity, the endpoint properties map MUST NOT contain
     reference or function objects. Properties MAY contain :ref:`template string <rules-engine-endpoint-rule-set-template-string>`
 
+.. _rules-engine-endpoint-rule-set-endpoint-authschemes:
+
+Endpoint ``authSchemes`` list property
+--------------------------------------
+
+The ``authSchemes`` property of an endpoint is used to specify the priority
+ordered list of authentication schemes and their configuration supported by the
+endpoint. The property is a list of configuration objects that MUST contain at
+least a ``name`` property and MAY contain additional properties. Each
+configuration object MUST have a unique value for its ``name`` property within
+the list of configuration objects within a given ``authSchemes`` property.
+
+If an ``authSchemes`` property is present on an `Endpoint object`_, clients
+MUST resolve an authentication scheme to use via the following process:
+
+#. Iterate through configuration objects in the ``authSchemes`` property.
+#. If the ``name`` property in a configuration object contains a supported
+   authentication scheme, resolve this scheme.
+#. If the ``name`` is unknown or unsupported, ignore it and continue iterating.
+#. If the list has been fully iterated and no scheme has been resolved, clients
+   MUST return an error.
+
+.. _rules-engine-standard-library-adding-authscheme-validators:
+
+Adding ``authSchemes`` configuration validators
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Extensions to the rules engine can provide additional validators for
+``authSchemes`` configuration objects. No validators are provided by default.
+
+The rules engine is highly extensible through
+``software.amazon.smithy.rulesengine.language.EndpointRuleSetExtension``
+`service providers`_. See the `Javadocs`_ for more information.
+
 
 .. _rules-engine-endpoint-rule-set-error-rule:
 
@@ -686,3 +720,6 @@ The following two expressions are equivalent:
             "{partResult#name}"
         ]
     }
+
+.. _Javadocs: https://smithy.io/javadoc/__smithy_version__/software/amazon/smithy/rulesengine/language/EndpointRuleSetExtension.html
+.. _service providers: https://docs.oracle.com/javase/tutorial/sound/SPI-intro.html
diff --git a/docs/source-2.0/aws/rules-engine/auth-schemes.rst b/docs/source-2.0/aws/rules-engine/auth-schemes.rst
new file mode 100644
index 000000000..2ca4e2270
--- /dev/null
+++ b/docs/source-2.0/aws/rules-engine/auth-schemes.rst
@@ -0,0 +1,105 @@
+.. _rules-engine-aws-authscheme-validators:
+
+=================================================
+AWS rules engine authentication scheme validators
+=================================================
+
+AWS-specific rules engine library :ref:`authentication scheme validators <rules-engine-endpoint-rule-set-endpoint-authschemes>`
+make it possible to validate configurations for AWS authentication schemes like
+`AWS signature version 4`_. An additional dependency is required to access
+these validators
+
+The following example adds ``smithy-aws-endpoints`` as a Gradle dependency
+to a Smithy project:
+
+.. tab:: Gradle
+
+    .. code-block:: kotlin
+
+        dependencies {
+            ...
+            implementation("software.amazon.smithy:smithy-aws-endpoints:__smithy_version__")
+            ...
+        }
+
+.. tab:: smithy-build.json
+
+    .. code-block:: json
+
+        {
+            "maven": {
+                "dependencies": [
+                    "software.amazon.smithy:smithy-aws-endpoints:__smithy_version__"
+                ]
+            }
+        }
+
+.. _rules-engine-aws-authscheme-validator-sigv4:
+
+-----------------------------------------
+``sigv4`` authentication scheme validator
+-----------------------------------------
+
+Requirement
+    The ``name`` property is the string value ``sigv4``.
+Properties
+    .. list-table::
+        :header-rows: 1
+        :widths: 10 20 70
+
+        * - Property
+          - Type
+          - Description
+        * - ``signingName``
+          - ``option<string>``
+          - The "service" value to use when creating a signing string for this
+            endpoint.
+        * - ``signingRegion``
+          - ``option<string>``
+          - The "region" value to use when creating a signing string for this
+            endpoint.
+        * - ``disableDoubleEncoding``
+          - ``option<boolean>``
+          - Default: ``false``. When ``true``, clients MUST NOT double-escape
+            the path during signing.
+        * - ``disableNormalizePath``
+          - ``option<boolean>``
+          - Default: ``false``. When ``true``, clients MUST NOT perform any
+            path normalization during signing.
+
+
+.. _rules-engine-aws-authscheme-validator-sigv4a:
+
+------------------------------------------
+``sigv4a`` authentication scheme validator
+------------------------------------------
+
+Requirement
+    The ``name`` property is the string value ``sigv4a``.
+Properties
+    .. list-table::
+        :header-rows: 1
+        :widths: 10 20 70
+
+        * - Property
+          - Type
+          - Description
+        * - ``signingName``
+          - ``option<string>``
+          - The "service" value to use when creating a signing string for this
+            endpoint.
+        * - ``signingRegionSet``
+          - ``array<string>``
+          - The set of signing regions to use when creating a signing string
+            for this endpoint.
+        * - ``disableDoubleEncoding``
+          - ``option<boolean>``
+          - Default: ``false``. When ``true``, clients MUST NOT double-escape
+            the path during signing.
+        * - ``disableNormalizePath``
+          - ``option<boolean>``
+          - Default: ``false``. When ``true``, clients MUST NOT perform any
+            path normalization during signing.
+
+
+.. _AWS signature version 4: https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
diff --git a/docs/source-2.0/aws/rules-engine/index.rst b/docs/source-2.0/aws/rules-engine/index.rst
index d02a75319..882597326 100644
--- a/docs/source-2.0/aws/rules-engine/index.rst
+++ b/docs/source-2.0/aws/rules-engine/index.rst
@@ -14,3 +14,4 @@ configuration options.
 
     built-ins
     library-functions
+    auth-schemes
diff --git a/smithy-aws-endpoints/src/main/java/software/amazon/smithy/rulesengine/aws/language/functions/EndpointAuthUtils.java b/smithy-aws-endpoints/src/main/java/software/amazon/smithy/rulesengine/aws/language/functions/EndpointAuthUtils.java
index 6fccdfb0d..a1d62e09f 100644
--- a/smithy-aws-endpoints/src/main/java/software/amazon/smithy/rulesengine/aws/language/functions/EndpointAuthUtils.java
+++ b/smithy-aws-endpoints/src/main/java/software/amazon/smithy/rulesengine/aws/language/functions/EndpointAuthUtils.java
@@ -12,7 +12,6 @@ import java.util.Optional;
 import java.util.function.BiFunction;
 import java.util.function.Function;
 import software.amazon.smithy.model.FromSourceLocation;
-import software.amazon.smithy.model.SourceLocation;
 import software.amazon.smithy.model.validation.ValidationEvent;
 import software.amazon.smithy.rulesengine.language.Endpoint;
 import software.amazon.smithy.rulesengine.language.syntax.Identifier;
@@ -32,8 +31,11 @@ public final class EndpointAuthUtils {
     private static final String SIGNING_REGION = "signingRegion";
     private static final String SIGNING_REGION_SET = "signingRegionSet";
 
-    private static final Identifier DISABLE_DOUBLE_ENCODING = Identifier.of("disableDoubleEncoding");
-    private static final Identifier DISABLE_NORMALIZE_PATH = Identifier.of("disableNormalizePath");
+    private static final Identifier ID_SIGNING_NAME = Identifier.of("signingName");
+    private static final Identifier ID_SIGNING_REGION = Identifier.of("signingRegion");
+    private static final Identifier ID_SIGNING_REGION_SET = Identifier.of("signingRegionSet");
+    private static final Identifier ID_DISABLE_DOUBLE_ENCODING = Identifier.of("disableDoubleEncoding");
+    private static final Identifier ID_DISABLE_NORMALIZE_PATH = Identifier.of("disableNormalizePath");
 
     private EndpointAuthUtils() {}
 
@@ -80,18 +82,40 @@ public final class EndpointAuthUtils {
         @Override
         public List<ValidationEvent> validateScheme(
                 Map<Identifier, Literal> authScheme,
-                SourceLocation sourceLocation,
+                FromSourceLocation sourceLocation,
                 BiFunction<FromSourceLocation, String, ValidationEvent> emitter
         ) {
-            Identifier signingRegion = Identifier.of(SIGNING_REGION);
-            Identifier signingName = Identifier.of(SIGNING_NAME);
             List<ValidationEvent> events = noExtraProperties(emitter, sourceLocation, authScheme,
                     ListUtils.of(RuleSetAuthSchemesValidator.NAME,
-                            signingName, signingRegion, DISABLE_DOUBLE_ENCODING, DISABLE_NORMALIZE_PATH));
-            validatePropertyType(emitter, authScheme,
-                    signingName, Literal::asStringLiteral).ifPresent(events::add);
-            validatePropertyType(emitter, authScheme,
-                    signingRegion, Literal::asStringLiteral).ifPresent(events::add);
+                            ID_SIGNING_NAME,
+                            ID_SIGNING_REGION,
+                            ID_DISABLE_DOUBLE_ENCODING,
+                            ID_DISABLE_NORMALIZE_PATH));
+
+            // Validate shared Sigv4 properties.
+            events.addAll(SigV4SchemeValidator.validateOptionalSharedProperties(authScheme, emitter));
+            // Signing region is also optional.
+            if (authScheme.containsKey(ID_SIGNING_REGION)) {
+                validateStringProperty(emitter, authScheme, ID_SIGNING_REGION).ifPresent(events::add);
+            }
+            return events;
+        }
+
+        private static List<ValidationEvent> validateOptionalSharedProperties(
+                Map<Identifier, Literal> authScheme,
+                BiFunction<FromSourceLocation, String, ValidationEvent> emitter
+        ) {
+            List<ValidationEvent> events = new ArrayList<>();
+            // The following properties are only type checked if present.
+            if (authScheme.containsKey(ID_SIGNING_NAME)) {
+                validateStringProperty(emitter, authScheme, ID_SIGNING_NAME).ifPresent(events::add);
+            }
+            if (authScheme.containsKey(ID_DISABLE_DOUBLE_ENCODING)) {
+                validateBooleanProperty(emitter, authScheme, ID_DISABLE_DOUBLE_ENCODING).ifPresent(events::add);
+            }
+            if (authScheme.containsKey(ID_DISABLE_NORMALIZE_PATH)) {
+                validateBooleanProperty(emitter, authScheme, ID_DISABLE_NORMALIZE_PATH).ifPresent(events::add);
+            }
             return events;
         }
     }
@@ -107,18 +131,38 @@ public final class EndpointAuthUtils {
         @Override
         public List<ValidationEvent> validateScheme(
                 Map<Identifier, Literal> authScheme,
-                SourceLocation sourceLocation,
+                FromSourceLocation sourceLocation,
                 BiFunction<FromSourceLocation, String, ValidationEvent> emitter
         ) {
-            Identifier signingRegionSet = Identifier.of(SIGNING_REGION_SET);
-            Identifier signingName = Identifier.of(SIGNING_NAME);
             List<ValidationEvent> events = noExtraProperties(emitter, sourceLocation, authScheme,
                     ListUtils.of(RuleSetAuthSchemesValidator.NAME,
-                            signingName, signingRegionSet, DISABLE_DOUBLE_ENCODING, DISABLE_NORMALIZE_PATH));
-            validatePropertyType(emitter, authScheme,
-                    signingName, Literal::asStringLiteral).ifPresent(events::add);
-            validatePropertyType(emitter, authScheme,
-                    signingRegionSet, Literal::asTupleLiteral).ifPresent(events::add);
+                            ID_SIGNING_NAME,
+                            ID_SIGNING_REGION_SET,
+                            ID_DISABLE_DOUBLE_ENCODING,
+                            ID_DISABLE_NORMALIZE_PATH));
+
+            // The `signingRegionSet` property will always be present.
+            Optional<ValidationEvent> event = validatePropertyType(emitter, authScheme.get(ID_SIGNING_REGION_SET),
+                    ID_SIGNING_REGION_SET, Literal::asTupleLiteral, "an array<string>");
+            // If we don't have a tuple, that's our main error.
+            // Otherwise, validate each entry is a string.
+            if (event.isPresent()) {
+                events.add(event.get());
+            } else {
+                List<Literal> signingRegionSet = authScheme.get(ID_SIGNING_REGION_SET).asTupleLiteral().get();
+                if (signingRegionSet.isEmpty()) {
+                    events.add(emitter.apply(authScheme.get(ID_SIGNING_REGION_SET),
+                            "The `signingRegionSet` property must not be an empty list."));
+                } else {
+                    for (Literal signingRegion : signingRegionSet) {
+                        validatePropertyType(emitter, signingRegion, Identifier.of("signingRegionSet.Value"),
+                                Literal::asStringLiteral, "a string").ifPresent(events::add);
+                    }
+                }
+            }
+
+            // Validate shared Sigv4 properties.
+            events.addAll(SigV4SchemeValidator.validateOptionalSharedProperties(authScheme, emitter));
             return events;
         }
     }
@@ -134,13 +178,12 @@ public final class EndpointAuthUtils {
         @Override
         public List<ValidationEvent> validateScheme(
                 Map<Identifier, Literal> authScheme,
-                SourceLocation sourceLocation,
+                FromSourceLocation sourceLocation,
                 BiFunction<FromSourceLocation, String, ValidationEvent> emitter
         ) {
-            Identifier signingName = Identifier.of(SIGNING_NAME);
             List<ValidationEvent> events = hasAllKeys(emitter, authScheme,
-                    ListUtils.of(RuleSetAuthSchemesValidator.NAME, signingName), sourceLocation);
-            validatePropertyType(emitter, authScheme, signingName, Literal::asStringLiteral).ifPresent(events::add);
+                    ListUtils.of(RuleSetAuthSchemesValidator.NAME, ID_SIGNING_NAME), sourceLocation);
+            validateStringProperty(emitter, authScheme, ID_SIGNING_NAME).ifPresent(events::add);
             return events;
         }
 
@@ -148,7 +191,7 @@ public final class EndpointAuthUtils {
                 BiFunction<FromSourceLocation, String, ValidationEvent> emitter,
                 Map<Identifier, Literal> authScheme,
                 List<Identifier> requiredKeys,
-                SourceLocation sourceLocation
+                FromSourceLocation sourceLocation
         ) {
             List<ValidationEvent> events = new ArrayList<>();
             for (Identifier key : requiredKeys) {
@@ -162,7 +205,7 @@ public final class EndpointAuthUtils {
 
     private static List<ValidationEvent> noExtraProperties(
             BiFunction<FromSourceLocation, String, ValidationEvent> emitter,
-            SourceLocation sourceLocation,
+            FromSourceLocation sourceLocation,
             Map<Identifier, Literal> properties,
             List<Identifier> allowedProperties
     ) {
@@ -176,21 +219,41 @@ public final class EndpointAuthUtils {
         return events;
     }
 
-    private static <U> Optional<ValidationEvent> validatePropertyType(
+    private static Optional<ValidationEvent> validateBooleanProperty(
             BiFunction<FromSourceLocation, String, ValidationEvent> emitter,
             Map<Identifier, Literal> properties,
+            Identifier propertyName
+    ) {
+        return validatePropertyType(emitter, properties.get(propertyName), propertyName,
+                Literal::asBooleanLiteral, "a boolean");
+    }
+
+    private static Optional<ValidationEvent> validateStringProperty(
+            BiFunction<FromSourceLocation, String, ValidationEvent> emitter,
+            Map<Identifier, Literal> properties,
+            Identifier propertyName
+    ) {
+        return validatePropertyType(emitter, properties.get(propertyName), propertyName,
+                Literal::asStringLiteral, "a string");
+    }
+
+    private static <U> Optional<ValidationEvent> validatePropertyType(
+            BiFunction<FromSourceLocation, String, ValidationEvent> emitter,
+            Literal value,
             Identifier propertyName,
-            Function<Literal, Optional<U>> validator
+            Function<Literal, Optional<U>> validator,
+            String expectedType
     ) {
-        Literal value = properties.get(propertyName);
         if (value == null) {
             return Optional.of(emitter.apply(propertyName,
-                    String.format("Expected auth property `%s` but didn't find one", propertyName)));
+                    String.format("Expected auth property `%s` of %s type but didn't find one",
+                            propertyName, expectedType)));
         }
 
         if (!validator.apply(value).isPresent()) {
             return Optional.of(emitter.apply(value,
-                    String.format("Unexpected type for auth property `%s`, found: `%s`", propertyName, value)));
+                    String.format("Unexpected type for auth property `%s`, found `%s` but expected %s value",
+                            propertyName, value, expectedType)));
         }
         return Optional.empty();
     }
diff --git a/smithy-cli/src/it/resources/software/amazon/smithy/cli/projects/clashing-lockfile/smithy-build.json b/smithy-cli/src/it/resources/software/amazon/smithy/cli/projects/clashing-lockfile/smithy-build.json
new file mode 100644
index 000000000..b70b9f2ae
--- /dev/null
+++ b/smithy-cli/src/it/resources/software/amazon/smithy/cli/projects/clashing-lockfile/smithy-build.json
@@ -0,0 +1,15 @@
+{
+    "version": "1.0",
+    "maven": {
+        "repositories": [
+            {
+                // Use HTTP instead of HTTPS because we're running a mock server during tests
+                "id": "Artifactory 1",
+                "url": "http://localhost:5678/artifactory-1"
+            }
+        ],
+        "dependencies": [
+            "com.example:artifact:[1.0,)"
+        ]
+    }
+}
diff --git a/smithy-cli/src/it/resources/software/amazon/smithy/cli/projects/clashing-lockfile/smithy-lock.json b/smithy-cli/src/it/resources/software/amazon/smithy/cli/projects/clashing-lockfile/smithy-lock.json
new file mode 100644
index 000000000..4a84728eb
--- /dev/null
+++ b/smithy-cli/src/it/resources/software/amazon/smithy/cli/projects/clashing-lockfile/smithy-lock.json
@@ -0,0 +1,1 @@
+{"AUTOGENERATED_DO_NOT_MODIFY":"","version":"1.0","configHash":1317746633,"repositories":["http://localhost:5678/artifactory-1","http://localhost:5678/artifactory-2"],"artifacts":{"com.example:artifact:1.1.0":{"sha1":"23d5b855da1e9fb13e41db21296815d38cb31c08"}}}
diff --git a/smithy-cli/src/it/resources/software/amazon/smithy/cli/projects/lockfile-used/smithy-build.json b/smithy-cli/src/it/resources/software/amazon/smithy/cli/projects/lockfile-used/smithy-build.json
new file mode 100644
index 000000000..7d6e52f1a
--- /dev/null
+++ b/smithy-cli/src/it/resources/software/amazon/smithy/cli/projects/lockfile-used/smithy-build.json
@@ -0,0 +1,20 @@
+{
+    "version": "1.0",
+    "maven": {
+        "repositories": [
+            {
+                // Use HTTP instead of HTTPS because we're running a mock server during tests
+                "id": "Artifactory 1",
+                "url": "http://localhost:5678/artifactory-1"
+            },
+            {
+                // Use HTTP instead of HTTPS because we're running a mock server during tests
+                "id": "Artifactory 2",
+                "url": "http://localhost:5678/artifactory-2"
+            }
+        ],
+        "dependencies": [
+            "com.example:artifact:[1.0,)"
+        ]
+    }
+}
diff --git a/smithy-cli/src/it/resources/software/amazon/smithy/cli/projects/lockfile-used/smithy-lock.json b/smithy-cli/src/it/resources/software/amazon/smithy/cli/projects/lockfile-used/smithy-lock.json
new file mode 100644
index 000000000..7401f7398
--- /dev/null
+++ b/smithy-cli/src/it/resources/software/amazon/smithy/cli/projects/lockfile-used/smithy-lock.json
@@ -0,0 +1,1 @@
+{"AUTOGENERATED_DO_NOT_MODIFY":"","version":"1.0","configHash":-1211145608,"repositories":["http://localhost:5678/artifactory-1","http://localhost:5678/artifactory-2"],"artifacts":{"com.example:artifact:1.1.0":{"sha1":"23d5b855da1e9fb13e41db21296815d38cb31c08"}}}
diff --git a/smithy-cli/src/it/resources/software/amazon/smithy/cli/projects/lockfile-written/smithy-build.json b/smithy-cli/src/it/resources/software/amazon/smithy/cli/projects/lockfile-written/smithy-build.json
new file mode 100644
index 000000000..e54f61666
--- /dev/null
+++ b/smithy-cli/src/it/resources/software/amazon/smithy/cli/projects/lockfile-written/smithy-build.json
@@ -0,0 +1,20 @@
+{
+    "version": "1.0",
+    "maven": {
+        "repositories": [
+            {
+                // Use HTTP instead of HTTPS because we're running a mock server during tests
+                "id": "Artifactory 1",
+                "url": "http://localhost:6789/artifactory-1"
+            },
+            {
+                // Use HTTP instead of HTTPS because we're running a mock server during tests
+                "id": "Artifactory 2",
+                "url": "http://localhost:6789/artifactory-2"
+            }
+        ],
+        "dependencies": [
+            "com.example:artifact:[1.0,)"
+        ]
+    }
+}
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/BuildCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/BuildCommand.java
index dc7ed6d3f..25508cdfd 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/BuildCommand.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/BuildCommand.java
@@ -86,11 +86,6 @@ final class BuildCommand implements Command {
         private String projection;
         private String plugin;
 
-        @Override
-        public boolean testOption(String name) {
-            return false;
-        }
-
         @Override
         public Consumer<String> testParameter(String name) {
             switch (name) {
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ClasspathAction.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ClasspathAction.java
index 75bf97a0f..9fd274c83 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ClasspathAction.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ClasspathAction.java
@@ -20,6 +20,7 @@ import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import java.util.function.Consumer;
 import java.util.logging.Logger;
@@ -41,11 +42,7 @@ import software.amazon.smithy.cli.dependencies.ResolvedArtifact;
  * A {@code CommandAction} that runs a wrapped action within a custom classpath.
  */
 class ClasspathAction implements CommandAction {
-
     private static final Logger LOGGER = Logger.getLogger(ClasspathAction.class.getName());
-    private static final MavenRepository CENTRAL = MavenRepository.builder()
-            .url("https://repo.maven.apache.org/maven2")
-            .build();
     private final DependencyResolver.Factory dependencyResolverFactory;
     private final CommandActionWithConfig action;
 
@@ -133,13 +130,33 @@ class ClasspathAction implements CommandAction {
         long lastModified = smithyBuildConfig.getLastModifiedInMillis();
         DependencyResolver delegate = new FilterCliVersionResolver(SmithyCli.getVersion(), baseResolver);
         DependencyResolver resolver = new FileCacheResolver(getCacheFile(buildOptions, smithyBuildConfig),
-                                                            lastModified,
-                                                            delegate);
-        addConfiguredMavenRepos(smithyBuildConfig, resolver);
-        maven.getDependencies().forEach(resolver::addDependency);
+                lastModified,
+                delegate);
+
+        Set<MavenRepository> repositories = ConfigurationUtils.getConfiguredMavenRepos(smithyBuildConfig);
+        repositories.forEach(resolver::addRepository);
+
+        // Use the pinned lockfile dependencies if a lockfile exists otherwise use the configured dependencies
+        Optional<LockFile> lockFileOptional = LockFile.load();
+        if (lockFileOptional.isPresent()) {
+            LockFile lockFile = lockFileOptional.get();
+            if (lockFile.getConfigHash() != ConfigurationUtils.configHash(maven.getDependencies(), repositories)) {
+                throw new CliError(
+                        "`smithy-lock.json` does not match configured dependencies. "
+                                + "Re-lock dependencies using the `lock` command or revert changes.");
+            }
+            LOGGER.fine(() -> "`smithy-lock.json` found. Using locked dependencies: "
+                    + lockFile.getDependencyCoordinateSet());
+            lockFile.getDependencyCoordinateSet().forEach(resolver::addDependency);
+        } else {
+            maven.getDependencies().forEach(resolver::addDependency);
+        }
+
         List<ResolvedArtifact> artifacts = resolver.resolve();
         LOGGER.fine(() -> "Classpath resolved with Maven: " + artifacts);
 
+        // Ensure resolved artifacts match pinned artifacts
+        lockFileOptional.ifPresent(lockFile -> lockFile.validateArtifacts(artifacts));
         List<Path> result = new ArrayList<>(artifacts.size());
         for (ResolvedArtifact artifact : artifacts) {
             result.add(artifact.getPath());
@@ -148,29 +165,6 @@ class ClasspathAction implements CommandAction {
         return result;
     }
 
-    private static void addConfiguredMavenRepos(SmithyBuildConfig config, DependencyResolver resolver) {
-        // Environment variables take precedence over config files.
-        String envRepos = EnvironmentVariable.SMITHY_MAVEN_REPOS.get();
-        if (envRepos != null) {
-            for (String repo : envRepos.split("\\|")) {
-                resolver.addRepository(MavenRepository.builder().url(repo.trim()).build());
-            }
-        }
-
-        Set<MavenRepository> configuredRepos = config.getMaven()
-                .map(MavenConfig::getRepositories)
-                .orElse(Collections.emptySet());
-
-        if (!configuredRepos.isEmpty()) {
-            configuredRepos.forEach(resolver::addRepository);
-        } else if (envRepos == null) {
-            LOGGER.finest(() -> String.format("maven.repositories is not defined in smithy-build.json and the %s "
-                                              + "environment variable is not set. Defaulting to Maven Central.",
-                                              EnvironmentVariable.SMITHY_MAVEN_REPOS));
-            resolver.addRepository(CENTRAL);
-        }
-    }
-
     private File getCacheFile(BuildOptions buildOptions, SmithyBuildConfig config) {
         return buildOptions.resolveOutput(config).resolve("classpath.json").toFile();
     }
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ConfigurationUtils.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ConfigurationUtils.java
new file mode 100644
index 000000000..9df1979ed
--- /dev/null
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/ConfigurationUtils.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.cli.commands;
+
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.logging.Logger;
+import software.amazon.smithy.build.model.MavenConfig;
+import software.amazon.smithy.build.model.MavenRepository;
+import software.amazon.smithy.build.model.SmithyBuildConfig;
+import software.amazon.smithy.cli.EnvironmentVariable;
+
+final class ConfigurationUtils {
+    private static final int FNV_OFFSET_BIAS = 0x811c9dc5;
+    private static final int FNV_PRIME = 0x1000193;
+    private static final Logger LOGGER = Logger.getLogger(ConfigurationUtils.class.getName());
+    private static final MavenRepository CENTRAL = MavenRepository.builder()
+            .url("https://repo.maven.apache.org/maven2")
+            .build();
+
+    private ConfigurationUtils() {
+        // Utility Class should not be instantiated
+    }
+
+    /**
+     * Gets the list of Maven Repos configured via smithy-build config and environment variables.
+     * <p>
+     * Environment variables take precedence over config file entries. If no repositories are
+     * defined in either environment variables or config file entries, then the returned set
+     * defaults to a singleton set of just the Maven Central repo.
+     *
+     * @param config Smithy build config to check for maven repository values.
+     */
+    public static Set<MavenRepository> getConfiguredMavenRepos(SmithyBuildConfig config) {
+        Set<MavenRepository> repositories = new LinkedHashSet<>();
+
+        String envRepos = EnvironmentVariable.SMITHY_MAVEN_REPOS.get();
+        if (envRepos != null) {
+            for (String repo : envRepos.split("\\|")) {
+                repositories.add(MavenRepository.builder().url(repo.trim()).build());
+            }
+        }
+
+        Set<MavenRepository> configuredRepos = config.getMaven()
+                .map(MavenConfig::getRepositories)
+                .orElse(Collections.emptySet());
+
+        if (!configuredRepos.isEmpty()) {
+            repositories.addAll(configuredRepos);
+        } else if (envRepos == null) {
+            LOGGER.finest(() -> String.format("maven.repositories is not defined in `smithy-build.json` and the %s "
+                            + "environment variable is not set. Defaulting to Maven Central.",
+                    EnvironmentVariable.SMITHY_MAVEN_REPOS));
+            repositories.add(CENTRAL);
+        }
+        return repositories;
+    }
+
+    /**
+     * Computes the combined hash of the artifact set and repositories set.
+     *
+     * @param artifacts set of requested artifact coordinates
+     * @param repositories set of repositories used for dependency resolution
+     * @return combined hash
+     */
+    public static int configHash(Set<String> artifacts, Set<MavenRepository> repositories) {
+        int result = 0;
+        for (String artifact : artifacts) {
+            result = 31 * result + getFnvHash(artifact);
+        }
+        for (MavenRepository repo : repositories) {
+            result = 31 * result + getFnvHash(repo.getUrl());
+        }
+        return result;
+    }
+
+    private static int getFnvHash(CharSequence text) {
+        int hashCode = FNV_OFFSET_BIAS;
+        for (int i = 0; i < text.length(); i++) {
+            hashCode = (hashCode ^ text.charAt(i)) * FNV_PRIME;
+        }
+        return hashCode;
+    }
+}
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/LockCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/LockCommand.java
new file mode 100644
index 000000000..4fd9299ae
--- /dev/null
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/LockCommand.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.cli.commands;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Logger;
+import software.amazon.smithy.build.model.MavenConfig;
+import software.amazon.smithy.build.model.MavenRepository;
+import software.amazon.smithy.build.model.SmithyBuildConfig;
+import software.amazon.smithy.cli.Arguments;
+import software.amazon.smithy.cli.Command;
+import software.amazon.smithy.cli.SmithyCli;
+import software.amazon.smithy.cli.dependencies.DependencyResolver;
+import software.amazon.smithy.cli.dependencies.FilterCliVersionResolver;
+import software.amazon.smithy.cli.dependencies.ResolvedArtifact;
+
+
+final class LockCommand implements Command {
+    private static final Logger LOGGER = Logger.getLogger(LockCommand.class.getName());
+
+    private final String parentCommandName;
+    private final DependencyResolver.Factory dependencyResolverFactory;
+
+    LockCommand(String parentCommandName, DependencyResolver.Factory dependencyResolverFactory) {
+        this.parentCommandName = parentCommandName;
+        this.dependencyResolverFactory = dependencyResolverFactory;
+    }
+
+    @Override
+    public String getName() {
+        return "lock";
+    }
+
+    @Override
+    public String getSummary() {
+        return "Resolves dependencies and generates a lockfile to pin to the resolved dependency versions.";
+    }
+
+    @Override
+    public int execute(Arguments arguments, Env env) {
+        arguments.addReceiver(new ConfigOptions());
+
+        CommandAction action = HelpActionWrapper.fromCommand(this, parentCommandName, this::run);
+
+        return action.apply(arguments, env);
+    }
+
+    private int run(Arguments arguments, Env env) {
+        ConfigOptions configOptions = arguments.getReceiver(ConfigOptions.class);
+        SmithyBuildConfig smithyBuildConfig = configOptions.createSmithyBuildConfig();
+
+        DependencyResolver baseResolver = dependencyResolverFactory.create(smithyBuildConfig, env);
+        DependencyResolver resolver = new FilterCliVersionResolver(SmithyCli.getVersion(), baseResolver);
+
+        Set<MavenRepository> repositories = ConfigurationUtils.getConfiguredMavenRepos(smithyBuildConfig);
+        Set<String> dependencies = smithyBuildConfig.getMaven()
+                .map(MavenConfig::getDependencies)
+                .orElse(Collections.emptySet());
+        dependencies.forEach(resolver::addDependency);
+        repositories.forEach(resolver::addRepository);
+
+        List<ResolvedArtifact> resolvedArtifacts = resolver.resolve();
+        LOGGER.fine(() -> "Resolved artifacts with Maven: " + resolvedArtifacts);
+        LockFile lock = LockFile.builder()
+                .configHash(ConfigurationUtils.configHash(dependencies, repositories))
+                .artifacts(resolvedArtifacts)
+                .repositories(repositories)
+                .build();
+        LOGGER.fine(() -> "Saving resolved artifacts to lockfile.");
+        lock.save();
+
+        return 0;
+    }
+}
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/LockFile.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/LockFile.java
new file mode 100644
index 000000000..32f0424e4
--- /dev/null
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/LockFile.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package software.amazon.smithy.cli.commands;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import software.amazon.smithy.build.model.MavenRepository;
+import software.amazon.smithy.cli.CliError;
+import software.amazon.smithy.cli.dependencies.DependencyResolverException;
+import software.amazon.smithy.cli.dependencies.ResolvedArtifact;
+import software.amazon.smithy.model.node.ArrayNode;
+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.node.ToNode;
+import software.amazon.smithy.utils.BuilderRef;
+import software.amazon.smithy.utils.IoUtils;
+import software.amazon.smithy.utils.ListUtils;
+import software.amazon.smithy.utils.SmithyBuilder;
+import software.amazon.smithy.utils.ToSmithyBuilder;
+
+final class LockFile implements ToSmithyBuilder<LockFile>, ToNode {
+    private static final Path LOCK_FILE = Paths.get("smithy-lock.json");
+    private static final String DEFAULT_VERSION = "1.0";
+    private static final String WARNING_STR = "AUTOGENERATED_DO_NOT_MODIFY";
+    private static final String VERSION_NAME = "version";
+    private static final String CONFIG_HASH_NAME = "configHash";
+    private static final String ARTIFACT_LIST_NAME = "artifacts";
+    private static final String REPOSITORIES_LIST_NAME = "repositories";
+    private static final List<String> EXPECTED_PROPERTIES = ListUtils.of(
+            WARNING_STR,
+            VERSION_NAME,
+            CONFIG_HASH_NAME,
+            ARTIFACT_LIST_NAME,
+            REPOSITORIES_LIST_NAME
+    );
+
+    private final Map<String, LockedArtifact> artifacts;
+    private final Set<String> repositories;
+    private final int configHash;
+    private final String version;
+
+    private LockFile(Builder builder) {
+        this.artifacts = builder.artifacts.copy();
+        this.repositories =  builder.repositories.copy();
+        this.version = builder.version;
+        this.configHash = builder.configHash;
+    }
+
+    public static LockFile fromNode(Node node) {
+        LockFile.Builder builder = builder();
+        node.expectObjectNode()
+                .warnIfAdditionalProperties(EXPECTED_PROPERTIES)
+                .getStringMember(VERSION_NAME, builder::version)
+                .getNumberMember(CONFIG_HASH_NAME, n -> builder.configHash(n.intValue()))
+                .getObjectMember(ARTIFACT_LIST_NAME, artifactList -> {
+                    for (Map.Entry<String, Node> entry : artifactList.getStringMap().entrySet()) {
+                        builder.artifact(LockedArtifact.fromCoordinateNode(entry.getKey(), entry.getValue()));
+                    }
+                })
+                .getArrayMember(REPOSITORIES_LIST_NAME, StringNode::getValue, builder::repositories);
+        return builder.build();
+    }
+
+    public static Optional<LockFile> load() {
+        return load(LOCK_FILE);
+    }
+
+    /**
+     * Loads lockfile from path if it exists.
+     *
+     * @param path path of file to load as a lockfile
+     * @return Lockfile if it exists otherwise Optional.empty().
+     */
+    public static Optional<LockFile> load(Path path) {
+        if (Files.exists(path) && Files.isRegularFile(path)) {
+            String contents = IoUtils.readUtf8File(path);
+            Node result = Node.parseJsonWithComments(contents);
+            return Optional.of(LockFile.fromNode(result));
+        }
+        return Optional.empty();
+    }
+
+
+    @Override
+    public Node toNode() {
+        ObjectNode.Builder builder = Node.objectNodeBuilder()
+                .withMember(WARNING_STR, "")
+                .withMember(VERSION_NAME, version)
+                .withMember(CONFIG_HASH_NAME, configHash)
+                .withMember(REPOSITORIES_LIST_NAME, ArrayNode.fromStrings(repositories));
+
+        ObjectNode.Builder artifactNodeBuilder = Node.objectNodeBuilder();
+        for (LockedArtifact artifact : artifacts.values()) {
+            artifactNodeBuilder.withMember(artifact.coordinates, artifact.toNode());
+        }
+        builder.withMember(ARTIFACT_LIST_NAME, artifactNodeBuilder.build());
+
+        return builder.build();
+    }
+
+    public void validateArtifacts(List<ResolvedArtifact> resolvedArtifacts) {
+        for (ResolvedArtifact artifact : resolvedArtifacts) {
+            LockedArtifact pin = artifacts.get(artifact.getCoordinates());
+            if (pin == null) {
+                throw new CliError("Resolved artifact `" + artifact + "` not found in locked artifacts.");
+            }
+            if (!pin.matchesResolved(artifact)) {
+                throw new CliError("Resolved artifact `" + artifact + "` does not match locked artifact `" + pin + "`");
+            }
+        }
+    }
+
+    public static LockFile.Builder builder() {
+        return new LockFile.Builder();
+    }
+
+    /**
+     * @return Gets the configuration hash for the lockfile
+     */
+    public int getConfigHash() {
+        return configHash;
+    }
+
+    /**
+     * @return Gets the list of pinned artifacts.
+     */
+    public Set<String> getDependencyCoordinateSet() {
+        return artifacts.keySet();
+    }
+
+    /**
+     * @return Gets the version of lockfile.
+     */
+    public String getVersion() {
+        return version;
+    }
+
+    /**
+     * @return Gets the pinned repositories in the lockfile.
+     */
+    public Set<String> getRepositories() {
+        return repositories;
+    }
+
+    /**
+     * Saves lockfile to current working directory.
+     */
+    public void save() {
+        try {
+            Files.write(LOCK_FILE, Node.printJson(this.toNode()).getBytes(StandardCharsets.UTF_8));
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
+
+    @Override
+    public LockFile.Builder toBuilder() {
+        return builder().repositories(repositories)
+                .artifacts(artifacts)
+                .version(version)
+                .configHash(configHash);
+    }
+
+    public static final class Builder implements SmithyBuilder<LockFile> {
+        private final BuilderRef<Map<String, LockedArtifact>> artifacts = BuilderRef.forOrderedMap();
+        private final BuilderRef<Set<String>> repositories = BuilderRef.forOrderedSet();
+        private String version = DEFAULT_VERSION;
+        private int configHash;
+
+        public LockFile.Builder artifacts(Map<String, LockedArtifact> artifacts) {
+            this.artifacts.clear();
+            this.artifacts.get().putAll(artifacts);
+            return this;
+        }
+
+        public LockFile.Builder artifacts(Collection<ResolvedArtifact> artifacts) {
+            this.artifacts.clear();
+            for (ResolvedArtifact artifact : artifacts) {
+                this.artifacts.get().put(artifact.getCoordinates(), LockedArtifact.from(artifact));
+            }
+            return this;
+        }
+
+        public LockFile.Builder artifact(ResolvedArtifact artifact) {
+            this.artifacts.get().put(artifact.getCoordinates(), LockedArtifact.from(artifact));
+            return this;
+        }
+
+        public LockFile.Builder artifact(LockedArtifact artifact) {
+            this.artifacts.get().put(artifact.coordinates, artifact);
+            return this;
+        }
+
+        public LockFile.Builder repositories(Set<MavenRepository> repositories) {
+            this.repositories.clear();
+            for (MavenRepository repository : repositories) {
+                this.repositories.get().add(repository.getUrl());
+            }
+            return this;
+        }
+
+        public LockFile.Builder repositories(Collection<String> repositories) {
+            this.repositories.clear();
+            this.repositories.get().addAll(repositories);
+            return this;
+        }
+
+        public LockFile.Builder version(String version) {
+            this.version = version;
+            return this;
+        }
+
+        public LockFile.Builder configHash(int configHash) {
+            this.configHash = configHash;
+            return this;
+        }
+
+        @Override
+        public LockFile build() {
+            return new LockFile(this);
+        }
+    }
+
+    private static final class LockedArtifact implements ToNode {
+        private static final String SHA_SUM_MEMBER_NAME = "sha1";
+        private final String coordinates;
+        private final String shaSum;
+
+        private LockedArtifact(String coordinates, String shaSum) {
+            this.coordinates = coordinates;
+            this.shaSum = shaSum;
+        }
+
+        private static LockedArtifact from(ResolvedArtifact resolvedArtifact) {
+            return new LockedArtifact(resolvedArtifact.getCoordinates(), resolvedArtifact.getShaSum());
+        }
+
+        private static LockedArtifact fromCoordinateNode(String coordinates, Node node) {
+            String[] parts = coordinates.split(":");
+            if (parts.length != 3) {
+                throw new DependencyResolverException("Invalid Locked coordinates: " + coordinates);
+            }
+            ObjectNode objectNode = node.expectObjectNode();
+            String shaSum = objectNode.expectMember(SHA_SUM_MEMBER_NAME).expectStringNode().getValue();
+
+            return new LockedArtifact(coordinates, shaSum);
+        }
+
+        @Override
+        public Node toNode() {
+            return Node.objectNodeBuilder()
+                    .withMember(SHA_SUM_MEMBER_NAME, shaSum)
+                    .build();
+        }
+
+        boolean matchesResolved(ResolvedArtifact resolved) {
+           return shaSum.equals(resolved.getShaSum());
+        }
+    }
+}
+
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/SmithyCommand.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/SmithyCommand.java
index 80bad5a3a..eb67a44ed 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/SmithyCommand.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/commands/SmithyCommand.java
@@ -51,7 +51,8 @@ public final class SmithyCommand implements Command {
             migrateCommand,
             deprecated1To2Command,
             new WarmupCommand(getName()),
-            new InitCommand(getName())
+            new InitCommand(getName()),
+            new LockCommand(getName(), dependencyResolverFactory)
         );
     }
 
diff --git a/smithy-cli/src/main/java/software/amazon/smithy/cli/dependencies/DependencyUtils.java b/smithy-cli/src/main/java/software/amazon/smithy/cli/dependencies/DependencyUtils.java
index d87ccad88..8ce4cdc9d 100644
--- a/smithy-cli/src/main/java/software/amazon/smithy/cli/dependencies/DependencyUtils.java
+++ b/smithy-cli/src/main/java/software/amazon/smithy/cli/dependencies/DependencyUtils.java
@@ -15,7 +15,9 @@ import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 
 final class DependencyUtils {
-    private DependencyUtils() {}
+    private DependencyUtils() {
+        // Utility Class should not be instantiated
+    }
 
     /**
      * Computes the sha1 digest of a file.
diff --git a/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/RangeTraitPlugin.java b/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/RangeTraitPlugin.java
index 5cf2e6386..058a82b84 100644
--- a/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/RangeTraitPlugin.java
+++ b/smithy-model/src/main/java/software/amazon/smithy/model/validation/node/RangeTraitPlugin.java
@@ -30,6 +30,7 @@ import software.amazon.smithy.model.validation.Severity;
 class RangeTraitPlugin implements NodeValidatorPlugin {
     private static final String MEMBER = "Member";
     private static final String TARGET = "Target";
+    private static final String INVALID_RANGE = "InvalidRange";
 
     @Override
     public final void apply(Shape shape, Node value, Context context, Emitter emitter) {
@@ -56,14 +57,16 @@ class RangeTraitPlugin implements NodeValidatorPlugin {
                 emitter.accept(node, getSeverity(context), String.format(
                         "Value provided for `%s` must be greater than or equal to %s, but found \"%s\"",
                         shape.getId(), trait.getMin().get(), node.getValue()),
-                        shape.isMemberShape() ? MEMBER : TARGET);
+                        shape.isMemberShape() ? MEMBER : TARGET,
+                        INVALID_RANGE);
             }
 
             if (trait.getMax().isPresent() && value.equals(NonNumericFloat.POSITIVE_INFINITY)) {
                 emitter.accept(node, getSeverity(context), String.format(
                         "Value provided for `%s` must be less than or equal to %s, but found \"%s\"",
                         shape.getId(), trait.getMax().get(), node.getValue()),
-                        shape.isMemberShape() ? MEMBER : TARGET);
+                        shape.isMemberShape() ? MEMBER : TARGET,
+                        INVALID_RANGE);
             }
         });
     }
@@ -75,7 +78,8 @@ class RangeTraitPlugin implements NodeValidatorPlugin {
                     emitter.accept(node, getSeverity(node, context), String.format(
                                            "Value provided for `%s` must be greater than or equal to %s, but found %s",
                                            shape.getId(), min, decimal),
-                                   shape.isMemberShape() ? MEMBER : TARGET);
+                                   shape.isMemberShape() ? MEMBER : TARGET,
+                                   INVALID_RANGE);
                 }
             });
         });
@@ -86,7 +90,8 @@ class RangeTraitPlugin implements NodeValidatorPlugin {
                     emitter.accept(node, getSeverity(node, context), String.format(
                                            "Value provided for `%s` must be less than or equal to %s, but found %s",
                                            shape.getId(), max, decimal),
-                                   shape.isMemberShape() ? MEMBER : TARGET);
+                                   shape.isMemberShape() ? MEMBER : TARGET,
+                                   INVALID_RANGE);
                 }
             });
         });
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/validators/AuthSchemeValidator.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/validators/AuthSchemeValidator.java
index 78fbfc3d1..325529848 100644
--- a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/validators/AuthSchemeValidator.java
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/validators/AuthSchemeValidator.java
@@ -10,14 +10,15 @@ import java.util.Map;
 import java.util.function.BiFunction;
 import java.util.function.Predicate;
 import software.amazon.smithy.model.FromSourceLocation;
-import software.amazon.smithy.model.SourceLocation;
 import software.amazon.smithy.model.validation.ValidationEvent;
 import software.amazon.smithy.rulesengine.language.syntax.Identifier;
 import software.amazon.smithy.rulesengine.language.syntax.expressions.literal.Literal;
+import software.amazon.smithy.utils.SmithyUnstableApi;
 
 /**
  * Validates an authentication scheme after passing a predicate check.
  */
+@SmithyUnstableApi
 public interface AuthSchemeValidator extends Predicate<String> {
     /**
      * Validates that the provided {@code authScheme} matches required modeling behavior,
@@ -30,6 +31,6 @@ public interface AuthSchemeValidator extends Predicate<String> {
      */
     List<ValidationEvent> validateScheme(
             Map<Identifier, Literal> authScheme,
-            SourceLocation sourceLocation,
+            FromSourceLocation sourceLocation,
             BiFunction<FromSourceLocation, String, ValidationEvent> emitter);
 }
diff --git a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/validators/RuleSetAuthSchemesValidator.java b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/validators/RuleSetAuthSchemesValidator.java
index 8da5768ba..0a83812b4 100644
--- a/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/validators/RuleSetAuthSchemesValidator.java
+++ b/smithy-rules-engine/src/main/java/software/amazon/smithy/rulesengine/validators/RuleSetAuthSchemesValidator.java
@@ -6,15 +6,16 @@
 package software.amazon.smithy.rulesengine.validators;
 
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 import java.util.function.BiFunction;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import software.amazon.smithy.model.FromSourceLocation;
 import software.amazon.smithy.model.Model;
-import software.amazon.smithy.model.SourceLocation;
 import software.amazon.smithy.model.shapes.ServiceShape;
 import software.amazon.smithy.model.validation.AbstractValidator;
 import software.amazon.smithy.model.validation.ValidationEvent;
@@ -60,37 +61,65 @@ public final class RuleSetAuthSchemesValidator extends AbstractValidator {
                 BiFunction<FromSourceLocation, String, ValidationEvent> emitter = getEventEmitter();
                 Optional<List<Literal>> authSchemeList = authSchemes.asTupleLiteral();
                 if (!authSchemeList.isPresent()) {
-                    return Stream.of(emitter.apply(authSchemes.getSourceLocation(),
+                    return Stream.of(emitter.apply(authSchemes,
                             String.format("Expected `authSchemes` to be a list, found: `%s`", authSchemes)));
                 }
-                for (Literal authScheme : authSchemeList.get()) {
-                    Optional<Map<Identifier, Literal>> authSchemeMap = authScheme.asRecordLiteral();
+
+                Set<String> authSchemeNames = new HashSet<>();
+                Set<String> duplicateAuthSchemeNames = new HashSet<>();
+                for (Literal authSchemeEntry : authSchemeList.get()) {
+                    Optional<Map<Identifier, Literal>> authSchemeMap = authSchemeEntry.asRecordLiteral();
                     if (authSchemeMap.isPresent()) {
-                        events.addAll(validateAuthScheme(authSchemeMap.get(), authScheme.getSourceLocation()));
+                        // Validate the name property so that we can also check that they're unique.
+                        Map<Identifier, Literal> authScheme = authSchemeMap.get();
+                        Optional<ValidationEvent> event = validateAuthSchemeName(authScheme, authSchemeEntry);
+                        if (event.isPresent()) {
+                            events.add(event.get());
+                            continue;
+                        }
+                        String schemeName = authScheme.get(NAME).asStringLiteral().get().expectLiteral();
+                        if (!authSchemeNames.add(schemeName)) {
+                            duplicateAuthSchemeNames.add(schemeName);
+                        }
+
+                        events.addAll(validateAuthScheme(schemeName, authScheme, authSchemeEntry));
                     } else {
-                        events.add(emitter.apply(authSchemes.getSourceLocation(),
+                        events.add(emitter.apply(authSchemes,
                                 String.format("Expected `authSchemes` to be a list of objects, but found: `%s`",
-                                        authScheme)));
+                                        authSchemeEntry)));
                     }
                 }
+
+                // Emit events for each duplicated auth scheme name.
+                for (String duplicateAuthSchemeName : duplicateAuthSchemeNames) {
+                    events.add(emitter.apply(authSchemes, String.format("Found duplicate `name` of `%s` in the "
+                            + "`authSchemes` list", duplicateAuthSchemeName)));
+                }
             }
 
             return events.stream();
         }
 
-        private List<ValidationEvent> validateAuthScheme(
+        private Optional<ValidationEvent> validateAuthSchemeName(
                 Map<Identifier, Literal> authScheme,
-                SourceLocation sourceLocation
+                FromSourceLocation sourceLocation
         ) {
-            BiFunction<FromSourceLocation, String, ValidationEvent> emitter = getEventEmitter();
-            if (!authScheme.containsKey(NAME)) {
-                return ListUtils.of(emitter.apply(sourceLocation,
-                        String.format("Expected `authSchemes` to have a `name` key but it did not: `%s`", authScheme)));
+            if (!authScheme.containsKey(NAME) || !authScheme.get(NAME).asStringLiteral().isPresent()) {
+                return Optional.of(error(serviceShape, sourceLocation,
+                        String.format("Expected `authSchemes` to have a `name` key with a string value but it did not: "
+                                + "`%s`", authScheme)));
             }
+            return Optional.empty();
+        }
 
+        private List<ValidationEvent> validateAuthScheme(
+                String schemeName,
+                Map<Identifier, Literal> authScheme,
+                FromSourceLocation sourceLocation
+        ) {
             List<ValidationEvent> events = new ArrayList<>();
-            Literal name = authScheme.get(NAME);
-            String schemeName = name.asStringLiteral().get().expectLiteral();
+
+            BiFunction<FromSourceLocation, String, ValidationEvent> emitter = getEventEmitter();
 
             boolean validatedAuth = false;
             for (AuthSchemeValidator authSchemeValidator : EndpointRuleSet.getAuthSchemeValidators()) {
@@ -103,7 +132,8 @@ public final class RuleSetAuthSchemesValidator extends AbstractValidator {
             if (validatedAuth) {
                 return events;
             }
-            return ListUtils.of(emitter.apply(name, String.format("Unexpected auth scheme: `%s`", schemeName)));
+            return ListUtils.of(warning(serviceShape, String.format("Did not find a validator for the `%s` "
+                    + "auth scheme", schemeName)));
         }
 
         private BiFunction<FromSourceLocation, String, ValidationEvent> getEventEmitter() {


Test patch:
diff --git a/smithy-aws-endpoints/src/test/resources/software/amazon/smithy/rulesengine/aws/language/functions/errorfiles/invalid/beta-auth-scheme-missing-field.errors b/smithy-aws-endpoints/src/test/resources/software/amazon/smithy/rulesengine/aws/language/functions/errorfiles/invalid/beta-auth-scheme-missing-field.errors
index 4e76db1d9..08da32f1e 100644
--- a/smithy-aws-endpoints/src/test/resources/software/amazon/smithy/rulesengine/aws/language/functions/errorfiles/invalid/beta-auth-scheme-missing-field.errors
+++ b/smithy-aws-endpoints/src/test/resources/software/amazon/smithy/rulesengine/aws/language/functions/errorfiles/invalid/beta-auth-scheme-missing-field.errors
@@ -1,3 +1,3 @@
 [WARNING] example#FizzBuzz: This shape applies a trait that is unstable: smithy.rules#clientContextParams | UnstableTrait
 [WARNING] example#FizzBuzz: This shape applies a trait that is unstable: smithy.rules#endpointRuleSet | UnstableTrait
-[ERROR] example#FizzBuzz: Expected auth property `signingName` but didn't find one | RuleSetAuthSchemes
+[ERROR] example#FizzBuzz: Expected auth property `signingName` of a string type but didn't find one | RuleSetAuthSchemes
diff --git a/smithy-aws-endpoints/src/test/resources/software/amazon/smithy/rulesengine/aws/language/functions/errorfiles/invalid/signing-optional-properties-mistyped.errors b/smithy-aws-endpoints/src/test/resources/software/amazon/smithy/rulesengine/aws/language/functions/errorfiles/invalid/signing-optional-properties-mistyped.errors
new file mode 100644
index 000000000..a460ce1e9
--- /dev/null
+++ b/smithy-aws-endpoints/src/test/resources/software/amazon/smithy/rulesengine/aws/language/functions/errorfiles/invalid/signing-optional-properties-mistyped.errors
@@ -0,0 +1,10 @@
+[WARNING] example#FizzBuzz: This shape applies a trait that is unstable: smithy.rules#endpointRuleSet | UnstableTrait
+[ERROR] example#FizzBuzz: Unexpected type for auth property `signingName`, found `1` but expected a string value | RuleSetAuthSchemes
+[ERROR] example#FizzBuzz: Unexpected type for auth property `signingRegion`, found `1` but expected a string value | RuleSetAuthSchemes
+[ERROR] example#FizzBuzz: Unexpected type for auth property `disableDoubleEncoding`, found `1` but expected a boolean value | RuleSetAuthSchemes
+[ERROR] example#FizzBuzz: Unexpected type for auth property `disableNormalizePath`, found `1` but expected a boolean value | RuleSetAuthSchemes
+[ERROR] example#FizzBuzz: Unexpected type for auth property `signingName`, found `1` but expected a string value | RuleSetAuthSchemes
+[ERROR] example#FizzBuzz: Unexpected type for auth property `signingRegionSet`, found `1` but expected an array<string> value | RuleSetAuthSchemes
+[ERROR] example#FizzBuzz: Unexpected type for auth property `disableDoubleEncoding`, found `1` but expected a boolean value | RuleSetAuthSchemes
+[ERROR] example#FizzBuzz: Unexpected type for auth property `disableNormalizePath`, found `1` but expected a boolean value | RuleSetAuthSchemes
+[ERROR] example#FizzBuzz: The `signingRegionSet` property must not be an empty list. | RuleSetAuthSchemes
diff --git a/smithy-aws-endpoints/src/test/resources/software/amazon/smithy/rulesengine/aws/language/functions/errorfiles/invalid/signing-optional-properties-mistyped.smithy b/smithy-aws-endpoints/src/test/resources/software/amazon/smithy/rulesengine/aws/language/functions/errorfiles/invalid/signing-optional-properties-mistyped.smithy
new file mode 100644
index 000000000..4018e5ffc
--- /dev/null
+++ b/smithy-aws-endpoints/src/test/resources/software/amazon/smithy/rulesengine/aws/language/functions/errorfiles/invalid/signing-optional-properties-mistyped.smithy
@@ -0,0 +1,125 @@
+$version: "2.0"
+
+namespace example
+
+use smithy.rules#endpointRuleSet
+use smithy.rules#endpointTests
+
+@endpointRuleSet({
+    "parameters": {
+        "Region": {
+            "type": "string",
+            "builtIn": "AWS::Region",
+            "documentation": "The region to dispatch this request, eg. `us-east-1`."
+        }
+    },
+    "rules": [
+        {
+            "documentation": "Show failing type validation for optional fields",
+            "conditions": [
+                {
+                    "fn": "isSet",
+                    "argv": [
+                        {
+                            "ref": "Region"
+                        }
+                    ]
+                },
+                {
+                    "fn": "stringEquals",
+                    "argv": [
+                        {
+                            "ref": "Region"
+                        },
+                        "a"
+                    ]
+                }
+            ],
+            "endpoint": {
+                "url": "https://{Region}.amazonaws.com",
+                "properties": {
+                    "authSchemes": [
+                        {
+                            "name": "sigv4",
+                            "signingName": 1,
+                            "signingRegion": 1,
+                            "disableDoubleEncoding": 1,
+                            "disableNormalizePath": 1
+                        }
+                    ]
+                }
+            },
+            "type": "endpoint"
+        },
+        {
+            "documentation": "Show failing type validation for optional fields",
+            "conditions": [
+                {
+                    "fn": "isSet",
+                    "argv": [
+                        {
+                            "ref": "Region"
+                        }
+                    ]
+                },
+                {
+                    "fn": "stringEquals",
+                    "argv": [
+                        {
+                            "ref": "Region"
+                        },
+                        "b"
+                    ]
+                }
+            ],
+            "endpoint": {
+                "url": "https://{Region}.amazonaws.com",
+                "properties": {
+                    "authSchemes": [
+                        {
+                            "name": "sigv4a",
+                            "signingName": 1,
+                            "signingRegionSet": 1,
+                            "disableDoubleEncoding": 1,
+                            "disableNormalizePath": 1
+                        }
+                    ]
+                }
+            },
+            "type": "endpoint"
+        },
+        {
+            "documentation": "Show non-empty requirement of signingRegionSet",
+            "conditions": [
+                {
+                    "fn": "isSet",
+                    "argv": [
+                        {
+                            "ref": "Region"
+                        }
+                    ]
+                },
+            ],
+            "endpoint": {
+                "url": "https://{Region}.amazonaws.com",
+                "properties": {
+                    "authSchemes": [
+                        {
+                            "name": "sigv4a",
+                            "signingRegionSet": []
+                        }
+                    ]
+                }
+            },
+            "type": "endpoint"
+        },
+        {
+            "documentation": "fallback when region is unset",
+            "conditions": [],
+            "error": "Region must be set to resolve a valid endpoint",
+            "type": "error"
+        }
+    ],
+    "version": "1.3"
+})
+service FizzBuzz {}
diff --git a/smithy-aws-endpoints/src/test/resources/software/amazon/smithy/rulesengine/aws/language/functions/errorfiles/valid/signing-optional-properties.errors b/smithy-aws-endpoints/src/test/resources/software/amazon/smithy/rulesengine/aws/language/functions/errorfiles/valid/signing-optional-properties.errors
new file mode 100644
index 000000000..a0b08ef46
--- /dev/null
+++ b/smithy-aws-endpoints/src/test/resources/software/amazon/smithy/rulesengine/aws/language/functions/errorfiles/valid/signing-optional-properties.errors
@@ -0,0 +1,1 @@
+[WARNING] example#FizzBuzz: This shape applies a trait that is unstable: smithy.rules#endpointRuleSet | UnstableTrait
diff --git a/smithy-aws-endpoints/src/test/resources/software/amazon/smithy/rulesengine/aws/language/functions/errorfiles/valid/signing-optional-properties.smithy b/smithy-aws-endpoints/src/test/resources/software/amazon/smithy/rulesengine/aws/language/functions/errorfiles/valid/signing-optional-properties.smithy
new file mode 100644
index 000000000..ff545c633
--- /dev/null
+++ b/smithy-aws-endpoints/src/test/resources/software/amazon/smithy/rulesengine/aws/language/functions/errorfiles/valid/signing-optional-properties.smithy
@@ -0,0 +1,84 @@
+$version: "2.0"
+
+namespace example
+
+use smithy.rules#endpointRuleSet
+use smithy.rules#endpointTests
+
+@endpointRuleSet({
+    "parameters": {
+        "Region": {
+            "type": "string",
+            "builtIn": "AWS::Region",
+            "documentation": "The region to dispatch this request, eg. `us-east-1`."
+        }
+    },
+    "rules": [
+        {
+            "documentation": "Template the region into the URI when region is set",
+            "conditions": [
+                {
+                    "fn": "isSet",
+                    "argv": [
+                        {
+                            "ref": "Region"
+                        }
+                    ]
+                },
+                {
+                    "fn": "stringEquals",
+                    "argv": [
+                        {
+                            "ref": "Region"
+                        },
+                        "a"
+                    ]
+                }
+            ],
+            "endpoint": {
+                "url": "https://{Region}.amazonaws.com",
+                "properties": {
+                    "authSchemes": [
+                        {
+                            "name": "sigv4",
+                        }
+                    ]
+                }
+            },
+            "type": "endpoint"
+        },
+        {
+            "documentation": "Template the region into the URI when region is set",
+            "conditions": [
+                {
+                    "fn": "isSet",
+                    "argv": [
+                        {
+                            "ref": "Region"
+                        }
+                    ]
+                },
+            ],
+            "endpoint": {
+                "url": "https://{Region}.amazonaws.com",
+                "properties": {
+                    "authSchemes": [
+                        {
+                            "name": "sigv4a",
+                            "signingRegionSet": ["*"]
+                        }
+                    ]
+                }
+            },
+            "type": "endpoint"
+        },
+        {
+            "documentation": "fallback when region is unset",
+            "conditions": [],
+            "error": "Region must be set to resolve a valid endpoint",
+            "type": "error"
+        }
+    ],
+    "version": "1.3"
+})
+service FizzBuzz {}
diff --git a/smithy-cli/src/it/java/software/amazon/smithy/cli/LockCommandTest.java b/smithy-cli/src/it/java/software/amazon/smithy/cli/LockCommandTest.java
new file mode 100644
index 000000000..55d29f25a
--- /dev/null
+++ b/smithy-cli/src/it/java/software/amazon/smithy/cli/LockCommandTest.java
@@ -0,0 +1,176 @@
+package software.amazon.smithy.cli;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Isolated;
+import org.mockserver.integration.ClientAndServer;
+import software.amazon.smithy.utils.ListUtils;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+import static org.mockserver.integration.ClientAndServer.startClientAndServer;
+import static software.amazon.smithy.cli.MavenResolverMultipleReposTest.mockArtifactAndSha;
+
+@Isolated
+public class LockCommandTest {
+
+    @Test
+    public void writesLockfile() throws IOException {
+        ClientAndServer mockServer = null;
+        try {
+            mockServer = startClientAndServer(6789);
+
+            // artifact
+            mockArtifactAndSha(
+                    mockServer,
+                    "/artifactory-2/com/example/artifact/1.0.0/artifact-1.0.0.jar",
+                    "FAKE JAR CONTENT"
+            );
+
+            mockArtifactAndSha(
+                    mockServer,
+                    "/artifactory-2/com/example/artifact/maven-metadata.xml",
+                    getMetadataForVersions(ListUtils.of("1.0.0"))
+            );
+
+            mockArtifactAndSha(
+                    mockServer,
+                    "/artifactory-2/com/example/artifact/1.0.0/artifact-1.0.0.pom",
+                    pomFileContents()
+            );
+
+            // Dependency
+            mockArtifactAndSha(
+                    mockServer,
+                    "/artifactory-1/com/example/dependency/1.0.0/dependency-1.0.0.jar",
+                    "ANOTHER FAKE JAR"
+            );
+
+            // Use an empty cache
+            String cacheDir = Files.createTempDirectory("foo").toString();
+            Map<String, String> env = new HashMap<>();
+            env.put(EnvironmentVariable.SMITHY_MAVEN_CACHE.toString(), cacheDir);
+
+            IntegUtils.withProject("lockfile-written", root -> {
+                // Create initial lock file
+                RunResult lockResult = IntegUtils.run(root, ListUtils.of("lock", "--debug"), env);
+                assertThat(lockResult.getExitCode(), equalTo(0));
+                assertThat(lockResult.getOutput(), containsString("software.amazon.smithy.cli.dependencies.DependencyResolver - Resolved Maven dependencies: [com.example:artifact:jar:1.0.0"));
+                assertThat(lockResult.getOutput(), containsString("Saving resolved artifacts to lockfile."));
+
+                // Confirm lockfile detected in path and used
+                RunResult validateResult = IntegUtils.run(root, ListUtils.of("validate", "--debug"), env);
+                assertThat(validateResult.getExitCode(), equalTo(0));
+                assertThat(validateResult.getOutput(), containsString("`smithy-lock.json` found. Using locked dependencies: "));
+            });
+        } finally {
+            if (mockServer!=null) {
+                mockServer.stop();
+            }
+        }
+    }
+
+    @Test
+    public void lockedDependenciesUsed() {
+        ClientAndServer mockServer = null;
+        try {
+            mockServer = startClientAndServer(5678);
+
+            // artifacts
+            mockArtifactAndSha(
+                    mockServer,
+                    "/artifactory-2/com/example/artifact/1.0.0/artifact-1.0.0.jar",
+                    "FAKE JAR CONTENT"
+            );
+
+            mockArtifactAndSha(
+                    mockServer,
+                    "/artifactory-2/com/example/artifact/1.1.0/artifact-1.1.0.jar",
+                    "FAKE JAR CONTENT"
+            );
+
+            mockArtifactAndSha(
+                    mockServer,
+                    "/artifactory-2/com/example/artifact/1.2.0/artifact-1.2.0.jar",
+                    "FAKE JAR CONTENT"
+            );
+
+            mockArtifactAndSha(
+                    mockServer,
+                    "/artifactory-2/com/example/artifact/maven-metadata.xml",
+                    getMetadataForVersions(ListUtils.of("1.2.0", "1.1.0", "1.0.0"))
+            );
+
+            IntegUtils.runWithEmptyCache("lockfile-used", ListUtils.of("validate", "--debug"),
+                    Collections.emptyMap(), result -> {
+                        assertThat(result.getExitCode(), equalTo(0));
+                        assertThat(result.getOutput(), containsString("software.amazon.smithy.cli.dependencies.DependencyResolver - Resolved Maven dependencies: [com.example:artifact:jar:1.1.0"));
+                    });
+        } finally {
+            if (mockServer!=null) {
+                mockServer.stop();
+            }
+        }
+    }
+
+    @Test
+    public void clashingLockAndConfigThrowsException() {
+        IntegUtils.runWithEmptyCache("clashing-lockfile", ListUtils.of("validate", "--debug"),
+                Collections.emptyMap(), result -> {
+                    assertThat(result.getExitCode(), equalTo(1));
+                    assertThat(result.getOutput(), containsString("`smithy-lock.json` does not match configured dependencies."));
+                });
+    }
+
+    private String getMetadataForVersions(List<String> versions) {
+        String versionSection = versions.stream().map(v -> "<version>" + v + "</version>\n").collect(Collectors.joining());
+        return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+                "<metadata modelVersion=\"1.1.0\">\n" +
+                "  <groupId>com.example</groupId>\n" +
+                "  <artifactId>artifact</artifactId>\n" +
+                "  <version>" + versions.get(0) + "</version>\n" +
+                "  <versioning>\n" +
+                "    <latest>" + versions.get(0) + "</latest>\n" +
+                "    <release>" + versions.get(0) + "</release>\n" +
+                "    <versions>\n" + versionSection +
+                "    </versions>\n" +
+                "    <lastUpdated>20230724184336</lastUpdated>\n" +
+                "  </versioning>\n" +
+                "</metadata>\n";
+    }
+
+    private String pomFileContents() {
+        return
+                "<?xml version='1.0' encoding='UTF-8'?>\n" +
+                        "<project xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://maven.apache.org/POM/4.0.0\">\n" +
+                        "    <modelVersion>4.0.0</modelVersion>\n" +
+                        "    <groupId>com.example</groupId>\n" +
+                        "    <artifactId>artifact</artifactId>\n" +
+                        "    <packaging>jar</packaging>\n" +
+                        "    <description>artifact</description>\n" +
+                        "    <version>1.0.0</version>\n" +
+                        "    <name>artifact</name>\n" +
+                        "    <organization>\n" +
+                        "        <name>com.example</name>\n" +
+                        "    </organization>\n" +
+                        "    <properties>\n" +
+                        "        <info.versionScheme>early-semver</info.versionScheme>\n" +
+                        "    </properties>\n" +
+                        "    <dependencies>\n" +
+                        "        <dependency>\n" +
+                        "            <groupId>com.example</groupId>\n" +
+                        "            <artifactId>dependency</artifactId>\n" +
+                        "            <version>1.0.0</version>\n" +
+                        "        </dependency>\n" +
+                        "    </dependencies>\n" +
+                        "</project>";
+    }
+}
diff --git a/smithy-cli/src/it/java/software/amazon/smithy/cli/MavenResolverMultipleReposTest.java b/smithy-cli/src/it/java/software/amazon/smithy/cli/MavenResolverMultipleReposTest.java
index 87a6eda4a..e951546c0 100644
--- a/smithy-cli/src/it/java/software/amazon/smithy/cli/MavenResolverMultipleReposTest.java
+++ b/smithy-cli/src/it/java/software/amazon/smithy/cli/MavenResolverMultipleReposTest.java
@@ -2,6 +2,7 @@ package software.amazon.smithy.cli;
 
 import org.apache.commons.codec.digest.DigestUtils;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Isolated;
 import org.mockserver.integration.ClientAndServer;
 import org.mockserver.model.HttpRequest;
 import org.mockserver.model.HttpResponse;
@@ -14,6 +15,7 @@ import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.containsString;
 import static org.mockserver.integration.ClientAndServer.startClientAndServer;
 
+@Isolated
 public class MavenResolverMultipleReposTest {
 
     @Test
@@ -57,13 +59,13 @@ public class MavenResolverMultipleReposTest {
                         assertThat(result.getOutput(), containsString("software.amazon.smithy.cli.dependencies.DependencyResolver - Resolved Maven dependencies: [com.example:artifact:jar:1.0.0-20230724.184336-2"));
                     });
         } finally {
-            if(mockServer!=null) {
+            if (mockServer!=null) {
                 mockServer.stop();
             }
         }
     }
 
-    private void mockArtifactAndSha(ClientAndServer mockServer, String path, String contents) {
+    public static void mockArtifactAndSha(ClientAndServer mockServer, String path, String contents) {
         mockSuccess(
             mockServer,
             path,
diff --git a/smithy-cli/src/it/java/software/amazon/smithy/cli/MavenResolverTest.java b/smithy-cli/src/it/java/software/amazon/smithy/cli/MavenResolverTest.java
index add3af72e..d856d7e62 100644
--- a/smithy-cli/src/it/java/software/amazon/smithy/cli/MavenResolverTest.java
+++ b/smithy-cli/src/it/java/software/amazon/smithy/cli/MavenResolverTest.java
@@ -101,7 +101,7 @@ public class MavenResolverTest {
                         assertThat(result.getOutput(), containsString("HttpAuthenticator - Authentication succeeded"));
                     });
         } finally {
-            if(mockServer!=null) {
+            if (mockServer!=null) {
                 mockServer.stop();
             }
         }
diff --git a/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/ConfigurationUtilsTest.java b/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/ConfigurationUtilsTest.java
new file mode 100644
index 000000000..99daab6ab
--- /dev/null
+++ b/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/ConfigurationUtilsTest.java
@@ -0,0 +1,48 @@
+package software.amazon.smithy.cli.commands;
+
+import java.util.Set;
+import org.junit.jupiter.api.Test;
+import software.amazon.smithy.build.model.MavenConfig;
+import software.amazon.smithy.build.model.MavenRepository;
+import software.amazon.smithy.build.model.SmithyBuildConfig;
+import software.amazon.smithy.utils.ListUtils;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class ConfigurationUtilsTest {
+    private static final MavenRepository CENTRAL = MavenRepository.builder()
+            .url("https://repo.maven.apache.org/maven2")
+            .build();
+
+    private static final MavenRepository TEST_REPO_1 = MavenRepository.builder()
+            .url("fake_fake_fake")
+            .build();
+    private static final MavenRepository TEST_REPO_2 = MavenRepository.builder()
+            .url("not_real")
+            .build();
+
+    @Test
+    public void returnsDefaultRepository() {
+        SmithyBuildConfig emptyConfig = SmithyBuildConfig.builder().version("1.0").build();
+        Set<MavenRepository> repositorySet = ConfigurationUtils.getConfiguredMavenRepos(emptyConfig);
+
+        assertEquals(repositorySet.size(), 1);
+        assertThat(repositorySet, contains(CENTRAL));
+    }
+
+    @Test
+    public void returnsCorrectRepositoriesInConfig() {
+        SmithyBuildConfig config = SmithyBuildConfig.builder()
+                .version("1.0")
+                .maven(MavenConfig.builder()
+                        .repositories(ListUtils.of(TEST_REPO_1, TEST_REPO_2))
+                        .build())
+                .build();
+        Set<MavenRepository> repositorySet = ConfigurationUtils.getConfiguredMavenRepos(config);
+
+        assertEquals(repositorySet.size(), 2);
+        assertThat(repositorySet, contains(TEST_REPO_1, TEST_REPO_2));
+    }
+}
diff --git a/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/LockFileTest.java b/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/LockFileTest.java
new file mode 100644
index 000000000..99a0dc7ac
--- /dev/null
+++ b/smithy-cli/src/test/java/software/amazon/smithy/cli/commands/LockFileTest.java
@@ -0,0 +1,109 @@
+package software.amazon.smithy.cli.commands;
+
+import java.io.File;
+import java.net.URISyntaxException;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import software.amazon.smithy.cli.CliError;
+import software.amazon.smithy.cli.dependencies.ResolvedArtifact;
+import software.amazon.smithy.model.node.ArrayNode;
+import software.amazon.smithy.model.node.Node;
+import software.amazon.smithy.utils.ListUtils;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasSize;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class LockFileTest {
+    @Test
+    public void hasNoDefaultsBuiltInToThePojo() {
+        LockFile lock = LockFile.builder().build();
+
+        assertThat(lock.getDependencyCoordinateSet(), empty());
+        assertThat(lock.getRepositories(), empty());
+    }
+
+    @Test
+    public void loadsEmptyLockfile() {
+        LockFile lock = LockFile.fromNode(Node.objectNode());
+
+        assertThat(lock.getDependencyCoordinateSet(), empty());
+        assertThat(lock.getRepositories(), empty());
+    }
+
+    @Test
+    public void loadsFromNode() {
+        LockFile lock = LockFile.fromNode(getNode());
+
+        assertThat(lock.getVersion(), equalTo("1.0"));
+        assertThat(lock.getConfigHash(), equalTo(-1856284556));
+        assertThat(lock.getRepositories(), hasSize(1));
+        assertThat(lock.getRepositories(), contains("repo"));
+        assertThat(lock.getDependencyCoordinateSet(), hasSize(1));
+        assertThat(lock.getDependencyCoordinateSet(), contains("software.amazon.smithy:smithy-aws-traits:1.37.0"));
+    }
+
+    @Test
+    public void validatesResolved() {
+        LockFile lock = LockFile.fromNode(getNode());
+        List<ResolvedArtifact> artifactList = ListUtils.of(
+                ResolvedArtifact.fromCoordinateNode("software.amazon.smithy:smithy-aws-traits:1.37.0",
+                        Node.objectNodeBuilder()
+                                .withMember("path", "/a")
+                                .withMember("sha1", "sum")
+                                .build()));
+        lock.validateArtifacts(artifactList);
+    }
+
+    @Test
+    public void throwsErrorOnIncompatibleDependencies() {
+        LockFile lock = LockFile.fromNode(getNode());
+        List<ResolvedArtifact> artifactList = ListUtils.of(
+                ResolvedArtifact.fromCoordinateNode("software.amazon.smithy:smithy-aws-traits:1.37.0",
+                        Node.objectNodeBuilder()
+                        .withMember("path", "/a")
+                        .withMember("sha1", "badSum")
+                        .build()));
+        Assertions.assertThrows(CliError.class, () -> lock.validateArtifacts(artifactList));
+    }
+
+    @Test
+    public void loadsSmithyLockfile() throws URISyntaxException {
+        File lockfileResource = new File(
+                Objects.requireNonNull(getClass().getResource("smithy-lock-test.json")).toURI());
+        Optional<LockFile> lockFileOptional = LockFile.load(lockfileResource.toPath());
+
+        assertTrue(lockFileOptional.isPresent());
+        LockFile lockFile = lockFileOptional.get();
+        assertEquals(lockFile.getConfigHash(), -1856284556);
+        assertEquals(lockFile.getVersion(), "1.0");
+        assertThat(lockFile.getDependencyCoordinateSet(), contains("software.amazon.smithy:smithy-aws-traits:1.37.0"));
+        assertThat(lockFile.getRepositories(), contains("https://repo.maven.apache.org/maven2"));
+    }
+
+    @Test
+    public void returnsEmptyWhenNoLockfile() {
+        Optional<LockFile> lockFileOptional = LockFile.load();
+        assertFalse(lockFileOptional.isPresent());
+    }
+
+
+    private Node getNode() {
+        return Node.objectNodeBuilder()
+                .withMember("version", "1.0")
+                .withMember("configHash", -1856284556)
+                .withMember("repositories", ArrayNode.fromNodes(Node.from("repo")))
+                .withMember("artifacts", Node.objectNode()
+                        .withMember("software.amazon.smithy:smithy-aws-traits:1.37.0",
+                                        Node.objectNode().withMember("sha1", "sum")))
+                .build();
+    }
+}
diff --git a/smithy-cli/src/test/java/software/amazon/smithy/cli/dependencies/FilterCliVersionResolverTest.java b/smithy-cli/src/test/java/software/amazon/smithy/cli/dependencies/FilterCliVersionResolverTest.java
index 264ed7e95..eaba5a5ff 100644
--- a/smithy-cli/src/test/java/software/amazon/smithy/cli/dependencies/FilterCliVersionResolverTest.java
+++ b/smithy-cli/src/test/java/software/amazon/smithy/cli/dependencies/FilterCliVersionResolverTest.java
@@ -4,7 +4,6 @@ import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.empty;
 
-import java.nio.file.Paths;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
diff --git a/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/smithy-lock-test.json b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/smithy-lock-test.json
new file mode 100644
index 000000000..e5e438f39
--- /dev/null
+++ b/smithy-cli/src/test/resources/software/amazon/smithy/cli/commands/smithy-lock-test.json
@@ -0,0 +1,11 @@
+{
+  "AUTOGENERATED_DO_NOT_MODIFY":"",
+  "version":"1.0",
+  "configHash":-1856284556,
+  "repositories":["https://repo.maven.apache.org/maven2"],
+  "artifacts":{
+    "software.amazon.smithy:smithy-aws-traits:1.37.0":{
+      "sha1":"723b00f4473c0f3f308b57b2c95b32fad669904c68d8e287606304d841cd52ff"
+    }
+  }
+}
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/detects-default-out-of-range.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/detects-default-out-of-range.errors
index e57de3671..9a98f25f2 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/detects-default-out-of-range.errors
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/detects-default-out-of-range.errors
@@ -1,10 +1,10 @@
-[WARNING] smithy.example#Integers$invalidMin: Error validating @default trait: Value provided for `smithy.example#Integers$invalidMin` must be greater than or equal to 1, but found 0 | DefaultTrait
-[WARNING] smithy.example#Integers$invalidMax: Error validating @default trait: Value provided for `smithy.example#Integers$invalidMax` must be less than or equal to -1, but found 0 | DefaultTrait
-[WARNING] smithy.example#Integers$invalidTargetMin: Error validating @default trait: Value provided for `smithy.example#MinOne` must be greater than or equal to 1, but found 0 | DefaultTrait
-[WARNING] smithy.example#Integers$invalidTagetMax: Error validating @default trait: Value provided for `smithy.example#MaxNegativeOne` must be less than or equal to -1, but found 0 | DefaultTrait
-[WARNING] smithy.example#Integers$invalidMinWithMax: Error validating @default trait: Value provided for `smithy.example#MinAndMax` must be greater than or equal to 1, but found 0 | DefaultTrait
-[WARNING] smithy.example#MinOne: Error validating @default trait: Value provided for `smithy.example#MinOne` must be greater than or equal to 1, but found 0 | DefaultTrait
-[WARNING] smithy.example#MinAndMax: Error validating @default trait: Value provided for `smithy.example#MinAndMax` must be greater than or equal to 1, but found 0 | DefaultTrait
-[WARNING] smithy.example#MaxNegativeOne: Error validating @default trait: Value provided for `smithy.example#MaxNegativeOne` must be less than or equal to -1, but found 0 | DefaultTrait
-[WARNING] smithy.example#Integers$doublyInvalidDefault: Error validating @default trait: Value provided for `smithy.example#Integers$doublyInvalidDefault` must be greater than or equal to 1, but found 0 | DefaultTrait.Member
-[WARNING] smithy.example#Integers$doublyInvalidDefault: Error validating @default trait: Value provided for `smithy.example#MinOne` must be greater than or equal to 1, but found 0 | DefaultTrait.Target
+[WARNING] smithy.example#Integers$invalidMin: Error validating @default trait: Value provided for `smithy.example#Integers$invalidMin` must be greater than or equal to 1, but found 0 | DefaultTrait.Member.InvalidRange
+[WARNING] smithy.example#Integers$invalidMax: Error validating @default trait: Value provided for `smithy.example#Integers$invalidMax` must be less than or equal to -1, but found 0 | DefaultTrait.Member.InvalidRange
+[WARNING] smithy.example#Integers$invalidTargetMin: Error validating @default trait: Value provided for `smithy.example#MinOne` must be greater than or equal to 1, but found 0 | DefaultTrait.Target.InvalidRange
+[WARNING] smithy.example#Integers$invalidTargetMax: Error validating @default trait: Value provided for `smithy.example#MaxNegativeOne` must be less than or equal to -1, but found 0 | DefaultTrait.Target.InvalidRange
+[WARNING] smithy.example#Integers$invalidMinWithMax: Error validating @default trait: Value provided for `smithy.example#MinAndMax` must be greater than or equal to 1, but found 0 | DefaultTrait.Target.InvalidRange
+[WARNING] smithy.example#MinOne: Error validating @default trait: Value provided for `smithy.example#MinOne` must be greater than or equal to 1, but found 0 | DefaultTrait.Target.InvalidRange
+[WARNING] smithy.example#MinAndMax: Error validating @default trait: Value provided for `smithy.example#MinAndMax` must be greater than or equal to 1, but found 0 | DefaultTrait.Target.InvalidRange
+[WARNING] smithy.example#MaxNegativeOne: Error validating @default trait: Value provided for `smithy.example#MaxNegativeOne` must be less than or equal to -1, but found 0 | DefaultTrait.Target.InvalidRange
+[WARNING] smithy.example#Integers$doublyInvalidDefault: Error validating @default trait: Value provided for `smithy.example#Integers$doublyInvalidDefault` must be greater than or equal to 1, but found 0 | DefaultTrait.Member.InvalidRange
+[WARNING] smithy.example#Integers$doublyInvalidDefault: Error validating @default trait: Value provided for `smithy.example#MinOne` must be greater than or equal to 1, but found 0 | DefaultTrait.Target.InvalidRange
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/detects-default-out-of-range.smithy b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/detects-default-out-of-range.smithy
index ebae7fa29..20beb5749 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/detects-default-out-of-range.smithy
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/detects-default-out-of-range.smithy
@@ -22,7 +22,7 @@ structure Integers {
     invalidTargetMin: MinOne,
 
     @default(0)
-    invalidTagetMax: MaxNegativeOne,
+    invalidTargetMax: MaxNegativeOne,
 
     @default(0)
     invalidMinWithMax: MinAndMax,
diff --git a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/numbers-must-match-constraints.errors b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/numbers-must-match-constraints.errors
index ee42f28e2..d953db709 100644
--- a/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/numbers-must-match-constraints.errors
+++ b/smithy-model/src/test/resources/software/amazon/smithy/model/errorfiles/validators/defaults/numbers-must-match-constraints.errors
@@ -1,1 +1,1 @@
-[WARNING] smithy.example#Foo$baz: Error validating @default trait: Value provided for `smithy.example#Baz` must be greater than or equal to 1, but found 0 | DefaultTrait
+[WARNING] smithy.example#Foo$baz: Error validating @default trait: Value provided for `smithy.example#Baz` must be greater than or equal to 1, but found 0 | DefaultTrait.Target.InvalidRange
diff --git a/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/invalid-auth-name-type.errors b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/invalid-auth-name-type.errors
new file mode 100644
index 000000000..a28f443a2
--- /dev/null
+++ b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/invalid-auth-name-type.errors
@@ -0,0 +1,3 @@
+[WARNING] example#FizzBuzz: This shape applies a trait that is unstable: smithy.rules#clientContextParams | UnstableTrait
+[WARNING] example#FizzBuzz: This shape applies a trait that is unstable: smithy.rules#endpointRuleSet | UnstableTrait
+[ERROR] example#FizzBuzz: Expected `authSchemes` to have a `name` key with a string value but it did not: `{name=true}` | RuleSetAuthSchemes
diff --git a/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/invalid-auth-name-type.smithy b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/invalid-auth-name-type.smithy
new file mode 100644
index 000000000..f8d19824a
--- /dev/null
+++ b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/invalid-auth-name-type.smithy
@@ -0,0 +1,67 @@
+$version: "2.0"
+
+namespace example
+
+use smithy.rules#clientContextParams
+use smithy.rules#endpointRuleSet
+use smithy.rules#endpointTests
+
+@clientContextParams(
+    bar: {type: "string", documentation: "a client string parameter"}
+)
+@endpointRuleSet({
+    version: "1.0",
+    parameters: {
+        bar: {
+            type: "string",
+            documentation: "docs"
+        }
+        endpoint: {
+            type: "string",
+            builtIn: "SDK::Endpoint",
+            required: true,
+            default: "asdf"
+            documentation: "docs"
+        },
+    },
+    rules: [
+        {
+            "documentation": "Shows invalid authScheme name type",
+            "conditions": [
+                {
+                    "fn": "isSet",
+                    "argv": [
+                        {
+                            "ref": "bar"
+                        }
+                    ]
+                }
+            ],
+            "endpoint": {
+                "url": "https://example.com/",
+                "properties": {
+                    "authSchemes": [
+                        {
+                            "name": true,
+                        }
+                    ]
+                }
+            },
+            "type": "endpoint"
+        },
+        {
+            "conditions": [],
+            "documentation": "error fallthrough",
+            "error": "endpoint error",
+            "type": "error"
+        }
+    ]
+})
+service FizzBuzz {
+    version: "2022-01-01",
+    operations: [GetThing]
+}
+
+operation GetThing {
+    input := {}
+}
diff --git a/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/invalid-duplicate-auth-name.errors b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/invalid-duplicate-auth-name.errors
new file mode 100644
index 000000000..c1933e566
--- /dev/null
+++ b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/invalid-duplicate-auth-name.errors
@@ -0,0 +1,5 @@
+[WARNING] example#FizzBuzz: This shape applies a trait that is unstable: smithy.rules#clientContextParams | UnstableTrait
+[WARNING] example#FizzBuzz: This shape applies a trait that is unstable: smithy.rules#endpointRuleSet | UnstableTrait
+[WARNING] example#FizzBuzz: Did not find a validator for the `duplicate` auth scheme | RuleSetAuthSchemes
+[WARNING] example#FizzBuzz: Did not find a validator for the `duplicate` auth scheme | RuleSetAuthSchemes
+[ERROR] example#FizzBuzz: Found duplicate `name` of `duplicate` in the `authSchemes` list | RuleSetAuthSchemes
diff --git a/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/invalid-duplicate-auth-name.smithy b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/invalid-duplicate-auth-name.smithy
new file mode 100644
index 000000000..669494346
--- /dev/null
+++ b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/invalid-duplicate-auth-name.smithy
@@ -0,0 +1,70 @@
+$version: "2.0"
+
+namespace example
+
+use smithy.rules#clientContextParams
+use smithy.rules#endpointRuleSet
+use smithy.rules#endpointTests
+
+@clientContextParams(
+    bar: {type: "string", documentation: "a client string parameter"}
+)
+@endpointRuleSet({
+    version: "1.0",
+    parameters: {
+        bar: {
+            type: "string",
+            documentation: "docs"
+        }
+        endpoint: {
+            type: "string",
+            builtIn: "SDK::Endpoint",
+            required: true,
+            default: "asdf"
+            documentation: "docs"
+        },
+    },
+    rules: [
+        {
+            "documentation": "Shows invalid duplicate authScheme names",
+            "conditions": [
+                {
+                    "fn": "isSet",
+                    "argv": [
+                        {
+                            "ref": "bar"
+                        }
+                    ]
+                }
+            ],
+            "endpoint": {
+                "url": "https://example.com/",
+                "properties": {
+                    "authSchemes": [
+                        {
+                            "name": "duplicate",
+                        },
+                        {
+                            "name": "duplicate",
+                        }
+                    ]
+                }
+            },
+            "type": "endpoint"
+        },
+        {
+            "conditions": [],
+            "documentation": "error fallthrough",
+            "error": "endpoint error",
+            "type": "error"
+        }
+    ]
+})
+service FizzBuzz {
+    version: "2022-01-01",
+    operations: [GetThing]
+}
+
+operation GetThing {
+    input := {}
+}
diff --git a/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/invalid-missing-auth-name.errors b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/invalid-missing-auth-name.errors
new file mode 100644
index 000000000..77d7a6784
--- /dev/null
+++ b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/invalid-missing-auth-name.errors
@@ -0,0 +1,3 @@
+[WARNING] example#FizzBuzz: This shape applies a trait that is unstable: smithy.rules#clientContextParams | UnstableTrait
+[WARNING] example#FizzBuzz: This shape applies a trait that is unstable: smithy.rules#endpointRuleSet | UnstableTrait
+[ERROR] example#FizzBuzz: Expected `authSchemes` to have a `name` key with a string value but it did not: `{}` | RuleSetAuthSchemes
diff --git a/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/invalid-missing-auth-name.smithy b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/invalid-missing-auth-name.smithy
new file mode 100644
index 000000000..dd643a2e4
--- /dev/null
+++ b/smithy-rules-engine/src/test/resources/software/amazon/smithy/rulesengine/language/errorfiles/invalid/invalid-missing-auth-name.smithy
@@ -0,0 +1,65 @@
+$version: "2.0"
+
+namespace example
+
+use smithy.rules#clientContextParams
+use smithy.rules#endpointRuleSet
+use smithy.rules#endpointTests
+
+@clientContextParams(
+    bar: {type: "string", documentation: "a client string parameter"}
+)
+@endpointRuleSet({
+    version: "1.0",
+    parameters: {
+        bar: {
+            type: "string",
+            documentation: "docs"
+        }
+        endpoint: {
+            type: "string",
+            builtIn: "SDK::Endpoint",
+            required: true,
+            default: "asdf"
+            documentation: "docs"
+        },
+    },
+    rules: [
+        {
+            "documentation": "Shows invalid auth without a signing name",
+            "conditions": [
+                {
+                    "fn": "isSet",
+                    "argv": [
+                        {
+                            "ref": "bar"
+                        }
+                    ]
+                }
+            ],
+            "endpoint": {
+                "url": "https://example.com/",
+                "properties": {
+                    "authSchemes": [
+                        {}
+                    ]
+                }
+            },
+            "type": "endpoint"
+        },
+        {
+            "conditions": [],
+            "documentation": "error fallthrough",
+            "error": "endpoint error",
+            "type": "error"
+        }
+    ]
+})
+service FizzBuzz {
+    version: "2022-01-01",
+    operations: [GetThing]
+}
+
+operation GetThing {
+    input := {}
+}