Skip to content

Commit

Permalink
Elixir: Padlock verification is case-insensitive
Browse files Browse the repository at this point in the history
Resolved an issue where padlock values sent using lowercase hex values were not
comparing properly. Added tests for the case as well as an additional validation
doctest.

A later update will add this to the integration test suite as a variable.

Signed-off-by: Austin Ziegler <aziegler@kineticcommerce.com>
  • Loading branch information
halostatue committed Sep 5, 2023
1 parent d30c72d commit d38823f
Show file tree
Hide file tree
Showing 9 changed files with 105 additions and 27 deletions.
9 changes: 8 additions & 1 deletion elixir/Changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# App Identity for Elixir Changelog

## 1.3.2 / 2023-09-05

- Resolved an issue where padlock values sent using lowercase hex values were
not comparing properly. Added tests for the case as well as an additional
validation doctest.

## 1.3.1 / 2023-07-20

- Packages released to hex.pm by default include the `priv/` directory, but
Expand All @@ -10,7 +16,8 @@

## 1.3.0 / 2023-07-19

- Rename all spec uses of `String.t()` to `binary()` as
- Rename many spec uses of `String.t()` to `binary()` as we do not necessarily
require UTF-8.

- Extensive reorganization of the `AppIdentity.Plug` documentation to improve
the readability of the configuration.
Expand Down
18 changes: 18 additions & 0 deletions elixir/lib/app_identity/validation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ defmodule AppIdentity.Validation do
iex> AppIdentity.Validation.validate(:padlock, "")
{:error, "padlock must not be an empty string"}
iex> AppIdentity.Validation.validate(:padlock, "zzz")
{:error, "padlock must be a hex string value"}
iex> AppIdentity.Validation.validate(:padlock, "fff")
{:ok, "FFF"}
iex> AppIdentity.Validation.validate(:secret, "")
{:error, "secret must not be an empty binary string"}
Expand Down Expand Up @@ -108,6 +114,18 @@ defmodule AppIdentity.Validation do
{:error, "padlock must be a string value"}
end

def validate(:padlock, "") do
{:error, "padlock must not be an empty string"}
end

def validate(:padlock, value) do
if Regex.match?(~r/^[a-fA-F0-9]+$/, value) do
{:ok, String.upcase(value)}
else
{:error, "padlock must be a hex string value"}
end
end

def validate(:nonce, value) when not is_binary(value) do
{:error, "nonce must be a string value"}
end
Expand Down
2 changes: 1 addition & 1 deletion elixir/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule AppIdentity.MixProject do
def project do
[
app: :app_identity,
version: "1.3.1",
version: "1.3.2",
description: "Fast, lightweight, cryptographically secure app authentication",
elixir: "~> 1.10",
start_permanent: Mix.env() == :prod,
Expand Down
2 changes: 1 addition & 1 deletion elixir/support/support.ex
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ defmodule AppIdentity.Support do

hash
|> :crypto.hash(raw)
|> Base.encode16(case: :upper)
|> Base.encode16(case: Keyword.get(options, :case, :upper))
end

def build_proof(app, padlock, options \\ []) do
Expand Down
63 changes: 42 additions & 21 deletions elixir/test/app_identity_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -83,34 +83,42 @@ defmodule AppIdentityTest do
assert_verify_proof_telemetry_span(v2, proof, error: "proof and app version mismatch")
end

test "verify success v1", %{v1: v1, v1_app: v1_app} do
padlock = build_padlock(v1_app)
proof = build_proof(v1_app, padlock)
for padlock_case <- [:upper, :lower] do
test "verify success v1 (padlock case #{padlock_case})", %{v1: v1, v1_app: v1_app} do
padlock = build_padlock(v1_app, case: unquote(padlock_case))

assert {:ok, verified(v1_app)} == Subject.verify_proof(proof, v1)
assert_verify_proof_telemetry_span(v1, proof)
end
proof = build_proof(v1_app, padlock)

test "verify success v2 default fuzz", %{v2: v2, v2_app: v2_app} do
nonce = timestamp_nonce(-6, :minutes)
padlock = build_padlock(v2_app, nonce: nonce)
proof = build_proof(v2_app, padlock, version: 2, nonce: nonce)
assert {:ok, verified(v1_app)} == Subject.verify_proof(proof, v1)
assert_verify_proof_telemetry_span(v1, proof)
end

assert {:ok, verified(v2_app)} == Subject.verify_proof(proof, v2)
assert_verify_proof_telemetry_span(v2, proof)
end
test "verify success v2 default fuzz (padlock case #{padlock_case})", %{
v2: v2,
v2_app: v2_app
} do
nonce = timestamp_nonce(-6, :minutes)

padlock = build_padlock(v2_app, nonce: nonce, case: unquote(padlock_case))

proof = build_proof(v2_app, padlock, version: 2, nonce: nonce)

assert {:ok, verified(v2_app)} == Subject.verify_proof(proof, v2)
assert_verify_proof_telemetry_span(v2, proof)
end

test "verify success v2 custom fuzz" do
v2 = v2(fuzz: 300)
{:ok, v2_app} = AppIdentity.App.new(v2)
test "verify success v2 custom fuzz (padlock case #{padlock_case})" do
v2 = v2(fuzz: 300)
{:ok, v2_app} = AppIdentity.App.new(v2)

nonce = timestamp_nonce(-2, :minutes)
padlock = build_padlock(v2_app, nonce: nonce)
proof = build_proof(v2_app, padlock, version: 2, nonce: nonce)
nonce = timestamp_nonce(-2, :minutes)
padlock = build_padlock(v2_app, nonce: nonce, case: unquote(padlock_case))
proof = build_proof(v2_app, padlock, version: 2, nonce: nonce)

assert verified(v2_app) == Subject.verify_proof!(proof, v2)
assert verified(v2_app) == Subject.verify_proof!(proof, v2)

assert_verify_proof_telemetry_span(v2, proof)
assert_verify_proof_telemetry_span(v2, proof)
end
end

test "verify fail on different app ids", %{v1: v1, v1_app: v1_app} do
Expand All @@ -129,4 +137,17 @@ defmodule AppIdentityTest do

assert_verify_proof_telemetry_span(v1, proof, app: :none)
end

test "verify fail on non-hex padlock", %{v1: v1, v1_app: v1_app} do
padlock =
v1_app
|> build_padlock()
|> String.replace(~r/[A-F]/, "z")

proof = build_proof(v1_app, padlock)

assert :error == Subject.verify_proof(proof, v1)

assert_verify_proof_telemetry_span(v1, proof, error: "padlock must be a hex string value")
end
end
3 changes: 2 additions & 1 deletion ruby/lib/app_identity/validation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ def validate_padlock(padlock) # :nodoc:
raise AppIdentity::Error, "padlock must be a string" unless padlock.is_a?(String)
validate_not_empty(:padlock, padlock)
validate_no_colons(:padlock, padlock)
}
raise AppIdentity::Error, "padlock must be a hex string" unless padlock.match?(/^[a-f0-9]+$/i)
}.upcase
end

private
Expand Down
5 changes: 4 additions & 1 deletion ts/support/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ export const buildPadlock = (

const hash = createHash(versionAlgorithm(version as number))
hash.update(raw, 'utf-8')
return hash.digest('hex').toUpperCase()

return options['case'] === 'lower'
? hash.digest('hex').toLowerCase()
: hash.digest('hex').toUpperCase()
}

export const buildProof = (
Expand Down
2 changes: 1 addition & 1 deletion ts/support/matchers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { App } from '../../src/app'

interface AppIdentityMatchers<R = unknown> {
// eslint-disable-next-line no-unused-vars
toBeVerified(expected: App): R
toBeVerified(expected: App | null): R
}

declare global {
Expand Down
28 changes: 28 additions & 0 deletions ts/test/app_identity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,34 @@ test('verify success v2 custom fuzz', () => {
expect(verifyProofWithDiagnostic(proof, v2)).toBeVerified(app)
})

test('verify success v1 (lowercase proof)', () => {
const v1 = Support.v1Input()
const app = new App(v1)
const padlock = Support.buildPadlock(app, { case: 'lower' })
const proof = Support.buildProof(app, padlock)

expect(verifyProofWithDiagnostic(proof, v1)).toBeVerified(app)
})

test('verify success v2 default fuzz (lowercase proof)', () => {
const v2 = Support.v2Input()
const app = new App(v2)
const nonce = Support.timestampNonce(-6, 'minutes')
const padlock = Support.buildPadlock(app, { nonce })
const proof = Support.buildProof(app, padlock, { nonce, case: 'lower' })
expect(verifyProofWithDiagnostic(proof, v2)).toBeVerified(app)
})

test('verify success v2 custom fuzz (lowercase proof)', () => {
const v2 = Support.v2Input(300)
const app = new App(v2)
const nonce = Support.timestampNonce(-2, 'minutes')
const padlock = Support.buildPadlock(app, { nonce })
const proof = Support.buildProof(app, padlock, { nonce, case: 'lower' })

expect(verifyProofWithDiagnostic(proof, v2)).toBeVerified(app)
})

test('verify fail on different app ids', () => {
const v1 = Support.v1Input()
const app = new App(v1)
Expand Down

0 comments on commit d38823f

Please sign in to comment.