raster-integrity 0.0.2__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- raster_integrity/__init__.py +1 -0
- raster_integrity/integrity.py +45 -0
- raster_integrity/setup.py +122 -0
- raster_integrity/technique_factory.py +92 -0
- raster_integrity/techniques/__init__.py +0 -0
- raster_integrity/techniques/archiving.py +99 -0
- raster_integrity/techniques/file_hash.py +73 -0
- raster_integrity/techniques/merkle_tree_hash.py +262 -0
- raster_integrity/techniques/technique.py +17 -0
- raster_integrity/techniques/timestamping.py +174 -0
- raster_integrity/verify.py +55 -0
- raster_integrity-0.0.2.dist-info/METADATA +85 -0
- raster_integrity-0.0.2.dist-info/RECORD +18 -0
- raster_integrity-0.0.2.dist-info/WHEEL +6 -0
- raster_integrity-0.0.2.dist-info/entry_points.txt +2 -0
- raster_integrity-0.0.2.dist-info/licenses/AUTHORS.rst +13 -0
- raster_integrity-0.0.2.dist-info/licenses/LICENSE +22 -0
- raster_integrity-0.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.2"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
from osgeo import gdal
|
|
4
|
+
from technique_factory import TechniqueFactory
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def parse_tech_args(item):
|
|
8
|
+
if "=" not in item:
|
|
9
|
+
raise argparse.ArgumentTypeError(f"Expected KEY=VALUE, given: {item}")
|
|
10
|
+
key, value = item.split("=", 1)
|
|
11
|
+
return key.strip(), value.strip()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main():
|
|
15
|
+
parser = argparse.ArgumentParser(
|
|
16
|
+
description="Execute integrity technique on raster file",
|
|
17
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
18
|
+
)
|
|
19
|
+
parser.add_argument("--input", "-i", required=True, help="Path of raster file")
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"--arg",
|
|
22
|
+
"-a",
|
|
23
|
+
action="append",
|
|
24
|
+
type=parse_tech_args,
|
|
25
|
+
metavar="KEY=VALUE",
|
|
26
|
+
help="Extra argument for the technique, repeatable (e.g. -a precision=2)",
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"--technique", "-t", required=True, help=TechniqueFactory.help_text()
|
|
30
|
+
)
|
|
31
|
+
args = parser.parse_args()
|
|
32
|
+
|
|
33
|
+
if not os.path.exists(args.input):
|
|
34
|
+
parser.error(f"File not found: {args.input}")
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
method = TechniqueFactory.create(args.technique, file=args.input)
|
|
38
|
+
except (ValueError, TypeError) as e:
|
|
39
|
+
parser.error(str(e))
|
|
40
|
+
|
|
41
|
+
print(method.create_integrity(**dict(args.arg or [])))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if __name__ == "__main__":
|
|
45
|
+
main()
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from osgeo import gdal
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import argparse, os, sys
|
|
4
|
+
|
|
5
|
+
gdal.UseExceptions()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def create_metadata_file(file_path, metadata=None):
|
|
9
|
+
if os.path.exists(f"{file_path}.aux.xml"):
|
|
10
|
+
os.remove(f"{file_path}.aux.xml")
|
|
11
|
+
if metadata is not None:
|
|
12
|
+
for key, value in metadata.items():
|
|
13
|
+
if not isinstance(key, str) or not key.strip():
|
|
14
|
+
raise ValueError(f"Invalid metadata-key: {key!r}")
|
|
15
|
+
if value is None:
|
|
16
|
+
raise ValueError(f"Metadata-value for '{key}' is empty")
|
|
17
|
+
|
|
18
|
+
gdalPamEnabled = gdal.GetConfigOption("GDAL_PAM_ENABLED")
|
|
19
|
+
gdalGeorefSrcs = gdal.GetConfigOption("GDAL_GEOREF_SOURCES")
|
|
20
|
+
|
|
21
|
+
gdal.SetConfigOption("GDAL_PAM_ENABLED", "YES")
|
|
22
|
+
gdal.SetConfigOption("GDAL_GEOREF_SOURCES", "PAM,INTERNAL")
|
|
23
|
+
|
|
24
|
+
ds = gdal.Open(file_path, gdal.GA_ReadOnly)
|
|
25
|
+
|
|
26
|
+
srs = ds.GetProjection()
|
|
27
|
+
gt = ds.GetGeoTransform()
|
|
28
|
+
band = ds.GetRasterBand(1)
|
|
29
|
+
nodata = band.GetNoDataValue()
|
|
30
|
+
|
|
31
|
+
if nodata == None:
|
|
32
|
+
max_val = band.GetMaximum()
|
|
33
|
+
min_val = band.GetMinimum()
|
|
34
|
+
# Compute if not available
|
|
35
|
+
if max_val is None or min_val is None:
|
|
36
|
+
# False means calculate on full resolution
|
|
37
|
+
min_val, max_val = band.ComputeRasterMinMax(False)
|
|
38
|
+
|
|
39
|
+
# make decision - first max at last min (min is prior)
|
|
40
|
+
if max_val > 99999:
|
|
41
|
+
nodata = max_val
|
|
42
|
+
if min_val < -9999:
|
|
43
|
+
nodata = min_val
|
|
44
|
+
|
|
45
|
+
ds = None
|
|
46
|
+
|
|
47
|
+
ds2 = gdal.OpenEx(
|
|
48
|
+
file_path,
|
|
49
|
+
gdal.OF_RASTER,
|
|
50
|
+
open_options=["GEOREF_SOURCES=PAM"], # PAM has priority
|
|
51
|
+
)
|
|
52
|
+
if metadata:
|
|
53
|
+
for key, value in metadata.items():
|
|
54
|
+
ds2.SetMetadataItem(key, str(value))
|
|
55
|
+
ds2.SetProjection(srs)
|
|
56
|
+
ds2.SetGeoTransform(gt)
|
|
57
|
+
if nodata is not None:
|
|
58
|
+
ds2.GetRasterBand(1).SetNoDataValue(nodata)
|
|
59
|
+
ds2.GetRasterBand(1).ComputeStatistics(False)
|
|
60
|
+
|
|
61
|
+
ds2.FlushCache()
|
|
62
|
+
ds2 = None
|
|
63
|
+
gdal.SetConfigOption("GDAL_PAM_ENABLED", gdalPamEnabled)
|
|
64
|
+
gdal.SetConfigOption("GDAL_GEOREF_SOURCES", gdalGeorefSrcs)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def parse_meta(item):
|
|
68
|
+
if "=" not in item:
|
|
69
|
+
raise argparse.ArgumentTypeError(f"Expected KEY=VALUE, given: {item}")
|
|
70
|
+
key, value = item.split("=", 1)
|
|
71
|
+
return key.strip(), value.strip()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def main():
|
|
75
|
+
parser = argparse.ArgumentParser(
|
|
76
|
+
description=(
|
|
77
|
+
"Enforce raster data to include metadata,"
|
|
78
|
+
"\n"
|
|
79
|
+
"ie: raster_integrity.setup -f raster.tif -m Corporation='CORP' Survey_Year=2025 Datum=LAT"
|
|
80
|
+
),
|
|
81
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
parser.add_argument(
|
|
85
|
+
"--meta",
|
|
86
|
+
"-m",
|
|
87
|
+
action="append",
|
|
88
|
+
type=parse_meta,
|
|
89
|
+
help="Metadata as KEY=VALUE (repeatable ie: -m Corporation='CORP' Survey_Year=2025 Datum=LAT)",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
parser.add_argument(
|
|
93
|
+
"--file", "-f", required=True, help="Raster file to create a metadatafile"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
args = parser.parse_args()
|
|
97
|
+
|
|
98
|
+
if not os.path.exists(args.file):
|
|
99
|
+
print(f"ERROR: File not found: {args.file}")
|
|
100
|
+
sys.exit(1)
|
|
101
|
+
metadata = dict(args.meta) if args.meta else None
|
|
102
|
+
create_metadata_file(args.file, metadata)
|
|
103
|
+
|
|
104
|
+
gdal.SetConfigOption("GDAL_PAM_ENABLED", "YES")
|
|
105
|
+
ds = gdal.Open(args.file, gdal.GA_ReadOnly)
|
|
106
|
+
band = ds.GetRasterBand(1)
|
|
107
|
+
band_metadata = band.GetMetadata()
|
|
108
|
+
print("+" + "-" * len(ds.GetDriver().GetName()) + "+")
|
|
109
|
+
print("|" + ds.GetDriver().GetName() + "|")
|
|
110
|
+
print("+" + "-" * len(ds.GetDriver().GetName()) + "+")
|
|
111
|
+
print(args.file)
|
|
112
|
+
print("-" * len(args.file))
|
|
113
|
+
print("DATASET METADATA")
|
|
114
|
+
print(ds.GetMetadata())
|
|
115
|
+
print("BAND 1 METADATA")
|
|
116
|
+
print(band_metadata)
|
|
117
|
+
ds.FlushCache()
|
|
118
|
+
ds = None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if __name__ == "__main__":
|
|
122
|
+
main()
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
technique_factory.py
|
|
3
|
+
=====================
|
|
4
|
+
Factory-implementation for creating of different types of
|
|
5
|
+
hashing- and validation techniques based on short codes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from techniques.file_hash import FileHash
|
|
10
|
+
from techniques.merkle_tree_hash import *
|
|
11
|
+
from techniques.archiving import *
|
|
12
|
+
from techniques.timestamping import *
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TechniqueType(Enum):
|
|
16
|
+
FILE_HASH = "fh"
|
|
17
|
+
MERKLE = "me"
|
|
18
|
+
MERKLE_ROBUST = "mr"
|
|
19
|
+
MERKLE_SQLITE = "ms"
|
|
20
|
+
BAGIT_ARCHIVING = "ba"
|
|
21
|
+
TIMESTAMPING = "ts"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TechniqueFactory:
|
|
25
|
+
_mapping = {
|
|
26
|
+
TechniqueType.FILE_HASH: FileHash,
|
|
27
|
+
TechniqueType.MERKLE: MerkleTreeInmemoryHash,
|
|
28
|
+
TechniqueType.MERKLE_ROBUST: MerkleTreeInmemoryRobustHash,
|
|
29
|
+
TechniqueType.MERKLE_SQLITE: MerkleTreeSqliteHash,
|
|
30
|
+
TechniqueType.BAGIT_ARCHIVING: BagItArchive,
|
|
31
|
+
TechniqueType.TIMESTAMPING: Timestamping,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def create(self, code: str, **kwargs):
|
|
36
|
+
try:
|
|
37
|
+
hash_type = TechniqueType(code)
|
|
38
|
+
except ValueError:
|
|
39
|
+
raise ValueError(
|
|
40
|
+
f"Unknown technique code '{code}'.\n"
|
|
41
|
+
f"Available codes:\n{self.help_text()}"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return self._mapping[hash_type](kwargs)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TechniqueFactory:
|
|
48
|
+
_mapping = {
|
|
49
|
+
TechniqueType.FILE_HASH: FileHash,
|
|
50
|
+
TechniqueType.MERKLE: MerkleTreeInmemoryHash,
|
|
51
|
+
TechniqueType.MERKLE_ROBUST: MerkleTreeInmemoryRobustHash,
|
|
52
|
+
TechniqueType.MERKLE_SQLITE: MerkleTreeSqliteHash,
|
|
53
|
+
TechniqueType.BAGIT_ARCHIVING: BagItArchive,
|
|
54
|
+
TechniqueType.TIMESTAMPING: Timestamping,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def create(cls, code: str, **kwargs):
|
|
59
|
+
try:
|
|
60
|
+
technique_type = TechniqueType(code)
|
|
61
|
+
except ValueError:
|
|
62
|
+
raise ValueError(
|
|
63
|
+
f"Unknown technique code '{code}'.\n"
|
|
64
|
+
f"Available codes:\n{cls.help_text()}"
|
|
65
|
+
) from None
|
|
66
|
+
|
|
67
|
+
return cls._mapping[technique_type](**kwargs)
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def help_text():
|
|
71
|
+
return (
|
|
72
|
+
"Techniques and their extra arguments (use -a KEY=VALUE):\n"
|
|
73
|
+
"\n"
|
|
74
|
+
" fh File hashing (digest of the file bytes)\n"
|
|
75
|
+
" algorithm hashlib algorithm [default: sha256]\n"
|
|
76
|
+
"\n"
|
|
77
|
+
" me Merkle tree with hashing (in memory)\n"
|
|
78
|
+
" algorithm hashlib algorithm [default: sha256]\n"
|
|
79
|
+
"\n"
|
|
80
|
+
" mr Merkle tree with robust hashing\n"
|
|
81
|
+
" algorithm hashlib algorithm [default: sha256]\n"
|
|
82
|
+
" precision decimals to round (int) [default: 3]\n"
|
|
83
|
+
"\n"
|
|
84
|
+
" ms Merkle tree with hashing, stored in SQLite (<file>.db)\n"
|
|
85
|
+
" algorithm hashlib algorithm [default: sha256]\n"
|
|
86
|
+
"\n"
|
|
87
|
+
" ba RFC 8493 BagIt archiving\n"
|
|
88
|
+
" root_hash fingerprint stored in the bag [required]\n"
|
|
89
|
+
"\n"
|
|
90
|
+
" ts RFC 3161 trusted timestamping\n"
|
|
91
|
+
" file file used for timestamping [required]\n"
|
|
92
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
archiving.py
|
|
3
|
+
===================
|
|
4
|
+
BagIt packaging for long-term archiving.
|
|
5
|
+
|
|
6
|
+
BagIt (RFC 8493) is a hierarchical file packaging format for the storage and
|
|
7
|
+
transfer of digital content. A "bag" is a directory containing the payload
|
|
8
|
+
files together with a manifest of checksums, so that the completeness and
|
|
9
|
+
integrity of the content can be verified at any time. This provides:
|
|
10
|
+
|
|
11
|
+
- Completeness check: all files in the manifest are present in the bag
|
|
12
|
+
- Integrity verification: checksums confirm that no file was altered
|
|
13
|
+
- Self-describing packages: metadata travels with the data
|
|
14
|
+
- Portability: plain files and text manifests, no special software required
|
|
15
|
+
|
|
16
|
+
Structure of a bag:
|
|
17
|
+
bag/
|
|
18
|
+
├── bagit.txt BagIt version and tag file encoding
|
|
19
|
+
├── bag-info.txt descriptive metadata (source, date, contact, ...)
|
|
20
|
+
├── manifest-sha256.txt checksums of all payload files
|
|
21
|
+
├── tagmanifest-sha256.txt checksums of the tag files
|
|
22
|
+
└── data/ the payload (e.g. the bathymetric datasets)
|
|
23
|
+
|
|
24
|
+
Difference from timestamping:
|
|
25
|
+
BagIt: checksums + metadata → proves what a package contains
|
|
26
|
+
Timestamping: independent third party → proves when it existed
|
|
27
|
+
The two are complementary: a bag manifest can be timestamped to prove
|
|
28
|
+
both the content and the time of archiving.
|
|
29
|
+
|
|
30
|
+
Use at Hydrographic Offices:
|
|
31
|
+
- Packaging bathymetric datasets (e.g. GeoTIFF) for long-term archiving
|
|
32
|
+
- Detecting bit rot or accidental modification of archived surveys
|
|
33
|
+
- Transferring datasets to other parties with verifiable integrity
|
|
34
|
+
|
|
35
|
+
Usage:
|
|
36
|
+
# Create a bag from a directory
|
|
37
|
+
bag = bagit.make_bag("survey_2024/", {"Source-Organization": "Flemish Hydrography"})
|
|
38
|
+
|
|
39
|
+
# Validate an existing bag
|
|
40
|
+
bag = bagit.Bag("survey_2024/")
|
|
41
|
+
bag.validate()
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
import bagit
|
|
45
|
+
import os
|
|
46
|
+
import shutil
|
|
47
|
+
from osgeo import gdal
|
|
48
|
+
from .technique import Technique
|
|
49
|
+
|
|
50
|
+
gdal.UseExceptions()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class BagItArchive(Technique):
|
|
54
|
+
def label(self):
|
|
55
|
+
return f"archive_bagit"
|
|
56
|
+
|
|
57
|
+
def _house_holding(self):
|
|
58
|
+
directory = self._get_directory(self._file)
|
|
59
|
+
|
|
60
|
+
if os.path.exists(directory):
|
|
61
|
+
shutil.rmtree(directory)
|
|
62
|
+
os.makedirs(directory)
|
|
63
|
+
|
|
64
|
+
filename = os.path.basename(self._file)
|
|
65
|
+
shutil.copy(self._file, os.path.join(directory, filename))
|
|
66
|
+
|
|
67
|
+
aux = f"{self._file}.aux.xml"
|
|
68
|
+
if os.path.exists(aux):
|
|
69
|
+
shutil.copy(aux, os.path.join(directory, f"{filename}.aux.xml"))
|
|
70
|
+
|
|
71
|
+
def create_integrity(self, **kwargs):
|
|
72
|
+
"""
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
root_hash : str
|
|
76
|
+
Integrity hash key for storing into bag, for validation on data/info content
|
|
77
|
+
"""
|
|
78
|
+
if "root_hash" not in kwargs:
|
|
79
|
+
raise TypeError("Missing required keyword argument: 'root_hash'")
|
|
80
|
+
|
|
81
|
+
self._root_hash = kwargs["root_hash"]
|
|
82
|
+
self._house_holding()
|
|
83
|
+
bag = bagit.make_bag(
|
|
84
|
+
self._get_directory(self._file),
|
|
85
|
+
{
|
|
86
|
+
"fingerprint": self._root_hash,
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
return bag
|
|
90
|
+
|
|
91
|
+
def _get_directory(self, file):
|
|
92
|
+
root, _ = os.path.splitext(file)
|
|
93
|
+
return f"{root}_bagit"
|
|
94
|
+
|
|
95
|
+
def _create_directory(self, file):
|
|
96
|
+
dir = self._get_directory(file)
|
|
97
|
+
if not os.path.exists(dir):
|
|
98
|
+
os.mkdir(dir)
|
|
99
|
+
return dir
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file_hash.py
|
|
3
|
+
============
|
|
4
|
+
Cryptographic file hashing for integrity verification.
|
|
5
|
+
|
|
6
|
+
A cryptographic hash function maps a file of arbitrary size to a fixed-length
|
|
7
|
+
digest (the "fingerprint" of the file). Any change to the file, however small,
|
|
8
|
+
results in a completely different digest. This provides:
|
|
9
|
+
|
|
10
|
+
- Integrity verification: detect accidental corruption or modification
|
|
11
|
+
- Change detection: compare a file against a previously stored digest
|
|
12
|
+
- Deduplication: identical content yields an identical digest
|
|
13
|
+
|
|
14
|
+
The default algorithm is SHA-256 (SHA-2 family, specified in NIST FIPS 180-4).
|
|
15
|
+
Any algorithm supported by Python's hashlib can be selected instead.
|
|
16
|
+
Files are read in 64 KB chunks, so large datasets (e.g. GeoTIFF rasters or
|
|
17
|
+
point clouds) can be hashed without loading them into memory.
|
|
18
|
+
|
|
19
|
+
Difference from HMAC and timestamping:
|
|
20
|
+
File hash: digest of the content only -> proves the file is unchanged
|
|
21
|
+
relative to a known digest
|
|
22
|
+
HMAC: digest + shared secret key -> also proves who created it
|
|
23
|
+
Timestamping: digest + independent third party -> proves when it existed
|
|
24
|
+
A plain hash offers no authenticity: anyone can recompute it. It is
|
|
25
|
+
therefore only trustworthy when the reference digest itself is stored
|
|
26
|
+
or transmitted through a trusted channel (e.g. in a signed or
|
|
27
|
+
timestamped manifest).
|
|
28
|
+
|
|
29
|
+
Used for:
|
|
30
|
+
- Fixity checks on archived bathymetric datasets
|
|
31
|
+
- Verifying that a dataset was transferred without corruption
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
hasher = FileHash(algorithm="sha256")
|
|
35
|
+
|
|
36
|
+
# Create the digest of a file
|
|
37
|
+
digest = hasher.create_integrity("dataset.tif")
|
|
38
|
+
|
|
39
|
+
# Verify a file against a stored digest
|
|
40
|
+
ok = hasher.verify("dataset.tif", integrity=digest)
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
import os
|
|
44
|
+
import hashlib
|
|
45
|
+
from osgeo import gdal
|
|
46
|
+
from .technique import Technique
|
|
47
|
+
|
|
48
|
+
gdal.UseExceptions()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class FileHash(Technique):
|
|
52
|
+
def label(self):
|
|
53
|
+
return "file_hash"
|
|
54
|
+
|
|
55
|
+
def create_integrity(self, **kwargs):
|
|
56
|
+
"""
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
algorithm : str
|
|
60
|
+
kind of algorithm to used for creating hash key
|
|
61
|
+
standard: "sha256"
|
|
62
|
+
"""
|
|
63
|
+
if "algorithm" in kwargs:
|
|
64
|
+
self._algorithm = kwargs["algorithm"]
|
|
65
|
+
else:
|
|
66
|
+
self._algorithm = "sha256"
|
|
67
|
+
|
|
68
|
+
hasher = hashlib.new(self._algorithm)
|
|
69
|
+
with open(self._file, "rb") as f:
|
|
70
|
+
while chunk := f.read(65536): # 64KB chunks
|
|
71
|
+
hasher.update(chunk)
|
|
72
|
+
digest = hasher.hexdigest()
|
|
73
|
+
return digest
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""
|
|
2
|
+
merkle_tree_hash.py
|
|
3
|
+
===================
|
|
4
|
+
Content-based integrity hashing of raster datasets with a Merkle tree.
|
|
5
|
+
|
|
6
|
+
Where a plain file hash (see FileHash) fingerprints the stored bytes of a
|
|
7
|
+
file, the classes in this module fingerprint the *content* of a raster: the
|
|
8
|
+
pixel values of the first band, read through GDAL. Every raster row becomes
|
|
9
|
+
one leaf of a Merkle tree (a binary tree in which every node is the hash of
|
|
10
|
+
its children). The root of the tree, hex-encoded, is the integrity value of
|
|
11
|
+
the dataset. This provides:
|
|
12
|
+
|
|
13
|
+
- Format independence: the same values stored with a different
|
|
14
|
+
compression or tiling yield the same fingerprint
|
|
15
|
+
- Low memory use: the raster is read and hashed row by row
|
|
16
|
+
- Change localisation: a persisted tree can be used to find which rows
|
|
17
|
+
changed, or to prove that a row is part of the dataset
|
|
18
|
+
- Tolerance (robust variant): insensitive to small rounding differences
|
|
19
|
+
|
|
20
|
+
Classes:
|
|
21
|
+
MerkleTreeInmemoryHash
|
|
22
|
+
Tree kept in memory. Nothing is written to disk.
|
|
23
|
+
MerkleTreeSqliteHash
|
|
24
|
+
Same fingerprint, but the tree is persisted in a SQLite database next
|
|
25
|
+
to the dataset ("<file>.db"). Suited when the tree is needed after
|
|
26
|
+
the calculation, for instance to generate proofs.
|
|
27
|
+
MerkleTreeInmemoryRobustHash
|
|
28
|
+
Rounds values before hashing, so that small numerical differences do
|
|
29
|
+
not change the fingerprint (see "Robust hashing").
|
|
30
|
+
|
|
31
|
+
What is hashed (in this order):
|
|
32
|
+
1. Dataset metadata items whose key starts with "STATISTICS" or
|
|
33
|
+
"Corporation", sorted by key and stored as "key=value"
|
|
34
|
+
2. The same selection from the metadata of band 1
|
|
35
|
+
3. For each raster row: the values with NoData removed, as float32 bytes
|
|
36
|
+
|
|
37
|
+
Not part of the fingerprint: georeferencing (geotransform, CRS), other
|
|
38
|
+
bands, other metadata, and the position of NoData cells within a row.
|
|
39
|
+
|
|
40
|
+
Robust hashing:
|
|
41
|
+
Before hashing, values are rounded to `precision` decimals (default 3)
|
|
42
|
+
and converted to absolute values. The sign-insensitivity makes the
|
|
43
|
+
fingerprint independent of the sign convention (e.g. depths stored as
|
|
44
|
+
positive or negative numbers). For datasets with a negative mean,
|
|
45
|
+
STATISTICS_MINIMUM and STATISTICS_MAXIMUM are swapped accordingly.
|
|
46
|
+
|
|
47
|
+
12.123 == 12.1234555 (precision=3)
|
|
48
|
+
12.123 != 12.456
|
|
49
|
+
|
|
50
|
+
Limitations:
|
|
51
|
+
- Rounding is not a true tolerance: two values that differ by less than
|
|
52
|
+
10**-precision can still fall on different sides of a rounding
|
|
53
|
+
boundary and produce different fingerprints.
|
|
54
|
+
- A value cannot be distinguished from its negative, so the robust
|
|
55
|
+
variant is only meaningful for datasets whose values share one sign.
|
|
56
|
+
|
|
57
|
+
Difference from a file hash:
|
|
58
|
+
File hash: digest of the stored bytes -> proves the file is unchanged
|
|
59
|
+
Merkle hash: digest of the raster values -> proves the data is unchanged,
|
|
60
|
+
even if the file was rewritten (e.g. recompressed)
|
|
61
|
+
|
|
62
|
+
Use at Flemish Hydrography:
|
|
63
|
+
- Fixity checks on archived bathymetric rasters (e.g. GeoTIFF)
|
|
64
|
+
- Verifying that a dataset kept its values after format conversion
|
|
65
|
+
- Producing the fingerprints that are timestamped (RFC 3161) or listed
|
|
66
|
+
in a BagIt manifest
|
|
67
|
+
|
|
68
|
+
Requires: GDAL (osgeo), NumPy and pymerkle.
|
|
69
|
+
|
|
70
|
+
Usage:
|
|
71
|
+
hasher = MerkleTreeInmemoryHash(algorithm="sha256")
|
|
72
|
+
|
|
73
|
+
# Create the fingerprint of a raster
|
|
74
|
+
fingerprint = hasher.create_integrity("dataset.tif")
|
|
75
|
+
|
|
76
|
+
# Verify a raster against a stored fingerprint
|
|
77
|
+
ok = hasher.verify("dataset.tif", integrity=fingerprint)
|
|
78
|
+
|
|
79
|
+
# Tolerant of rounding differences beyond the third decimal
|
|
80
|
+
robust = MerkleTreeInmemoryRobustHash(precision=3)
|
|
81
|
+
fingerprint = robust.create_integrity("dataset.tif")
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
from osgeo import gdal
|
|
85
|
+
import numpy as np
|
|
86
|
+
from pymerkle import SqliteTree, InmemoryTree
|
|
87
|
+
import hashlib
|
|
88
|
+
import sys
|
|
89
|
+
import os
|
|
90
|
+
from .technique import Technique
|
|
91
|
+
|
|
92
|
+
gdal.UseExceptions()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class MerkleTreeInmemoryHash(Technique):
|
|
96
|
+
def _getDatasource(self, file):
|
|
97
|
+
ds = gdal.Open(file)
|
|
98
|
+
if ds is None:
|
|
99
|
+
raise FileNotFoundError(f"GDAL cannot open file: {file}")
|
|
100
|
+
return ds
|
|
101
|
+
|
|
102
|
+
def _getMerkleTree(self):
|
|
103
|
+
self._house_holding()
|
|
104
|
+
return InmemoryTree(algorithm=self._algorithm)
|
|
105
|
+
|
|
106
|
+
def _normalize_to_prec(self, value):
|
|
107
|
+
return value
|
|
108
|
+
|
|
109
|
+
def _addMetadataToTree(self, metadata, tree):
|
|
110
|
+
# ADDING metadata
|
|
111
|
+
# Sorted, be sure all metadata items are stored in the same way
|
|
112
|
+
# Dictionary sort is not guaranteed
|
|
113
|
+
filtered = sorted(
|
|
114
|
+
(
|
|
115
|
+
(k, self._normalize_to_prec(v))
|
|
116
|
+
for k, v in metadata
|
|
117
|
+
if k.startswith("STATISTICS") or k.startswith("Corporation")
|
|
118
|
+
),
|
|
119
|
+
key=lambda x: x[0], # sorteer op key
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
for meta_key, meta_value in filtered:
|
|
123
|
+
tree.append_entry(f"{meta_key}={meta_value}".encode("utf-8"))
|
|
124
|
+
|
|
125
|
+
def label(self):
|
|
126
|
+
return "merkle_tree_inmemory_hash"
|
|
127
|
+
|
|
128
|
+
def _house_holding(self):
|
|
129
|
+
db_path = f"{self._file}.db"
|
|
130
|
+
if os.path.exists(db_path):
|
|
131
|
+
os.remove(db_path)
|
|
132
|
+
|
|
133
|
+
def create_integrity(self, **kwargs):
|
|
134
|
+
"""
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
algorithm : str
|
|
138
|
+
kind of algorithm to used for creating hash key
|
|
139
|
+
standard: "sha256"
|
|
140
|
+
"""
|
|
141
|
+
self._algorithm = "sha256"
|
|
142
|
+
if "algorithm" in kwargs:
|
|
143
|
+
self._algorithm = kwargs["algorithm"]
|
|
144
|
+
|
|
145
|
+
ds = self._getDatasource(self._file)
|
|
146
|
+
band = ds.GetRasterBand(1)
|
|
147
|
+
nodata = band.GetNoDataValue()
|
|
148
|
+
|
|
149
|
+
cols = ds.RasterXSize
|
|
150
|
+
rows = ds.RasterYSize
|
|
151
|
+
|
|
152
|
+
tree = self._getMerkleTree()
|
|
153
|
+
|
|
154
|
+
# ADDING metadata
|
|
155
|
+
self._addMetadataToTree(ds.GetMetadata().items(), tree)
|
|
156
|
+
self._addMetadataToTree(band.GetMetadata().items(), tree)
|
|
157
|
+
|
|
158
|
+
for y in range(rows):
|
|
159
|
+
row = band.ReadAsArray(0, y, cols, 1)[0]
|
|
160
|
+
# mask nodata
|
|
161
|
+
if nodata is not None:
|
|
162
|
+
row = row[row != nodata]
|
|
163
|
+
# No X/Y, no geotransform, only structure + value
|
|
164
|
+
packed = row.astype(np.float32).tobytes()
|
|
165
|
+
tree.append_entry(packed)
|
|
166
|
+
root = tree.get_state()
|
|
167
|
+
# band.SetMetadataItem("fingerprint", root.hex())
|
|
168
|
+
ds = None
|
|
169
|
+
band = None
|
|
170
|
+
return root.hex()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class MerkleTreeSqliteHash(MerkleTreeInmemoryHash):
|
|
174
|
+
def label(self):
|
|
175
|
+
return "merkle_tree_sqlite_hash"
|
|
176
|
+
|
|
177
|
+
def _getMerkleTree(self):
|
|
178
|
+
self._house_holding()
|
|
179
|
+
return SqliteTree(f"{self._file}.db", algorithm=self._algorithm)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class MerkleTreeInmemoryRobustHash(MerkleTreeInmemoryHash):
|
|
183
|
+
"""
|
|
184
|
+
Merkle Tree integriteitscontrole with robust hashing.
|
|
185
|
+
|
|
186
|
+
The difference with regular hashing:
|
|
187
|
+
- Values will be rounded on `precision` decimals before hashing.
|
|
188
|
+
It makes the hash tolerant for non-content changes such as:
|
|
189
|
+
12.123 == 12.1234555 (precision=3)
|
|
190
|
+
-5.25 == -5.2499999 (precision=2)
|
|
191
|
+
It detects real content changes:
|
|
192
|
+
12.123 != 12.456
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
def label(self):
|
|
196
|
+
return f"merkle_tree_robust_hash_p{self._precision}"
|
|
197
|
+
|
|
198
|
+
def _normalize_to_prec(self, value):
|
|
199
|
+
try:
|
|
200
|
+
return abs(round(float(value), self._precision))
|
|
201
|
+
except ValueError:
|
|
202
|
+
return value
|
|
203
|
+
|
|
204
|
+
def _robust_metadata(self, metadata):
|
|
205
|
+
if "STATISTICS_MEAN" in metadata and float(metadata["STATISTICS_MEAN"]) < 0:
|
|
206
|
+
new_min = metadata["STATISTICS_MAXIMUM"]
|
|
207
|
+
metadata["STATISTICS_MAXIMUM"] = metadata["STATISTICS_MINIMUM"]
|
|
208
|
+
metadata["STATISTICS_MINIMUM"] = new_min
|
|
209
|
+
return metadata
|
|
210
|
+
|
|
211
|
+
def create_integrity(self, **kwargs):
|
|
212
|
+
"""
|
|
213
|
+
Parameters
|
|
214
|
+
----------
|
|
215
|
+
algorithm : str
|
|
216
|
+
kind of algorithm to used for creating hash key
|
|
217
|
+
standard: "sha256"
|
|
218
|
+
precision : int
|
|
219
|
+
Number of decimals to be rounded before hashing.
|
|
220
|
+
High value = sensitive for small changes.
|
|
221
|
+
Low value = more tolerant for rounding differences.
|
|
222
|
+
|
|
223
|
+
Recommendation for raster data:
|
|
224
|
+
precision=2 -> tolerant up to 0.01m (ie. after lossy compression)
|
|
225
|
+
precision=3 -> tolerant up to 0.001m (recommended standard)
|
|
226
|
+
precision=4 -> tolerant up to 0.0001m (strict)
|
|
227
|
+
"""
|
|
228
|
+
self._algorithm = "sha256"
|
|
229
|
+
if "algorithm" in kwargs:
|
|
230
|
+
self._algorithm = kwargs["algorithm"]
|
|
231
|
+
|
|
232
|
+
self._precision = 3
|
|
233
|
+
if "precision" in kwargs:
|
|
234
|
+
self._precision = kwargs["precision"]
|
|
235
|
+
|
|
236
|
+
ds = self._getDatasource(self._file)
|
|
237
|
+
band = ds.GetRasterBand(1)
|
|
238
|
+
nodata = band.GetNoDataValue()
|
|
239
|
+
|
|
240
|
+
cols = ds.RasterXSize
|
|
241
|
+
rows = ds.RasterYSize
|
|
242
|
+
|
|
243
|
+
tree = self._getMerkleTree()
|
|
244
|
+
|
|
245
|
+
# ADDING metadata
|
|
246
|
+
self._addMetadataToTree(self._robust_metadata(ds.GetMetadata()).items(), tree)
|
|
247
|
+
self._addMetadataToTree(self._robust_metadata(band.GetMetadata()).items(), tree)
|
|
248
|
+
|
|
249
|
+
for y in range(rows):
|
|
250
|
+
row = band.ReadAsArray(0, y, cols, 1)[0]
|
|
251
|
+
# mask nodata
|
|
252
|
+
if nodata is not None:
|
|
253
|
+
row = row[row != nodata]
|
|
254
|
+
|
|
255
|
+
row = np.abs(np.round(row, decimals=self._precision))
|
|
256
|
+
packed = row.astype(np.float32).tobytes()
|
|
257
|
+
tree.append_entry(packed)
|
|
258
|
+
|
|
259
|
+
root = tree.get_state()
|
|
260
|
+
band = None
|
|
261
|
+
ds = None
|
|
262
|
+
return root.hex()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Technique:
|
|
5
|
+
def __init__(self, **kwargs):
|
|
6
|
+
"""
|
|
7
|
+
Parameters
|
|
8
|
+
----------
|
|
9
|
+
file : str
|
|
10
|
+
Raster file for creating integrity
|
|
11
|
+
"""
|
|
12
|
+
if "file" not in kwargs:
|
|
13
|
+
raise TypeError("Missing required keyword argument: 'file'")
|
|
14
|
+
|
|
15
|
+
self._file = kwargs["file"]
|
|
16
|
+
if not os.path.exists(self._file):
|
|
17
|
+
raise ValueError(f"File '{self._file}' does not exist")
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
timestamping.py
|
|
3
|
+
===============
|
|
4
|
+
RFC 3161 Trusted Timestamping.
|
|
5
|
+
|
|
6
|
+
RFC 3161 is an internet standard for trusted timestamping. A
|
|
7
|
+
Timestamp Authority (TSA) cryptographically certifies that a given
|
|
8
|
+
hash value existed at a specific point in time. This provides:
|
|
9
|
+
|
|
10
|
+
- Integrity verification: the data has not been modified since the timestamp
|
|
11
|
+
- Non-repudiation: the time has been confirmed by an independent party
|
|
12
|
+
- Legal evidentiary value: relevant for liability and archiving
|
|
13
|
+
|
|
14
|
+
Difference from HMAC:
|
|
15
|
+
HMAC: shared key between two parties → internal use
|
|
16
|
+
Timestamping: independent third party → external proof, legally enforceable
|
|
17
|
+
|
|
18
|
+
Use at Hydrographic Offices:
|
|
19
|
+
- Long-term archiving of bathymetric datasets
|
|
20
|
+
- Legal proof of the state of a dataset at a specific point in time
|
|
21
|
+
- Demonstrating when a survey was completed
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
# Compute timestamp (online via FreeTSA)
|
|
25
|
+
openssl ts -reply -in file.tsr -text
|
|
26
|
+
openssl ts -verify -in file.tsr -data file.txt -CAfile tsa_cert/cacert.pem
|
|
27
|
+
|
|
28
|
+
# Verify a timestamp
|
|
29
|
+
python timestamping.py --verify dataset.tif --timestamp dataset.tif.tsr
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from rfc3161_client import (
|
|
34
|
+
TimestampRequestBuilder,
|
|
35
|
+
decode_timestamp_response,
|
|
36
|
+
VerifierBuilder,
|
|
37
|
+
VerificationError,
|
|
38
|
+
)
|
|
39
|
+
import secrets
|
|
40
|
+
import requests
|
|
41
|
+
from cryptography import x509
|
|
42
|
+
from cryptography.hazmat.primitives import hashes
|
|
43
|
+
import certifi
|
|
44
|
+
import os
|
|
45
|
+
import subprocess
|
|
46
|
+
from pathlib import Path
|
|
47
|
+
import urllib.request
|
|
48
|
+
|
|
49
|
+
TSA_EINDPUNTEN = {
|
|
50
|
+
"freetsa": "https://freetsa.org/tsr",
|
|
51
|
+
"digicert": "https://timestamp.digicert.com",
|
|
52
|
+
"globalsign": "http://timestamp.globalsign.com/tsa/r45standard",
|
|
53
|
+
}
|
|
54
|
+
CACERT_URL = "https://freetsa.org/files/cacert.pem"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Timestamping:
|
|
58
|
+
|
|
59
|
+
def __init__(self, **kwargs):
|
|
60
|
+
"""
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
file : str or Path
|
|
64
|
+
File used for timestamping
|
|
65
|
+
"""
|
|
66
|
+
if "file" not in kwargs:
|
|
67
|
+
raise TypeError("Missing required keyword argument: 'file'")
|
|
68
|
+
|
|
69
|
+
self._file = Path(kwargs["file"])
|
|
70
|
+
if not self._file.is_file():
|
|
71
|
+
raise FileNotFoundError(f"File not found for Timestamping: {self._file}")
|
|
72
|
+
|
|
73
|
+
def label(self):
|
|
74
|
+
return f"timestamping"
|
|
75
|
+
|
|
76
|
+
# def _house_holding(self, file=""):
|
|
77
|
+
# path = f"{file}.tsr"
|
|
78
|
+
# if os.path.exists(path):
|
|
79
|
+
# os.remove(path)
|
|
80
|
+
|
|
81
|
+
# def verify(self, url="output.xyz", integrity=""):
|
|
82
|
+
# """
|
|
83
|
+
# Clones this functionality
|
|
84
|
+
# openssl ts -verify -in file.tsr -data file.txt -CAfile tsa_cert/cacert.pem
|
|
85
|
+
# """
|
|
86
|
+
# ca_path = "data/tsa_cert/cacert.pem"
|
|
87
|
+
# self._ensure_file(ca_path)
|
|
88
|
+
# data_path = self._file
|
|
89
|
+
# try:
|
|
90
|
+
# with open(url, "rb") as f:
|
|
91
|
+
# tsr_bytes = f.read()
|
|
92
|
+
|
|
93
|
+
# tsr_object = decode_timestamp_response(tsr_bytes)
|
|
94
|
+
|
|
95
|
+
# with open(data_path, "rb") as f:
|
|
96
|
+
# message_data = f.read()
|
|
97
|
+
|
|
98
|
+
# with open(ca_path, "rb") as f:
|
|
99
|
+
# ca_certs = x509.load_pem_x509_certificates(f.read())
|
|
100
|
+
|
|
101
|
+
# builder = VerifierBuilder()
|
|
102
|
+
# for cert in ca_certs:
|
|
103
|
+
# builder.add_root_certificate(cert)
|
|
104
|
+
|
|
105
|
+
# verifier = builder.build()
|
|
106
|
+
|
|
107
|
+
# verifier.verify_message(tsr_object, message_data)
|
|
108
|
+
|
|
109
|
+
# print(" Verification: OK")
|
|
110
|
+
# return True
|
|
111
|
+
|
|
112
|
+
# except VerificationError as ve:
|
|
113
|
+
# print(f" Verification FAILED: Cryptografische fout of mismatch: {ve}")
|
|
114
|
+
# return False
|
|
115
|
+
# except Exception as e:
|
|
116
|
+
# print(f" Fout bij het uitvoeren van de verificatie: {e}")
|
|
117
|
+
# return False
|
|
118
|
+
|
|
119
|
+
def create_integrity(self, **kwargs):
|
|
120
|
+
"""
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
tsr_name : str
|
|
124
|
+
Name given when timestamp is done and a timestamp is made
|
|
125
|
+
"""
|
|
126
|
+
if "tsr_name" not in kwargs:
|
|
127
|
+
raise TypeError("Missing required keyword argument: 'tsr_name'")
|
|
128
|
+
|
|
129
|
+
tsr_name = kwargs["tsr_name"]
|
|
130
|
+
tsa_name = "freetsa"
|
|
131
|
+
tsa_url = TSA_EINDPUNTEN.get(tsa_name)
|
|
132
|
+
|
|
133
|
+
with open(self._file, "rb") as f:
|
|
134
|
+
message = f.read()
|
|
135
|
+
|
|
136
|
+
# Verzoek bouwen
|
|
137
|
+
request = TimestampRequestBuilder().data(message).nonce().build()
|
|
138
|
+
with open(f"{tsr_name}.tsq", "wb") as f:
|
|
139
|
+
f.write(request.as_bytes())
|
|
140
|
+
# print(request.nonce)
|
|
141
|
+
# Sturen naar TSA
|
|
142
|
+
response = requests.post(
|
|
143
|
+
tsa_url,
|
|
144
|
+
data=request.as_bytes(),
|
|
145
|
+
headers={"Content-Type": "application/timestamp-query"},
|
|
146
|
+
)
|
|
147
|
+
timestamp_response = decode_timestamp_response(response.content)
|
|
148
|
+
tsr = timestamp_response.as_bytes()
|
|
149
|
+
with open(f"{tsr_name}.tsr", "wb") as f:
|
|
150
|
+
f.write(tsr)
|
|
151
|
+
|
|
152
|
+
return f"{tsr_name}.tsr"
|
|
153
|
+
|
|
154
|
+
def _ensure_file(self, path, url=CACERT_URL):
|
|
155
|
+
"""Download `url` to `path` if the file does not exist yet."""
|
|
156
|
+
if os.path.exists(path):
|
|
157
|
+
return path
|
|
158
|
+
|
|
159
|
+
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
|
|
160
|
+
|
|
161
|
+
tmp_path = f"{path}.part"
|
|
162
|
+
try:
|
|
163
|
+
with urllib.request.urlopen(url, timeout=30) as response, open(
|
|
164
|
+
tmp_path, "wb"
|
|
165
|
+
) as out:
|
|
166
|
+
out.write(response.read())
|
|
167
|
+
os.replace(
|
|
168
|
+
tmp_path, path
|
|
169
|
+
) # atomair: nooit een half bestand onder de echte naam
|
|
170
|
+
except Exception:
|
|
171
|
+
if os.path.exists(tmp_path):
|
|
172
|
+
os.remove(tmp_path)
|
|
173
|
+
raise
|
|
174
|
+
return path
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from technique_factory import TechniqueFactory, TechniqueType
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def parse_tech_args(item):
|
|
8
|
+
if "=" not in item:
|
|
9
|
+
raise argparse.ArgumentTypeError(f"Expected KEY=VALUE, given: {item}")
|
|
10
|
+
key, value = item.split("=", 1)
|
|
11
|
+
return key.strip(), value.strip()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main():
|
|
15
|
+
parser = argparse.ArgumentParser(
|
|
16
|
+
description="Raster data verification",
|
|
17
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
18
|
+
)
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
"--source",
|
|
21
|
+
"-s",
|
|
22
|
+
required=True,
|
|
23
|
+
help="Hash key or Timestamp checking the intregrity",
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"--arg",
|
|
27
|
+
"-a",
|
|
28
|
+
action="append",
|
|
29
|
+
type=parse_tech_args,
|
|
30
|
+
metavar="KEY=VALUE",
|
|
31
|
+
help="Extra argument for the technique, repeatable (e.g. -a precision=2)",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--technique",
|
|
35
|
+
"-t",
|
|
36
|
+
default="fh",
|
|
37
|
+
required=True,
|
|
38
|
+
help=f"Used technique for verification: \n {TechniqueFactory.help_text()}",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
args = parser.parse_args()
|
|
42
|
+
|
|
43
|
+
if not os.path.exists(args.source):
|
|
44
|
+
print(f"ERROR: File not found: {args.source}")
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
try:
|
|
47
|
+
techn = TechniqueFactory.create(args.technique, **dict(args.arg or []))
|
|
48
|
+
print(f"Verification result:{techn.verify(args.source)}")
|
|
49
|
+
except ValueError as e:
|
|
50
|
+
print(f"FOUT: {e}")
|
|
51
|
+
sys.exit(1)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
main()
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: raster_integrity
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Technical methods for checking the integrity of raster data
|
|
5
|
+
Author-email: Dedulle Wim <wim.dedulle@proton.me>
|
|
6
|
+
License: MIT License
|
|
7
|
+
Project-URL: Homepage, https://github.com/wdedulle/raster_integrity
|
|
8
|
+
Keywords: raster_integrity
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Natural Language :: English
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
License-File: AUTHORS.rst
|
|
21
|
+
Requires-Dist: bagit
|
|
22
|
+
Requires-Dist: cryptography
|
|
23
|
+
Requires-Dist: numpy
|
|
24
|
+
Requires-Dist: pymerkle
|
|
25
|
+
Requires-Dist: rfc3161_client
|
|
26
|
+
Provides-Extra: all
|
|
27
|
+
Requires-Dist: raster_integrity[extra]; extra == "all"
|
|
28
|
+
Provides-Extra: extra
|
|
29
|
+
Requires-Dist: pandas; extra == "extra"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# raster_integrity
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
[](https://pypi.python.org/pypi/raster_integrity)
|
|
36
|
+
[](https://anaconda.org/conda-forge/raster_integrity)
|
|
37
|
+
|
|
38
|
+
[](https://pyup.io/repos/github/wdedulle/raster_integrity)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
**Technical methods for checking the integrity of raster data**
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
- Free software: MIT License
|
|
45
|
+
- Documentation: https://wdedulle.github.io/raster_integrity
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
## Features
|
|
49
|
+
|
|
50
|
+
- Setting up metadata for raster files: it includes common and custom raster information.
|
|
51
|
+
```shell
|
|
52
|
+
python .\raster_integrity\setup.py -f .\data\classified.xyz -m "used_for"="classification for AI"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- Creating integrity hash
|
|
56
|
+
```shell
|
|
57
|
+
python .\raster_integrity\integrity.py -i .\data\classified.jpg -t me
|
|
58
|
+
8eb3bdddc021b045622f9811ff925d132ad6274f0bba6546dcbf6014dac19bdf
|
|
59
|
+
```
|
|
60
|
+
- Archive all artefacts with its integrity hash
|
|
61
|
+
```shell
|
|
62
|
+
python .\raster_integrity\integrity.py -i .\data\classified.jpg -t ba -a root_hash=8eb3bdddc021b045622f9811ff925d132ad6274f0bba6546dcbf6014dac19bdf
|
|
63
|
+
.\data\classified_bagit
|
|
64
|
+
```
|
|
65
|
+
- Seal the tagmanifest with timestamping
|
|
66
|
+
```shell
|
|
67
|
+
python .\raster_integrity\integrity.py -i .\data\classified_bagit\tagmanifest-sha256.txt -t ts -a tsr_name=.\data\tagmanifest_cassified_jpg
|
|
68
|
+
.\data\tagmanifest_cassified_jpg.tsr
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- Verification - To Be
|
|
72
|
+
```shell
|
|
73
|
+
python .\raster_integrity\verify.py -a file=.\data\classified_bagit\tagmanifest-sha256.txt -t ts -s .\data\classified.jpg.tsr
|
|
74
|
+
python .\raster_integrity\verify.py -a root_hash=8eb3bdddc021b045622f9811ff925d132ad6274f0bba6546dcbf6014dac19bdf -t me -s .\data\classified.jpg
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Required package
|
|
78
|
+
GDAL: Geospatial Data Abstraction Library version 3.13.3
|
|
79
|
+
### Installation for Python 3.15
|
|
80
|
+
Wheel packages can be found at https://github.com/cgohlke/geospatial-wheels/releases
|
|
81
|
+
|
|
82
|
+
This a specific GDAL version 3.13.3 installation for Windows AMD64 with Python version 3.15.*
|
|
83
|
+
```
|
|
84
|
+
pip install https://github.com/cgohlke/geospatial-wheels/releases/download/v2026.8.20/gdal-3.13.3-cp315-cp315-win_amd64.whl
|
|
85
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
raster_integrity/__init__.py,sha256=QvlVh4JTl3JL7jQAja76yKtT-IvF4631ASjWY1wS6AQ,22
|
|
2
|
+
raster_integrity/integrity.py,sha256=jARfjBikXtqtvDiKM-fdy4aS2nLckSjGiLhkhA-LUKs,1282
|
|
3
|
+
raster_integrity/setup.py,sha256=ZS68cjz3ptqr7LRpNFHUsUYwid_eVkQv3-Iom5ulp6o,3720
|
|
4
|
+
raster_integrity/technique_factory.py,sha256=QVySneWWYlSRpWaH74tefh9V7ZIInQrgNFlX63GgPfY,3137
|
|
5
|
+
raster_integrity/verify.py,sha256=u4nDoyhRK7s9wpQj0lGuE8ydLUwz8rAt7ZLXBPXnu_0,1461
|
|
6
|
+
raster_integrity/techniques/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
raster_integrity/techniques/archiving.py,sha256=V2WZ58zWuA53Iw5vIHBing4fAasgTsbmtDpHM78-XmY,3355
|
|
8
|
+
raster_integrity/techniques/file_hash.py,sha256=Z4fPRKBk933ZbYWufC1sOaU7WURsqdUYkDSdRpv7cTo,2506
|
|
9
|
+
raster_integrity/techniques/merkle_tree_hash.py,sha256=NRitljjMpxECYPJs2pxaSg31QqSNj8gxwvjJh_F9LUI,9405
|
|
10
|
+
raster_integrity/techniques/technique.py,sha256=qu761tiiPq-w_Fq8yXDRIIBrFo7mTbDqna_MjayFzu4,445
|
|
11
|
+
raster_integrity/techniques/timestamping.py,sha256=OWz2gA-u1XA8tXQuYeKjkiHfnauvdJ799fmBUwgFwW0,5522
|
|
12
|
+
raster_integrity-0.0.2.dist-info/licenses/AUTHORS.rst,sha256=GGH30hXv6Zfjg7QDp6TkeVbAnuCksKxBgb2kqXnfBtw,158
|
|
13
|
+
raster_integrity-0.0.2.dist-info/licenses/LICENSE,sha256=MRMIkJs8bKcYa_8_GHofsYEfcCVb_0I8y2kUQyeI2_E,1070
|
|
14
|
+
raster_integrity-0.0.2.dist-info/METADATA,sha256=o3HIQmZJkkCaEw5KIXedCdAaU1wxqQ8_K_lKWWXqgpk,3230
|
|
15
|
+
raster_integrity-0.0.2.dist-info/WHEEL,sha256=Uvlg2aNlEKdilsyFSJt1QX5me0eEbCjQzR3nZeFkvy4,109
|
|
16
|
+
raster_integrity-0.0.2.dist-info/entry_points.txt,sha256=fAMaL9chR--gr6a4LHdx2M-pPYUrGRbG7CyTPmj89as,63
|
|
17
|
+
raster_integrity-0.0.2.dist-info/top_level.txt,sha256=D9igPYFLAm8QOh9rTevgj95GiGzQq7BzS9EMqmAxSTA,17
|
|
18
|
+
raster_integrity-0.0.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Dedulle Wim
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
raster_integrity
|