Skip to content

Commit

Permalink
Add the concept of a detached hash
Browse files Browse the repository at this point in the history
  • Loading branch information
hughsie committed Sep 25, 2023
1 parent 48267e3 commit e164a74
Show file tree
Hide file tree
Showing 8 changed files with 265 additions and 3 deletions.
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ XML document:
version-scheme = multipartnumeric
product = ColorHug
summary = Open Source Display Colorimeter
colloquial-version = b2ed6f1ed8587bf01a2951d74512a70f1a512d38
edition = v2021+
colloquial-version = b2ed6f1ed8587bf01a2951d74512a70f1a512d38 # of all the source files
edition = v2021+ # identifier of the project tree, e.g. the output of 'git describe'
revision = 2
persistent-id = com.hughski.colorhug

Expand Down Expand Up @@ -121,6 +121,17 @@ using multiple files on `--load`) then you can specify the correct identity usin
name = OEM Vendor
regid = oem.homepage.com

If we're talking about a "detached" binary, and want to make sure that we can verify
the blob is valid, we can also add one or more file hashes:

[uSWID-Hash]
value = 067cb8292dc062eabbe05734ef7987eb1333b6b6

Additional hashes can also be provided:

[uSWID-Hash:SHA256]
value = 5525fbd0911b8dcbdc6f0c081ac27fd55b75d6d261c62fa05b9bdc0b72b481f6

Adding Deps
-----------

Expand Down
23 changes: 22 additions & 1 deletion uswid/format_coswid.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
#
# pylint: disable=too-few-public-methods,protected-access

from typing import Dict, Any, Optional
from typing import Dict, Any, Optional, Tuple

import io
import uuid
Expand All @@ -20,6 +20,7 @@
from .identity import uSwidIdentity
from .entity import uSwidEntity, uSwidEntityRole
from .link import uSwidLink, uSwidLinkRel
from .hash import uSwidHash, uSwidHashAlg


class uSwidFormatCoswid(uSwidFormatBase):
Expand Down Expand Up @@ -66,6 +67,10 @@ def _save_link(self, link: uSwidLink) -> Dict[uSwidGlobalMap, Any]:
data[uSwidGlobalMap.REL] = LINK_MAP.get(link.rel, link.rel)
return data

def _save_hash(self, ihash: uSwidHash) -> Tuple[int, bytes]:
"""exports a uSwidHash CoSWID section"""
return (ihash.alg_id, bytes.fromhex(ihash.value))

def _save_entity(self, entity: uSwidEntity) -> Dict[uSwidGlobalMap, Any]:
"""exports a uSwidEntity CoSWID section"""

Expand Down Expand Up @@ -124,6 +129,11 @@ def _save_identity(self, identity: uSwidIdentity) -> bytes:
metadata[uSwidGlobalMap.PERSISTENT_ID] = identity.persistent_id
data[uSwidGlobalMap.SOFTWARE_META] = metadata

# hashes
data[uSwidGlobalMap.HASH] = [
self._save_hash(ihash) for ihash in identity.hashes
]

# entities
if not identity._entities:
raise NotSupportedError("at least one entity MUST be provided")
Expand Down Expand Up @@ -182,6 +192,11 @@ def _load_link(self, link: uSwidLink, data: Dict[uSwidGlobalMap, Any]) -> None:
)
) from e

def _load_hash(self, ihash: uSwidHash, data: Dict[uSwidGlobalMap, Any]) -> None:
"""imports a uSwidHash CoSWID section"""
ihash.alg_id = uSwidHashAlg(data[0])
ihash.value = bytes.hex(data[1])

def _load_entity(
self,
entity: uSwidEntity,
Expand Down Expand Up @@ -252,6 +267,12 @@ def _load_identity(
elif key == uSwidGlobalMap.PERSISTENT_ID:
identity.persistent_id = value

hashes = data.get(uSwidGlobalMap.HASH, [])
for hash_data in hashes:
ihash = uSwidHash()
self._load_hash(ihash, hash_data)
identity.add_hash(ihash)

# entities
entities = data.get(uSwidGlobalMap.ENTITY, [])
if isinstance(entities, dict):
Expand Down
38 changes: 38 additions & 0 deletions uswid/format_goswid.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
)
from .entity import uSwidEntity
from .link import uSwidLink
from .hash import uSwidHash, uSwidHashAlg
from .format_swid import _ENTITY_MAP_FROM_XML, _ENTITY_MAP_TO_XML


Expand Down Expand Up @@ -58,6 +59,16 @@ def _save_link(self, link: uSwidLink) -> Dict[str, str]:
node["rel"] = link.rel
return node

def _save_hash(self, ihash: uSwidHash) -> Dict[str, str]:
"""exports a uSwidLink goSWID section"""

node: Dict[str, str] = {}
if ihash.alg_id:
node["alg_id"] = ihash.alg_id
if ihash.value:
node["value"] = ihash.value
return node

def _save_entity(self, entity: uSwidEntity) -> Dict[str, Any]:
"""exports a uSwidEntity goSWID section"""

Expand Down Expand Up @@ -124,6 +135,12 @@ def _save_identity_internal(self, identity: uSwidIdentity) -> Dict[str, Any]:
# the CoSWID spec says: 'software-meta => one-or-more'
root["software-meta"] = [node]

# checksum
if identity.hashes:
root["hash"] = []
for link in identity.links:
root["hash"].append(self._save_hash(link))

# entities
if identity.entities:
root["entity"] = []
Expand Down Expand Up @@ -151,6 +168,18 @@ def _load_link(self, link: uSwidLink, node: Dict[str, str]) -> None:
link.href = node.get("href")
link.rel = node.get("rel")

def _load_hash(self, ihash: uSwidHash, node: Dict[str, str]) -> None:
"""imports a uSwidLink goSWID section"""

ihash.alg_id = uSwidHashAlg.from_string(node.get("alg_id"))
ihash.value = node.get("value")

def _load_hash(self, ihash: uSwidHash, node: Dict[str, str]) -> None:
"""imports a uSwidLink goSWID section"""

ihash.alg_id = node.get("alg_id")
ihash.value = node.get("value")

def _load_entity(
self,
entity: uSwidEntity,
Expand Down Expand Up @@ -218,6 +247,15 @@ def _load_identity_internal(
except KeyError:
pass

# hashes
try:
for node in data["hashes"]:
ihash = uSwidHash()
self._load_hash(ihash, node)
identity.add_hash(ihash)
except KeyError:
pass

def _load_identity(self, identity: uSwidIdentity, blob: bytes) -> None:
"""imports a uSwidIdentity goSWID blob"""

Expand Down
40 changes: 40 additions & 0 deletions uswid/format_ini.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
)
from .entity import uSwidEntity, uSwidEntityRole
from .link import uSwidLink
from .hash import uSwidHash, uSwidHashAlg

_ENTITY_MAP_FROM_INI = {
"TagCreator": uSwidEntityRole.TAG_CREATOR,
Expand Down Expand Up @@ -68,6 +69,14 @@ def _save_link(self, link: uSwidLink) -> Dict[str, Any]:
data["href"] = link.href
return data

def _save_hash(self, ihash: uSwidHash) -> Dict[str, Any]:
"""exports a uSwidLink INI section"""

data: Dict[str, Any] = {}
if ihash.value:
data["value"] = ihash.value
return data

def _save_entity(self, entity: uSwidEntity) -> Dict[str, Any]:
"""exports a uSwidEntity INI section"""

Expand Down Expand Up @@ -110,6 +119,8 @@ def _save_identity(self, identity: uSwidIdentity) -> bytes:
main["edition"] = identity.edition
if identity.colloquial_version:
main["colloquial-version"] = identity.colloquial_version
if identity.hashes:
main["hash"] = identity.hashes
if identity.persistent_id:
main["persistent-id"] = identity.persistent_id
config["uSWID"] = main
Expand All @@ -122,6 +133,10 @@ def _save_identity(self, identity: uSwidIdentity) -> bytes:
if identity.links:
config["uSWID-Link"] = self._save_link(identity.links[0])

# hash
for ihash in identity.hashes:
config[f"uSWID-Hash:{ihash.alg_id.name}"] = self._save_hash(ihash)

# as string
with io.StringIO() as f:
config.write(f)
Expand All @@ -143,6 +158,27 @@ def _load_link(
if not link.href:
raise NotSupportedError("all entities MUST have a href")

def _load_hash(
self,
ihash: uSwidHash,
data: Union[configparser.SectionProxy, Dict[str, str]],
alg_hint: Optional[str] = None,
) -> None:
"""imports a uSwidHash INI section"""

if alg_hint:
try:
ihash.alg_id = uSwidHashAlg.from_string(alg_hint.split(":")[1])
except (KeyError, TypeError, IndexError):
pass
for key, value in data.items():
if key == "value":
ihash.value = value
else:
print("unknown key {} found in ini file!".format(key))
if not ihash.value:
raise NotSupportedError("all hashes MUST have a value")

def _load_entity(
self,
entity: uSwidEntity,
Expand Down Expand Up @@ -219,3 +255,7 @@ def _load_identity(self, identity: uSwidIdentity, blob: bytes) -> None:
link = uSwidLink()
self._load_link(link, config[group])
identity.add_link(link)
if group.startswith("uSWID-Hash"):
ihash = uSwidHash()
self._load_hash(ihash, config[group], alg_hint=group)
identity.add_hash(ihash)
22 changes: 22 additions & 0 deletions uswid/format_swid.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
)
from .entity import uSwidEntity, uSwidEntityRole
from .link import uSwidLink
from .hash import uSwidHash, uSwidHashAlg

_ENTITY_MAP_FROM_XML = {
"tagCreator": uSwidEntityRole.TAG_CREATOR,
Expand Down Expand Up @@ -66,6 +67,14 @@ def _save_link(self, link: uSwidLink, root: ET.Element) -> None:
if link.rel:
node.set("rel", link.rel)

def _save_hash(self, ihash: uSwidHash, root: ET.Element) -> None:
"""exports a uSwidHash SWID section"""

node = ET.SubElement(root, "Hash")
node.set("alg_id", ihash.alg_id.name)
if ihash.value:
node.set("value", ihash.value)

def _save_entity(self, entity: uSwidEntity, root: ET.Element) -> None:
"""exports a uSwidEntity SWID section"""

Expand Down Expand Up @@ -118,6 +127,8 @@ def _save_identity(self, identity: uSwidIdentity) -> bytes:
self._save_entity(entity, root)
for link in identity._links.values():
self._save_link(link, root)
for ihash in identity.hashes:
self._save_hash(ihash, root)

# optional metadata
if (
Expand Down Expand Up @@ -155,6 +166,11 @@ def _load_link(self, link: uSwidLink, node: ET.SubElement) -> None:
rel_data = node.get("rel")
link.rel = LINK_MAP.get(rel_data, rel_data)

def _load_hash(self, ihash: uSwidHash, node: ET.SubElement) -> None:
"""imports a uSwidHash SWID section"""
ihash.value = node.get("value")
ihash.alg_id = uSwidHashAlg.from_string(node.get("alg_id"))

def _load_entity(
self,
entity: uSwidEntity,
Expand Down Expand Up @@ -218,3 +234,9 @@ def _load_identity(self, identity: uSwidIdentity, blob: bytes) -> None:
link = uSwidLink()
self._load_link(link, node)
identity.add_link(link)

# hashes
for node in element.xpath("ns:Hash", namespaces=namespaces):
ihash = uSwidHash()
self._load_hash(ihash, hash)
identity.add_hash(ihash)
64 changes: 64 additions & 0 deletions uswid/hash.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2023 Richard Hughes <[email protected]>
#
# SPDX-License-Identifier: LGPL-2.1+
#
# pylint: disable=too-few-public-methods

from enum import IntEnum

from typing import Optional


class uSwidHashAlg(IntEnum):
SHA1 = 0
SHA256 = 1
SHA384 = 7
SHA512 = 8

@classmethod
def from_string(cls, alg_id: str) -> "uSwidHashAlg":
return cls(
{
"SHA1": uSwidHashAlg.SHA1,
"SHA256": uSwidHashAlg.SHA256,
"SHA384": uSwidHashAlg.SHA384,
"SHA512": uSwidHashAlg.SHA512,
}[alg_id]
)


class uSwidHash:
"""represents a SWID link"""

def __init__(
self,
alg_id: Optional[uSwidHashAlg] = None,
value: Optional[str] = None,
):
self.alg_id: Optional[uSwidHashAlg] = alg_id
self.value: Optional[str] = value

@property
def value(self) -> Optional[str]:
return self._value

@value.setter
def value(self, value: Optional[str]) -> None:
if self.alg_id is None and value:
if len(value) == 40:
self.alg_id = uSwidHashAlg.SHA1
elif len(value) == 64:
self.alg_id = uSwidHashAlg.SHA256
elif len(value) == 96:
self.alg_id = uSwidHashAlg.SHA384
elif len(value) == 128:
self.alg_id = uSwidHashAlg.SHA512
self._value = value

def __repr__(self) -> str:
return "uSwidHash({},{})".format(
self.alg_id.name if self.alg_id else uSwidHashAlg.SHA1.name, self.value
)
Loading

0 comments on commit e164a74

Please sign in to comment.