Skip to content

Commit

Permalink
Merge pull request #344 from sklump/verify-raw-vs-encoding
Browse files Browse the repository at this point in the history
Verify raw vs encoding
  • Loading branch information
andrewwhitehead authored Jan 28, 2020
2 parents e4c6426 + c802851 commit 0000f92
Show file tree
Hide file tree
Showing 6 changed files with 806 additions and 49 deletions.
2 changes: 1 addition & 1 deletion aries_cloudagent/issuer/indy.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
import indy.anoncreds

from ..core.error import BaseError
from ..messaging.util import encode

from .base import BaseIssuer
from .util import encode


class IssuerError(BaseError):
Expand Down
43 changes: 0 additions & 43 deletions aries_cloudagent/issuer/util.py

This file was deleted.

103 changes: 103 additions & 0 deletions aries_cloudagent/messaging/tests/test_util.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import re

from datetime import datetime, timezone
from unittest import mock, TestCase

from ..util import (
canon,
datetime_now,
datetime_to_str,
encode,
epoch_to_str,
I32_BOUND,
str_to_datetime,
str_to_epoch,
time_now
Expand Down Expand Up @@ -59,3 +64,101 @@ def test_epoch(self):

assert epoch_now == str_to_epoch(dt_now) and isinstance(epoch_now, int)
assert epoch_to_str(epoch_now) == datetime_to_str(dt_now.replace(microsecond=0))

def test_canon(self):
assert canon("a B c") == 'abc'
assert canon(None) is None
assert canon("") == ""
assert canon("canon") == "canon"

def test_encode(self):
values = {
"address2": {
"raw": "101 Wilson Lane",
"encoded": "68086943237164982734333428280784300550565381723532936263016368251445461241953"
},
"zip": {
"raw": "87121",
"encoded": "87121"
},
"city": {
"raw": "SLC",
"encoded": "101327353979588246869873249766058188995681113722618593621043638294296500696424"
},
"address1": {
"raw": "101 Tela Lane",
"encoded": "63690509275174663089934667471948380740244018358024875547775652380902762701972"
},
"state": {
"raw": "UT",
"encoded": "93856629670657830351991220989031130499313559332549427637940645777813964461231"
},
"Empty": {
"raw": "",
"encoded": "102987336249554097029535212322581322789799900648198034993379397001115665086549"
},
"Null": {
"raw": None,
"encoded": "99769404535520360775991420569103450442789945655240760487761322098828903685777"
},
"str None": {
"raw": "None",
"encoded": "99769404535520360775991420569103450442789945655240760487761322098828903685777"
},
"bool True": {
"raw": True,
"encoded": "1"
},
"bool False": {
"raw": False,
"encoded": "0",
},
"str True": {
"raw": "True",
"encoded": "27471875274925838976481193902417661171675582237244292940724984695988062543640"
},
"str False": {
"raw": "False",
"encoded": "43710460381310391454089928988014746602980337898724813422905404670995938820350"
},
"max i32": {
"raw": 2147483647,
"encoded": "2147483647"
},
"max i32 + 1": {
"raw": 2147483648,
"encoded": "26221484005389514539852548961319751347124425277437769688639924217837557266135"
},
"min i32": {
"raw": -2147483648,
"encoded": "-2147483648"
},
"min i32 - 1": {
"raw": -2147483649,
"encoded": "68956915425095939579909400566452872085353864667122112803508671228696852865689"
},
"float 0.0": {
"raw": 0.0,
"encoded": "62838607218564353630028473473939957328943626306458686867332534889076311281879"
},
"str 0.0": {
"raw": "0.0",
"encoded": "62838607218564353630028473473939957328943626306458686867332534889076311281879"
},
"chr 0": {
"raw": chr(0),
"encoded": "49846369543417741186729467304575255505141344055555831574636310663216789168157"
},
"chr 1": {
"raw": chr(1),
"encoded": "34356466678672179216206944866734405838331831190171667647615530531663699592602"
},
"chr 2": {
"raw": chr(2),
"encoded": "99398763056634537812744552006896172984671876672520535998211840060697129507206"
}
}

for tag, value in values.items():
print('{}: {} -> {}'.format(tag, value["raw"], encode(value["raw"])))
assert encode(value["raw"]) == value["encoded"]
44 changes: 41 additions & 3 deletions aries_cloudagent/messaging/util.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
"""Utils for messages."""

from datetime import datetime, timedelta, timezone

import logging
from math import floor
import re
from typing import Union

from datetime import datetime, timedelta, timezone
from hashlib import sha256
from math import floor
from typing import Any, Union


LOGGER = logging.getLogger(__name__)
I32_BOUND = 2 ** 31


def datetime_to_str(dt: Union[str, datetime]) -> str:
Expand Down Expand Up @@ -98,6 +103,39 @@ def time_now() -> str:
return datetime_to_str(datetime_now())


def encode(orig: Any) -> str:
"""
Encode a credential value as an int.
Encode credential attribute value, purely stringifying any int32
and leaving numeric int32 strings alone, but mapping any other
input to a stringified 256-bit (but not 32-bit) integer.
Predicates in indy-sdk operate
on int32 values properly only when their encoded values match their raw values.
Args:
orig: original value to encode
Returns:
encoded value
"""

if isinstance(orig, int) and -I32_BOUND <= orig < I32_BOUND:
return str(int(orig)) # python bools are ints

try:
i32orig = int(str(orig)) # don't encode floats as ints
if -I32_BOUND <= i32orig < I32_BOUND:
return str(i32orig)
except (ValueError, TypeError):
pass

rv = int.from_bytes(sha256(str(orig).encode()).digest(), "big")

return str(rv)


def canon(raw_attr_name: str) -> str:
"""
Canonicalize input attribute name for indy proofs and credential offers.
Expand Down
71 changes: 71 additions & 0 deletions aries_cloudagent/verifier/indy.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import indy.anoncreds

from ..messaging.util import canon, encode
from .base import BaseVerifier


Expand All @@ -22,6 +23,69 @@ def __init__(self, wallet):
self.logger = logging.getLogger(__name__)
self.wallet = wallet

@staticmethod
def check_encoding(pres_req: dict, pres: dict) -> bool:
"""
Check for tampering in presentation.
Visit encoded attribute values against raw, and predicate bounds,
in presentation, cross-reference against presentation request.
Args:
pres_req: presentation request
pres: corresponding presentation
Returns:
True for OK, False for tamper evidence
"""
for (uuid, req_pred) in pres_req["requested_predicates"].items():
canon_attr = canon(req_pred["name"])
for ge_proof in pres["proof"]["proofs"][
pres["requested_proof"]["predicates"][
uuid
]["sub_proof_index"]
]["primary_proof"]["ge_proofs"]:
pred = ge_proof["predicate"]
if pred["attr_name"] == canon_attr:
if pred["value"] != req_pred["p_value"]:
return False
break
else:
return False # missing predicate in proof

for (uuid, req_attr) in pres_req["requested_attributes"].items():
if "name" in req_attr:
pres_req_attr_spec = {
req_attr["name"]: pres["requested_proof"]["revealed_attrs"][uuid]
}
else:
group_spec = pres["requested_proof"].get(
"revealed_attr_groups",
{}
).get(uuid)
if group_spec is None:
return False
pres_req_attr_spec = {
attr: {
"sub_proof_index": group_spec["sub_proof_index"],
**pres["requested_proof"]["revealed_attr_groups"][
uuid
]["values"][attr]
} for attr in req_attr["names"]
}

for (attr, spec) in pres_req_attr_spec.items():
primary_enco = pres["proof"]["proofs"][
spec["sub_proof_index"]
]["primary_proof"]["eq_proof"]["revealed_attrs"].get(canon(attr))
if primary_enco != spec["encoded"]:
return False
if primary_enco != encode(spec["raw"]):
return False

return True

async def verify_presentation(
self, presentation_request, presentation, schemas, credential_definitions
) -> bool:
Expand All @@ -35,6 +99,13 @@ async def verify_presentation(
credential_definitions: credential definition data
"""

if not IndyVerifier.check_encoding(presentation_request, presentation):
self.logger.error(
f"Presentation on nonce={presentation_request['nonce']} "
"demonstrates tampering with raw values"
)
return False

verified = await indy.anoncreds.verifier_verify_proof(
json.dumps(presentation_request),
json.dumps(presentation),
Expand Down
Loading

0 comments on commit 0000f92

Please sign in to comment.