Repository: hyperledger/aries-cloudagent-python

Issue:
Timezone handling in W3C Standard VC Credentials and Proofs
Two issues have come up with time handling in ACA-Py around timestamps related to W3C Standard VCs and VPs:

1. By default, the python ISO date formatter does not include the timezone and we should include the timezone. Please update places where we are writing date to include a timezone in all cases.
2. The ISO860 format, as required in the W3C VC Spec., does not require a timezone. As such, in reading dates, make sure dates with and without a timezone can be handled.  Where timezone is not included, I think the right thing is to assume UTC.  Happy to have that confirmed on rocketchat.

Thanks


Discussion:

Resolving patch:
diff --git a/aries_cloudagent/vc/ld_proofs/suites/bbs_bls_signature_2020.py b/aries_cloudagent/vc/ld_proofs/suites/bbs_bls_signature_2020.py
--- a/aries_cloudagent/vc/ld_proofs/suites/bbs_bls_signature_2020.py
+++ b/aries_cloudagent/vc/ld_proofs/suites/bbs_bls_signature_2020.py
@@ -1,6 +1,7 @@
 """BbsBlsSignature2020 class."""
 
-from datetime import datetime
+from datetime import datetime, timezone
+from pytz import utc
 from typing import List, Union
 
 from ....wallet.util import b64_to_bytes, bytes_to_b64
@@ -59,7 +60,9 @@ async def create_proof(
         # Set created if not already set
         if not proof.get("created"):
             # Use class date, or now
-            date = self.date or datetime.now()
+            date = self.date or datetime.now(timezone.utc)
+            if not date.tzinfo:
+                date = utc.localize(date)
             proof["created"] = date.isoformat()
 
         # Allow purpose to update the proof; the `proof` is in the
diff --git a/aries_cloudagent/vc/ld_proofs/suites/linked_data_signature.py b/aries_cloudagent/vc/ld_proofs/suites/linked_data_signature.py
--- a/aries_cloudagent/vc/ld_proofs/suites/linked_data_signature.py
+++ b/aries_cloudagent/vc/ld_proofs/suites/linked_data_signature.py
@@ -1,8 +1,9 @@
 """Linked Data Signature class."""
 
 from abc import abstractmethod, ABCMeta
-from datetime import datetime
+from datetime import datetime, timezone
 from hashlib import sha256
+from pytz import utc
 from typing import Union
 
 from ..constants import SECURITY_CONTEXT_URL
@@ -97,7 +98,9 @@ async def create_proof(
         # Set created if not already set
         if not proof.get("created"):
             # Use class date, or now
-            date = self.date or datetime.now()
+            date = self.date or datetime.now(timezone.utc)
+            if not date.tzinfo:
+                date = utc.localize(date)
             proof["created"] = date.isoformat()
 
         # Allow purpose to update the proof; the `proof` is in the
diff --git a/aries_cloudagent/vc/vc_ld/models/credential.py b/aries_cloudagent/vc/vc_ld/models/credential.py
--- a/aries_cloudagent/vc/vc_ld/models/credential.py
+++ b/aries_cloudagent/vc/vc_ld/models/credential.py
@@ -1,6 +1,7 @@
 """Verifiable Credential marshmallow schema classes."""
 
 from datetime import datetime
+from pytz import utc
 from typing import List, Optional, Union
 
 from marshmallow import INCLUDE, fields, post_dump, ValidationError
@@ -168,6 +169,8 @@ def issuance_date(self):
     def issuance_date(self, date: Union[str, datetime]):
         """Setter for issuance date."""
         if isinstance(date, datetime):
+            if not date.tzinfo:
+                date = utc.localize(date)
             date = date.isoformat()
 
         self._issuance_date = date
@@ -181,6 +184,8 @@ def expiration_date(self):
     def expiration_date(self, date: Union[str, datetime, None]):
         """Setter for expiration date."""
         if isinstance(date, datetime):
+            if not date.tzinfo:
+                date = utc.localize(date)
             date = date.isoformat()
 
         self._expiration_date = date


Test patch:
diff --git a/aries_cloudagent/vc/ld_proofs/tests/test_doc.py b/aries_cloudagent/vc/ld_proofs/tests/test_doc.py
--- a/aries_cloudagent/vc/ld_proofs/tests/test_doc.py
+++ b/aries_cloudagent/vc/ld_proofs/tests/test_doc.py
@@ -30,10 +30,10 @@
     "image": "https://manu.sporny.org/images/manu.png",
     "proof": {
         "proofPurpose": "assertionMethod",
-        "created": "2019-12-11T03:50:55",
+        "created": "2019-12-11T03:50:55+00:00",
         "type": "Ed25519Signature2018",
         "verificationMethod": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL",
-        "jws": "eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0IjogWyJiNjQiXX0..Q6amIrxGiSbM7Ce6DxlfwLCjVcYyclas8fMxaecspXFUcFW9DAAxKzgHx93FWktnlZjM_biitkMgZdStgvivAQ",
+        "jws": "eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0IjogWyJiNjQiXX0..FX3xSAN3BxpHnclqtiCKsHa3f6O1pi_fulEoCNs2YQplYBU7lYSdnIm1BoPo_YCw8AS25pOQo1ufW05mXJlxAw",
     },
 }
 
@@ -131,10 +131,10 @@
         "image": "https://manu.sporny.org/images/manu.png",
         "proof": {
             "proofPurpose": "assertionMethod",
-            "created": "2019-12-11T03:50:55",
+            "created": "2019-12-11T03:50:55+00:00",
             "type": "Ed25519Signature2018",
             "verificationMethod": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL",
-            "jws": "eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0IjogWyJiNjQiXX0..Q6amIrxGiSbM7Ce6DxlfwLCjVcYyclas8fMxaecspXFUcFW9DAAxKzgHx93FWktnlZjM_biitkMgZdStgvivAQ",
+            "jws": "eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0IjogWyJiNjQiXX0..FX3xSAN3BxpHnclqtiCKsHa3f6O1pi_fulEoCNs2YQplYBU7lYSdnIm1BoPo_YCw8AS25pOQo1ufW05mXJlxAw",
         },
     },
     results=[
@@ -151,10 +151,10 @@
                     },
                 ],
                 "proofPurpose": "assertionMethod",
-                "created": "2019-12-11T03:50:55",
+                "created": "2019-12-11T03:50:55+00:00",
                 "type": "Ed25519Signature2018",
                 "verificationMethod": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL",
-                "jws": "eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0IjogWyJiNjQiXX0..Q6amIrxGiSbM7Ce6DxlfwLCjVcYyclas8fMxaecspXFUcFW9DAAxKzgHx93FWktnlZjM_biitkMgZdStgvivAQ",
+                "jws": "eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0IjogWyJiNjQiXX0..FX3xSAN3BxpHnclqtiCKsHa3f6O1pi_fulEoCNs2YQplYBU7lYSdnIm1BoPo_YCw8AS25pOQo1ufW05mXJlxAw",
             },
             purpose_result=PurposeResult(
                 valid=True,
diff --git a/aries_cloudagent/vc/ld_proofs/tests/test_ld_proofs.py b/aries_cloudagent/vc/ld_proofs/tests/test_ld_proofs.py
--- a/aries_cloudagent/vc/ld_proofs/tests/test_ld_proofs.py
+++ b/aries_cloudagent/vc/ld_proofs/tests/test_ld_proofs.py
@@ -1,6 +1,6 @@
 import pytest
 
-from datetime import datetime
+from datetime import datetime, timezone
 
 from asynctest import TestCase
 
@@ -65,7 +65,7 @@ async def test_sign_Ed25519Signature2018(self):
                 key_type=KeyType.ED25519,
                 public_key_base58=self.ed25519_key_info.verkey,
             ),
-            date=datetime.strptime("2019-12-11T03:50:55Z", "%Y-%m-%dT%H:%M:%SZ"),
+            date=datetime(2019, 12, 11, 3, 50, 55, 0, timezone.utc),
         )
         signed = await sign(
             document=DOC_TEMPLATE,
@@ -103,7 +103,7 @@ async def test_sign_BbsBlsSignature2020(self):
                 key_type=KeyType.BLS12381G2,
                 public_key_base58=self.bls12381g2_key_info.verkey,
             ),
-            date=datetime.strptime("2019-12-11T03:50:55Z", "%Y-%m-%dT%H:%M:%SZ"),
+            date=datetime(2019, 12, 11, 3, 50, 55, 0),
         )
         signed = await sign(
             document=DOC_TEMPLATE_BBS,
diff --git a/aries_cloudagent/vc/vc_ld/models/tests/test_credential.py b/aries_cloudagent/vc/vc_ld/models/tests/test_credential.py
--- a/aries_cloudagent/vc/vc_ld/models/tests/test_credential.py
+++ b/aries_cloudagent/vc/vc_ld/models/tests/test_credential.py
@@ -1,4 +1,4 @@
-from datetime import datetime
+from datetime import datetime, timezone
 from unittest import TestCase
 
 from marshmallow.utils import INCLUDE
@@ -149,17 +149,29 @@ def test_properties(self):
         with self.assertRaises(Exception):
             credential.issuer = {"not-id": "not-id"}
 
-        date = datetime.now()
+        date = datetime.now(timezone.utc)
         credential.issuance_date = date
         assert credential.issuance_date == date.isoformat()
         credential.issuance_date = date.isoformat()
         assert credential.issuance_date == date.isoformat()
+        date = datetime(2019, 12, 11, 3, 50, 55, 0)
+        credential.issuance_date = date
+        assert (
+            credential.issuance_date
+            == datetime(2019, 12, 11, 3, 50, 55, 0, timezone.utc).isoformat()
+        )
 
-        date = datetime.now()
+        date = datetime.now(timezone.utc)
         credential.expiration_date = date
         assert credential.expiration_date == date.isoformat()
         credential.expiration_date = date.isoformat()
         assert credential.expiration_date == date.isoformat()
+        date = datetime(2019, 12, 11, 3, 50, 55, 0)
+        credential.expiration_date = date
+        assert (
+            credential.expiration_date
+            == datetime(2019, 12, 11, 3, 50, 55, 0, timezone.utc).isoformat()
+        )
 
         assert not credential.credential_subject
         assert not credential.credential_subject_ids
diff --git a/aries_cloudagent/vc/vc_ld/tests/test_credential.py b/aries_cloudagent/vc/vc_ld/tests/test_credential.py
--- a/aries_cloudagent/vc/vc_ld/tests/test_credential.py
+++ b/aries_cloudagent/vc/vc_ld/tests/test_credential.py
@@ -37,9 +37,9 @@
     "proof": {
         "type": "Ed25519Signature2018",
         "verificationMethod": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL",
-        "created": "2019-12-11T03:50:55",
+        "created": "2019-12-11T03:50:55+00:00",
         "proofPurpose": "assertionMethod",
-        "jws": "eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0IjogWyJiNjQiXX0..lKJU0Df_keblRKhZAS9Qq6zybm-HqUXNVZ8vgEPNTAjQKBhQDxvXNo7nvtUBb_Eq1Ch6YBKY7UBAjg6iBX5qBQ",
+        "jws": "eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0IjogWyJiNjQiXX0..tELgWSCo0jSOKLpHWLOe2ZWj6WxLFc6FcqBZzgQFdcaawC5mARYmdr0XA37TMUp2Q9jUwriNP4gQm3GFIHV9BQ",
     },
 }
 
@@ -80,9 +80,9 @@
     "proof": {
         "type": "BbsBlsSignature2020",
         "verificationMethod": "did:key:zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa#zUC72Q7XD4PE4CrMiDVXuvZng3sBvMmaGgNeTUJuzavH2BS7ThbHL9FhsZM9QYY5fqAQ4MB8M9oudz3tfuaX36Ajr97QRW7LBt6WWmrtESe6Bs5NYzFtLWEmeVtvRYVAgjFcJSa",
-        "created": "2019-12-11T03:50:55",
+        "created": "2019-12-11T03:50:55+00:00",
         "proofPurpose": "assertionMethod",
-        "proofValue": "saCdwWlzoYB0Ayo7xthCGK462T4x95lkI7yINra+r/DRY8PH4udviBebYIMA0pHkFX/nW+ilcdipr8jdN+WbHElg2wIrpWEqdvbT/vrjTWM2iXS7MmsMvpQbLfJVohDCBrm4b6BuE6QYO4Va6tYsKw==",
+        "proofValue": "ke/4UsKMbYyFQP2WAsQs0QGJ0PCfvN3tWMoD8oBjroY3mrgXA7FmUqVy0HaZIoKvFHl1FnE1jqh0OnqErooFBMTu0a4qe3klziRSuRuLdUhgbgzOJeJiWPlrs6Y5/D5d0CRp8ifPj7xZBIy3Iuoz4g==",
     },
 }
 
@@ -107,9 +107,9 @@
         "proof": {
             "type": "Ed25519Signature2018",
             "verificationMethod": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL",
-            "created": "2019-12-11T03:50:55",
+            "created": "2019-12-11T03:50:55+00:00",
             "proofPurpose": "assertionMethod",
-            "jws": "eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0IjogWyJiNjQiXX0..lKJU0Df_keblRKhZAS9Qq6zybm-HqUXNVZ8vgEPNTAjQKBhQDxvXNo7nvtUBb_Eq1Ch6YBKY7UBAjg6iBX5qBQ",
+            "jws": "eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0IjogWyJiNjQiXX0..tELgWSCo0jSOKLpHWLOe2ZWj6WxLFc6FcqBZzgQFdcaawC5mARYmdr0XA37TMUp2Q9jUwriNP4gQm3GFIHV9BQ",
         },
     },
     results=[
@@ -122,9 +122,9 @@
                 ],
                 "type": "Ed25519Signature2018",
                 "verificationMethod": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL",
-                "created": "2019-12-11T03:50:55",
+                "created": "2019-12-11T03:50:55+00:00",
                 "proofPurpose": "assertionMethod",
-                "jws": "eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0IjogWyJiNjQiXX0..lKJU0Df_keblRKhZAS9Qq6zybm-HqUXNVZ8vgEPNTAjQKBhQDxvXNo7nvtUBb_Eq1Ch6YBKY7UBAjg6iBX5qBQ",
+                "jws": "eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0IjogWyJiNjQiXX0..tELgWSCo0jSOKLpHWLOe2ZWj6WxLFc6FcqBZzgQFdcaawC5mARYmdr0XA37TMUp2Q9jUwriNP4gQm3GFIHV9BQ",
             },
             purpose_result=PurposeResult(
                 valid=True,
@@ -188,9 +188,9 @@
             "proof": {
                 "type": "Ed25519Signature2018",
                 "verificationMethod": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL",
-                "created": "2019-12-11T03:50:55",
+                "created": "2019-12-11T03:50:55+00:00",
                 "proofPurpose": "assertionMethod",
-                "jws": "eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0IjogWyJiNjQiXX0..lKJU0Df_keblRKhZAS9Qq6zybm-HqUXNVZ8vgEPNTAjQKBhQDxvXNo7nvtUBb_Eq1Ch6YBKY7UBAjg6iBX5qBQ",
+                "jws": "eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0IjogWyJiNjQiXX0..tELgWSCo0jSOKLpHWLOe2ZWj6WxLFc6FcqBZzgQFdcaawC5mARYmdr0XA37TMUp2Q9jUwriNP4gQm3GFIHV9BQ",
             },
         }
     ],
@@ -221,18 +221,18 @@
             "proof": {
                 "type": "Ed25519Signature2018",
                 "verificationMethod": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL",
-                "created": "2019-12-11T03:50:55",
+                "created": "2019-12-11T03:50:55+00:00",
                 "proofPurpose": "assertionMethod",
-                "jws": "eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0IjogWyJiNjQiXX0..lKJU0Df_keblRKhZAS9Qq6zybm-HqUXNVZ8vgEPNTAjQKBhQDxvXNo7nvtUBb_Eq1Ch6YBKY7UBAjg6iBX5qBQ",
+                "jws": "eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0IjogWyJiNjQiXX0..tELgWSCo0jSOKLpHWLOe2ZWj6WxLFc6FcqBZzgQFdcaawC5mARYmdr0XA37TMUp2Q9jUwriNP4gQm3GFIHV9BQ",
             },
         }
     ],
     "proof": {
         "type": "Ed25519Signature2018",
         "verificationMethod": "did:key:z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL#z6Mkgg342Ycpuk263R9d8Aq6MUaxPn1DDeHyGo38EefXmgDL",
-        "created": "2020-12-11T03:50:55",
+        "created": "2020-12-11T03:50:55+00:00",
         "proofPurpose": "authentication",
         "challenge": "2b1bbff6-e608-4368-bf84-67471b27e41c",
-        "jws": "eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0IjogWyJiNjQiXX0..IOjNkdpk4lF8uU8N7n0OMc3opqU1wtCTu3KbJcdDIKvSt6QLEy-ofRDVgN2xo-21yxzx36mXVjiilWdB6A-dDg",
+        "jws": "eyJhbGciOiAiRWREU0EiLCAiYjY0IjogZmFsc2UsICJjcml0IjogWyJiNjQiXX0..vLk6iXrPxdt4xWq_Lwnd85cy17Npol1ALxYoMhZrKpmkaWDOoz_exP1ggurPik7Rhpa7a82AfrT0OnigJnJsAQ",
     },
 }